不安全的归档解压:从原理到检测与防御实战

本文深入探讨了在多种编程语言中因归档文件解压处理不当而导致路径遍历漏洞的机制,提供了Python、Ruby等语言的漏洞代码示例,并分享了如何使用Semgrep编写规则进行自动化检测以及有效的修复方案。

不安全的归档解压:从原理到检测与防御实战

引言 在我最近于Doyensec实习期间,我有机会研究了不同编程语言中的解压缩攻击。由于归档文件格式在软件开发中被广泛使用,开发人员理解处理这些文件时潜在的安全风险至关重要。

我的研究目标是识别、分析和检测在几种用于Web和应用程序开发的流行编程语言(包括Python、Ruby、Swift、Java、PHP和JavaScript)中的易受攻击的实现。这些语言拥有用于归档解压缩的库,如果使用不当,可能会导致漏洞。

为了演示不安全解压的风险,我为每种方法和每种语言创建了具有不同易受攻击实现的漏洞证明代码。我的工作还重点关注了每一种易受攻击实现的安全替代方案。此外,我创建了一个Web应用程序,用于上传和测试特定实现中使用的代码是否安全。

为了在大型代码库中高效地搜索漏洞,我使用了一个流行的SAST工具——Semgrep。具体来说,我编写了一组规则来自动检测那些易受攻击的实现,这将使识别漏洞变得更加容易。

所有编程语言的安全和不安全代码、实验以及Semgrep规则都已发布在 https://github.com/doyensec/Unsafe-Unpacking

理解归档路径遍历 提取归档文件通常涉及读取其所有内容并将其写入指定的提取路径。归档路径遍历旨在将文件提取到预期提取路径之外的目录。

当归档提取处理不当时,可能会发生这种情况,因为归档可能包含引用父目录的文件名。如果未进行适当检查,这些路径序列可能导致文件被提取到预期目录之外。

例如,考虑一个具有以下结构的ZIP文件:

1
2
3
4
/malicious
    /foo.txt
    /foo.py
    /../imbad.txt

当将此归档解压到 /home/output 时,如果提取方法未验证或清理文件路径,则内容可能会写入以下位置:

1
2
3
/home/output/foo.txt
/home/output/foo.py
/home/imbad.txt

结果,imbad.txt 将被写入预期目录之外。如果易受攻击的程序以高权限运行,这可能使攻击者能够覆盖敏感文件,例如 /etc/passwd —— Unix系统在其中存储用户账户信息。

验证概念:代码示例 为了演示该漏洞,我用各种编程语言创建了几个漏洞证明示例。这些代码片段展示了归档提取处理不当的易受攻击实现。

Python ZipFile库作为读取器和shutil.copyfileobj()作为写入器的组合使程序员负责正确处理提取。

shutil.copyfileobj() 的使用很简单:作为第一个参数,我们传递要提取内容的文件的文件描述符;作为第二个参数,我们传递目标文件的文件描述符。由于该方法接收的是文件描述符而不是路径,因此它不知道路径是否在输出目录之外,这使得以下实现变得脆弱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def unzip(file_name, output):
    # bad
    with zipfile.ZipFile(file_name, 'r') as zf:
        for filename in zf.namelist():
            # Output
            output_path = os.path.join(output, filename)
            with zf.open(filename) as source:
                with open(output_path, 'wb') as destination:
                    shutil.copyfileobj(source, destination)
                    
