由于现在做的项目是以框架Yii2为主,所以在没事的时候决定看一下Yii源代码,看看底层的实现过程。当看到input表单在做csrf验证的时候,发现Yii在实现csrf验证的时候存在一个bug,我们可以手动生成 csrf 然后通过 post 提交,可以通过 Yii 的 csrf 验证成功提交。
Yii在生成 csrf 时,首先调用 Security 组件的方法 generateRandomString 生成一个随籍字符串(默认长度为32位)token,然后调用如下方法生成csrf:
public function maskToken($token) { // The number of bytes in a mask is always equal to the number of bytes in a token. $mask = $this->generateRandomKey(StringHelper::byteLength($token)); return StringHelper::base64UrlEncode($mask . ($mask ^ $token)); }
其中参数 $token 为 Security 组件中 generateRandomString 方法生成的 $token,从 maskToken 方法可以看出,生成的 csrf 只与原始的字符串 $token 有关系,那么我们如果能拿到 $token 不就可以手动生成 csrf 了吗? 所以要想手动生成 csrf,我们需要拿到 $token。
我们再看 cookie 中保存的 _csrf,在 cookie 中保存的 _csrf 并没有加密,是通过下面方法生成的:
public function hashData($data, $key, $rawHash = false) { $hash = hash_hmac($this->macHash, $data, $key, $rawHash); if (!$hash) { throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash); } return $hash . $data; }
参数 $data 为一个字符串,通过在 web\response 组件中 sendCookies 方法中看到 $data的具体内容,是 cookie 对应的键与值组成的数组序列化以后生成的字符串,如下:
protected function sendCookies() { if ($this->_cookies === null) { return; } $request = Yii::$app->getRequest(); if ($request->enableCookieValidation) { if ($request->cookieValidationKey == '') { throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); } $validationKey = $request->cookieValidationKey; } foreach ($this->getCookies() as $cookie) { $value = $cookie->value; if ($cookie->expire != 1 && isset($validationKey)) { $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); } setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); } }
参数 $key 为在 config/web.php 中 Request 组件中配置的 cookieValidationKey。所以看到这里,我们就知道可以从cookie中拿到 $token 了,也就是说可以手动生成 csrf 了。
这里推荐一款 chrome 插件 editthiscookie,可以很容易看到浏览器的 cookie 并且可以清除、修改 cokkie,十分方便。
以下为我自己手动生成 csrf 的代码:
/** * 方法 byteSubstr、maskToken、base64UrlEncode、base64UrlDecode、byteLength、 * byteSubstr 都是从 Yii 框架中 Security 组件中复制的 * @var string */ // 从 cookie 中复制的cookie $str = 'ddd3c581a357494049eee3898b23c029f517d02dc29428332933416f227c5ef0a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22fa0Oo51Z0GpOC5zhdbejkLuVVKwWsird%22%3B%7D'; $csrf = urldecode($str)."<br><br>"; echo "cookie 中_csrf: ".$csrf."<br>"; // 默认使用的hash算法为 sha256,sha256算法生成的hash长度为64,所以截取64位之后的字符串序列化后的字符串 // $hash . serialize([$name, $value]); $csrf = byteSubstr($csrf, 64); echo "cookie 中截取后的_csrf: ".$csrf."<br>"; $arr = unserialize($csrf); print_r($arr); echo '<br>'; $token = $arr[1]; $newCsrf = maskToken($token); echo $newCsrf; function maskToken($token){ $mask = random_bytes(byteLength($token)); return base64UrlEncode($mask . ($mask ^ $token)); } function base64UrlEncode($input){ return strtr(base64_encode($input), '+/', '-_'); } function base64UrlDecode($input){ return base64_decode(strtr($input, '-_', '+/')); } function byteLength($string){ return mb_strlen($string, '8bit'); } function byteSubstr($string, $start, $length = null){ return mb_substr($string, $start, $length === null ? mb_strlen($string, '8bit') : $length, '8bit'); }
通过上面的代码生成 csrf 字符串 0sRlA3HYp_4tApepZt9N1GpMBUiAdj0AgfWn7uCwlsC0pVVMHu2WpB1F5-Yl6je8Di5gIus6SFbXvtC5k9nkpA==,然后通过 postman 向服务器发送 post 请求,发送送请求的时候,需要在 header 头中加上对应的 cookie ,可以验证通过,由此可证明 Yii 的 csrf 可以手动生成。
首先先在 cookie 中添加上 _csrf :
当填写的 csrf 错误时,结果为:
当 csrf 填写正确时(把接收到的 post数据与解密后的 $token 打印出来了):
到这里,已证明 Yii 的 csrf 可以手动生成。如果一些网站使用了 Yii,在登录或注册时如果没有添加验证码或其他方面的验证,只有 Yii 默认实现的 csrf,可能会造成撞库或生成大量垃圾数据。
注:
以上所有过程是在使用 Yii 默认加密组件 Security 基础上实现的。至于从 cookie 中拿到 _csrf 后截取64个字符是因为 hashData 方法默认使用了 sha256 算法,此算法生成的 hash 值长度为64个字符,至于使用其他 hash 算法(可以在config/web.php 中配置 Security 组件 macHash 项以更新默认使用的 hash 算法),可能截取的长度会有变化。总之,一共就那么几种 hash 算法,一种一种试也可以试出来,最差也可以手动去截取 _csrf 字符串以让 php 能反反序列化即可。
在通过 postman 验证手动生成的 csrf 是否能够通过 Yii 验证时,在自己的电脑上测试是可以通过的,以上截图是通过是在自己的电脑上做的截图,但是在公司电脑上不能验证通过。自己电脑服务器装的是 nginx,公司电脑装的是 apahce,不知道是否与 web server 有关,这个有待研究。