본문 바로가기

프로젝트/DVWA 실습

DVWA 실습 #3 - Command Injection

DVWA의 두 번째 실습 대상인 Command Injection이다. 주어진 IP 주소로 ping 명령을 수행하는 시스템이며 이를 이용하여 커맨드 인젝션을 실습해볼 수 있다.

커맨드 인젝션?

커맨드 인젝션이란 사용자가 취약한 웹사이트의 입력 폼이나 기타 방법을 이용하여 서버에 직접적, 간접적으로 명령어(Bash, CMD 등)를 전송하여 실행시키는 공격 방법이다. RCE(Remote Command Execution)이라고도 하며 입력 폼뿐만이 아니라 사용자의 쿠키값, 레퍼러, HTTP 헤더 등을 서버 측에서 읽어 적절한 검증 없이 시스템 호출에 포함한다면 공격 벡터가 될 수 있다. 이 공격이 가능하려면 웹 애플리케이션에서 사용자 입력을 기반으로 system(), exec(), os.system() 같은 시스템 호출 함수를 사용해야 한다. 이 함수는 웹서버의 권한으로 호출되기 때문에 일반 사용자보다 높은 권한으로 실행될 수 있으며 일반적으로 읽지 못하는 파일이나 디렉토리를 읽을 수 있다. 그래서 웹 애플리케이션을 구동하는 사용자 계정을 따로 분리하고 권한을 세부적으로 설정해주는 것이 커맨드 인젝션을 예방하는 데 도움이 된다. 물론 제일 좋은 방법은 직접적으로 시스템 호출을 사용하지 않고 라이브러리로 대체하거나 호출에 사용되는 입력값을 최대한 검증하는 것이다(OWASP 문서).

 

Command Injection | OWASP

Command Injection on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.

owasp.org

좀 특이한 경우로 커맨드 인젝션을 통해 특정 명령어(바이너리)를 다른 파일로 교체할 수도 있다. 쉘에서 ls, cat 등의 명령어를 실행할 때는 보통 /bin/ls, /bin/cat에 위치한 명령어를 사용하며 이 절대 경로를 전부 입력하지 않고도 실행할 수 있도록 환경변수의 PATH 항목에 실행할 바이너리를 탐색할 디렉토리가 지정되어 있다.

WSL Ubuntu 기반 env 명령어 실행 결과에서 PATH 항목

/usr/local/sbin, /usr/local/bin, /usr/sbin 등의 여러 바이너리(bin) 디렉터리가 지정되어 있는데 처음 디렉토리부터 순서대로 실행하려는 명령어 파일을 찾는다. 예를 들어 그냥 ls를 입력하면 /usr/local/sbin에 ls 파일이 있는지, 없다면 /usr/local/bin에 ls 파일이 있는지, /usr/sbin에 ls 파일이 있는지 순서대로 탐색하는 것이다. 현재 ls는 /usr/bin에 있기 때문에 /usr/bin/ls가 존재한다는 것을 파악하면 해당 디렉토리에 위치한 명령어 파일을 실행하여 ls를 수행한다. 그렇다면 여기서 커맨드 인젝션을 통해 PATH 환경변수의 맨 앞에 다른 디렉토리를 집어넣으면 어떻게 될까? 당연히 그곳에 있는 동일한 이름의 명령어를 실행할 것이다.

그렇기 때문에 이를 악용할 수 있는 방법 중 하나는 동일한 이름의 다른 바이너리(공격자가 업로드했거나 이미 시스템에 존재하거나)를 PATH 환경변수에서 원래 명령어가 들어있는 디렉토리보다 앞쪽의 디렉토리(위의 예제에서는 /home/kwonkyu/malicious)로 옮겨서 원래 명령어 대신 실행되도록 하는 것이다. 위의 예제에서는 PATH 환경변수 맨 앞에 별도의 디렉토리를 지정한 후 해당 디렉토리 내부에 id 명령어 파일을 복사해서 ls 명령으로 이름을 바꿔두었다. 이때 ls는 "ls --color=auto" 로 alias가 설정되어 있었기 때문에 잠시 해제해두고 실습을 진행했다. 그리고 아무 디렉토리나 가서 어떤 파일들이 있는지 보기 위해 ls 명령어를 입력하면 어떻게 될까?