unzip1(./payloads/payload.zip", "./test_case")

如果我们运行前面的代码,我们会发现ZIP内容(poc.txt)没有被提取到test_case文件夹,而是被提取到了父文件夹:

1
2
3
4
5
6
7
$ python3 zipfile_shutil.py

$ ls test_case
# No output, empty folder

$ ls
payloads  poc.txt  test_case  zipfile_shutil.py

Ruby

1
Zip::File.open(file_name).extract(entry, file_path)

Ruby zip库中的extract()方法用于将条目从归档提取到file_path目录。这个方法是不安全的,因为它不会移除多余的点和路径分隔符。调用者有责任确保file_path是安全的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
require 'zip'
 
def unzip1(file_name, file_path)
  # bad
  Zip::File.open(file_name) do |zip_file|
    zip_file.each do |entry|
      extraction_path = File.join(file_path, entry.name)
      FileUtils.mkdir_p(File.dirname(extraction_path))
      zip_file.extract(entry, extraction_path) 
    end
  end
end

unzip1('./payloads/payload.zip', './test_case/')
1
2
3
4
5
6
7
$ ruby zip_unsafe.rb

$ ls test_case
# No output, empty folder

$ ls
payloads  poc.txt  test_case  zip_unsafe.rb

PHP、Swift、JS 和 Java 所有其他案例都在Doyensec的存储库中进行了记录,同时还有Semgrep规则和实验。

不安全解压实验 作为研究的一部分,我开发了一些Web应用程序,允许用户测试特定的归档提取实现是否容易受到解压缩攻击。

  • RUN: 在不上传归档的情况下,应用程序将提取一个预构建的恶意归档。如果用户上传归档,则将解压该归档。
  • Clear TXT Files: 应用程序将删除先前归档提取的所有文件。
  • Fetch Directory Contents: Web应用程序将向您显示归档目录(文件应被提取到的位置)和当前目录(文件不应被提取到的位置)。

这些Web应用程序实验适用于除Swift之外的所有语言,对于Swift,我们提供了一个桌面应用程序。

开发用于漏洞检测的Semgrep规则 检测开源项目中漏洞的最有效方法之一是使用静态应用程序分析工具。Semgrep是一个快速、开源、静态分析工具,用于搜索代码、发现错误以及强制执行安全护栏和编码标准。

Semgrep通过扫描源代码中的特定语法模式来工作。由于它支持各种编程语言并且易于编写自定义规则,因此它非常适合我的研究目的。

在以下示例中,我使用了GitHub存储库中的Unsafe-Unpacking/Python/PoC/src文件夹,其中包含5个解压缩漏洞。您可以使用以下命令运行Semgrep规则:

1
semgrep scan --config=../../rules/zip_shutil_python.yaml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...
┌─────────────────┐
│ 5 Code Findings │
└─────────────────┘

    zipfile_shutil.py
   ❯❯❱ rules.unsafe_unpacking
          Unsafe Zip Unpacking

           13┆ shutil.copyfileobj(source, destination)
            ⋮┆----------------------------------------
           21┆ shutil.copyfileobj(source, destination)
            ⋮┆----------------------------------------
           31┆ shutil.copyfileobj(source, destination)
            ⋮┆----------------------------------------
           41┆ shutil.copyfileobj(source, destination)
            ⋮┆----------------------------------------
           57┆ shutil.copyfileobj(source_file, target_file)

可以在GitHub存储库中找到一组15条规则。

缓解措施 由于在大多数易受攻击的实现中,程序员负责清理或验证输出路径,他们可以采取两种方法来解决此问题。

1. 路径清理 为了清理路径,应规范化文件名。在Ruby中,可以使用Path.basename方法,该方法移除多余的點,并将类似 ../../../../bad.txt 的路径转换为 bad.txt

在以下代码中,当使用File.join计算输出路径时,调用File.basename来清理归档中的条目文件名,从而缓解漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def safe_unzip(file_name, output)
  # good
  Zip::File.open(file_name) do |zip_file|
    zip_file.each do |entry|
      # sanitize the entry path
      file_path = File.join(output, File.basename(entry.name))
      FileUtils.mkdir_p(File.dirname(file_path))
      zip_file.extract(entry, file_path) 
    end
  end
end

这种缓解措施的副作用是归档的文件夹结构被扁平化,所有文件都被提取到单个文件夹中。因此,该解决方案可能不适用于许多应用程序。

另一种解决方案是使用Pathname.new().cleanpath,即pathname(一个内置的Ruby类)。它可以规范化路径并移除任何../序列:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
require 'pathname'

def safe_unzip(file_name, output)
  output += File::SEPARATOR unless output.end_with?(File::SEPARATOR)

  Zip::File.open(file_name) do |zip_file|
    zip_file.each do |entry|
      # Remove any relative path components like "../"
      sanitized_name = Pathname.new(entry.name).cleanpath.to_s
      sanitized_path = File.join(output, sanitized_name)

      FileUtils.mkdir_p(File.dirname(sanitized_path))
      zip_file.extract(entry, sanitized_path)
    end
  end
end

然而,如果开发人员希望通过使用任何类型的替换来移除../来自行清理路径,他们应确保重复应用清理,直到没有../序列为止。否则,可能会出现以下情况,导致绕过:

1
2
entry = "..././bad.txt"
sanitized_name = entry.gsub(/(\.\.\/)/, '') # ../bad.txt

2. 路径验证 在将条目的内容写入目标路径之前,应确保写入路径位于预期的目标目录内。这可以通过使用start_with?来检查写入路径是否以目标路径开头来实现,从而防止目录遍历攻击。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def safe_unzip(file_name, output)
  output += File::SEPARATOR unless output.end_with?(File::SEPARATOR)
  # good
  Zip::File.open(file_name) do |zip_file|
    zip_file.each do |entry|
      safe_path = File.expand_path(entry.name, output)

      unless safe_path.start_with?(File.expand_path(output))
        raise "Attempted Path Traversal Detected: #{entry.name}"
      end

      FileUtils.mkdir_p(File.dirname(safe_path))
      zip_file.extract(entry, safe_path) 
    end
  end
end

需要注意的是,应使用File.expand_path而不是File.join。使用File.expand_path()至关重要,因为它将相对文件路径转换为绝对文件路径,确保正确的验证并防止路径遍历攻击。

例如,考虑以下使用File.expand_path的安全方法:

1
2
3
4
5
6
7
8
9
# output = Ruby/PoC/test_case

# path = Ruby/PoC/secret.txt
path = File.expand_path(entry_var, output)

# Check for path traversal
unless path.start_with?(File.expand_path(output))
    raise "Attempted Path Traversal Detected: #{entry_var}"
end

在这种情况下,File.expand_path将路径转换为绝对路径,并且使用start_with进行检查可以正确验证提取的文件路径是否在预期的输出目录内。

另一方面,如果使用File.join构建输出路径,则可能导致漏洞:

1
2
3
4
5
6
7
8
9
# output = Ruby/PoC/test_case

# path = Ruby/PoC/test_case/../secret.txt
path = File.join(output, entry_var)

# Incorrect check
unless path.start_with?(File.expand_path(output))
    raise "Attempted Path Traversal Detected: #{entry_var}"
end

即使路径实际上指向预期目录之外(test_case/../secret.txt),检查也会错误地返回true,从而使攻击者能够绕过验证并执行路径遍历。关键点是在验证之前始终规范化路径。

我忽略了一个细节,我的导师(Savio Sisco)指出的,是在原始的安全方法中,我没有包含以下行:

1
output += File::SEPARATOR unless output.end_with?(File::SEPARATOR)

如果没有这一行,仍然有可能绕过start_with检查。尽管在这种情况下路径遍历是不可能的,但它仍可能导致写入预期目录之外:

1
2
3
4
output = "/home/user/output"
entry.name = "../output_bypass/bad.txt"
safe_path = File.expand_path(entry.name, output) # /home/user/output_bypass/bad.txt
safe_path.start_with?(File.expand_path(output))# true

结论 这项研究深入探讨了跨各种编程语言的不安全归档提取问题。文章展示了给予开发人员更多自由的同时也让他们承担了更多责任。虽然手动实现很重要,但它们也可能引入严重的安全风险。

此外,作为安全研究人员,理解漏洞的根本原因非常重要。通过开发Semgrep规则和实验,我们希望帮助其他人识别、测试和缓解这些漏洞。所有这些资源都可以在Doyensec存储库中找到。

解压缩攻击是一个广泛的研究领域。虽然这篇博客涵盖了一些与文件提取相关的案例,但仍然有许多其他攻击需要考虑,例如ZIP炸弹和符号链接攻击。

关于我实习的一些思考 虽然这篇博客文章不是关于实习的,但我想借此机会也谈谈我的经历。

两年前,在我准备OSWE考试期间,我偶然看到一篇Doyensec的博客文章,并将其用作学习资源。几个月后,我发现他们正在招聘实习生,我认为这是一个绝佳的机会。

我第一次申请时,收到了我的第一个技术挑战——一组易受攻击的代码,如果你喜欢阅读代码,那么研究起来会很有趣。然而,那一年我没能通过挑战。今年,在与Luca和John进行了两轮面试后,我终于被录取了。面试是360度全方位的,涵盖了各个方面,例如如何修复漏洞、计算机工作原理、如何使安全代码段变得脆弱以及如何进行威胁建模。

在最初的几周里,我被分配到一些项目,并得到了其他安全工程师的大量指导。我有机会与他们谈论他们在Doyensec的工作,甚至与一位前实习生谈论了他的实习经历。我学到了很多关于公司方法论的知识,不仅是在漏洞挖掘方面,还包括如何更有条理——无论是在工作还是生活中。就像许多CTF玩家一样,我习惯于工作到深夜,但由于我不是独自在这些项目上工作,这个习惯开始影响沟通。起初,当太阳还高挂时打开Burp感觉很奇怪,但随着时间的推移,我习惯了。直到完全适应后,我才意识到这个简单的改变能多大程度地提高我的工作效率。

在处理大型代码库或复杂审计的项目时,确实促使我即使在看似绝境时也坚持寻找漏洞。有时候,在连续几天没有发现任何有趣的东西后,我会变得非常紧张。然而,Savio在这些时刻给了我很大帮助,他建议我保持冷静,坚持清晰的方法论,而不是让紧张情绪驱使我盲目搜寻。最终,我能够在这些项目中找到一些很酷的漏洞。

尽管我的期望非常高,但这段经历绝对没有让我失望。非常感谢团队,特别是Luca和Savio,他们在整个过程中给予了我极大的照顾。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计