pwn
Beginnerのやつだけとけた。
rewriter
これなどを見て,ソースコードを読んで,objdumpして,関数のアドレスを取ってきて,書き換えて,実行させる。
Web
osoba
サーバの/flagにアクセスしたいが,そのままでは当然弾かれてしまう。
/?page=public/xxx.html というふうにそれぞれの遷移先のページにアクセスしていることがわかる。ここにあからさまにディレクトリトラバーサル脆弱性がありそうなので,試すと通る。
Warewolf
コードが公開されているので,読む。
人狼になればいいことが分かるが,Player classの初期化で,乱数でroleを決めるときには,WAREWOLFは除かれている。
self.__role = random.choice('VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN')
ではどうすればいいのかというと,POSTのパラメータでcolorとnameが送られるので,要素数だけforを回してplayerのdictに突っ込む処理が走っており,そこをハックすれば良い。
@app.route("/", methods="GET", "POST")
def index():
if request.method == 'GET':
return render_template('index.html')
if request.method == 'POST':
player = Player()
for k, v in request.form.items():
player.__dict__k = v
しかし,愚直に,role=WEREWOLFとしても通らない。なぜなら,roleにはアンダースコアがついており,private変数だからである。ここで詰んだと思ってはいけない。
pythonでprivate変数にアクセスする方法をググる。すると,実はpythonにおいては,private変数はpublicで,_Class__varnameに名前が変わっているだけだということがわかる。やp糞
これでもう人狼になることができる。
$ curl -X POST 'https://werewolf.quals.beginners.seccon.jp/' --data 'name=hoge&color=fuga&_Player__role=WEREWOLF'
とか叩けば,flagがもらえる。
check_url
SSRFのお手本っぽい状況設定。そもそも,配布されているphpコードに"Hi, Admin or SSSSRFer"と書いてある。
GETのとき,パラメータでurl=……と指定して送ると,取得されたそのサイトの内容が表示される。
コードを見るとif ($_SERVER["REMOTE_ADDR"] === "127.0.0.1")
でflagを表示するというふうになっている。
しかし,英大文字小文字数字/以外の文字は,正規表現によりサニタイズされているので,普通にドメインを書くと,"."がサニタイズされてしまう。これを抜けたい。
この問題はさらに親切で,c(heck)_urlという題名もhexの暗示っぽい。
ipアドレスの表現は一つではない。ドメインだけでなく,当然ipアドレス"xxx.xxx.xxx"でもokだし,これの16進数表記でもアクセスできる。16進数で表現されたipには,特殊文字はない。おしまい。
json
個人的には一番勉強になった。
やらなければならないことは,ipアドレスの偽装と,ちょっとした工夫である。
私はip偽装の問題は初めてだったので,そこでまず手間取った。
ググってヒットしたIssue( https://github.com/gin-gonic/gin/issues/1684 )が実はかなりドンピシャだったのだが,知識が足りず,nginxのproxyについてや,本気でipを偽装する方法などを調べ回ってしまった。
先のIssueが全てだが,簡潔に言えば,ginでの,c.ClientIP()
は,X-Forwarded-ForやX-Real-Ipを見てClientIPを取得しているので,proxyでX-Forwarded-Forしか処理していない場合,X-Forwarded-Forの偽装は容易なので,ClientIPも余裕で偽装できてしまう。これを使えばよい。
$ curl -X GET -H "X-Forwarded-For: 192.168.111.1" https://json.quals.beginners.seccon.jp/
などと叩くと,内部ページにアクセスできる。やったー。
それで,flagはどこにあるのかというと,内部APIを叩いた結果として返ってくる。
r.POST("/", func(c *gin.Context) {
// get request body
body, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to read body."})
return
}
// parse json
var info Info
if err := json.Unmarshal(body, &info); err != nil {
c.JSON(400, gin.H{"error": "Invalid parameter."})
return
}
// validation
if info.ID < 0 || info.ID > 2 {
c.JSON(400, gin.H{"error": "ID must be an integer between 0 and 2."})
return
}
if info.ID == 2 {
c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."})
return
}
// get data from api server
req, err := http.NewRequest("POST", "http://api:8000", bytes.NewReader(body))
if err != nil {
c.JSON(400, gin.H{"error": "Failed to request API."})
return
}
req.Header.Set("Content-Type", "application/json")
client := new(http.Client)
resp, err := client.Do(req)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to request API."})
return
}
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to request API."})
return
}
c.JSON(200, gin.H{"result": string(result)})
})
でも,バリデーションで,idが2だと,だめだよ!flagとれないよ!と言われてしまう。
ここで,json.Unmarshal(body, &info);
に注目する。infoを見ると,jsonのkeyが"id"の要素をinfoではIDというkeyで保存するというふうに指定しているらしい。
それでは,"id"をIDと共存させたらいい感じにすり抜けられそうだということになる。つまり,jsonでは"id"である"ID"と,jsonで"id"であってdictでも"id"であるidを作ればバイパス可能っぽい。実際,http.NewRequest("POST", "http://api:8000", bytes.NewReader(body))とかbodyを丸々渡しているし,apiの方ではjsonparser.GetInt(body, "id")
で "id"を読んでいる。なんとなくいけそう。
細かい言語仕様とかによる原理は調べていないけれど,試すのは2通りなので適当にやればいけた。
$ curl -X POST -H "X-Forwarded-For: 192.168.111.1" https://json.quals.beginners.seccon.jp -d '{"id":2,"id":1}'
こういう感じでokでした。
cant_use_db
ぽちぽちしていたら想定解法に到達してしまった。
記録や読み取りのためにファイルを開いて処理を止めて……としている間に個数はどんどん増えてしまうので,個数:増加,金額:不定というふうになってしまう。
これで本来残高が足りなくて実行できないはずのeatが実行できる。
感想
ほぼWebしか解けませんでした。精進ですね……