如果我们选择JavaScript
作为编程的首选语言,那么我们不必担心字符串连接问题。然而我们经常遇到的一个反复出现的问题是必须等待JavaScript
的npm
包管理器安装完成所有必需的依赖项。
在这篇文章中,我们研究了为什么字符串连接是一个复杂的问题,为什么不能在没有转换的情况且在低级编程语言中连接不同类型的两个值,以及字符串连接如何导致漏洞。 我们还将分析如果格式字符串包含某些类型数据的占位符,如果它们受到攻击者控制,则会导致严重问题。之后,我们将选择一种简单的方法来解决他们。
首先,让我们看一下JavaScript字符串连接,它在连接不同类型的数据时会遇到不同的情况。 这里有些例子:
1 + 1 === 2 // 很显然
'1' + 1 === '11' // 很直观
1 + true === 2 // 对吗?
1 + {} === '1[object Object]' // 什么时候需要这种情况?
true + undefined === NaN // 这显然不是一个数字,但它是什么?
typeof NaN === 'number' // 好 那接下来呢?
毕竟,JavaScript并不是那么完美。 而且,虽然在JavaScript中连接两个不同类型的值很容易,但它不一定会产生预期且有用的结果。 一些混淆是因为:在JavaScript中,连接运算符与添加运算符相同,因此它必须决定是连接值还是添加它们。
让我们来看看另一种不是非常严谨的语言,比如PHP。 它如何处理串联? 好吧,在PHP中你没有plus运算符。 而是使用如图所示的点:
"The number one: " . 1 // 如其工作
"10" . 1 === "101" // 这只是在字符串10中添加1
"10" . 1.0 + "1.0" // 猜测下这个语句是用来做什么的?
显然,这是(float)102
。我们将字符串“10”
与float 1.0
连接起来,这生成字符串“101”
。 然后将字符串“1.0”添加到字符串“101”中,最后得到(浮点)102。 这非常容易理解。 我们可以将两个字符串一起添加,最后得到float类型的值。
我们可以讨论PHP的输出系统存在的问题以及它如何导致现实生活中的漏洞(参见PHP类型漏洞的详细说明)。 但是我们不得不承认JavaScript和PHP中的字符串连接非常方便。
如果使用像C这样的低级语言这样做会发生什么吗? 如果我们试图在字符串中添加一个浮点数,C语言并不会预期执行。
在此示例中,C不知道我们要实现的目标。 C甚至不允许你将两个字符串添加到一起来连接它们,更不用说不同数据类型的字符串了。 这里,字符串只是一个字符序列,末尾有一个NULL
字节。 通常,有一个指向字符串第一个字节的指针。 错误消息中的char *
引用该指针。
在JavaScript
中,我们需要担心NULL
字节或指针。 字符串在JavaScript
中也是不可变的,因此我们不能只更改其中的数据。 相反,如果需要更改数据,则需要创建一个新字符串,并保持旧字符串不变。
在这个例子中,我们试图将“String”一词改为“Strong”,但它不起作用。 x变量不会改变。 虽然这个功能作用有限,但是它实际上是一个非常好的功能。它能够提示我们曾用过JavaScript中的对象。
double
是指所谓的“双精度浮点数”。对于那些不知道的人,浮点数作为一个神秘的字节序列存储在内存中,但似乎这样没有意义。这样做的原因是它们通常以IEEE 754中定义的格式存储在科学记数法中(参见分步教程二进制分数和浮点数)。
C如何将一个字符串加入一个奇怪的字节组合,即浮点数,在内存中一起保留它们的初始含义?
答案是:不能。如果将浮点数附加到字符串的末尾并相应地移动NULL字节,则只会将这些字节视为文本,并尝试打印无法显示的垃圾或字符。这在JavaScript中是不可想象的;这是C编程中的常见问题。
因此,为了将浮点数添加到字符串,C需要首先将其IEEE 754格式转换为人类可读的字符串表示。为此,C库提供* printf
系列函数。
如果要将任何类型的数据放入字符串中,或将字符串连接在一起,则只需使用printf
即可。 我们需要传递一个格式字符串,其中包含指定数据类型的占位符等。 我们来看一个例子。
如果我们编译此代码并运行它,它将只打印'This is a string。'
。 可能已经猜到,%s
是字符串的占位符,但也存在其他数据类型的占位符,例如整数,浮点数甚至单个字符。 您还可以以十六进制表示形式打印数据。 然而,有一个问题。 如果你错误地使用它,它是完全不安全的。
应该如何在C中连接两个字符串?
许多人都会咨询Stackoverflow
,因为它是一个关于常见问题的直截了当的回答网站。
最高投票的答案使用strcat
,一个根本不检查任何大小的函数,这在C中总是一件坏事,因为如果你不小心它会导致缓冲区溢出。 问题下方的评论建议使用strlcat
,人们认为它是一个更安全的strcat版本。 但后来其他人建议使用strcat_s
。 这似乎是另一个更安全的strcat版本。
Stackoverflow
中的其他人喜欢使用snprintf
,根据评论,这显然是“大不了”,因为显然_snprintf
是不安全的。 但是,另一个帖子叫做停止使用strncpy
了! 提到它可以使用。 如果您还不熟悉Stackoverflow
或C,那么这个线程完美地说明了两者的问题。
我已经提到如果使用不正确,` printf`函数会很危险。 但是,一个功能,其唯一目的是格式化输出,如何导致可能导致任意代码执行的漏洞?
详细答案超出了本博文的范围,但这里是一个概述。 局部变量和函数参数存储在内存中的特殊位置 - 堆栈上。 对于x86 Linux二进制文件,这通常意味着一旦调用函数,它将直接从堆栈中获取函数参数。 那么如果你有一个没有参数的printf调用会发生什么呢,如下所示?
printf("%x%x%x%x%x");
它将简单地抓取堆栈中的数据并以十六进制格式打印。 这可能包括堆栈和返回地址,堆栈cookie(旨在防止缓冲区溢出利用的安全机制),变量和函数参数的内容,以及对攻击者非常有用的所有其他内容。 因此,如果printf
中的格式字符串是用户可控制的,那就非常危险。 此外,如果使用足够的格式说明符,最终可能会得到一个指向用户可控输入的堆栈指针,就像格式字符串本身一样。
然后,我们可以向内存中的任何位置提供地址,并使用%s
读取数据。 此外,如果启用了%n
,则可以在内存中的任何位置写入任意数据,即已打印的字节数。 这听起来不是很多,但它可能允许攻击者覆盖返回地址,并将代码流重定向。
格式字符串不仅在C中可用。以下是其他语言,我们还可以在其中找到它们用于Web应用程序。
如果熟悉PHP,那么可能知道它还具有printf
函数。 但是,PHP将检查是否有比函数参数更多的格式说明符(除了少数例外)。 如果在C中以不安全的方式使用printf,通常只会收到编译器警告.PHP会简单地中止脚本的执行。
Ruby类似于PHP。 当尝试使用Ruby的printf函数时,它会将参数的数量与其对应的格式说明符进行比较。 如果说明符多于参数,则执行将停止。
Perl是不同的。 Perl很乐意接受传递给它的格式说明符,但结果不会很有用。 但是,我们可以使用%n说明符覆盖已经打印的字符数的变量。 这是一个代码示例:
$str = "This is a string";
printf("AAAAAAAAAAAAAAAAAAAAAAAA%n\n",$str);
print($str); # this is 24 now, as there were 24 'A's printed
还有一个不那么明显的问题。 Perl中大量的比较结果是出乎意料的。 它与PHP类似,我们需要在比较时使用“eq”标识符而不是==
符号,以便进行或多或少的严格比较。 因此,而不是1 == 1
你会写1 eq 0. ==
显然是大多数人的方式,比在每次比较中键入“eq”
更方便。 如果我们查看Perl表,我们可以看到整数0与任何字符串相比,返回一个匹配项。 现在看下面的代码:
$databasePassword = "secret pass"; #unknown to attacker
$password = "A password"; #user supplied string
# ... some more code ...
printf("%n <somehow user controlled format string>", $password); #writes integer 0 into $password variable
# ... some more code ...
if($databasePassword == $password) { # this will match!
print("Password matches");
} else {
print("Password does not match");
}
此代码将打印“密码匹配”。 这个问题是你甚至不需要一个整数来通过检查。 即使字符串“0”也会起作用。 (PHP有类似的问题,但它至少需要一个整数,你不能作为GET或POST参数传递。)
如果它允许更改可能不是用户可控制的输入值,则此方法仍然有用。 这可能会导致代码中的潜在可利用行为。 如果您想了解有关Perl中其他不安全行为的更多信息,请参阅Perl Jam 2。
是否知道人们在Lua中编写Web应用程序? 甚至还有一个用于特定目的的Lua Apache模块。 例如,我在路由器中看到了基于Lua的Web应用程序。 这可能是由于Lua的体积非常小,因此适合在磁盘空间紧张的路由器中使用。 Lua也可用作“魔兽世界”界面自定义的脚本语言。 似乎下一个逻辑步骤是使用它来编写Web应用程序。
它还有一个类似于printf
的string.format
函数。 它仅支持一组有限的格式说明符,%n不是其中之一。 另外,它检查参数的数量是否与格式说明符的数量相匹配。 如果不匹配,则会发生错误。
Java中有一个System.out.printf
函数。 与大多数其他语言一样,它检查参数的数量是否与格式说明符的数量相匹配。 同样,有一个%n
说明符,但它没有做到你所期望的。 出于某种原因,它将为正在运行的平台打印相应的行分隔符。 如果你是来自C,这会令人困惑,但你不能指望与Java的格式字符串兼容,即使两个函数具有相同的名称。
Python
是一个非常有趣的案例,因为它有两种不同的常用字符串格式化方法。 PyFormat
网站致力于Python中的字符串格式化,认为Python自己的文档“过于理论化和技术化”。
首先,使用我们已经知道的格式说明符的旧方法。如下所示:
print("This is %s." % "a string")
如我们所见,这使用格式字符串后跟百分号。 参数写在符号后面。 如果有多个参数,则需要使用元组。 这里%n
不受支持。 此外,将参数的数量与格式说明符的数量进行比较,如果python
不匹配则抛出错误。
但是还有一种新的字符串格式化方法。 我们可以通过在字符串上调用内置的.format
方法来使用它。
print("{} is {} years old".format("Alice", 42))
它甚至允许我们从格式字符串中访问传递的对象的属性。
print("{person[name]} is {person[age]} years old".format(person={"name":"Alice","age":42}))
正如我们所看到的,他们两个都打印出“爱丽丝已经42岁了”。 它们并不真正需要格式说明符,因为Python会在大多数时间自动将它们转换为正确的字符串表示形式。 但是,第二种方法可能会导致信息泄露漏洞。
一篇名为Be Careful with Python
的新式字符串格式的博客文章很好地描述了这种方法。 基本上,根据传递的数据,攻击者可以读取敏感信息,远远超出我们的意图。 我们来看一个例子吧。
API_KEY = "1a2b3c4d5e6f"
class Person:
def __init__(self):
"""This is the Person class"""
self.name = "Alice"
self.age = "42"
print("{person.name} is {person.age} years old".format(person=Person()))
虽然这一开始似乎没有漏洞,但如果攻击者可以在此处控制格式字符串,则可以轻松打印API_KEY
变量。但它是如何工作的?
首先,知道person对象包含的不仅仅是我们设置的名称和年龄属性,这一点很重要。它还具有可直接访问的__init__
功能。 Python实例化Person类时会自动调用它。它仍然是格式字符串中可用的用户定义函数。但是我们能在这做什么呢?我们无法从格式字符串中调用函数。但是,我们可以访问属性。
在Python中,函数具有一些通常不需要使用的特定属性。例如__name__
表示函数的名称。但是,这里有另一个有用的属性,叫做__globals__
。文档描述如下:
'对包含函数全局变量的字典的引用 - 定义函数的模块的全局命名空间。
这很有趣,因为根据定义,API_KEY
变量位于__init__
函数的全局命名空间中。这给我们留下了以下格式字符串。
"API_KEY: {person.__init__.__globals__[API_KEY]}".format(person=Person())
之后输出的结果为:
API_KEY: 1a2b3c4d5e6f
还有其他这样的键,例如__doc__
,它打印函数的文档字符串(可能产生一些有用的信息)。 还有其他文件名。 完整列表在Python文档内容中提供。
我对Python不太熟悉,不知道博客文章中提供的修复是否合适,但它类似于我建议避免使用的黑名单方法。
如果需要向用户提供格式字符串,则可能需要使用旧格式。 但是,一般来说,我建议尝试避免格式字符串中的用户输入。
通常,JavaScript
不需要任何格式字符串。 你应该使用内置的替换功能自己实现它们,这完全不令人沮丧和过于复杂。 有外部库模仿它们,但由于JavaScript
库的总数很大,因此检查每个库中的意外行为是一项不可能完成的任务。
尽管如此,JavaScript
为我们提供了一种格式化输出的方法,无需替换和连接。 我们可以使用所谓的模板文字。 顾名思义,你不能在运行时动态创建这样的字符串,除非你使用像eval
这样的函数,但我强烈建议不要这样做。
这是一个例子:
const name = "Alice";
const age = 42;
console.log(`${name} is ${age} years old`)
我们使用反引号,在美元符号后面的花括号之间写入变量名:$ {var}
。
由于没有简单的方法让用户能够定义他们自己的模板文字而不允许他们执行任意JavaScript
代码,我想指出模板文字在绕过黑名单过滤器时是JavaScript
中最好的东西。 为什么? 这样的过滤器可以删除所有"(" and ")"
字符,因此很难执行JavaScript代码。 例如,仍然可以使用onerror
事件处理程序和throw
关键字,但模板文字更方便,具体取决于要执行的函数。 我们只需在函数名称后面编写模板文字即可执行:
alert`some popup message`
这就是为什么黑名单永远不是一个好主意。
XSS漏洞和格式字符串还存在另一个威胁。如果使用格式字符串函数生成的输出容易受到XSS的攻击,则应尽快修复此问题。但是大多数用户都有一个安全网,除了那些在iOS之外使用Firefox的用户。大多数版本的谷歌浏览器,Safari,IE和Edge都有一个内置的XSS过滤器,定期更新。
问题在于这些过滤器的工作方式。它们比较用户输入和服务器发回的生成输出。如果它们足够相似并且包含危险的标签或参数,则不会呈现页面或删除危险输入。这些过滤器随着时间的推移变得更好,但它们仍然不是万无一失的。因此,如果在服务器端处理输入并进行更改,则筛选器将不会检测到该漏洞。在下面的示例中,Chrome目前不会检测到XSS漏洞,并会执行JavaScript代码。
//server side code
printf(user_input, "Alice", 42);
URL:
https://example.com/?user_input=<iframe src="javascript:alert(1)||%s">
输出将是<iframe src = "javascript:alert(1)||Alice">
,并将出现一个警告弹出窗口。
格式字符串漏洞的影响在很大程度上取决于我们使用的语言。 一般的经验法则是避免使用包含用户输入的格式字符串。 相反,我们应该始终将该输入作为参数传递给格式化函数,这是避免与格式字符串相关的漏洞的通用方法。 当然,我们应该始终根据将要使用的上下文清理用户提供的输入。
我们现在应该对Web应用程序和其他地方的格式字符串利用有一个基本概述。 我建议测试Netsparker Web
应用程序安全扫描程序。 它与最流行的持续集成解决方案和问题跟踪系统无缝集成,让我们有更多时间阅读Web应用程序漏洞,减少对它们的担忧。
本文为翻译文章,原文为:https://www.netsparker.com/blog/web-security/string-concatenation-format-string-vulnerabilities/