LEAKFREE-2013-001 / CVE-2013-4167

CmsMadeSimple admin/login.php stored XSS

[+] CVE: CVE-2013-4167
[+] LF-ID: LEAKFREE-2013-001
[+] CVSS: 7.0
[+] Vendor: CMS Made Simple
[+] Product: CMS Made Simple
[+] Versions affected: 1.10.0 up to 1.11.7

Vulnerability

CMS Made Simple versions 1.10.0 up to 1.11.7 suffer from a preauthenticated stored XSS vulnerability. This allows an attacker with access to admin/login.php to insert arbitrary text into the admin-logging interface.

The problem is in get_real_ip(); in lib/classes/class.cms_utils.php Probably the assumption is made that get_real_ip(); always returns an ip and thus doesn't need any sanitation.


public static function get_real_ip()
{
    $ip = null;
    if (!empty($_SERVER['HTTP_CLIENT_IP']))   //check ip from share internet
    {
        $ip=$_SERVER['HTTP_CLIENT_IP'];
    }
    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))   //to check ip is pass from proxy
    {
        $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
    }
    else
    {
        $ip=$_SERVER['REMOTE_ADDR'];
    }
    return $ip;
  }

However, $_SERVER['HTTP_CLIENT_IP'] and $_SERVER['HTTP_X_FORWARDED_FOR'] are user-supplied values, via the following http-headers respectively: "Client-ip:" and "X-Forwarded-For". These values are in no way guaranteed to be an IP-adress: they can be any arbitrary value.

In admin/login.php, on a failed login attempt, the admin is notified by inserting a row with the ip from get_real_ip() and showing it in the admin-logging interface.

        
    if (isset($_POST["username"]) && isset($_POST["password"])) {
        [...]

        if ($username != "" && $password != "" && isset($oneuser) && $oneuser == true 
        && isset($_POST["loginsubmit"]))
        {
            [...]
        }
        else if (isset($_POST['loginsubmit'])) { //No error if changing languages
            [...]

            Events::SendEvent('Core','LoginFailed',array('user'=>$_POST['username']));;
            // put mention into the admin log
            $ip_login_failed = cms_utils::get_real_ip(); 
            audit('', "Admin Username: ".$username.' (IP: '.$ip_login_failed.')', 'Login Failed');

            [...]
?>
        
    

our $ip_login_failed (retreived from cms_utils::get_real_ip() via Client-ip or X-Forwarded-For ) is passed to audit(); We have the following restriction: the item_name column is a varchar(50) mysql field, so the following string is taken off that count:

    
    "Admin Username:  (IP: ".$ip_login_failed.")"   /* assuming $username is empty*/
    /*   22 characters    *//*   OUR PAYLOAD *//* 1 character */
    
    

So 50 - 22 - 1 = 28. Because a too-long string will be truncated by mysql on INSERT, we can truncate the extra ")" Leaving us with 28 characters to inject unsanitized in the message.

    
    function audit($itemid, $itemname, $action)
    {
        [...]
        $query = "INSERT INTO ".cms_db_prefix()."adminlog (timestamp, user_id, username, item_id, item_name, action, ip_addr) VALUES (?,?,?,?,?,?,?)";
        $db->Execute($query,array(time(),$userid,$username,$itemid,$itemname,$action,$ip_addr));
    }
    
    

So we can inject a message that shows up in the admin logging with 28 characters of unsanitized input.

Proof of Concept

download exploit

perform a POST-request to /admin.login.php with the following parameters as post-body:


        username=&password=&loginsubmit=Submit
    

and the following headers:


        Client-ip: RAWPAYLOAD
    

OR:


        X-Forwarded-For: RAWPAYLOAD
    

Mitigation

Upgrade to CmsMadeSimple version 1.11.7 [1] or higher

References

1. http://www.cmsmadesimple.org/downloads/