위처럼 ls 명령어를 실행했음에도 아까 복사한 id 명령어가 실행되어 현재 사용자의 uid등이 노출되었다. 지금은 간단한 실습이지만 나중에 정교한 커맨드 인젝션에 가능하다면 더 큰 피해를 불러올 수 있다.

 

이처럼 커맨드 인젝션을 수행하려면 당연히 커맨드, 즉 명령어에 대한 이해가 필요하다. 기본적으로 터미널에서 자주 사용되는 명령어는 cat(파일 읽기), ls(현재 폴더 내 파일 목록), cd(디렉터리 이동), mv(파일 변경)등이 있으며 여러 명령어를 수행하려면 ';'나 '&&', '|' 같은 구분자, 재지정자(redirect command), 연결자(chaining command)가 필요하다. 그리고 서버가 구동되는 환경에 대한 조사 역시 필요한데 만약 윈도우 기반이라면 ifconfig 같은 명령어는 통하지 않을 것이다. 반대로 리눅스 기반이라면 ipconfig가 통하지 않을 것이다. 간단한 방법은 ping 명령어의 TTL 차이로 알 수 있다.

위의 127.0.0.1은 현재 운영체제인 Windows 10으로 전송되며 아래의 192.168.56.103은 실습용으로 사용하고 있는 Ubuntu로 전송된다. 각 ping 패킷의 TTL 값을 보면 윈도우즈는 128, 우분투는 64인 것을 알 수 있다.

Security Level: low

이 단계에서는 다음과 같은 요청이 전송된다.

POST /dvwa/vulnerabilities/exec/ HTTP/1.1
Host: 192.168.56.103
Content-Length: 24
Cache-Control: max-age=0
Origin: http://192.168.56.103
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.56.103/dvwa/vulnerabilities/exec/
Accept-Encoding: gzip, deflate
Accept-Language: ko,en;q=0.9,en-US;q=0.8
Cookie: security=low; security_level=2; PHPSESSID=8a1201308dd6e8b15d81d0fd100f4694
Connection: close

ip=8.8.8.8&Submit=Submit

이전 Brute Force와는 달리 이제부터 POST로 데이터를 전송하고 있으며 그에 따른 Content-Length, Content-Type 헤더 등이 포함된 것을 볼 수 있다. Cookie의 security_level은 같은 도메인에서 동작하고 있는 bWAPP의 쿠키기 때문에 신경쓸 필요가 없으며 바디 부분에 ip 파라미터와 Submit 파라미터로 데이터를 전송하고 있는 것을 볼 수 있다. 이 단계에서는 다음과 같은 소스 코드가 적용된다.

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = $_REQUEST[ 'ip' ];

    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}

?>

사용자 입력에 대한 필터링 없이 그대로 shell_exec() 함수에 전달하여 ping 명령어를 호출하고 있다. 이때 리눅스 기반에서는 ping 명령어 실행 시 무한정 동작하기 때문에 php_uname() 함수로 현재 서버 운영체제의 종류를 알아낸 후 리눅스에서는 c 옵션으로 4번만 ping 하도록 실행하고있다.

정상적으로 ping 했을 시 위처럼 수행 결과가 출력된다. 올바르지 않은 ip 주소를 입력했을 경우 아무것도 나타나지 않는다.

Security Level: medium

이 단계에서는 이전과 동일한 요청이 전송된다.

이 단계에서는 다음과 같은 소스 코드가 적용된다.

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = $_REQUEST[ 'ip' ];

    // Set blacklist
    $substitutions = array(
        '&&' => '',
        ';'  => '',
    );

    // Remove any of the charactars in the array (blacklist).
    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );

    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}

?>

Low 단계와 달리 사용자 입력값에 대한 기본적인 필터링이 이루어지고 있다. str_replace() 메서드를 이용하여 '&&'이나 ';' 문자가 포함되어 있으면 이를 ''으로 변환함으로써 삭제하고 있는데 리눅스 쉘에서는 '&&', ';' 말고도 '|'이나 '||', '&' 등 복수의 명령어를 실행시킬 수 있는 문자가 여러개 존재한다. 그렇기때문에 불완전한 필터링이라 할 수 있으며 결정적으로 이전에도 워게임을 풀면서 언급한 내용이지만 str_replace() 함수는 오직 단 한번만 실행되기 때문에 '&&&' 같은 사용자 입력값을 '&'으로 치환하게 된다. '&' 문자는 해당 명령을 백그라운드로 실행하도록 지정하는 문자다. 이를 이용하면 ping 명령어를 백그라운드로 보내고 새 명령어를 하나 더 실행할 수 있게 된다.

