PHP 等弱类型语言支持通过隐式转换它们的类型和值来松散比较(Loose Comparison)两个操作数。这种语言特征被广泛使用,但也可能构成严重的安全威胁。在某些情况下,松散比较可能会导致意外结果,从而导致身份验证绕过和其他功能问题。
在本文中,首次对此类松散比较错误进行了深入研究。本研究开发了 LChecker,这是一个静态检测 PHP 松散比较错误的工具。它采用上下文敏感的过程间数据流分析以及几种新技术。还增强了 PHP 解释器,以帮助动态验证检测到的错误。评估表明,LChecker 可以有效且高效地检测 PHP 松散比较错误,并且误报率相当低。它还成功地检测到评估数据集中所有以前已知的错误,没有出现漏报。使用 LChecker,发现了 42 个新的松散比较错误,并分配了 9 个新的 CVE ID(CVE-2020-23352, CVE-2020-23353, CVE-2020-23355, CVE-2020-23356, CVE-2020-23357, CVE-2020-23358, CVE-2020-23359, CVE-2020-23360, CVE-2020-2336)。
0x01 Introduction
比较是必不可少的编程功能。强类型语言,例如 Python执行严格的比较,同时考虑比较操作数的值和类型。相比之下,弱类型语言为严格比较提供相同但不相同的运算符 (===, !==),为松散比较提供相等和不相等运算符 (==, !=)。松散比较可以隐式转换操作数类型,从而允许比较不同类型的操作数。
许多编程语言都支持松散比较,例如 PHP、JavaScript 和 Perl。特别是,松散比较在 PHP 程序中被广泛使用。在运行时,PHP 可以自动转换操作数的类型并专门解释其值。这也称为类型窜改(type juggling)。例如, (“0e12345” == “0e67890”) 被评估为 True,因为 PHP 将这两个字符串操作数解释为整数 0,而大多数(如果不是全部)人可能会直觉地认为结果为假。因此,松散比较有时会产生意想不到的比较结果并破坏预期的程序功能。将这种导致意外结果的松散比较称为松散比较错误。
松散比较错误可被用于恶意目的,并具有严重的安全影响。之前的报告表明,攻击者可以利用松散比较错误来绕过身份验证并提升权限。这些错误还可以用于执行系统命令和覆盖系统文件。虽然之前的研究已经研究了一些类型系统错误,但松散比较错误和相关的安全问题还没有得到很好的研究。 TypeDevil和 Phanm可以检测 JavaScript 和 PHP 中的类型不一致错误-一种不同的类型系统错误。目前不存在可以有效检测松散比较错误的工具。在本文中旨在通过开发方法来检测用 PHP 开发的应用程序中的松散比较错误(78.8% 的网站 使用的最流行的服务器端编程语言)并研究它们的安全影响,从而填补这一空白。
检测松散比较错误具有挑战性,首先松散比较是一种语言特性,除非在程序中使用不当,否则不是错误。开发方法来区分错误使用的松散比较与正常比较是非常重要的。特别是,需要涵盖不同类型滥用的规范。其次,松散比较错误属于一般逻辑错误,因为它们不会破坏代码属性和语义。因此,很难找到利用它们的攻击指标。此外,在现代应用程序中发现此类错误可能具有挑战性,这些应用程序可能具有包含数百万行代码的庞大代码库。最后但并非最不重要的一点是,PHP 的动态特性使得变量类型的推断非常困难,这对于确定是否可以利用松散比较是必要的。
为了克服这些挑战,首先根据经验研究已知的松散比较错误并表征它们。根据研究,提供了松散比较错误的正式定义,同时考虑了操作数的来源和类型,并排除了松散比较的合法用途。然后开发了 LChecker——一个专门检测松散比较错误的工具。 LChecker 执行污点分析以识别攻击者可以通过恶意输入控制的松散比较。它还采用类型推断算法来克服在 PHP 等动态语言中确定变量类型的挑战。 LChecker 的静态分析是可扩展的,因为它在其高效的过程间分析中采用了上下文敏感的函数摘要。摘要可以帮助查找仅在使用特殊输入调用某些函数时才会出现的嵌套错误。还增强了 PHP 解释器,以帮助以最少的人力来验证错误。
为 PHP 实现了 LChecker 的原型,并将发布原型实现的源代码。在 26 个流行的 PHP 应用程序上彻底评估了 LChecker,包括大约 3.87 万个源文件和 700 万行代码。评估表明 LChecker 可以有效地检测松散比较错误。 LChecker 仅在 75 分钟内成功识别了所有 8 个已知错误和 42 个以前未知的错误,错误报告数量相当少。此外,LChecker 检测了 37 个以上的错误,性能优于唯一相关的最先进的分析工具。向相关供应商报告了所有新错误,并获得了 9 个新的 CVE ID。
0x02 Background
松散比较,例如 ==,是弱类型语言中的一种语言特性,常用于软件开发。许多语言在计算松散比较结果时遵循明确定义的类型标准。在本节中,将介绍关于了解实际应用中松散比较普遍性的初步研究。在 CVE 数据库中收集已知的松散比较错误,以研究它们的特征和安全影响。此外,讨论了有关松散比较错误的现有工作。
A.松散比较
在 2020 年 5 月下载了 GitHub 上排名前 1000(按星级)的 PHP 存储库。由于其中的语法错误,未能解析和分析 32 个项目。计算了其余 968 个项目中(松散)相等比较和(严格)相同比较的数量。
结果如上表所示。使用 LC 和 SC 分别表示松散比较和严格比较。综上所述,742(76.65%)个项目使用了259K个松散比较,11个项目只使用了松散比较; 957 (98.86%) 个项目使用了 335K个严格比较,226 个项目仅使用了严格比较; 731 个项目同时使用了松散和严格的比较。结果表明,这些 PHP 开发人员广泛使用松散比较,其中 43.62% 的比较是松散比较。
B.松散比较错误
与强类型语言不同,弱类型语言采用隐式类型转换来允许不同类型操作数之间的比较。 PHP 遵循下表中列出的过程来处理松散比较。例如,当与数字进行比较时,字符串被隐式转换为数字。即使操作数的类型相同,隐式类型转换仍然可能发生。例如,在 (“12” == “10”) 中,两个 String 操作数实际上作为数字进行比较,如 (12 == 10)。
但是,在松散比较中自动转换操作数类型和值可能会出现问题。开发人员通常在条件语句中使用松散比较。比较策略可能会导致奇怪的比较结果,从而导致意外的程序行为,例如,采用了意外的路径。通常将这些意外的松散比较问题称为松散比较错误。
(1)示例
下图显示了 UseBB (1.0.12) (CVE-2020-8088) 中的一个松散比较错误的示例,该错误利用了一个魔术散列错误。如果没有正确的凭据,可以轻松绕过用户身份验证。在第 3-4 行中,首先使用用户提供的用户名 ($_POST[‘user’]) 从数据库中检索用户信息。第 7 行通过检查 (1) 用户 ID ($userdata[‘id’]) 是否已设置,以及 (2) 用户提供的密码是否与数据库中的密码匹配来验证凭证。 stripslashes() 函数从字符串中删除反斜杠。 md5() 函数通过生成 32 个字符(128 位)的十六进制字符串来计算字符串的哈希值。存储在数据库中的密码 ($userdata[‘passwd’]) 之前已被散列。如果第 7 行中的条件语句评估为 False,则程序确定身份验证成功并采用 else 分支(第 9-11 行)更新用户会话数据。
然而,由于第 7 行中的松散比较错误,如果散列密码字符串是特殊格式的,即使没有提供正确的密码,攻击者也可以欺骗程序进入 else 分支。例如,如果正确的原始密码是“QLTHNDT”,则与许多其他字符串的散列值(例如“PJNPDWY”)的松散比较可能为真。这是因为两个不同密码的哈希值是科学记数法中零的字符串表示,如下图:
它们都表示像 0 × 10^n 这样的数字,其中指数 n 是在字符串中的字母“e”之后指定的一个巨大整数。两个hash值虽然都是String类型,但是在松散比较中会自动转换成整数0。结果,第 7 行中的条件变为 False 并且程序采用 else 分支,就好像提供的密码是正确的一样,从而授予攻击者受害用户的特权。
(2)调查已知松散比较错误
最近五年,从 CVE 数据库中收集了 13 个已知的松散比较错误。这些错误跨越 12 个 PHP 应用程序,包括流行的应用程序,如 WordPress。
发现1:13个已知松散比较错误中有12个是身份验证绕过漏洞
身份验证是验证某些用户或各方身份的过程。它是现代软件中用于执行各种任务(例如访问控制)的必要组件。即使没有正确的凭据,这些漏洞也允许攻击者绕过某些关键的身份验证检查。绕过身份验证会导致权限提升,并进一步允许攻击者在不同的安全上下文下执行其他攻击,例如敏感信息泄露和跨站点脚本。
发现2:13个已知松散比较错误中有11个利用哈希字符串
PHP 中的散列函数通常生成 base-16 编码的字符串,例如“0e405967…”,如上例所示。在松散比较中,如果前缀“0e”后面的字符都是数字,则整个字符串被视为数字。虽然哈希值是 String 类型的,但在松散比较的上下文中它会被转换为整数 0。发现这种为零的科学记数法字符串是现有错误的主要原因。
发现3:松散比较错误会导致严重安全后果
将松散比较错误的安全影响总结为以下三类:
(1) 权限提升:攻击者可能会通过绕过某些松散检查来访问通常不受未经授权用户访问的资源。在上图的示例中,攻击者可以在没有正确凭据的情况下进行身份验证,然后获得受害用户的特权。
(2) 恶意内容注入:松散比较错误可用于将任意内容注入应用程序以执行恶意操作。这带来了一些二阶攻击、任意代码执行等的可能性。
(3) 正确性违规:松散比较错误也可能导致功能错误,因为程序执行可能会偏离预期的流程。如果某些必要的代码块没有执行或由于松散比较错误而执行了错误的代码块,则该功能可能会被破坏。因此,程序无法完成预期任务并可能崩溃。
0x03 Problem Statement
A.松散比较错误的定义
作为语言特性的松散比较,如果使用不当,可能会导致安全问题。详细地根据以下三个条件定义了松散比较错误:
Cond1:用户可控的松散比较,松散比较错误应该涉及来自不受信任来源的数据,例如用户输入,以便攻击者可以利用它。要形成松散比较错误,松散比较的至少一个操作数需要来自不受信任的来源。
Cond2:可利用的操作数类型,只有某些类型的数据才能在松散比较中隐式转换为另一种类型,从而带来潜在的安全问题。可以在松散比较中将 Null、Bool、Number 或 String 操作数转换为不同的类型(并因此被利用)。仅将操作数在这些可利用类型中的松散比较视为潜在错误。
Cond3:不一致的比较结果,除了类型之外,操作数的值也很重要。在研究的示例中,字符串操作数被解释为数字,其常规字符串表示(例如,“0”)不同于操作数的文字字符串(例如,“0e405967…”)。因此,松散比较结果变得出乎意料和不一致。这种不一致的比较结果会导致松散比较错误,当通过比较隐式转换的操作数获得的实际运行时比较结果与通过比较操作数文字值获得的(预期)结果不同时,就会发生这种情况。
B.威胁模型
在威胁模型中,假设远程攻击者可以仅通过开发人员指定的正常接口与弱类型语言 (PHP) 的应用程序进行交互。攻击者可以访问目标应用程序(可以是开源项目)的源代码并识别松散比较错误。攻击者试图制作特殊输入以利用松散比较错误,例如绕过使用错误松散比较的安全检查。不考虑威胁模型中的其他正交攻击。
C.研究目标与研究范围
在这项工作中,旨在系统地研究松散比较错误。目标是开发方法以低误报率检测 PHP 应用程序中的此类错误。然而,目标不是检测所有松散比较错误,即方法不合理。还旨在研究现实世界场景中松散比较错误的主要原因和安全影响。在工作中特别研究了三类常见的松散比较错误。在每个类中,攻击者都可以利用一种意外的值来绕过松散比较检查。在本作品中,将以下类型的特殊字符串称为魔术字符串(magic strings)。
U1:科学记数法字符串,PHP 可以将科学记数法字符串解释为数字。例如,与科学记数法正则表达式“0e\d*”匹配的任何字符串在与类似的字符串或数字进行松散比较时都会被评估为整数零。已展示了魔术散列错误如何使用魔术零字符串绕过松散比较检查。除了magic zero,科学记数法字符串也可以自动转换为其他数字。在字符串到字符串的比较中,将科学记数法字符串隐式转换为数值在大多数情况下是不可接受的。只有在极少数情况下,开发人员才会故意使用这种间接的科学记数法字符串表示进行简单的数字比较。因此,在大多数程序中不应松散地比较科学记数法字符串。
U2:数字字符串,与科学记数法一样,其他数字字符串也可以解释为数字。如表 2 的第一行所示,将数字字符串转换为数字以进行数字比较。这意味着,如果两个操作数是数字字符串,则松散比较实际上是作为数字比较执行的。此外,可以在字符串前面添加多个额外的前导零,而无需更改数值。比如让($x == “0.1”)为True,$x可以为”0.1”,以及类似的带有很多前导零的字符串,例如”00.1”、”000.1”等。不管有多少前导零,它们在松散比较中都被评估为 0.1,尽管它们实际上是不同的字符串。
U3:数字前缀字符串,某些字符串作为一个整体可能不是有效数字。但是,如果其前缀之一是有效的数字字符串,则该前缀将转换为数字,以便与 Number 和 Bool 值进行松散比较 [39]。例如,让($x == 0.1)为True,除了0.1和”0.1”,$x还可以是许多其他以”0.1”开头的字符串,例如”0.1xy”、”0.1xyz”,等等。这是因为所有这些字符串都被裁剪并评估为松散比较中的浮点数 0.1。
上述松散比较情况在语义上是正确的。然而,它们可能会导致问题,因为松散比较检查可以以隐式和意外的方式绕过。
(1)攻击可行性
攻击者在绕过使用有问题的松散比较的安全检查方面具有非常高的优势。假设开发人员比较由抗碰撞哈希函数生成的两个字符串,该函数将任何输入统一哈希为 32 个字符(128 位)的十六进制字符串。如果攻击者可以控制输入字符串进行哈希处理,然后与存储的密码哈希值进行比较,严格比较,发现与密码“QLTHNDT”冲突的概率为单次尝试(1/2)^ 128 = 2.94 × 10^(−39)次。
然而在松散比较中,为了匹配相同的密码,攻击者只需要产生一个概率为 10^30/2^128 = 2.94 × 10^(−9)的magic zero哈希字符串(U1),即 30 个数量级幅度更高。进一步考虑以零为前缀的字符串(U2),概率增加到∑(1/16)^i · (10/16) ^(32−i) = 3.27 × 10^(−9)。此外,现有的魔零字符串集合可以很容易地应用于绕过这种有问题的松散比较检查。同样,绕过 U3 类型的松散比较检查也比使用蛮力的严格检查容易得多。总之,不一致的松散比较会造成严重的安全威胁,需要被检测到。
D.研究挑战
本研究面临多项技术挑战。首先,现代应用程序非常复杂,可能有数百万行代码。松散比较错误通常跨越许多函数并且是上下文敏感的,只有当操作数在多个操作数类型内并且具有特殊值时才能触发它们。在庞大的代码库中执行精确的程序分析自然是具有挑战性的。
其次,很难识别误报率低的松散比较错误。松散比较是一种语言特性,只能在非常特殊的情况下使用。应用程序可能会使用大量的松散比较操作。大多数松散比较操作都不是错误。不将那些正常的松散比较报告为错误是具有挑战性的。最后但并非最不重要的是,PHP 的弱类型特性使得程序分析非常具有挑战性。
0x04 Design
本研究提出了一种检测 PHP 中松散比较错误的方法。这是第一种检测弱类型语言中松散比较错误的程序分析方法。方法的架构如下图所示。方法包括 (1) 一个静态分析组件 LChecker,用于识别松散比较错误 (LCB),以及 (2) 一个动态分析以帮助验证真正的正错误 (LCBtp) )。具体来说,静态分析部分 LChecker 使用污点分析来识别不受信任的用户数据 (Cond1),并通过类型推断算法 (Cond2) 解决弱类型语言中确定变量类型的挑战。 LChecker 还集成了上下文相关的函数摘要,以执行有效的过程间分析。为了减少错误报告,动态分析驱动了一个增强的程序执行引擎,以帮助专家验证真正的错误 (Cond3)。
A.静态分析
LChecker 基于使用 PHP-Parser的程序的控制流图 (CFG) 和调用图 (CG) 执行源代码级静态分析。本研究开发了自己的静态分析,因为由于 PHP 的动态特性,没有可用的工具将 PHP 完全转换为静态程序分析的通用中间表示。在构建 CFG 时,文件包含可能会将新代码引入分析范围,因此 LChecker 使用模糊字符串匹配算法来识别可能的文件并将其包含到当前分析上下文中。这是实践中常见的设计选择。如果整个包含文件字符串是从字面上识别的,LChecker 将直接包含该文件;否则,它会考虑它的前缀或后缀并包括所有满足的文件候选。 CFG 中的基本节点以由左操作数 (LeftOp)、右操作数 (RightOp) 和运算符组成的格式表示。操作数可以表示其他嵌套的表达式节点。例如,在语句$x = $a + $b 中,运算符为赋值(=),LeftOp 为$x,RightOp 为$a + $b,表示加运算的表达式。
(1)污点分析
LChecker 执行标准的污点分析以找到可以被攻击者控制的松散比较语句 (Cond1)。
LChecker 将一些不受信任的数据源设置为污点源,这些数据源可能会使 PHP 程序面临潜在风险。它包括超全局变量(例如,$_GET、$_POST)、用户文件上传、数据库等,作为污染源。它还维护一组污染变量,用于在分析过程中识别变量的污染状态。
LChecker 在类似赋值的语句中将污点从 RightOp 传播到 LeftOp。例如,在语句 $a = $b 中,如果 $b 被污染,则 $a 被污染,因此被放入被污染的变量集中。在分支语句中,分析首先被分叉,然后在它之后加入。如果分支节点后的变量在任何分支中被污染,则该变量将被标记为被污染。这是合理的,因为实际上至少存在一种攻击者控制变量的路径。
LChecker 将所有松散比较操作视为接收器。通常,如果任何被污染的数据到达操作数之一,那么松散比较操作就会被污染。然而观察到这在实践中是不够的,因为另一个未受污染的操作数通常设置为不满足松散比较错误的值。因此,LChecker 仅报告两个操作数都受到污染的松散比较。
(2)类型推断
LChecker 使用类型推断算法来检查 Cond2 并缩小潜在错误的范围。与一些先前的类型推断工作不同,需要在类型分析中考虑两个新问题。首先,在 PHP 和其他动态类型语言(例如 JavaScript)中,变量的值和类型仅在运行时确定。这与一些静态类型语言(例如,C/C++)不同,它们的变量在使用前用显式类型声明。其次,PHP 变量不绑定到一种类型。 PHP 变量可以通过程序执行被不同类型的值覆盖。因此,为了进行类型推断分析,LChecker 需要监控所有赋值语句以跟踪变量类型的变化。
LChecker 通过数据流分析推断变量类型信息。除了污点状态之外,每个变量都与一个类型集相关联,类型集表示变量可以属于的可能类型。在分析中定义了以下八种基本变量类型:(1)Null,(2)Bool,( 3) Int, (4) Float, (5) String, (6) Object, (7) Array, (8) Mixed。请注意,前面提到的 Numeric 类型包括 Int 和 Float 类型。然后 LChecker 使用不同的策略识别变量和操作的类型。
对于标量和常量,LChecker 从字面上推断它们的类型,例如,“1”在 String 类型中。对于变量,LChecker 直接从它们的类型集中获取它们的类型。这是因为,无论何时遇到一个变量,它之前都必须被赋值为某个值和类型,或者已经被赋值为其他变量。通过使用数据流传播类型,LChecker 能够推断大多数变量的类型。
PHP 代码中经常使用不同的运算符。它们决定了运算结果的类型。例如,在 $x = 1 的简单赋值中。 “string”,连接运算符 (‘.’) 连接一个整数 1 和一个字符串 “string”。 $x 的类型将始终是 String,因为它由连接运算符 (‘.’) 确定。同样,所有其他二元运算、一元运算、类型转换运算等的结果类型也可以相应地推断出来。
发现内置函数通常都有明确的规范说明。因此,每当调用内置函数时,LChecker 都可以推断参数的类型和返回值。但是,参数类型并不总是准确的,因为 PHP 中的内置函数通常允许用户违反参数类型规范而不会引发运行时错误。例如,PHP 内置函数 strlen(String):Int 可以接受整数参数(例如 strlen(1))。
不同的是,对于自定义函数,LChecker 直接解析源代码来推断类型。 LChecker 在过程间分析中使用上下文敏感的函数摘要来推断返回值类型。函数摘要描述了参数和返回值之间的类型关系。因此,一旦推断出参数类型,也可以知道返回值类型。详细信息将在第 4.1.3 节的程序间分析中介绍。
变量类型在类似赋值的语句中从 RightOp 转移到 LeftOp。条件语句可能导致多个程序执行路径,并且变量在不同分支中的符号可能不同。因此,对于每个变量,LChecker 将所有分支中所有可能的类型累积到分支之后的类型集中。这可能会引入误报,因为在运行时只能采用一个分支,并且变量只能在该分支之后属于一种类型。然而,它仍然是可以接受的,因为观察到大多数变量在不同的分支之间被分配了相同的类型。
(3)上下文敏感的程序间分析
需要执行过程间分析,因为不同的调用站点可以调用具有不同参数类型和值的用户定义函数,这些参数类型和值稍后会影响调用者函数的状态。 LChecker 遵循 PHP 程序分析中的常见做法来识别动态方法调用中的被调用者。具体来说,它在整个程序中搜索方法名称以找到唯一匹配项。如果多个方法具有相同的方法名称,它会进一步比较方法声明中的参数数量和调用点中的参数数量。
在每个调用点使用显式参数分析函数的成本很高,因此 LChecker 使用函数摘要来实现有效的过程间分析,以检测松散比较错误。函数摘要为每个函数维护一些必要的信息,包括函数名、类名等。每当调用以前未分析的用户定义函数时,LChecker 构造函数摘要,然后将函数摘要应用到调用站点。一个函数只需要分析一次,就可以在所有调用点之间的整个分析中使用函数摘要。这显着提高了过程间分析的效率。但是,为了检测松散比较错误,需要解决两个主要问题。
首先,PHP 不要求开发人员在函数定义中包含参数类型。参数类型确实会影响函数内部的变量类型,最终影响类型推断和松散比较错误检测。此外,不同的调用点可以提供不同的参数来调用函数。在上图的示例中,直接返回函数 f() 的原始参数。第 2 行和第 3 行分别使用数组类型和字符串类型的参数调用 f()。因此,$a 和 $b 应为 Array 类型和 String 类型。因此,函数摘要应该对模型返回值类型上下文敏感。
其次,松散比较错误是上下文相关的。还需要在检测中考虑被调用者的调用上下文。在上图中,单独为 f() 构造函数摘要时,未检测到第 7 行的松散比较错误。但是,可以在某些调用上下文中利用这种松散比较语句。使用第 3 行中的输入,可以利用松散比较错误,因为局部变量 $x 成为受污染的字符串。因此,函数摘要应该是上下文敏感的,以便可以检测到被调用者中的错误。
LChecker 中的函数摘要是上下文敏感的,以支持函数的不同调用上下文。为此,在类型推断的步骤中为参数(Paramidx)添加了一个类型占位符,其中 idx 表示参数的对应索引。 Paramidx 在类型推断期间以与其他类型相同的方式传播。此外,LChecker 挂钩所有返回表达式并记录函数返回值的返回类型。在上图的示例中,第 7 行中的 $x 获取类型 Param1 而不是显式类型(例如,数组或字符串)。在第 10 行,LChecker 将 Param1 添加到 f() 的函数摘要的 Returntypes 中。在第 2 行和第 3 行的调用点,LChecker 用显式参数类型替换类型占位符以获得返回值类型。因此,$a 和 $b 分别被推断为 Array 和 String 类型。
为了在第 3 行的 f() 调用点成功检测这种松散比较错误,LChecker 在摘要构造的数据流分析过程中记录了每个变量的数据源(例如,参数和超全局变量)。然后,它包括所有松散比较(sinks)以及函数摘要中操作数的类型、值、污点状态、数据源。在示例中,LChecker 在其摘要中包含第 7 行($x == $passwordDB)。 LeftOp($x)的数据源是第一个参数;它的类型是Param1;它没有被污染。 RightOp($passwordDB)的数据源是数据库;它的类型是字符串;它被污染了。因此,在第 3 行的调用站点,LChecker 将参数的污点和类型状态应用于摘要中的松散比较。因此,第 7 行可以被识别为一个松散比较错误。此外,为了处理嵌套的用户定义函数调用,LChecker 递归地将所有被调用者的松散比较(sinks)添加到调用者的函数摘要中。
(4)检测认证相关错误
由于其普遍性和严重的安全影响,LChecker 专门处理与身份验证相关的错误。观察到,身份验证过程通常会将用户提供的密码与存储在应用程序后端(例如数据库)中的密码进行比较。密码最有可能在比较之前进行哈希处理,以避免在数据泄露时意外泄露。因此,LChecker 在数据流分析过程中将数据库访问和哈希计算标记为附加信息。
当松散比较的操作数同时设置了数据库访问和散列计算标签时,LChecker 会报告与身份验证相关的错误。此外,LChecker 忽略了一些哈希函数,因为它们不会导致不一致的松散比较结果。例如,PHP 内置函数 password_hash() 不会生成魔法字符串。
LChecker 挂钩了处理数据库查询和分析中计算散列值的内置函数。一旦遇到这些操作,相应的标签就会被设置为 True。此外,由于某些应用程序在某些后端文件中维护密码或直接在源代码中对其进行硬编码,因此提出了一种关键字匹配策略来辅助检测与身份验证相关的错误。具体来说,收集了一组常用的密码标识符,例如 $passwd。使用这些标识符的变量被标记为具有数据库访问权限。之后遵循类似的规则将这些标签从它们的来源传播到其他变量。
B.动态分析
静态数据流分析可以检测许多潜在的松散比较错误,其中一些可能是误报。此外,静态类型推断不能 100% 准确,因为一次考虑多个不同的执行路径。因此,进一步使用动态分析以半自动方式验证松散比较错误。增强了 PHP 解释器和挂钩松散比较操作,以验证静态检测到的松散比较情况。在运行时,可以精确地获取比较操作数的类型和值。增强的 PHP 解释器还在松散比较的操作数之间进行了影子严格类型比较,以检查两次比较是否产生不同的比较结果。它还进一步检查它们是否属于三种不一致的比较情况(U1-U3),即操作数是否为魔术字符串并隐式转换为不同的值。安全警告在运行时生成,以将真正的正错误通知给分析师。
由于静态分析已经为每个已识别的可疑案例确定了数量非常有限的潜在脆弱路径。因此,可以利用非常有限的人力来构建输入来辅助增强的程序执行引擎。
0x05 Implementation
用大约 3K 行 PHP 代码和 600 行 C 代码实现了本文方法。未来将发布原型实现的源代码。使用 PHP-Parser 将 PHP 源代码解析为抽象语法树 (AST),然后构建 CFG 和 CG。污点分析和类型推断是通过遍历 CFG 和 CG 来执行的。还为关键字匹配策略手动识别了一些密码和数据库相关的内置函数。在 PHP 解释器中实现了动态分析,没有修改 PHP Zend 虚拟机中 PHP AST 和操作码的生成步骤。相反,在比较处理程序中插入代码以执行额外的严格比较和警告生成。接下来讨论遇到的一些重要的实施问题和解决方案。
数组:数组通常使用键访问(例如,$a[$key])。但是,有时由于条件语句(例如,if、循环)、内置函数等的存在,无法静态推断出键的具体值。数据流分析选择仅对具有具体键的数组项进行建模可以静态推断的值。对于其他数组项,将数组的污点状态应用于它们并将它们的类型指定为 Mixed。如果数组中至少有一项被污染,将其标记为被污染。这是一种常用的数组建模方法。
循环:为避免路径爆炸,将循环语句(例如,for、while 和 foreach)视为 if 语句,并按照常见做法将它们仅展开一次。为了实现这一点,在 CFG 构建过程中删除了从循环体指向循环头的后边缘。在 foreach($array as $key=>$value) 循环的情况下,会创建一个 $value 和一个 $key(可选)来帮助迭代数组项。它们仅应用于循环体的范围。由于上述数组分析的相同挑战,选择通过将数组的污点状态和一种 Mixed 类型分配给 $value 和 $key 来简化它。这能够分析循环体内的代码部分,实施了 LChecker 以从默认应用程序部署设置中的主要功能开始分析。一些用户定义的函数从未被分析过,因为它们不能直接或间接地从主函数中调用。然而,这些未经分析的函数本身可能会通过从超全局变量、文件等中检索值来直接与不受信任的数据源交互。因此,它们也可能导致松散比较错误。 LChecker 可能会遗漏当前实现中的一些松散比较错误。尽管如此,认为实现选择是合理的,因为没有导致这些错误的执行路径。
0x06 Evalution
在本节中评估 LChecker 在检测松散比较错误方面的有效性。应用 LChecker 来检测几个流行的 PHP 应用程序中的松散比较错误,并将其与相关工作进行比较。然后分析其性能并讨论 LChecker 检测到的一些有趣的错误。
A.实验设置
选择了 26 个流行的 PHP 应用程序。它们列在上表的第一列中。它们总共包含 38.7K PHP 源文件和 7M LoC,并且有 49.8K 严格比较和 49.3K 松散比较。评估数据集是通过在密切相关的工作中使用的数据集中选择(1)个应用程序构建的——Nemesis; (2) WordPress、MediaWiki、HotCRP等流行的大型PHP应用; (3) GitHub 上几个知名且评价很高的 PHP 项目。尝试在 CVE 数据库中包含所有具有已知松散比较错误的应用程序作为评估的基本事实(上表中的最后七个应用程序)。但是,某些应用程序无法包含在内,因为它们的完整源代码未公开提供(例如,CVE-2020-10568)或者鉴于 CVE 中的信息有限(例如,CVE- 2019-10231)。从其官网或 GitHub 下载每个应用程序的源代码,并在实验中使用默认设置对其进行配置。
还与 PHP Joern 进行了比较,这是唯一一个尝试检测松散比较错误的相关工具。为了公平比较,尝试使用与他们论文中相同的设置。首先为每个应用程序构建代码属性图。然后,应用相同污点传播规则并遍历图以检测魔术哈希错误。所有实验均在运行 Debian GNU/Linux 9.12、4 核 Intel Xeon CPU 和 16GB RAM 的计算机上进行。
B.漏洞检测
评价结果如上表所示,在列标题中用上标L表示LChecker的结果,下标taint、type、tp表示只运行污点分析、运行类型推断、最终true的结果积极的一面。 LChecker 的污点分析检查 Cond1 以过滤掉那些正常的松散比较。在 26 个 PHP 应用程序中总共 49.3K 个松散比较 (LC) 中,LChecker 在所有应用程序中确定了 958 个 (1.92%) 污染松散比较 (LCBL t aint ),而 48,363 个 (98.08%) 松散比较不符合定义被直接排除。这表明污点分析可以非常有效地缩小有问题的松散比较案例的范围。
在应用类型推断来检查 Cond2 后,LChecker 删除了污点分析中确定的 773 (80.69%) 个松散比较,并在 26 个应用程序中的 17 个中仅报告了 185 个案例 (LCBtLype)。然后,使用增强的 PHP 解释器分析了报告的错误,并确认了 50 个松散比较错误 (LCBL tp)。 42 个错误以前是未知的。为了验证所有报告的 185 个案例,在增强的 PHP 解释器的帮助下,一位研究者总共花费了 12 个小时。人工主要花在检查Cond3是否可以保持。在大多数情况下,潜在易受攻击的路径已经在污点分析和类型推断分析中报告,因此手动分析很简单。本研究负责任地向相关开发人员报告了新检测到的错误。截至 2021 年 2 月 17 日,包括 9 个 CVE1 在内的 10 个错误已得到及时确认或修补。
(1)关键词匹配策略影响
正如前文中提到的,收集了一些常用的标识符来帮助检测与身份验证相关的错误。发现在污点分析中报告的 958 个案例中的 230 个是在这些标识符的帮助下识别出来的。此外,在类型推断中报告的 185 个案例中有 44 个和 50 个真正的松散比较错误中有 13 个使用此类标识符进行识别。这表明关键字匹配策略可以帮助提高错误检测的有效性
(2)错误表征
分类:手动调查了漏洞的特征并将其分为两类:(1)身份验证绕过漏洞,允许攻击者在没有有效凭据的情况下绕过身份验证; (2) 违反正确性的错误,破坏应用程序的功能或正确性,导致程序行为异常。分别有 14 个身份验证绕过漏洞和 36 个正确性违规错误。结果显示在前表中的 AuthL tp 和 CVL tp 列中。
还根据错误可能导致的不一致松散比较的类型对错误进行分类,如上表所示。大多数有缺陷的程序执行直接纯文本(字符串)松散比较,因此可以被科学记数法字符串(U1)和数字字符串(U2)利用。在六个错误中,程序使用哈希函数处理操作数,只能生成科学记数法字符串(U1)。只有三个错误涉及相同类型(字符串)和交叉类型(字符串和数字)的松散比较,并且属于所有三种类型的错误(U1 + U2 + U3)。
有缺陷的应用程序的流行: LChecker 在各种应用程序中检测到松散比较错误。它发现了内容管理系统应用程序中的错误,例如 Codiad (2.8.4) 和 Monstra (3.0.4) 。它还在几个流行且维护良好的应用程序中发现了错误。例如,LChecker 检测到 osCommerce (2.3.4) 中的七个错误,这是一个被超过 20 万个网站使用的流行电子商务软件; PHP-ML (2.0)中的两个错误,这是一个流行的 PHP 机器学习库,在 GitHub 上拥有超过 6000 颗星。
调查了松散比较错误与错误应用程序的流行之间的关系。将数据集中的应用程序分类为相对流行的应用程序,并在表第一列中分别用上标 ‡ 和 † 标记它们。具体而言,拥有超过 0.1% 的市场份额、在 GitHub 上拥有超过 2000 颗星或分叉或被超过 10 万个网站使用的应用程序被归类为相对更受欢迎的应用程序。其他的则相对不那么受欢迎。
如第一列所示,在 18 个相对不太流行的应用程序(具有 3.0M LoC)中的 14 个中发现了 40 个错误。其余 10 个错误仅在 8 个相对较受欢迎的应用程序(具有 4.0M LoC)中的三个中被发现。没有在这些包含大量代码行的应用程序(例如 WordPress、MediaWiki 和 Drupal)中检测到任何错误。这表明应用程序的大小可能与松散比较错误没有直接关系。然而,相对不那么流行的应用程序更可能有松散比较错误,可能是因为它们的维护不太好。
(3)漏报
对存在已知错误的应用程序的评估结果表明,LChecker 在此地面实况数据集中没有漏报的最后七个应用程序有八个先前已知的松散比较错误,包括七个 CVE。 LChecker 确定了所有八个已知错误。有趣的是,LChecker 还在旧版本的 PHPList (3.5.0) 中检测到了两个新的 bug。在向开发人员报告之前,它们一直保留在最新版本中。
但是,LChecker 可能仍然会遗漏潜在的松散比较错误并导致漏报。首先,LChecker 在调用目标推断方面存在局限性,因为它像其他工具一样构建了一个不完整的调用图。因此,可能不会分析一些实际可达的功能。其次,循环只展开一次,因此没有研究许多路径。一些错误可能只有在多次迭代后才会暴露。
(4)误报
LChecker 采用静态分析,因此会出现误报。许多静态检测到的松散比较错误未被验证为真正的错误,特别是在报告的 185 个案例中,只有 50 个被确认为真正的松散比较错误,135 个是误报。讨论误报的主要原因如下。
Custom sanitizers:开发人员可以编写他们的自定义函数来清理不受信任的用户数据。一些受污染的松散比较案例实际上使用这些自定义sanitizer进行了消毒,并报告为误报。但是,很难自动识别所有这些sanitizer。大约 10% 的误报属于这一类。
部分依赖:松散比较可以仅用作条件语句中整个逻辑公式的一部分。因此,执行流程不仅仅依赖于松散比较。尽管可以通过 U1-U3 绕过单独的松散比较操作,但整体条件可能仍然保持不变并导致相同的程序执行路径。这引入了大约 20% 的误报。
无法访问的代码:许多静态检测到的松散比较错误实际上无法访问。这是因为无法满足到达它们的全局路径约束。手册显示,大约 30% 的误报是由这个原因引起的。为了解决这个问题,建议在未来的工作中结合使用符号执行和约束求解。
安全函数使用:许多误报松散比较错误不会产生不一致的比较结果,因为操作数是由一些安全的内置或用户定义的函数产生的。例如,某些密码加密函数总是生成不会导致比较结果不一致的输出。大约 20% 的误报属于这一类。
其他:其余 20% 的误报是由其他问题引起的,例如数组建模不准确、分支语句中的类型推断不准确以及动态函数调用。
C.相关工作比较
PHP Joern 的检测结果显示在标有 J 的列中。由于 PHP Joern 不支持类型推断,列出了仅运行污点分析(LCBJ t aint)和真阳性案例( LCBJ tp)。总体而言,它报告了 759 个案例,其中 26 个应用程序中有 8 个中的 13 个 (1.71%) 错误被确认为真正的错误。下图每个步骤中调查并演示了 LChecker 和 PHP Joern 的检测结果。
LChecker 以更多已确认的错误和更少的错误报告来胜过 PHP Joern。 LChecker 成功检测到 PHP Joern 检测到的所有真实的松散比较错误,以及 PHP Joern 未能检测到的另外 37 个真实错误。在污点分析中,LChecker 和 PHP Joern 在 26 个应用程序中分别报告了 985 个和 759 个松散比较案例。两种工具都发现了 743 个案例(A、C 和 E),其中只有 13 个案例(A)被确认为真正的错误。由于 PHP Joern 仅针对魔术哈希问题 (U3),LChecker 报告了另外 215 个案例(B、D 和 F),但 PHP Joern 没有报告。因此,只有 LChecker 识别出 37 个松散比较错误 (B)。通过 LChecker 的类型推断,它将可能的案例数量减少到 185 个——其中 50 个 (27.03%) 是真阳性,大大限制了需要由人类分析师半手动验证的案例。相比之下,使用 PHP Joern 的分析师必须检查所有 759 个潜在案例,其中只有 13 个 (1.73%) 是真正的阳性。注意PHP Joern 报告了 16 个案例 (G),但 LChecker 没有报告。进一步研究发现,由于 LChecker 对阵列的不精确建模,它们被遗漏了。
D.性能
LChecker 的静态分析既高效又可扩展。它在 75 分钟内静态分析了 26 个 PHP 应用程序中的 7M LoC,可与 PHP Joern 在 64 分钟内完成分析相媲美。每个应用程序的分析时间显示在TimeL (LChecker) 列和 TimeJ (PHP Joern) 列中。 LChecker 在一分钟内分析了大多数应用程序,因为它采用了上下文相关的过程间分析。它只用了 12 分钟来分析 MediaWiki (1.3.41) ,它有超过一百万行的代码。这表明 LChecker 能够有效地分析复杂的现代应用程序。
E.案例研究
(1)PHPList身份验证绕过漏洞
PHPList (3.5.0)中识别的一个身份验证绕过漏洞 LChecker 显示在下图的第 9 行。 validateLogin() 函数的参数 $login 和 $password 由尝试登录的远程用户提供作为管理员。该函数从数据库中检索管理员帐户先前散列的密码到 $passwordDB 中。第 6 行通过使用 md5() 函数计算其散列值来“加密”用户提供的密码 ($password)。在第 9 行粗略地比较了这两个密码。
这种松散比较很容易受到攻击,因为两个操作数可以是两个不同的科学记数法字符串,导致 ($encryptedPass == $passwordDB) 为 True。因此,即使没有正确的凭据,用户也可以通过密码验证并提升权限以充当管理员。此外,在绕过身份验证后,攻击者可以使用仅针对管理员的特权 API 发起其他攻击,例如代码注入、SQL 注入等
LChecker 还在 PHPList (3.5.0) 中发现了另外两个身份验证绕过漏洞和两个正确性违规错误,包括一个已知的身份验证绕过漏洞 (CVE-2020-8547)。一个有趣的事实是,这个 CVE 在 3.5.1 版本中被报告并修复,然而,另外两个身份验证绕过漏洞仍然存在五个月,直到检测到并将它们报告给在 3.5.4 版本中修复它们的开发人员。这表明开发人员可能不太了解松散比较错误及其安全影响。
(2)PHP-ML正确性违规错误
PHP-ML (2.0) 是一个流行的 PHP 机器学习库。它提供了大量的分类、聚类等算法。用户可以直接使用提供的算法来训练他们的分类模型。 LChecker 在 PHP-ML (2.0) 的线性分类器中检测到两个可能导致错误预测的正确性违规错误。
决策树桩算法中的一个如下图所示。第 4 行的样本数组 ($samples) 映射到第 6 行和第 10 行的标签数组 ($labels)。给定训练数据,输入 [5.5]显然与第二类更相关。所以正常的 $classifier 将输入 [5.5] 预测到 label1 的第二个类别(第 6-8 行)。然而,如果标签被命名为带有magic zero字符串的第 10 行,分类器就会错误地将相同的输入数据预测到第一个类别 0e2 中。
这是由在分类器的训练代码中发现的松散比较错误引起的。对于此类二元分类情况,函数calculateErrorRate() 被迭代调用以计算多个训练阈值的错误率。然后将具有最小错误率的阈值用于该模型的预测。在函数 calculateErrorRate() 中,它首先根据 $samples 中的每个样本值和阈值(第 18 行)预测一个标签。然后它将预测标签与 $targets 中的训练数据指定的真实标签进行比较(第 20 行)。然而,第 20 行的松散比较总是有一个 False 值,因为“0e2”和“0e1”都被评估为 0。因此第一个标签总是被预测。
如果将此类标签用作训练数据,则训练模型无法生成正确的预测。这对使用这种模型的其他组件构成了严重威胁。例如,如果该模型用于身份验证等关键场景,则整个系统可能会出现故障。 LChecker 还在 PHP-ML (2.0) 的感知器分类器中检测到另一个类似的正确性违规错误。
0x07 Discussion
验证松散比较错误:在解释器中增强了 PHP,以帮助人类验证松散比较错误。由于静态分析已经确定了一些潜在的易受攻击的路径,因此半手动验证的难度大大降低。评估结果还表明,用于验证错误的手动工作是可以接受的。然而,符号执行和定向模糊测试等技术可能会被应用于进一步自动化此类过程。将来,计划利用符号执行来收集到达错误的路径约束,并查询约束求解器以获取可用于动态错误验证过程的可能解决方案。
修补松散比较错误:一些松散比较错误可以在本地修补,即通过简单地更改比较操作。将松散比较转换为严格比较以消除隐式类型转换并强制操作数类型可以避免一些错误。修复一些其他松散比较错误可能需要更改整体逻辑。例如,程序可能被设计为允许比较操作数为不同路径的多种类型。因此,直接将其松散比较转换为严格比较可能会破坏某些类型或路径中的预期功能。因此,这可能需要开发人员对每个案例进行不同的修补。
可移植性:除了相等和不等运算符,隐式操作数类型转换也可能发生在其他松散比较运算符中,例如大于运算符 (>)。这些松散比较运算符也受到类似的松散比较错误的影响。目前只使用相等和不等运算符实现方法来检测有问题的松散比较。然而,所提出的定义和方法可以移植到其他松散比较运算符而不失一般性。将其作为未来的工作。
0x08 Conclusion
松散比较错误会带来严重的安全威胁,例如权限提升和功能破坏。 在本文中,对 PHP 中的松散比较错误进行了第一次系统的研究。 研究了几个已知漏洞,正式定义了松散比较错误并展示了它们的安全影响。 然后开发了 LChecker,这是一种静态分析工具,可以检测松散比较错误。 它采用上下文敏感的过程间数据流分析和类型推断算法来识别错误。 LChecker 发现了 42 个新的松散比较错误,其中包括 9 个新的 CVE,这证明了其在错误检测方面的有效性。