我不生产代码
我只是代码的搬运工

PHP 框架Yii2 CSRF Bug

由于现在做的项目是以框架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,十分方便。

1534435055536676.png

以下为我自己手动生成 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 :

1534435816257741.png

当填写的 csrf 错误时,结果为:

1534435923628421.png

当 csrf 填写正确时(把接收到的 post数据与解密后的 $token 打印出来了):

1534436291749007.png

到这里,已证明 Yii 的 csrf 可以手动生成。如果一些网站使用了 Yii,在登录或注册时如果没有添加验证码或其他方面的验证,只有 Yii 默认实现的 csrf,可能会造成撞库或生成大量垃圾数据。

注:

  1. 以上所有过程是在使用 Yii 默认加密组件 Security 基础上实现的。至于从 cookie 中拿到 _csrf 后截取64个字符是因为 hashData 方法默认使用了 sha256 算法,此算法生成的 hash 值长度为64个字符,至于使用其他 hash 算法(可以在config/web.php 中配置 Security 组件 macHash 项以更新默认使用的 hash 算法),可能截取的长度会有变化。总之,一共就那么几种 hash 算法,一种一种试也可以试出来,最差也可以手动去截取 _csrf 字符串以让 php 能反反序列化即可。

  2. 在通过 postman 验证手动生成的 csrf 是否能够通过 Yii 验证时,在自己的电脑上测试是可以通过的,以上截图是通过是在自己的电脑上做的截图,但是在公司电脑上不能验证通过。自己电脑服务器装的是 nginx,公司电脑装的是 apahce,不知道是否与 web server 有关,这个有待研究。

本文章为本站原创,如转载请注明文章出处:https://www.sviping.com/archives/37

分享到:
上一篇: Let's Encrypt 泛域名证书申请与安装 下一篇: HTTP 响应状态码
12