├── bin ├── parser │ ├── url.go │ ├── url.class │ ├── url.rb │ ├── url.php │ ├── url.addressable.rb │ ├── url.pl │ └── url.js ├── requester │ ├── get.open.rb │ ├── get.go │ ├── get.class │ ├── get.rb │ ├── get.pl │ ├── get.curl.php │ ├── get.php │ └── get.js ├── tuf_web.js └── tuf_dns.py ├── src ├── java │ ├── url.java │ └── get.java └── go │ ├── get.go │ └── url.go ├── try.py ├── const.py ├── util ├── __init__.py └── fuzz.py ├── LICENSE ├── samples.txt ├── fuzz_single.py ├── .gitignore ├── run_me.py ├── README.md └── fuzz.py /bin/parser/url.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangetw/Tiny-URL-Fuzzer/HEAD/bin/parser/url.go -------------------------------------------------------------------------------- /bin/requester/get.open.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'open-uri' 3 | puts open(ARGV[0]).gets 4 | -------------------------------------------------------------------------------- /bin/parser/url.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangetw/Tiny-URL-Fuzzer/HEAD/bin/parser/url.class -------------------------------------------------------------------------------- /bin/requester/get.go: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangetw/Tiny-URL-Fuzzer/HEAD/bin/requester/get.go -------------------------------------------------------------------------------- /bin/requester/get.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangetw/Tiny-URL-Fuzzer/HEAD/bin/requester/get.class -------------------------------------------------------------------------------- /bin/requester/get.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'net/http' 3 | require 'uri' 4 | 5 | res = Net::HTTP.get_response( URI.parse(ARGV[0]) ) 6 | puts res.body -------------------------------------------------------------------------------- /bin/parser/url.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | require 'uri' 3 | 4 | x = URI(ARGV[0]) 5 | puts 'scheme=' + x.scheme + ', host=' + x.host + ', port=' + x.port.to_s 6 | -------------------------------------------------------------------------------- /bin/parser/url.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | new($url); 7 | print 'scheme=' . $x->scheme . ', host=' . $x->host . ", port=" . $x->port; 8 | } -------------------------------------------------------------------------------- /bin/parser/url.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/nodejs 2 | var url = require('url'); 3 | 4 | var x = url.parse(process.argv[2]); 5 | console.log('scheme=' + x.protocol.replace(':', '') + ', host=' + x.host + ', port=' + (x.port?x.port:'')); 6 | -------------------------------------------------------------------------------- /bin/requester/get.curl.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | > 3) & 15 # Opcode bits 10 | if tipo == 0: # Standard query 11 | ini = 12 12 | lon = ord(data[ini]) 13 | while lon != 0: 14 | self.dominio += data[ini+1:ini+lon+1] + '.' 15 | ini += lon+1 16 | lon = ord(data[ini]) 17 | 18 | def respuesta(self, ip): 19 | print [self.dominio] 20 | 21 | packet = '' 22 | if self.dominio: 23 | packet += self.data[:2] + "\x81\x80" 24 | packet += self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00' # Questions and Answers Counts 25 | packet += self.data[12:] # Original Domain Name Question 26 | packet += '\xc0\x0c' # Pointer to domain name 27 | packet += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' # Response type, ttl and resource data length -> 4 bytes 28 | packet += str.join('',map(lambda x: chr(int(x)), ip.split('.'))) # 4bytes of IP 29 | return packet 30 | 31 | if __name__ == '__main__': 32 | print 'fakeDNS:: dom.query. 60 IN A %s' % fake 33 | 34 | udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 35 | udps.bind(('',53)) 36 | 37 | try: 38 | while 1: 39 | data, addr = udps.recvfrom(1024) 40 | p = DNSQuery(data) 41 | udps.sendto(p.respuesta(fake), addr) 42 | except KeyboardInterrupt: 43 | udps.close() 44 | -------------------------------------------------------------------------------- /fuzz_single.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: UTF-8 3 | import sys 4 | from itertools import product 5 | from multiprocessing.dummy import Pool as ThreadPool 6 | 7 | import util.fuzz as fuzz 8 | from util import cmd, pprint, execute 9 | from const import PARSERS, REQUESTERS 10 | 11 | def run_parser(url): 12 | res = {} 13 | for key, binary in PARSERS.iteritems(): 14 | lang, libname = key.split('.', 1) 15 | r = execute(lang, binary, url, base='bin/parser/') 16 | 17 | # parse host, change here to get the result you want 18 | if 'host=' in r and 'port=' in r: 19 | res[key] = r.split('host=')[-1].split(', ')[0] 20 | else: 21 | res[key] = 'err' 22 | 23 | return res 24 | 25 | def run_requester(url): 26 | res = {} 27 | for key, binary in REQUESTERS.iteritems(): 28 | lang, libname = key.split('.', 1) 29 | 30 | r = execute(lang, binary, url, base='bin/requester/') 31 | res[key] = r 32 | return res 33 | 34 | 35 | 36 | urls = run_parser(sys.argv[1]) 37 | gets = run_requester(sys.argv[1]) 38 | 39 | total = {} 40 | for k,v in urls.iteritems(): 41 | if total.get(v): 42 | total[v].append(k) 43 | else: 44 | total[v] = [k] 45 | 46 | print [sys.argv[1]] 47 | print '[parsers]' 48 | for k, v in sorted(total.iteritems(), key=lambda x: len(x[1]), reverse=True): 49 | print '%-24s %d = '%([k], len(v)), v 50 | 51 | total = {} 52 | for k,v in gets.iteritems(): 53 | v = v.split('/')[0] 54 | if total.get(v): 55 | total[v].append(k) 56 | else: 57 | total[v] = [k] 58 | 59 | print '\n[requesters]' 60 | for k, v in sorted(total.iteritems(), key=lambda x: len(x[1]), reverse=True): 61 | print '%-24s %d = '%(k, len(v)), v 62 | print '' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # my filter 104 | tmp/ 105 | _tmp* 106 | -------------------------------------------------------------------------------- /run_me.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | from hashlib import md5 6 | from subprocess import check_output, Popen 7 | 8 | RESOLVER = '/etc/resolv.conf' 9 | DNS_CONF_NEW = 'nameserver 127.0.0.1\nsearch localdomain' 10 | DNS_CONF_OLD = 'nameserver 8.8.8.8\nsearch localdomain' 11 | 12 | 13 | def install_iptables(): 14 | Popen('iptables -A INPUT -p tcp --dport 22 -j ACCEPT', shell=True) 15 | Popen('iptables -A OUTPUT -p tcp --sport 22 -j ACCEPT', shell=True) 16 | Popen('iptables -A INPUT -p tcp -d 127.0.0.1/8 -j ACCEPT', shell=True) 17 | Popen('iptables -A OUTPUT -p tcp -d 127.0.0.1/8 -j ACCEPT', shell=True) 18 | Popen('iptables -A OUTPUT -p tcp --dport 0:32768 -j REJECT --reject-with tcp-reset', shell=True) 19 | def restore_iptables(): 20 | Popen('iptables -F', shell=True) 21 | 22 | def install_dns(): 23 | with open(RESOLVER, 'w+') as fp: 24 | fp.write(DNS_CONF_NEW) 25 | def restore_dns(): 26 | with open(RESOLVER, 'w+') as fp: 27 | fp.write(DNS_CONF_OLD) 28 | 29 | def install_server(): 30 | Popen('python bin/tuf_dns.py &', shell=True) 31 | Popen('nodejs bin/tuf_web.js 80 &', shell=True) 32 | Popen('nodejs bin/tuf_web.js 81 &', shell=True) 33 | def restore_server(): 34 | for pid in check_output('pgrep -f "tuf_dns.py"', shell=True).splitlines(): 35 | Popen('kill -9 %s 2>/dev/null;true'%pid, shell=True) 36 | 37 | for pid in check_output('pgrep -f "tuf_web.js"', shell=True).splitlines(): 38 | Popen('kill -9 %s 2>/dev/null;true'%pid, shell=True) 39 | 40 | def _md5_file(f): 41 | with open(f, 'rb') as fp: 42 | c = fp.read() 43 | return _md5(c) 44 | def _md5(data): 45 | return md5(data).hexdigest() 46 | 47 | if __name__ == '__main__': 48 | ''' 49 | This script will change your IPTABLES and DNS config 50 | Be awared before you change it 51 | ''' 52 | print 'Read the source code' 53 | exit() 54 | 55 | m = sys.argv[1] 56 | if m == 'install': 57 | install_dns() 58 | install_iptables() 59 | install_server() 60 | elif m == 'restore': 61 | restore_dns() 62 | restore_iptables() 63 | restore_server() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny URL Fuzzer 2 | 3 | A tiny and cute URL fuzzer in my talk of [Black Hat USA 2017](https://www.blackhat.com/us-17/speakers/Orange-Tsai.html) and [DEFCON 25](https://www.defcon.org/html/defcon-25/dc-25-speakers.html). 4 | 5 | Slides: 6 | 7 | * [A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages!](https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf) 8 | 9 | Case Study: 10 | 11 | * [How I Chained 4 vulnerabilities on GitHub Enterprise, From SSRF Execution Chain to RCE!](http://blog.orange.tw/2017/07/how-i-chained-4-vulnerabilities-on.html) 12 | 13 | 14 | # How to use? 15 | 16 | All the code are written for hackers, and under PoC. Read the source! Some URL samples you can check [samples.txt](samples.txt) 17 | 18 | 19 | ### Install / Restore 20 | ```bash 21 | $ run_me.py install 22 | $ run_me.py restore 23 | ``` 24 | 25 | ### Try 26 | ```bash 27 | $ try.py http://127.0.0.1 28 | 29 | Go.net/url scheme=http, host=127.0.0.1, port= 30 | Java.net.URL scheme=http, host=127.0.0.1, port=-1 31 | NodeJS.url scheme=http, host=127.0.0.1, port= 32 | PHP.parseurl scheme=http, host=127.0.0.1, port= 33 | Perl.URI scheme=http, host=127.0.0.1, port=80 34 | Python.urlparse scheme=http, host=127.0.0.1, port= 35 | Ruby.addressable/uri scheme=http, host=127.0.0.1, port= 36 | Ruby.uri scheme=http, host=127.0.0.1, port=80 37 | 38 | 39 | Go.net/http 127.0.0.1:80/ 40 | Java.URL 127.0.0.1:80/ 41 | NodeJS.http 127.0.0.1:80/ 42 | PHP.curl 127.0.0.1:80/ 43 | PHP.open 127.0.0.1:80/ 44 | Perl.LWP 127.0.0.1:80/ 45 | Python.httplib 127.0.0.1:80/ 46 | Python.requests 127.0.0.1:80/ 47 | Python.urllib 127.0.0.1:80/ 48 | Python.urllib2 127.0.0.1:80/ 49 | Ruby.Net/HTTP 127.0.0.1:80/ 50 | Ruby.open_uri 127.0.0.1:80/ 51 | ``` 52 | 53 | ### Fuzz 54 | ```bash 55 | $ fuzz.py 56 | ``` 57 | -------------------------------------------------------------------------------- /fuzz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: UTF-8 3 | 4 | import sys 5 | from itertools import product 6 | from multiprocessing.dummy import Pool as ThreadPool 7 | 8 | import util.fuzz as fuzz 9 | from util import cmd, pprint, execute 10 | from const import PARSERS, REQUESTERS 11 | 12 | DEBUG = 'debug' in sys.argv 13 | 14 | INS_COUNT = [0, 3, 0] 15 | WHITELIST = ['127.0.0.1', '127.1.1.1', '127.2.2.2', '127.1.1.1\n127.2.2.2'] 16 | CHARSETS = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0cA01\x00' 17 | FORMAT = 'http://%s127.1.1.1%s127.2.2.2%s' % ('%c'*INS_COUNT[0], '%c'*INS_COUNT[1], '%c'*INS_COUNT[2]) 18 | 19 | def _print(msg): 20 | sys.stdout.write("%s\n" % msg) 21 | 22 | def run_parser(url): 23 | res = {} 24 | for key, binary in PARSERS.iteritems(): 25 | lang, libname = key.split('.', 1) 26 | r = execute(lang, binary, url, base='bin/parser/') 27 | 28 | # parse host, change here to get the result you want 29 | if 'host=' in r and 'port=' in r: 30 | res[key] = r.split('host=')[-1].split(', ')[0] 31 | else: 32 | res[key] = 'err' 33 | 34 | return res 35 | 36 | def run_requester(url): 37 | res = {} 38 | for key, binary in REQUESTERS.iteritems(): 39 | lang, libname = key.split('.', 1) 40 | 41 | r = execute(lang, binary, url, base='bin/requester/') 42 | res[key] = r 43 | return res 44 | 45 | def run(random_data): 46 | url = FORMAT % random_data 47 | url = url + '/' + (''.join(random_data).encode('hex')) 48 | url = url.replace('\x00', '%00') 49 | 50 | urls = run_parser(url) 51 | gets = run_requester(url) 52 | 53 | total_urls = {} 54 | for k,v in urls.iteritems(): 55 | 56 | # filter 57 | if v not in WHITELIST: 58 | continue 59 | 60 | if total_urls.get(v): 61 | total_urls[v].append(k) 62 | else: 63 | total_urls[v] = [k] 64 | 65 | if len(total_urls) > 1: 66 | msg = 'parsed url = %s\n' % url 67 | for k, v in sorted(total_urls.iteritems(), key=lambda x: len(x[1]), reverse=True): 68 | msg += '%-16s %d = %s\n'%(k, len(v), repr(v)) 69 | 70 | _print(msg) 71 | 72 | 73 | total_gets = {} 74 | for k, v in gets.iteritems(): 75 | v = v.split('/')[0] 76 | 77 | # filter 78 | if v == 'err': 79 | continue 80 | 81 | if total_gets.get(v): 82 | total_gets[v].append(k) 83 | else: 84 | total_gets[v] = [k] 85 | 86 | if len(total_gets) > 1: 87 | msg = 'got url = %s\n'%url 88 | for k, v in sorted(total_gets.iteritems(), key=lambda x: len(x[1]), reverse=True): 89 | msg += '%-24s %d = %s\n'%(k, len(v), repr(v)) 90 | _print(msg) 91 | 92 | 93 | data_set = product(list(CHARSETS), repeat=sum(INS_COUNT)) 94 | if DEBUG: 95 | for i in data_set: run(i) 96 | else: 97 | pool = ThreadPool( 32 ) 98 | result = pool.map_async( run, data_set ).get(0xfffff) 99 | --------------------------------------------------------------------------------