Security Level: high

이 단계에서는 이전과 동일한 요청이 전송된다.

이 단계에서는 다음과 같은 소스 코드가 적용된다.

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $target = trim($_REQUEST[ 'ip' ]);

    // Set blacklist
    $substitutions = array(
        '&'  => '',
        ';'  => '',
        '| ' => '',
        '-'  => '',
        '$'  => '',
        '('  => '',
        ')'  => '',
        '`'  => '',
        '||' => '',
    );

    // Remove any of the charactars in the array (blacklist).
    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );

    // Determine OS and execute the ping command.
    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        // Windows
        $cmd = shell_exec( 'ping  ' . $target );
    }
    else {
        // *nix
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }

    // Feedback for the end user
    echo "<pre>{$cmd}</pre>";
}

?>

Medium 단계와 동일한 방식이지만 필터링하는 문자가 더 많아졌는데 '&', ';', '|', '||' 등 여러 명령어를 실행할 수 있는 문자들은 모두 필터링하고 있다. 사실상 쉘에서 쓸 수 있는 특수문자들은 거의 필터링됐다고 볼 수 있는데 이를 어떻게 우회할 수 있을까? 그 해답은 개발자의 사소한 실수에서 비롯됀다. 3번째 필터링 항목을 보면 '|' 명령어를 필터링하는 것을 볼 수 있는데 자세히 보면 '|'가 아닌 '| '를 필터링하고 있다. 쉘 명령어에서 '|' 명령은 파이프라인 명령어로 왼쪽에서 실행된 명령의 결과값을 오른쪽 명령으로 전달해주는 역할을 수행한다. 그래서 대부분 사용할 때 'cat ./text | grep hello' 처럼 사용하기 때문에 '| ' 부분을 필터링하고자 한 것인데 파이프라인은 'cat ./text |grep hello' 처럼도 사용될 수 있기 때문에 공백이 없는 경우 필터링되지 않는다.

Security Level: impossible

이 단계에서는 이전과 동일한 요청이 전송된다.

이 단계에서는 다음과 같은 소스 코드가 적용된다.

<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $target = $_REQUEST[ 'ip' ];
    $target = stripslashes( $target );

    // Split the IP into 4 octects
    $octet = explode( ".", $target );

    // Check IF each octet is an integer
    if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
        // If all 4 octets are int's put the IP back together.
        $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

        // Determine OS and execute the ping command.
        if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
            // Windows
            $cmd = shell_exec( 'ping  ' . $target );
        }
        else {
            // *nix
            $cmd = shell_exec( 'ping  -c 4 ' . $target );
        }

        // Feedback for the end user
        echo "<pre>{$cmd}</pre>";
    }
    else {
        // Ops. Let the user name theres a mistake
        echo '<pre>ERROR: You have entered an invalid IP.</pre>';
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Brute Force 때처럼 CSRF 토큰을 이용하여 사용자 검증을 하고있다. 특이한 점은 문자열 필터링이 아닌 IP 주소 자체가 올바른 형식인지 검증하는 방식을 사용했다는 것이다. PHP의 explode() 함수는 첫번째 매개변수로 전달된 문자열을 기준으로 두 번째 매개변수로 전달된 문자열을 분리하여 배열로 반환한다. 즉 문자열을 일정 간격으로 폭발(explode)시켜주는 느낌인데 정상적인 IP 주소라면 255.255.255.255처럼 전달될 것이기 때문에 "."로 분리하면 총 4개의 "255" 문자열이 반환된다. 이후 각 문자열마다 is_numeric() 함수를 적용하여 숫자로만 이루어져 있는지 파악하고 다시 "."을 사이에 두고 재조립하여 ping 명령을 수행하게 된다. 만약 이전처럼 "ping 127.0.0.1; cat /etc/passwd"나 "ping 127.0.0.1 |cat /etc/passwd" 같은 명령어가 전달된다면 "."으로 분리된 맨 마지막 문자열이 "1; cat /etc/passwd", "1 |cat /etc/passwd"처럼 숫자로만 이루어져 있지 않기 때문에 is_numeric() 함수에 위배되어 실행이 중단된다.