NexusPHP vulnerable to persistent XSS and SQL Injection

NexusPHP is an open-sourced BitTorrent private tracker script written in PHP. It forks from the TBSource project and some other open-sourced projects. The project is widely used in the network, especially in educational websites to share resources.

persistent XSS

A persistent XSS was found when you comment on an article or send a message to someone. You can insert a url like javascript:javascript codes, and if someone clicks the url, you can get his/her cookie.

Actually NexusPHP has a function to manage with contents to output. The function looks like below:

function format_comment($text, $strip_html = true, $xssclean = false, $newtab = false, $imageresizer = true, $image_max_width = 700, $enableimage = true, $enableflash = true , $imagenum = -1, $image_max_height = 0, $adid = 0)
{
    global $lang_functions;
    global $CURUSER, $SITENAME, $BASEURL, $enableattach_attachment;
    global $tempCode, $tempCodeCount;
    $tempCode = array();
    $tempCodeCount = 0;
    $imageresizer = $imageresizer ? 1 : 0;
    $s = $text;

    if ($strip_html) {
        $s = htmlspecialchars($s);
    }
    // Linebreaks
    $s = nl2br($s);

    if (strpos($s,"[code]") !== false && strpos($s,"[/code]") !== false) {
        $s = preg_replace("/\[code\](.+?)\[\/code\]/eis","formatCode('\\1')", $s);
    }

    $originalBbTagArray = array('[siteurl]', '[site]','[*]', '[b]', '[/b]', '[i]', '[/i]', '[u]', '[/u]', '[pre]', '[/pre]', '[/color]', '[/font]', '[/size]', "  ");
    $replaceXhtmlTagArray = array(get_protocol_prefix().$BASEURL, $SITENAME, '<img class="listicon listitem" src="pic/trans.gif" alt="list" />', '<b>', '</b>', '<i>', '</i>', '<u>', '</u>', '<pre>', '</pre>', '</span>', '</font>', '</font>', ' &nbsp;');
    $s = str_replace($originalBbTagArray, $replaceXhtmlTagArray, $s);

    $originalBbTagArray = array("/\[font=([^\[\(&\\;]+?)\]/is", "/\[color=([#0-9a-z]{1,15})\]/is", "/\[color=([a-z]+)\]/is", "/\[size=([1-7])\]/is");
    $replaceXhtmlTagArray = array("<font face=\"\\1\">", "<span style=\"color: \\1;\">", "<span style=\"color: \\1;\">", "<font size=\"\\1\">");
    $s = preg_replace($originalBbTagArray, $replaceXhtmlTagArray, $s);

    if ($enableattach_attachment == 'yes' && $imagenum != 1){
        $limit = 20;
        $s = preg_replace("/\[attach\]([0-9a-zA-z][0-9a-zA-z]*)\[\/attach\]/ies", "print_attachment('\\1', ".($enableimage ? 1 : 0).", ".($imageresizer ? 1 : 0).")", $s, $limit);
    }

    if ($enableimage) {
        $s = preg_replace("/\[img\]([^\<\r\n\"']+?)\[\/img\]/ei", "formatImg('\\1',".$imageresizer.",".$image_max_width.",".$image_max_height.")", $s, $imagenum, $imgReplaceCount);
        $s = preg_replace("/\[img=([^\<\r\n\"']+?)\]/ei", "formatImg('\\1',".$imageresizer.",".$image_max_width.",".$image_max_height.")", $s, ($imagenum != -1 ? max($imagenum-$imgReplaceCount, 0) : -1));
    } else {
        $s = preg_replace("/\[img\]([^\<\r\n\"']+?)\[\/img\]/i", '', $s, -1);
        $s = preg_replace("/\[img=([^\<\r\n\"']+?)\]/i", '', $s, -1);
    }

    // [flash,500,400]http://www/image.swf[/flash]
    if (strpos($s,"[flash") !== false) { //flash is not often used. Better check if it exist before hand
        if ($enableflash) {
            $s = preg_replace("/\[flash(\,([1-9][0-9]*)\,([1-9][0-9]*))?\]((http|ftp):\/\/[^\s'\"<>]+(\.(swf)))\[\/flash\]/ei", "formatFlash('\\4', '\\2', '\\3')", $s);
        } else {
            $s = preg_replace("/\[flash(\,([1-9][0-9]*)\,([1-9][0-9]*))?\]((http|ftp):\/\/[^\s'\"<>]+(\.(swf)))\[\/flash\]/i", '', $s);
        }
    }
    //[flv,320,240]http://www/a.flv[/flv]
    if (strpos($s,"[flv") !== false) { //flv is not often used. Better check if it exist before hand
        if ($enableflash) {
            $s = preg_replace("/\[flv(\,([1-9][0-9]*)\,([1-9][0-9]*))?\]((http|ftp):\/\/[^\s'\"<>]+(\.(flv)))\[\/flv\]/ei", "formatFlv('\\4', '\\2', '\\3')", $s);
        } else {
            $s = preg_replace("/\[flv(\,([1-9][0-9]*)\,([1-9][0-9]*))?\]((http|ftp):\/\/[^\s'\"<>]+(\.(flv)))\[\/flv\]/i", '', $s);
        }
    }

    // [url=http://www.example.com]Text[/url]
    if ($adid) {
        $s = preg_replace("/\[url=([^\[\s]+?)\](.+?)\[\/url\]/ei", "formatAdUrl(".$adid." ,'\\1', '\\2', ".($newtab==true ? 1 : 0).", 'faqlink')", $s);
    } else {
        $s = preg_replace("/\[url=([^\[\s]+?)\](.+?)\[\/url\]/ei", "formatUrl('\\1', ".($newtab==true ? 1 : 0).", '\\2', 'faqlink')", $s);
    }

    // [url]http://www.example.com[/url]
    $s = preg_replace("/\[url\]([^\[\s]+?)\[\/url\]/ei",
    "formatUrl('\\1', ".($newtab==true ? 1 : 0).", '', 'faqlink')", $s);

    $s = format_urls($s, $newtab);
    // Quotes
    if (strpos($s,"[quote") !== false && strpos($s,"[/quote]") !== false) { //format_quote is kind of slow. Better check if [quote] exists beforehand
        $s = format_quotes($s);
    }

    $s = preg_replace("/\[em([1-9][0-9]*)\]/ie", "(\\1 < 192 ? '<img src=\"pic/smilies/\\1.gif\" alt=\"[em\\1]\" />' : '[em\\1]')", $s);
    reset($tempCode);
    $j = 0;
    while(count($tempCode) || $j > 5) {
        foreach($tempCode as $key=>$code) {
            $s = str_replace("<tempCode_$key>", $code, $s, $count);
            if ($count) {
                unset($tempCode[$key]);
                $i = $i+$count;
            }
        }
        $j++;
    }
    return $s;
}

