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


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
    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))   //to check ip is pass from proxy
    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

            // 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 (?,?,?,?,?,?,?)";

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:


and the following headers:

        Client-ip: RAWPAYLOAD


        X-Forwarded-For: RAWPAYLOAD


Upgrade to CmsMadeSimple version 1.11.7 [1] or higher


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