CTF Writeup: 3v@l (Challenge #484)
1. Initial Analysis & Reconnaissance
The challenge provides a web application with a simple “Bank-Loan Calculator”. The description and a quick look at the page’s source code (view-source
) immediately reveal the core vulnerability and the defenses we need to overcome.
-
Vulnerability: The backend is a Python Flask application that uses the dangerous
eval()
function to process user input from a form. This is a classic Remote Code Execution (RCE) vulnerability. -
Goal: Exploit the RCE to read a flag file on the server.
-
Defenses: The developers attempted to secure the
eval()
function by implementing two blacklists, helpfully detailed in the HTML comments:- Keyword Blacklist: Blocks common malicious strings like
os
,eval
,exec
,ls
,cat
,shell
, etc. - Regex Blacklist:
r'0x...|\u...|%...|\.[A-Za-z0-9]{1,3}\b|[\\\/]|\.\.'
- This blocks hexadecimal, unicode, and URL-encoded characters.
- Crucially, it blocks file extensions (e.g.,
.py
,.txt
), forward/backward slashes (/
,\
), and directory traversal (..
).
- Keyword Blacklist: Blocks common malicious strings like
2. Step-by-Step Exploitation
The exploitation process was a multi-stage discovery, requiring us to bypass each filter systematically.
Step 2.1: Achieving Basic Code Execution
Our first goal was to confirm we could run code. The standard approach for RCE in Python is to use the os
module.
- Bypassing the Keyword Blacklist: The string
os
is blocked. We can bypass this by constructing the string using concatenation ('o'+'s'
) or by using the__import__()
function, which is not blacklisted. - Initial Payload:
__import__('o'+'s').system('l'+'s')
- Problem: This payload executed successfully (returning an exit code of
0
), butos.system()
does not return the command’s output, only its exit status. We couldn’t see the result of ourls
command. - Solution: We switched to
os.popen().read()
, which executes a command and returns its standard output as a string.
Step 2.2: Listing Files and Hitting a Dead End
Using our improved technique, we listed the files in the current directory (/app
).
- Payload:
__import__('o'+'s').popen('l'+'s').read()
- Result: A list of files including
app.py
,static
, andtemplates
. - Hypothesis: The flag might be hardcoded in the application’s source code,
app.py
.
Step 2.3: Bypassing the File Extension Filter
We tried to read app.py
, but the regex filter \.[A-Za-z0-9]{1,3}\b
blocked any payload containing .py
.
- Bypass: We used a shell wildcard (
*
). The web server’s shell expandsapp*
toapp.py
after our payload has passed the filter. - Payload:
__import__('o'+'s').popen('grep . app*').read()
(usinggrep .
to ensure the whole file is read). - Result (Red Herring): We successfully exfiltrated the entire source code of
app.py
. However, after careful review, we confirmed the flag was not in the source code.
Step 2.4: Bypassing the Path Filter and Finding the Flag
Since the flag wasn’t in the local directory, the source code, or in environment variables (which we also checked), the next logical step was to look in other directories, starting with the root directory (/
).
-
Bypass: The regex filter
[\\\/]
blocks the/
character. We bypassed this by generating the character dynamically within our Python payload using its ASCII code. The functionchr(47)
produces the/
character. -
Discovery Payload:
__import__('o'+'s').popen('l'+'s '+chr(47)).read()
-
Result (Success!): This command listed the contents of the root directory, and among the system folders, we found
flag.txt
.app bin ... flag.txt ... var
3. The Final Payload
Now that we knew the flag’s location was /flag.txt
, we had to craft a final payload that combined all of our bypass techniques to read it.
- Command needed:
more /flag.txt
(usingmore
sincecat
is blacklisted). - Bypass for
/
:chr(47)
- Bypass for
.txt
:flag*
This led to the final, successful payload:
__import__('o'+'s').popen('more ' + chr(47) + 'flag*').read()
Executing this payload returned the contents of /flag.txt
, revealing the flag and solving the challenge.
Comments
Post a Comment