蓝凌OA RCE分析

正文

路由是在==/data/sys-common/datajson==处,我们直接在源码中找到该路由的位置,位于WEB-INF/libSrc/com/landray/kmss/common/actions/DataController.java,是在他的datajson方法中

image-20220817100151403

找到路由所对应的代码,主要代码如下

String s_bean = request.getParameter("s_bean");
JSONArray array = new JSONArray();
JSONArray jsonArray = null;

try {
   Assert.notNull(s_bean, "参数s_bean不能为空!");
   RequestContext requestInfo = new RequestContext(request, true);
   String[] beanList = s_bean.split(";");
   List result = null;

   for(int i = 0; i < beanList.length; ++i) {
      IXMLDataBean treeBean = (IXMLDataBean)SpringBeanUtil.getBean(beanList[i]);
      result = treeBean.getDataList(requestInfo);

接收了一个s_bean的参数,然后用分号分隔成数组,遍历调用SpringBeanUtil.getBean(beanList[i]),并且遍历调用getBean返回对象的getDataList方法。

根据payload我们可以知道具体调用的应该是sysFormulaSimulateByJS这个Bean的getDataList方法,来到这个方法

public List getDataList(RequestContext requestContext) throws Exception {
   String strRequestType = requestContext.getParameter("requestType");
   return "getFdHandlerRoleInfoIds".equals(strRequestType) ? this.getFdHandlerRoleInfoIds(requestContext) : this.formulaSimula(requestContext);
}

判断是否传入参数requestType为getFdHandlerRoleInfoIds,如果不是则调用formulaSimula方法。我们这里payload没有传入这个参数,直接来到formulaSimula方法,这里只放主要代码

public List formulaSimula(RequestContext requestContext) throws Exception {
   List rtnVal = new ArrayList();
   Map node = new HashMap();
   String msg = null;
   String confirm = null;
   Object result = null;

   try {
      String script = requestContext.getParameter("script");
      String type = requestContext.getParameter("returnType");
      Enumeration ele = requestContext.getRequest().getParameterNames();
      JSONObject returnData = new JSONObject();

      while(ele.hasMoreElements()) {
         String key = ele.nextElement().toString();
         if (!paramsExcept.contains(key) && !key.endsWith(".scriptJstype")) {
            String value = requestContext.getParameter(key);
            String scriptJstype = requestContext.getParameter(key + ".scriptJstype");
            Object newValue = FormulaParserByJS.formatValue(value, (String)scriptJstype);
            returnData.put(key, newValue);
         }
      }

      FormulaParser parse = FormulaParserByJS.getInstance(returnData, new SysFormulaSimulateByJS.SimulateVarGetter((SysFormulaSimulateByJS.SimulateVarGetter)null), (String)null);
      result = parse.parseValueScript(script, type);

接收了script和returnType参数,while循环里的if判断得满足key不在paramsExcept数组里并且不以.scriptJstype结尾会进到if循环

static {
   paramsExcept.add("script");
   paramsExcept.add("method");
   paramsExcept.add("s_bean");
   paramsExcept.add("s_ajax");
}

我们这有传了个type和script,type不在这数组里,满足条件啊,但是这里我好像暂时没感觉和这个参数有关系?不急,先不看这,先继续往下看。

image-20220817102041620

然后就是通过getInstance获取一个FormulaParserByJS类的对象,然后调用他的parseValueScript方法,这个方法是俩参数的,但是他有重载方法,是一个参数的,并且在这两个参数的方法里调用了this.parseValueScript(script),所以我们直接看一个参数的这个方法。由于方法太长,就一部分一部分来讲了。

if (StringUtil.isNull(script)) {
   return null;
} else {
   if (script.startsWith("#static#")) {
      script = script.substring(8);
   }

   validateScript(script);
   ScriptEngineManager factory = new ScriptEngineManager();
   ScriptEngine engine = factory.getEngineByMimeType("text/javascript");
   StringBuffer leftScript = new StringBuffer();

判断script是否为空并且是否以#static#开头,如果是就截取这个后面的部分,然后就是调用了一下validateScript方法,如果不满足条件会抛出异常

public static void validateScript(String script) {
   if (StringUtil.isNotNull(script) && script.indexOf("importPackage(") > -1) {
      throw new KmssRuntimeException(new KmssMessage("公式定义器非法"));
   }
}

接着就是工厂模式创建了一个ScriptEngine对象。

try {
   String rightScript = script.trim();
   Map funcScriptMap = new HashMap();
   ScriptContext context = engine.getContext();
   int k = 0;
   int index = rightScript.indexOf("$");

这里的index是获取script.trim()以后在script中whileindex1出现的下标。到下面的这个while方法,首先判断index是否大于-1,我们这里没出现,直接跳过判断。

while(true) {
   if (index > -1) {
      int nxtIndex = rightScript.indexOf("$", index + 1);
      if (nxtIndex != -1) {

在while循环中的if判断的代码我就不放了,因为进不来,大概就是下面这样

while(true) {
   String m_script = leftScript.append(rightScript).toString();
   if (logger.isDebugEnabled()) {
      logger.debug("原始公式:" + script);
      logger.debug("执行公式:" + m_script);
   }

   runningData.set(this.contextData);
   Object var21 = engine.eval(m_script);
   return var21;
}

最后是直接通过ScriptEngine对象的eval方法执行m_script,m_script是通过leftScript和rightScript进行拼接以后的结果。leftScript是没东西的,rightScript是传入的那个script。直接通过eval方法造成代码执行。直接本地写个demo测试一下

image-20220817105201439

疑惑的点

其实还有个问题,如果是单纯这样的话,应该不是前台RCE,经过我的测试我感觉是一个认证绕过+RCE的组合造成的前台RCE,原因是在于我在第一次找路径的时候看到他写的是==/data/sys-common/datajson.js==这个路由,然后我找了半天也没找到,最后还找错了,真是尴尬,但是其实这应该是他的一个绕过认证的手段,因为我尝试了直接访问/data/sys-common/datajson路由是需要认证的,但是访问/data/sys-common/datajson.js或者/data/sys-common/datajson.html这种,是可以直接来到控制器里的,我目前的猜测就是为了绕过filter中写的认证,但是具体在哪进行处理我并未找到。