首页 > PHP > 论discuz中使用formhash的真实目的

论discuz中使用formhash的真实目的

在discuz的各个版本中总能看到formhash的身影,号称是能够防止灌水机从外部提交表单,且看看几个版本中的formhash生成。

DiscuzX2.5:/source/function/function_core.php

function formhash($specialadd = '') {
    global $_G;
    $hashadd = defined('IN_ADMINCP') ? 'Only For Discuz! Admin Control Panel' : '';
    return substr(md5(substr($_G['timestamp'], 0, -7).$_G['username'].$_G['uid'].$_G['authkey'].$hashadd.$specialadd), 8, 8);
}

首先,substr($_SGLOBAL['timestamp'], 0, -7),截取时间戳前3位。(是保证formhash在一定的时间里生效且不变,截取前3位,大概是115天。其实可以更短点),然后跟用户UID、md5后的sitekey等连接得出字符串,然后再md5,并截取字符串的8位。由于我们的sitekey是唯一的,再加上uid,而且都是MD5的,别人破解的机会几乎是0(不排除MD5以后会被完全破解)。别人无法仿造FORMHASH,就无法远程提交数据了。

115天是这么算的:10个整型长度的时间戳去掉后面7位,有效时间介于1-9999999秒之间,也就是说115.74天。

Discuz! X3.2的升级后的代码:/uc_server/model/base.php

function formhash() {
	return substr(md5(substr($this->time, 0, -4).UC_KEY), 16);
}

然后在/source/class/discuz/discuz_application.php的function _xss_check()函数中找到一处调用:

if(isset($_GET['formhash']) && $_GET['formhash'] !== formhash()) {
	system_error('request_tainting');
}

于是,我将发帖时的验证码去掉之后:

  1. 尝试使用手动构造http header数据模拟cookie,referer和form表单;

  2. 使用http请求库抓取发帖空白页面,使用字符串函数提取formhash值,然后附加到form表单中;

  3. 发送http请求给discuz发帖提交的脚本。

不到10分钟成功实现灌水机功能,discuz设计formhash的初衷并非是为了防止灌水机提交,为何我们仍然在使用这种并不安全的formhash?答案是防御CSRF攻击——防止外站提交

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

简单一句话CSRF就是冒充用户之手,比如说A站点有一个发帖的功能(假如是GET方式发帖):

http://exampleA.com/bbs/create_post.php?title=标题&content=内容

A站点在create_post.php程序是通过cookie检查用户的登陆状态和发帖权限,但是并未检查请求来路referer;

用户之前登陆过A站点,cookie有效期非常长(通过cookie保存用户的登陆状态在国内非常常见,并且时间非常长),然后某一天用户访问了B站点,B站点存在XSS漏洞——跨站脚本(Cross-site scripting),被别有用心的人注入了乱七八糟的可执行js脚本。加入B站某个页面刷新后自动执行:

<script>
location.href="http://exampleA.com/bbs/create_post.php?title=标题&content=内容";
</script>

之后用户便会跳转到A站自动发帖,当然现在没有哪个论坛傻到使用GET方式提交发帖表单,一般使用referer校验请求来源。

可是如果A站和B站是2个子域名,具有相同的根域名,并在页面中都申明了document.domain = 'example.com';为了方便A站和B站的页面嵌套(在国内也有很多网站是这么做的, 主要是为了嵌入评论框什么的)

A站域名为:a.example.com

B站域名为:b.example.com

如果B站被XSS了,攻击者使用js构造了这么一个表单:

$comment = "<script>
	var formhtml = '<form method=\"POST\" id=\"transfer\" action=\"http://a.example.com/create_post.php\">\
	        <input type=\"hidden\" name=\"toBankId\" value=\"11\">\
	        <input type=\"hidden\" name=\"money\" value=\"1000\">\
	      </form>';
	document.write(formhtml);
	document.getElementById(\"transfer\").submit();
</script>";
echo $comment;

然后用户访问这个页面之后会自动提交表单到a.example.com,在a.example.com中使用referer来检测来源,将会得到b.example.com的网址,此时是否屏蔽来自b.example.com呢?

很多情况下我们是允许该referer的,并且a.example.com和b.example.com的用户登录的cookie都会通过example.com共享,此时B站的XSS也会威胁到A站的功能。

首先,我们知道互联网充斥了大量XSS漏洞,尤其是Discuz这样的通用建站系统,无论经过多少个版本的迭代都会存在各种各样的XSS漏洞,详情可参见乌云漏洞平台。门户网站通常会使用子域名直接安装Discuz等系统,如果爆出XSS漏洞,其他子站点跟着遭殃。

此时,使用discuz的formhash校验即可。

  1. 首先在A站的发帖的界面生成一个formhash,该formhash是通过UC_KEY(UCenter的唯一密钥)与时间戳生成的,B页面的攻击者再高明也无法获取到这个formhash。

  2. 然后A站在后端处理表单的时候通过检测formhash值是不是本站页面生成,如果不是则拒绝提交,这样就能有效阻止外站使用js构造表单提交。

也许你会说如果b.example.com可能也存在formhash,攻击者通过js获取B站的formhash就可以构造合法的form表单提交了,建议将A站和B站的formhash的UC_KEY设置不同,但是在A站仍然接受B站referer请求。


另外,现在越来越多的站点开始使用ajax代替form表单,如果整个页面是纯静态的html,formhash无法动态生成,于是使用ajax从A站的某个接口获取一个合法的formhash然后附加到form中提交,这无疑是你锁上了大门,却又把钥匙放在门口。这时建议将获取formhash的接口严格校验referer并使用POST方式请求,例如:

<?php
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
if(strpos($referer, 'http://a.example.com/') === 0)
{
    if($_POST['act'] == 'formhash')
    {
        echo formhash();
    }else{
        echo 'invalid action';
    }
}else{
    echo 'invalid referer';
}

A站在ajax发送form数据之前请求自己的接口获取合法的formhash,然后提交表单。

B站一般情况下无法获取A站的formhash。

不过formhash还是会存在漏洞的,例如B站通过纯js构造一个iframe页面指向含有formhash的A站动态页面,然后提取出formhash的值,然后构造表单,太复杂了不说了。


总体来说,formhash对于小站来说CSRF防御上还是比较OK的,因为小站就自己来玩玩足以,大一点的站点还是建议使用验证码来防御,好的验证码不仅用户体验好,而且破解难度极大,推荐使用功能。


本人封装了一个简单的formhash类,和discuz类似,都是没有使用SESSION或COOKIE介质存储,直接使用算法生成的,不同之处在于过期时间上重新做了优化。

项目地址:


文章网址:http://blog.zhengshuiguang.com/php/formhash.html

随意转载^^但请附上教程地址。

标签:formhash

仅有一条评论

  1. That alone wwas an egregious oversight on thheir own part, since ddekkedecdedgbga

评论已关闭