Server-Side Request Forgery (SSRF) vulnerabilities occur when an attacker can coerce the server to fetch remote resources using HTTP requests; this might allow an attacker to identify and enumerate services running on the local network of the web server, which an external attacker would generally be unable to access due to a firewall blocking access.
Confirming SSRF
Let us consider the following vulnerable web application to illustrate how a developer might address SSRF vulnerabilities.
Code Review - Identifying the Vulnerability
Our sample web application allows us to take screenshots of websites for which we provide URLs.
The web application contains two endpoints. The first one handles taking screenshots, while the second endpoint responds with a debug page and is only accessible from localhost:
Since our target is to obtain unauthorized access to the debug page, we need to bypass the check in the /debug endpoint. However, we cannot manipulate the request.remote_addr variable, as this represents the IP address from which the request originates (i.e., our external IP address).
Screenshot Function
The web application performs basic checks including the scheme (blocking file://), but does not restrict the domain or IP address.
Exploitation
Since the web application only restricts us to the http and https schemes but does not restrict the domain or IP address, we can provide a URL pointing to the /debug endpoint:
The web application will visit its own debug endpoint such that the request originates from 127.0.0.1, granting access.
SSRF Basic Filter Bypasses
1. Obfuscation of localhost
The simplest SSRF filter explicitly blocks certain domains like localhost or 127.0.0.1:
Bypass Methods - Many ways exist to represent localhost:
Method
Value
Localhost Address Block
127.0.0.0 - 127.255.255.255
Shortened IP Address
127.1
Prolonged IP Address
127.000000000000000.1
All Zeroes
0.0.0.0
Shortened All Zeroes
0
Decimal Representation
2130706433
Octal Representation
0177.0000.0000.0001
Hex Representation
0x7f000001
IPv6 loopback address
0:0:0:0:0:0:0:1 (also ::1)
IPv4-mapped IPv6 loopback
::ffff:127.0.0.1
Example bypass:
2. Bypass via DNS Resolution
Improved filter that blocks private IP ranges:
Problem: The filter only blocks IP addresses, not domain names that resolve to private IPs.
Bypass: Use a domain that resolves to 127.0.0.1:
Example bypass:
3. Bypass via HTTP Redirect
Further improved filter that resolves domain names:
Problem: The filter does not account for HTTP redirects.
Bypass: Host a redirect on your server:
Then provide your server URL:
Note: Blocking redirects completely is difficult. Other redirect methods exist: JavaScript, meta tags, etc. Even if all redirects are prevented, DNS rebinding can still bypass the filter.
Prevention
The simplest and safest way to prevent SSRF is via firewall rules. The system running the vulnerable application should be separated from internal web applications, with firewall rules preventing incoming connections from the vulnerable system to internal services.
Question Walkthrough
Task: Bypass the SSRF filter to obtain the flag. The staging environment is behind a firewall that blocks all outgoing web requests.
Code Analysis
Download and analyze the source:
The /flag endpoint only allows localhost:
The check_domain function:
Vulnerability
The is_loopback function does not consider 0.0.0.0 as a loopback address (which includes all IPv4 addresses on a local machine, including 127.0.0.1).
def take_screenshot(url, filename=f'./screen_{os.urandom(8).hex()}.png'):
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
driver.save_screenshot(filename)
driver.quit()
return filename
def screenshot_url(url):
scheme = urlparse(url).scheme
domain = urlparse(url).hostname
if not domain or not scheme:
raise Exception('Malformed URL')
if scheme not in ['http', 'https']:
raise Exception('Invalid scheme')
return take_screenshot(url)
http://127.0.0.1/debug
def check_domain(domain):
if 'localhost' in domain:
return False
if domain == '127.0.0.1':
return False
return True
http://0.0.0.0/debug
def check_domain(domain):
if 'localhost' in domain:
return False
try:
# parse IP
ip = ipaddress.ip_address(domain)
# check internal IP address space
if ip in ipaddress.ip_network('127.0.0.0/8'):
return False
if ip in ipaddress.ip_network('10.0.0.0/8'):
return False
if ip in ipaddress.ip_network('172.16.0.0/12'):
return False
if ip in ipaddress.ip_network('192.168.0.0/16'):
return False
if ip in ipaddress.ip_network('0.0.0.0/8'):
return False
except:
pass
return True
def check_domain(domain):
try:
# resolve domain
ip = socket.gethostbyname(domain)
# parse IP
ip = ipaddress.ip_address(ip)
# check internal IP address space
if ip in ipaddress.ip_network('127.0.0.0/8'):
return False
if ip in ipaddress.ip_network('10.0.0.0/8'):
return False
if ip in ipaddress.ip_network('172.16.0.0/12'):
return False
if ip in ipaddress.ip_network('192.168.0.0/16'):
return False
if ip in ipaddress.ip_network('0.0.0.0/8'):
return False
return True
except:
pass
return False