审计迅睿cms,发现了两个XSS,漏洞已提交补天,分享一下
迅睿CMS是采用PHP7技术全新开发的产品,程序只能运行在PHP7的环境之上
迅睿CMS在原有FineCMS的基础上进行优化结构和吸取编程经验,采用国外CodeIgniter4框架。
本地起phpstudy安装迅睿cms
后台路径:/admin.php
后台密码:admin admin
后台开启注册
进入注册界面,构造如图所示数据
账号:" onclick="alert(1)"
密码:test
登录后台查看用户
可以看到有这个用户,当点击时触发代码
查看源代码
查看数据库,已经插入到了数据库中
查看注册处代码(碍于篇幅仅放了关键代码)
......
$post = \Phpcmf\Service::L('input')->post('data', true);
...... # 进行格式之类的验证
# 表单验证
list($data, $return, $attach) = \Phpcmf\Service::L('Form')->validation($post, null, $field);
......
$rt = \Phpcmf\Service::M('member')->register($groupid, [
'username' => (string)$post['username'],
'phone' => (string)$post['phone'],
'email' => (string)$post['email'],
'password' => dr_safe_password($post['password']),
], $data[1]);
if ($rt['code']) {
// 注册成功
$this->member = $rt['data'];
$remember = 0;
// 保存本地会话
\Phpcmf\Service::M('member')->save_cookie($this->member, $remember);
// 附件归档
SYS_ATTACHMENT_DB && $attach && \Phpcmf\Service::M('Attachment')->handle(
$this->member['id'],
\Phpcmf\Service::M()->dbprefix('member').'-'.$rt['code'],
$attach
);
// 手机认证成功
if ($this->member_cache['register']['sms']) {
\Phpcmf\Service::M()->db->table('member_data')->where('id', $this->member['id'])->update(['is_mobile' => 1]);
}
$this->_json(1, 'ok', [
'url' => urldecode(\Phpcmf\Service::L('input')->xss_clean($_POST['back'] ? $_POST['back'] : MEMBER_URL)),
'sso' => \Phpcmf\Service::M('member')->sso($this->member, $remember),
'member' => $this->member,
]);
} else {
$this->_json(0, $rt['msg'], ['field' => $rt['data']['field']]);
}
``
跟踪post方法
// post解析
public function post($name, $xss = true) {
$value = isset($_POST[$name]) ? $_POST[$name] : false;
return $xss ? $this->xss_clean($value) : $value;
}
传入的data数据,其实就是获取的username,password和password2
一路跟踪xss_clean函数(代码很长)
public function xss_clean($str, $is_image = FALSE)
{
if (is_numeric($str)) {
return $str;
} elseif (!$str) {
return '';
}
// Is the string an array?
if (is_array($str))
{
foreach ($str as $key => &$value)
{
$str[$key] = $this->xss_clean($value);
}
return $str;
}
// Remove Invisible Characters
$str = remove_invisible_characters($str);
/*
* URL Decode
*
* Just in case stuff like this is submitted:
*
* <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
*
* Note: Use rawurldecode() so it does not remove plus signs
*/
if (stripos($str, '%') !== false)
{
do
{
$oldstr = $str;
$str = rawurldecode($str);
$str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str);
}
while ($oldstr !== $str);
unset($oldstr);
}
/*
* Convert character entities to ASCII
*
* This permits our tests below to work reliably.
* We only convert entities that are within tags since
* these are the ones that will pose security problems.
*/
$str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
$str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);
// Remove Invisible Characters Again!
$str = remove_invisible_characters($str);
/*
* Convert all tabs to spaces
*
* This prevents strings like this: ja vascript
* NOTE: we deal with spaces between characters later.
* NOTE: preg_replace was found to be amazingly slow here on
* large blocks of data, so we use str_replace.
*/
$str = str_replace("\t", ' ', $str);
// Capture converted string for later comparison
$converted_string = $str;
// Remove Strings that are never allowed
$str = $this->_do_never_allowed($str);
/*
* Makes PHP tags safe
*
* Note: XML tags are inadvertently replaced too:
*
* <?xml
*
* But it doesn't seem to pose a problem.
*/
if ($is_image === TRUE)
{
// Images have a tendency to have the PHP short opening and
// closing tags every so often so we skip those and only
// do the long opening tags.
$str = preg_replace('/<\?(php)/i', '<?\\1', $str);
}
else
{
$str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str);
}
/*
* Compact any exploded words
*
* This corrects words like: j a v a s c r i p t
* These words are compacted back to their correct state.
*/
$words = array(
'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
'vbs', 'script', 'base64', 'applet', 'alert', 'document',
'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
);
foreach ($words as $word)
{
$word = implode('\s*', str_split($word)).'\s*';
// We only want to do this when it is followed by a non-word character
// That way valid stuff like "dealer to" does not become "dealerto"
$str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
}
/*
* Remove disallowed Javascript in links or img tags
* We used to do some version comparisons and use of stripos(),
* but it is dog slow compared to these simplified non-capturing
* preg_match(), especially if the pattern exists in the string
*
* Note: It was reported that not only space characters, but all in
* the following pattern can be parsed as separators between a tag name
* and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
* ... however, remove_invisible_characters() above already strips the
* hex-encoded ones, so we'll skip them below.
*/
do
{
$original = $str;
if (preg_match('/<a/i', $str))
{
$str = preg_replace_callback('#<a(?:rea)?[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
}
if (preg_match('/<img/i', $str))
{
$str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
}
if (preg_match('/script|xss/i', $str))
{
$str = preg_replace('#</*(?:script|xss).*?>#si', '[removed]', $str);
}
}
while ($original !== $str);
unset($original);
/*
* Sanitize naughty HTML elements
*
* If a tag containing any of the words in the list
* below is found, the tag gets converted to entities.
*
* So this: <blink>
* Becomes: <blink>
*/
$pattern = '#'
.'<((?<slash>/*\s*)((?<tagName>[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character
.'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator
// optional attributes
.'(?<attributes>(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons
.'[^\s\042\047>/=]+' // attribute characters
// optional attribute-value
.'(?:\s*=' // attribute-value separator
.'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value
.')?' // end optional attribute-value group
.')*)' // end optional attributes group
.'[^>]*)(?<closeTag>\>)?#isS';
// Note: It would be nice to optimize this for speed, BUT
// only matching the naughty elements here results in
// false positives and in turn - vulnerabilities!
do
{
$old_str = $str;
$str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str);
}
while ($old_str !== $str);
unset($old_str);
/*
* Sanitize naughty scripting elements
*
* Similar to above, only instead of looking for
* tags it looks for PHP and JavaScript commands
* that are disallowed. Rather than removing the
* code, it simply converts the parenthesis to entities
* rendering the code un-executable.
*
* For example: eval('some code')
* Becomes: eval('some code')
*/
$str = preg_replace(
'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
'\\1\\2(\\3)',
$str
);
// Same thing, but for "tag functions" (e.g. eval`some code`)
// See https://github.com/bcit-ci/CodeIgniter/issues/5420
$str = preg_replace(
'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si',
'\\1\\2`\\3`',
$str
);
// Final clean up
// This adds a bit of extra precaution in case
// something got through the above filters
$str = $this->_do_never_allowed($str);
/*
* Images are Handled in a Special Way
* - Essentially, we want to know that after all of the character
* conversion is done whether any unwanted, likely XSS, code was found.
* If not, we return TRUE, as the image is clean.
* However, if the string post-conversion does not matched the
* string post-removal of XSS, then it fails, as there was unwanted XSS
* code found and removed/changed during processing.
*/
if ($is_image === TRUE)
{
return ($str === $converted_string);
}
return $str;
}
,过滤了/%0[0-8bcef]/
,'/%1[0-9a-f]/'
,'/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'
,对特殊字符进行转义,将\t
过滤为空,过滤了<a
,<img
,<script|xss
标签,神奇的是下面这串代码
function _compact_exploded_words($matches)
{
return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
}
$words = array(
'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
'vbs', 'script', 'base64', 'applet', 'alert', 'document',
'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
);
foreach ($words as $word)
{
$word = implode('\s*', str_split($word)).'\s*';
$str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', '_compact_exploded_words', $str);
}
乍一看像是过滤了XSS的关键字,事实上它只是过滤了关键字中的空白符,类似于这种a l e r t(1)
,会被过滤为alert(1)
,但对关键字并没有任何过滤
下面才是针对关键字做的过滤,理论上是配合上面的空白符删除配合使用的
#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si
匪夷所思的是代码要求关键字后必须跟两个反引号,即alert`1`
会被过滤,但普通的alert(1)
反而只是进行了html实体转换,而onclick中的代码是被当做JavaScript代码执行的,即使符号被转义也可以执行
继续往下
下面的验证字段参数为除了$post
变量外,其他为空值,相当于没有验证,略过
继续向下
跟踪register方法
......
$member['name'] = !$member['name'] ? '' : dr_strcut($member['name'], intval(\Phpcmf\Service::C()->member_cache['register']['cutname']), '');
$member['salt'] = substr(md5(rand(0, 999)), 0, 10); // 随机10位密码加密码
$member['password'] = $member['password'] ? md5(md5($member['password']).$member['salt'].md5($member['password'])) : '';
$member['money'] = 0;
$member['freeze'] = 0; $member['spend'] = 0;
$member['score'] = 0;
$member['experience'] = 0; $member['regip'] = (string)\Phpcmf\Service::L('input')->ip_address();
$member['regtime'] = SYS_TIME;
$member['randcode'] = rand(100000, 999999);
!$member['username'] && $member['username'] = '';
$rt = $this->table('member')->insert($member);
......
由于根本不存在name字段,因此直接将数据插入到表中,插入途中也没有任何过滤
可以直接闭合双引号插入XSS代码
需要注意的是该账号只能登录一次,即退出后再次登录会提示该用户不存在,应该是因为闭合了某个标签,具体原因并未深入分析
除了注册有一个,在登录时还有一个存储型XSS,不过它并不能在后台中起作用,只能在用户的个人界面,因此不再分析,只贴出来供各位表哥了解一下
注册一个test用户
登录时抓包并将user-agent头修改为如图所示代码
在账号管理
->登陆记录
中即可触发代码
以下为效果图
但是这个洞其实并没有实际意义2333,因为在后台,它是这样的
根本没有标签,不过可能会有别的利用方式,,如果有所发现再来更新文章