不安全归档解压:实验与Semgrep规则解析

本文深入探讨了多种编程语言中不安全的归档文件解压实现,展示了路径遍历漏洞的具体案例,提供了安全修复方案,并介绍了使用Semgrep进行自动化漏洞检测的方法。

不安全归档解压:实验与Semgrep规则

引言

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

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

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

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

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

理解归档路径遍历

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

当归档提取处理不当时,可能会发生这种情况,因为归档可能包含引用父目录的文件名(例如使用../)。如果没有正确检查,这些序列可能导致提取发生在预期目录之外。

例如,考虑具有以下结构的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应用程序,允许用户测试特定的归档提取实现是否容易受到解压缩攻击。

  • 运行:不上传归档文件,应用程序将提取一个预构建的恶意归档文件。如果用户上传归档文件,该归档文件将被解压。
  • 清除TXT文件:应用程序将删除先前归档提取的所有文件。
  • 获取目录内容: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)

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

缓解措施

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

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 设计