Просмотр исходного кода

若依定时任务远程代码执行rce

xw 6 дней назад
Родитель
Сommit
53d2d264a8

+ 33 - 0
fs-common/src/main/java/com/fs/common/constant/Constants.java

@@ -171,5 +171,38 @@ public class Constants
      */
     public static final String LOOKUP_LDAP = "ldap://";
 
+    /**
+     * LDAPS 远程方法调用
+     */
+    public static final String LOOKUP_LDAPS = "ldaps://";
+
+    /**
+     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+     */
+    public static final String[] JOB_WHITELIST_STR = { "com.fs" };
+
+    /**
+     * 定时任务违规的字符(危险类、远程协议、反射与动态类加载等)
+     */
+    public static final String[] JOB_ERROR_STR = {
+            "java.net.URL",
+            "java.net.URLClassLoader",
+            "javax.naming.InitialContext",
+            "javax.naming",
+            "javax.script.ScriptEngineManager",
+            "javax.script",
+            "org.yaml.snakeyaml",
+            "org.springframework",
+            "org.apache",
+            "java.lang.Runtime",
+            "java.lang.ProcessBuilder",
+            "java.lang.reflect",
+            "sun.misc",
+            "com.sun",
+            "com.fs.common.utils.file",
+            "com.fs.common.config",
+            "com.fs.generator"
+    };
+
     public static final Integer PAGE_SIZE =10;
 }

+ 7 - 22
fs-quartz/src/main/java/com/fs/quartz/controller/SysJobController.java

@@ -13,17 +13,16 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
-import com.fs.common.constant.Constants;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.job.TaskException;
-import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.quartz.domain.SysJob;
 import com.fs.quartz.service.ISysJobService;
 import com.fs.quartz.util.CronUtils;
+import com.fs.quartz.util.ScheduleUtils;
 
 /**
  * 调度任务信息操作处理
@@ -84,17 +83,10 @@ public class SysJobController extends BaseController
         {
             return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
         }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
+        String validateMsg = ScheduleUtils.validateInvokeTarget(job.getInvokeTarget());
+        if (validateMsg != null)
         {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap://'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
-        {
-            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
+            return error("新增任务'" + job.getJobName() + "'失败," + validateMsg);
         }
         job.setCreateBy(getUsername());
         return toAjax(jobService.insertJob(job));
@@ -112,17 +104,10 @@ public class SysJobController extends BaseController
         {
             return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
         }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi://'调用");
-        }
-        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_LDAP))
-        {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap://'调用");
-        }
-        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
+        String validateMsg = ScheduleUtils.validateInvokeTarget(job.getInvokeTarget());
+        if (validateMsg != null)
         {
-            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)//'调用");
+            return error("修改任务'" + job.getJobName() + "'失败," + validateMsg);
         }
         job.setUpdateBy(getUsername());
         return toAjax(jobService.updateJob(job));

+ 10 - 0
fs-quartz/src/main/java/com/fs/quartz/service/impl/SysJobServiceImpl.java

@@ -9,6 +9,8 @@ import org.quartz.SchedulerException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import com.fs.common.constant.ScheduleConstants;
 import com.fs.common.exception.job.TaskException;
 import com.fs.quartz.domain.SysJob;
@@ -25,6 +27,8 @@ import com.fs.quartz.util.ScheduleUtils;
 @Service
 public class SysJobServiceImpl implements ISysJobService
 {
+    private static final Logger log = LoggerFactory.getLogger(SysJobServiceImpl.class);
+
     @Autowired
     private Scheduler scheduler;
 
@@ -41,6 +45,12 @@ public class SysJobServiceImpl implements ISysJobService
         List<SysJob> jobList = jobMapper.selectJobAll();
         for (SysJob job : jobList)
         {
+            String validateMsg = ScheduleUtils.validateInvokeTarget(job.getInvokeTarget());
+            if (validateMsg != null)
+            {
+                log.error("定时任务[{}]调用目标未通过安全校验,已跳过加载:{}", job.getJobName(), validateMsg);
+                continue;
+            }
             ScheduleUtils.createScheduleJob(scheduler, job);
         }
     }

+ 6 - 1
fs-quartz/src/main/java/com/fs/quartz/util/JobInvokeUtil.java

@@ -23,6 +23,11 @@ public class JobInvokeUtil
     public static void invokeMethod(SysJob sysJob) throws Exception
     {
         String invokeTarget = sysJob.getInvokeTarget();
+        String validateMsg = ScheduleUtils.validateInvokeTarget(invokeTarget);
+        if (validateMsg != null)
+        {
+            throw new Exception("定时任务调用目标安全校验失败:" + validateMsg);
+        }
         String beanName = getBeanName(invokeTarget);
         String methodName = getMethodName(invokeTarget);
         List<Object[]> methodParams = getMethodParams(invokeTarget);
@@ -34,7 +39,7 @@ public class JobInvokeUtil
         }
         else
         {
-            Object bean = Class.forName(beanName).newInstance();
+            Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
             invokeMethod(bean, methodName, methodParams);
         }
     }

+ 55 - 0
fs-quartz/src/main/java/com/fs/quartz/util/ScheduleUtils.java

@@ -10,9 +10,12 @@ import org.quartz.Scheduler;
 import org.quartz.SchedulerException;
 import org.quartz.TriggerBuilder;
 import org.quartz.TriggerKey;
+import com.fs.common.constant.Constants;
 import com.fs.common.constant.ScheduleConstants;
 import com.fs.common.exception.job.TaskException;
 import com.fs.common.exception.job.TaskException.Code;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.quartz.domain.SysJob;
 
 /**
@@ -110,4 +113,56 @@ public class ScheduleUtils
                         + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR);
         }
     }
+
+    /**
+     * 校验调用目标字符串安全性,返回 null 表示通过,否则返回错误信息
+     */
+    public static String validateInvokeTarget(String invokeTarget)
+    {
+        if (StringUtils.isEmpty(invokeTarget))
+        {
+            return "调用目标字符串不能为空";
+        }
+        if (StringUtils.containsIgnoreCase(invokeTarget, Constants.LOOKUP_RMI))
+        {
+            return "目标字符串不允许'rmi'调用";
+        }
+        if (StringUtils.containsAnyIgnoreCase(invokeTarget, new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
+        {
+            return "目标字符串不允许'ldap(s)'调用";
+        }
+        if (StringUtils.containsAnyIgnoreCase(invokeTarget, new String[] { Constants.HTTP, Constants.HTTPS }))
+        {
+            return "目标字符串不允许'http(s)'调用";
+        }
+        if (StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_ERROR_STR))
+        {
+            return "目标字符串存在违规";
+        }
+        if (!whiteList(invokeTarget))
+        {
+            return "目标字符串不在白名单内";
+        }
+        return null;
+    }
+
+    /**
+     * 检查包名是否为白名单配置
+     *
+     * @param invokeTarget 目标字符串
+     * @return 结果
+     */
+    public static boolean whiteList(String invokeTarget)
+    {
+        String packageName = StringUtils.substringBefore(invokeTarget, "(");
+        int count = StringUtils.countMatches(packageName, ".");
+        if (count > 1)
+        {
+            return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR);
+        }
+        Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
+        String beanPackageName = obj.getClass().getPackage().getName();
+        return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR)
+                && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR);
+    }
 }