The code uses htmlspecialchars to manage with &, ", < and > which means we can not get out of the " and insert JavaScript code directly. However, it allows we insert url with the format like [url="user input"]user input[/url] and the user input would be treated as url without any check.

Once you get someone’s cookie you can obtain his/her account just by visiting the site with his/her cookie. Cookies of NexusPHP look like below.

c_secure_ssl=bm9wZQ%3D%3D; c_secure_uid=MQ%3D%3D; c_secure_pass=58ffa98fdb39aa21c9d510509b12e7b4; c_secure_tracker_ssl=bm9wZQ%3D%3D; c_secure_login=bm9wZQ%3D%3D

c_secure_uid is the uid of user. c_secure_pass is calculated by md5($row["passhash"]) which would not change until the user changes his/her password. So that we do not need to crack user’s password and could control the account directly until the user change his/her password.

SQL Injection

If the user is in the $forummanage_class, he would be able to manage the forum through the forummanage.php page. There is some codes like below which would cause SQL injection.

//EDIT FORUM ACTION
elseif ($_POST['action'] == "editforum") {
    $name = $_POST['name'];
    $desc = $_POST['desc'];
    $id = $_POST['id'];
    if (!$name && !$desc && !$id) {
        header("Location: " . get_protocol_prefix() . "$BASEURL/forummanage.php");
        die();
    }
    if ($_POST["moderator"]){
    $moderator = $_POST["moderator"];
    set_forum_moderators($moderator,$id);
    }
    else{
        sql_query("DELETE FROM forummods WHERE forumid=".sqlesc($id)) or sqlerr(__FILE__, __LINE__);
    }
    sql_query("UPDATE forums SET sort = '" . $_POST['sort'] . "', name = " . sqlesc($_POST['name']). ", description = " . sqlesc($_POST['desc']). ", forid = ".sqlesc(($_POST['overforums'])).", minclassread = '" . $_POST['readclass'] . "', minclasswrite = '" . $_POST['writeclass'] . "', minclasscreate = '" . $_POST['createclass'] . "' where id = ".sqlesc($id)) or sqlerr(__FILE__, __LINE__);
    $Cache->delete_value('forums_list');
    $Cache->delete_value('forum_moderator_array');
    header("Location: forummanage.php");
    die();
}

The code using sqlesc to prevent SQL injection and it is quite useful. But when the action is editforum, some post datas are put into the sql query directly. The post data like below could be used.

name=rrrfrrf&desc=frfrfr&overforums=1&moderator=s0m3&readclass=0&writeclass=0&createclass=0 or updatexml(0,concat(0x7e,(select username from users where id=1)),0) or''&sort=0&action=addforum&Submit=Make+Forum

However, it requires the user be a forum manager! How could we be a manager? Thanks to the XSS, we could be anyone!