SECCON Beginners CTF 2021 Writeup（自分の勉強用）

## pwn
Beginnerのやつだけとけた。

### rewriter
[これ]( https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c006.html )などを見て，ソースコードを読んで，objdumpして，関数のアドレスを取ってきて，書き換えて，実行させる。

## Web

### osoba
サーバの/flagにアクセスしたいが，そのままでは当然弾かれてしまう。
/?page=public/xxx.html というふうにそれぞれの遷移先のページにアクセスしていることがわかる。ここにあからさまにディレクトリトラバーサル脆弱性がありそうなので，試すと通る。

### Warewolf
コードが公開されているので，読む。
人狼になればいいことが分かるが，Player classの初期化で，乱数でroleを決めるときには，WAREWOLFは除かれている。

```app.py
    self.__role = random.choice('VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN')
```

ではどうすればいいのかというと，POSTのパラメータでcolorとnameが送られるので，要素数だけforを回してplayerのdictに突っ込む処理が走っており，そこをハックすれば良い。

```app.py
    @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を叩いた結果として返ってくる。

```bff/main.go
        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しか解けませんでした。精進ですね……