免责声明:
本公众号致力于安全研究和红队攻防技术分享等内容,本文中所有涉及的内容均不针对任何厂商或个人,同时由于传播、利用本公众号所发布的技术或工具造成的任何直接或者间接的后果及损失,均由使用者本人承担。请遵守中华人民共和国相关法律法规,切勿利用本公众号发布的技术或工具从事违法犯罪活动。最后,文中提及的图文若无意间导致了侵权问题,请在公众号后台私信联系作者,进行删除操作。
为了能更直观的看到是怎么造成的命令/代码执行,所以先看下流程
JobController的runImmediately方法负责执行任务,
任务调度的处理过程有点复杂,详情看
https://blog.csdn.net/qq_42985872/article/details/128500740
定位到类:WorkerActor
onReceiveServerScheduleJobReq方法负责处理runjob节点的请求。此时空属性的ServerScheduleJobReq对象会传递进入下一级onReceiveServerScheduleJobReq方法
onReceiveServerScheduleJobReq方法会根据任务类型进入不同的处理,轻量级任务进入LightTaskTracker

而重量级任务则会进入HeavyTaskTracker

跟进isLightweightTask方法查看如何判断的级别

从isLightweightTask方法得知,单机、固定频率、固定延迟模式都属于轻量,其他则是重量
先跟进轻量,LightTaskTracker#create

请求传进LightTaskTracker对象:

在构造方法中做了大量的初始化设定,具体为:会使用父类对请求先做一次参数初始化

此时的req是包含有控制台传来的各项参数,继续往下走,constructTaskContext方法会将各项参数封装到taskContext对象


随后进入到load方法加载控制台传来的ProcessorInfo

在PowerJobProcessorLoader#load方法中,ProcessorInfo信息又被传进pf.build方法

pf是包含BuiltInDefaultProcessorFactory、JarContainerProcessorFactory的对象

在load流程里,会根据传进的处理器类型进入不同的Factory

得到处理器参数:BUILT_IN、EXTERNAL

分别查看两个Factory
JarContainerProcessorFactory:
根据注释得知该类是从容器加载class,从传进的参数中获取容器id以及容器里的全限定类名,格式大概是这样id#xx.xxx.xxx,当processorType是EXTERNAL时就会用到这个Factory

BuiltInDefaultProcessorFactory:
BuiltInDefaultProcessorFactory是内建的默认处理器工厂,可以通过全限定类名加载处理器,在build方法中,会根据传进的ProcessorInfo信息来实例化对象,即ProcessorInfo参数就是全限定类名。当processorType是BUILT_IN时就会用到这个Factory

load方法走完了,回到构造方法LightTaskTracker中,实例化后的ProcessorInfo信息被封装到processorBean对象


在初始化完所有信息后,进入到processTask方法,其会在线程池中完成被调用

跟进processTask方法,参数传进process

因为在此之前设置了当前线程上下文加载器

所以此时被实例化的processorBean会使用到前面实例化时设置的classloader加载

进入到
CommonBasicProcessor#process
该类是一个通用处理器类

process方法中,将包含有任务参数、任务instanceId等信息的TaskContext对象传进process0方法


process0方法属于AbstractScriptProcessor类,该类属于通用脚本处理器类,所有脚本插件都要继承该类。在process0方法中,会使用prepareScriptFile方法来根据控制台传来的内容生成脚本文件,文件名是instanceId


跟进getScriptName方法发现是一个抽象方法

所在的抽象类由多个插件类继承,用于应对不同系统的调用。在powerjob本身中自带有多个脚本处理器(插件)

被重写的getScriptName方法大同小异,均返回对应系统的脚本文件名:cmd_instanceId.bat/shell_instanceId.sh


回到prepareScriptFile方法,最终把参数写入脚本文件

回到process0方法,方法里会调用到抽象方法getRunCommand,根据子类重写的方法返回来判断要调用哪个应用来执行脚本。
也就是说,当控制台传进的processorInfo(即封装后的processorBean)是CMDProcessor类时,getRunCommand则是CMDProcessor里重写的getRunCommand方法,返回的是cmd.exe,否则就是剩余的几个Processor(python、sh、powershell)

最终由ProcessBuilder完成对getRunCommand方法返回值的调用,scriptPath被当成返回值的参数进行执行
剩下的HeavyTaskTracker.create不看了,可能更复杂,但大同小异
验证:
根据saveJob方法里jobInfoDO的信息构造参数保存任务,其中jobParams参数是恶意命令,最后使用runImmediately方法执行任务就会触发漏洞
保存任务

查看实体类,可以看到有一个处理器选项
SaveJobInfoRequest

ProcessorType

在V3版本中可以直接通过shell处理器的方式执行系统命令,但仅限于linux


新建任务,designatedWorkers可指定机器,不指定机器默认全部执行
可以查看所有机器地址

processorInfo的参数为命令执行参数

搜索刚刚创建的任务关键字得出任务id

运行任务id得到结果id

根据id获取命令执行结果

v4以后的处理器都是通过插件的方式调用,此时双系统都可以执行命令
-
windows:
通过内置的全限定类名执行
按照v3的方式添加任务(v4的参数有些许不一样),然后执行查询任务id

运行得到的id就会触发

-
linux:
ShellProcessor
可以通过查询执行成功的id来查看命令回显

假如通过v3的方式在v4增加了任务,可以尝试通过接口转换执行

但需要目标有这个依赖
<dependency>
<groupId>tech.powerjob</groupId>
<artifactId>powerjob-official-processors</artifactId>
<version>4.3.3</version>
</dependency>
检测工具请关注我们的圈子,后台回复“加群”或“小助手”加入我们的群吧
原文始发于微信公众号(Lambda小队):首发!PowerJob 全版本 RCE(另附检测脚本)