大部分网站存在漏洞-Perl最安全
程序设计语言通常不构成安全风险,风险是由程序员带来的。几乎每种语言都有某些缺陷,这些缺陷在某种程度上可能有助于创建不安全的软件,但软件的整体安全性仍然在很大程度上取决于开发者的安全意识。Perl也有安全“陷阱”,然而大多数Perl程序员并不了解这些陷阱。Google的开发者大使Anthony Ferrara分析了PHP网站安装的版本号,比较了安全版本和不安全版本的安装比例,发现只有21.71%的网站是安全的。这个数字其实还是乐观的估计,没有考虑没有维护的发行版支持版本。
PHP的安全版本包括:5.6.4、5.5.20和5.4.36;而Linux发行版维护的版本包括:5.4.4,5.3.3,5.5.12,5.5.9....等等。相比之下,Perl和Python网站的安全比例要高得多。
Platform:% Installs That Are Secure
Perl:82.27%
Python:77.59%
Nginx:64.48%
Apache:61.96%
WordPress:60.45%
Drupal:45.23%
PHP:25.94%
Perl脚本中存在的问题与存在的安全风险
在此介绍一些被广泛误用和忽视的Perl特性,展示perl语言不正确的使用方式,错误使用方式又是如何对运行程序的用户,及系统构成威胁。也会展示如何利用这些漏洞,以及如何修复或避免它们。
基本用户输入漏洞
Basic user input vulnerabilities
Perl脚本中安全问题的一个主要来源是未正确验证(或未验证)的用户输入。任何时候你的程序可能会从一个不受信任的用户那里获取输入,即使是间接的,都应该小心。例如使用Perl编写CGI脚本,那么恶意用户可能会向您发送虚假输入。
如果未经验证就使用,对此类应用程序的不当输入就可能会导致许多问题。在没有正确验证的情况下,使用用户提供的参数执行其他程序,使最常见的错误。
system() 和exec()函数
Perl语言以其“粘贴语言”而出名,它可以出色地调用其他程序来帮协助完成它的工作。通过收集一个程序的输出,以特定的方式重新格式化,并将其作为输入传递给其他程序,仔细地协调它们的活动,从而使一切都能顺利运行。
执行外部程序或系统命令的一种方法是调用exec()函数。当Perl遇到exec()语句时,它会查看调用exec()时使用的参数,然后启动一个执行指定命令的新进程。Perl从不将控制权返回到调用exec()的原始进程。
另一个类似的函数是system(),system()与exec()函数非常相像。唯一的区别是perl首先从父进程中派生一个子进程。父进程等待着子进程结束,然后继续执行程序的其他部分。
下面我们将详细讨论system()与exec()的函数调用。
system()抽象列表
1.程序名称
2.其余元素(作为传递参数传递给程序)
如果只要一个参数,system()调用方式会有不同。这种情况下perl会扫描参数,查看是否含有shell字符。如果是则继续解释,perl将生成一个命令shell,如果perl不了解特殊的shell字符,perl会将字符串分解成单词,并调用更高效的C库调用execvp()
假设我们有一个CGI表单,它要求输入用户名,并显示一些包含该用户统计信息的文件。我们可以使用system()调用cat。
system ("cat /usr/stats/$username");
$username来自以下表单:
$username = param ("username");
用户填写表单时,例如;username=jdimov,然后提交表单。
perl在字符串”cat /usr/stats/jdimov“中找不到任何元字符,它运行cat程序,然后返回脚本。这个脚本看起来无害,但实际上可能被恶意攻击者利用。问题在于,通过表单的‘username’字段中使用特殊的字符,攻击者可以通过shell执行任意命令。
例如假设攻击者发送字符串"jdimov; cat /etc/passwd"
perl将分号识别为元字符,并传递给shell
cat /usr/stats/jdimov; cat /etc/passwd
攻击者同时获得虚拟统计文件和密码文件。如果想具有破坏性,可以祭出“rm -rf”。
前面提到过,system()接受一个参数列表,并将第一个元素作为命令执行,将其余元素作为参数传给它,只需要稍微更改脚本,以便执行我们的程序。
system ("cat", "/usr/stats/$username");
因此我们分别为程序指定每个参数,所以永远不会调用shell。所以使得rm -rf不管用,因为攻击字符串被解释为文件名。
这种方法比单参数版本要好得多,因为它避免了使用shell,但仍然存在潜在的缺陷。特别是,我们需要担心username的值是否会被用来利用正在执行的程序(在本例中为“cat”)的弱点。例如,攻击者仍然可以通过将$username设置为字符串“../../etc/passwd”,利用我们重写的代码来显示系统密码文件。
根据程序的不同,许多其他事情可能会出错。例如一些应用程序将特殊字符序列解释为执行shell命令的请求。一个常见的问题是,某些版本的Unix“mail”实用程序,在看到~!上下文中的转义序列。因此用户输入包含!rm -”在某些情况下可能会导致问题。
就安全性而言,上面提到的 system()函数同样适用于exec()。
open()函数
Perl中的open()函数用于打开文件。在最常见的形式中,它的使用方式如下:
open (FILEHANDLE, "filename");
像这样使用,“filename”以只读模式打开。如果“filename”的前缀带有“>”符号,则会打开该文件进行输出,如果该文件已经存在,则会覆盖该文件。如果它的前缀为“>>”,则可以进行追加。前缀“<”打开文件进行输入,但如果没有使用前缀,这也是默认模式。使用未经验证的用户输入作为文件名的一部分的一些问题应该已经很明显了。例如,反向目录遍历技巧在这里同样有效。这里还有其他担忧的地方。让我们修改脚本以使用open()而不是“cat”。会有类似于:
open (STATFILE, "/usr/stats/$username");
然后是一些从文件中读取并显示的代码。Perl文档告诉我们:
如果文件名以“|”开头,则该文件名将被解释为将输出传输到的命令;如果文件名以“|”结尾,则该文件名将被解释为将输出传输到我们的命令。然后,用户可以在/usr/stats目录下运行任何命令,只需修复一个“|”。向后目录遍历允许用户在系统上执行任何程序。 解决此问题的一种方法是,始终使用“<”符号作为前缀,明确指定要打开该文件进行输入:
open (STATFILE, "</usr/stats/$username");
有时我们确实希望调用外部程序。例如,假设我们希望更改脚本,使其读取旧的纯文本文件/usr/stats/username,但在向用户显示之前将其通过HTML过滤器。比方说,我们有一个方便的实用工具,就为了这个目的。一种方法是这样做:
open (HTML, "/usr/bin/txt2html /usr/stats/$username|");
print while <HTML>;
不幸的是,程序仍然是通shell。我们可以使用另一种形式的open()调用来避免生成shell:
open (HTML, "-|") or exec ("/usr/bin/txt2html", "/usr/stats/$username");
print while <HTML>;
当将管道打开到“-”时,无论是用于读取(“-|”)还是用于写入(“|-”),Perl都会分叉当前进程,并将子进程的PID返回给父进程,将0返回给子进程。“or”语句用来判定父子进程。如果我们在父进程(open()的返回值为非零),则继续执行print()语句。否则,我们就是子进程,所以我们执行txt2html程序,使用exec()和多个参数来避免通过shell传递任何内容。发生的情况是,子进程将txt2html生成的输出打印到STDOUT,然后安静地小时,同时父进程从STDIN读取结果。同样的技术也可用于管道输出到外部程序:
open (PROGRAM, "|-") or exec ("/usr/bin/progname", "$userinput");
print PROGRAM, "This is piped to /usr/bin/progname";
当需要管道时,这些形式的open()应该总是优先于直接管道open(),因为它们不穿过shell。现在假设我们将统计数据文件转换为格式良好的HTML页面,为了方便起见,我们决定将它们存储在显示它们的Perl脚本所在的目录中。
那么我们的open()语句可能如下所示:
open (STATFILE, "<$username.html");
当用户传递给username=jdimov时,脚本显示jdimov.html。
这里仍然有可能被攻击。与C和C++不同,Perl不使用null字节来终止字符串。因此字符串"jdimov\0blah"在大多数C库调用仅仅解释为"jdimov" ,但在Perl中仍然是 "jdimov\0blah"。
当Perl将包含null的字符串传递给用C编写的内容时,问题就出现了。UNIX内核和大多数UNIX shell都是纯C编写的。Perl本身主要也是用C编写的。
statscript.pl?username=statscript.pl%00
如果该脚本与我们的html文件位于同一目录中,那么我们可以使用此输入来欺骗这个脚本。在这种情况下,可能不会对安全造成太大威胁,但对其他程序肯定会造成威胁,因为它允许攻击者分析源代码中的其他可利用弱点。
Backticks
在Perl中,读取外部程序输出的另一种方法是将命令包含在反标记中。因此,如果我们想将stats文件的内容存储在标量$stats中,我们可以执行以下操作:
$stats = `cat /usr/stats/$username`;
这是通过shell的。任何一行命令只要涉及到用户输入的脚本都会面临讨论过的所有安全问题。有几种不同的方法可以使shell不解释可能的元字符,但最安全的方法是不使用反勾号。相反,打开一个STDIN的管道,然后fork并执行外部程序,就像我们使用open()所做的操作。
eval()和/e regex修饰符
eval()函数可以在运行时执行一段Perl代码,返回最后一条经过计算的语句的值。这种功能通常用于配置文件之类的东西,这些文件可以写成perl代码。除非您完全信任要传递给eval()的代码源,否则不要执行eval$userinput之类的操作。这也适用于正则表达式中的/e修饰符,该修饰符使Perl在处理表达式之前对其进行解释。
黑名单输入
本节讨论的大多数问题的一种常见方法是过滤掉不需要的元字符和其他有问题的数据。例如,我们可以过滤掉所有句点,以避免向后遍历目录。同样,每当我们看到无效字符时,也可能失败。
这种策略被称为“黑名单”。实际上是,如果某件事没有被明确禁止,那么它一定是好的。一个更好的策略是“白名单”,它规定,如果某件事情没有明确允许,那么它必须被禁止。
黑名单最重要的问题是很难保持完整和更新。您可能忘记过滤某个字符,或者您的程序可能必须切换到具有不同元字符集的不同shell。
与其过滤掉不需要的元字符和其他危险的输入,不如只过滤合法的输入。例如,如果用户输入包含字母、数字、点或@符号(用户电子邮件地址中可能包含的字符)以外的任何内容,则以下代码段将停止执行安全关键操作:
unless ($useraddress =~ /^([-\@\w.]+)$/) {
print "Security error.\n";
exit (1);
}
基本思想不是试图编写一个要防范的特殊值列表,而是提出一个可以安全接受的值列表。当然,可接受输入值的选择会因应用程序而异。选择可接受的值时,应当尽量减少其造成损害的可能性。
避开Shell
当然,我们应该尽量少使用shell。然而,这种技术使用得更广。如果调用具有特殊序列的编辑器,可以确保不允许使用这些序列。一般通过使用Perl模块,可以避免使用外部程序执行函数。
安全问题的其他来源
不安全的环境变量
用户输入确实是Perl语言的安全隐患之一,但是我们在编写perl程序时还需要考虑到其他因素。在shell下或由web服务器运行的脚本的一个常见弱点是不安全的环境变量,最常见的是路径变量。当你仅通过指定外部应用程序或实用程序的相对路径从代码中访问该外部应用程序或实用程序时,你会使整个程序及其运行系统的安全性受到影响。假设你有这样一个system()调用:
system ("txt2html", "/usr/stats/jdimov");
为了使调用起作用,你假设txt2html文件位于PATH变量中某个位置包含的目录中。但是,如果发生这种情况,使攻击者改变你的路径,指向其他恶意程序的路径,则使你的系统安全将不再得到保证。为了防止这种情况发生,每个需要远程安全意识的程序都应该从以下内容开始:
#!/usr/bin/perl -wT
require 5.001;
use strict;
$ENV{PATH} = join ':' => split (" ", << '__EOPATH__');
/usr/bin
/bin
/maybe/something/else
__EOPATH__
如果程序依赖于其他环境变量,则在使用前还应明确重新定义这些变量。另一个危险的变量(这一个更特定于Perl)是@INC数组变量,它非常类似于PATH,只是它指定Perl应该在何处查找要包含在程序中的模块。@INC的问题与PATH的问题几乎相同,有人可能会将您的Perl指向一个与您所期望的模块具有相同名称和执行相同操作的模块,但它也会在后台执行颠覆性操作。因此,@INC不应该比PATH更受信任,应该在包含任何外部模块之前完全重新定义。
setuid脚本
通常,Perl程序以执行它的用户的权限运行。通过创建脚本setuid,可以将其有效用户ID设置为能够访问实际用户不访问的资源的用户ID(即,包含程序的文件的所有者ID)。例如,passwd程序使用setuid获取对系统密码文件的写入权限,从而允许用户更改自己的密码。由于通过CGI接口执行的程序是以运行web服务器的用户的权限运行的(通常是用户“nobody”,其权限非常有限),CGI程序员经常试图使用setuid技术让他们的脚本执行他们无法执行的技巧。这可能有用,但也可能非常危险。首先,如果攻击者找到了利用脚本弱点的方法,他们不仅可以访问系统,还可以使用该脚本的有效UID(通常是“根”UID)的权限访问系统。
为了避免这种情况,Perl程序应在任何文件操作之前将有效UID和GID设置为进程的真实UID和GID:
$> = $< # set effective user ID to real UID.
$) = $( # set effective group ID to real GID.
CGI脚本应该始终以尽可能低的权限运行。请注意,在setuid脚本中小心操作并不总能解决问题。某些操作系统的内核中存在bug,这使得setuid脚本本身就不安全。出于这个原因和其他原因,Perl在运行setuid或setgid脚本时会自动切换到特殊的安全模式(污染模式)
rand()函数
在确定性机器上生成随机数是一个非常重要的问题。在安全关键型应用程序中,随机数被广泛用于从密码生成到密码学等许多重要任务。为此,生成的数字必须尽可能接近真正的随机数字,这使得攻击者很难(但决不是不可能)预测算法生成的未来数字。Perl rand()函数只调用标准C库中相应的rand(3)函数。这个例行程序不是很安全。函数的作用是:根据称为种子的初始值生成一系列伪随机数。给定相同的种子,使用rand()的程序的两个不同实例将产生相同的随机值。在许多C实现中,以及5.004之前的所有Perl版本中,如果未明确指定种子,则将根据系统计时器的当前值计算种子,该值不是随机的。任何一个有自尊心的破解者都可以在给定的时间点上获得一些关于rand()生成的值的信息,从而准确地预测rand()接下来将生成的数字序列,从而获得危害系统所必需的内容。
为了解决rand问题(),其中一个方案式使用Linux系统内置随机数生成器/dev/random and /dev/urandom
这样得到的随机数字比rand()更好,但与其他函数一样,他们都有缺点。这两个设备的区别在于/dev/random当它的随机池没有随机数字时会停止提供随机数字。这时候,/dev/urandom 用户能使用破译生成的密码数字。
竞态条件Race Conditions
Race Conditions通常与缓冲区溢出是老手黑客的惯用手段。
unless (-e "/tmp/a_temporary_file") {
open (FH, ">/tmp/a_temporary_file");
}
如果不仔细看,我们会认为这是一个合法程序,不会造成任何伤害。我们首先检查tmp临时文件是否存在,如果不存在,则使用Perl创建。
此程序问题在于,打开文件,检查是正确的。当然完全有可能这个文件的状态发生改变。假设文件不存在,可以让攻击者有机可乘,例如执行命令:
ln -s /tmp/a_temporary_file /etc/an_important_config_file
现在,我们做的临时文件,实际上是为了配置重要文件。因为我们相信临时文件不存在。因为echeck提示这个临时文件不存在,所以我们继续打开它进行写入。结果,我们配置的文件被删除。有些情况就像这样,攻击者可以抢占两个操作并且更改某些东西,这种情况被称为Race condition竞态条件。这意味着只使用一个系统调用来检查一个文件并同时创建文件,而不给处理器切换另一个进程的机会。这并不代表不可能。
下面程序使用sysopen并且指定只写模式。这样即使文件被伪造,也不会在打开文件进行写入时杀死它。
unless (-e "/tmp/a_temporary_file") {
#open (FH, ">/tmp/a_temporary_file");
sysopen (FH, "/tmp/a_temporary_file", O_WRONLY);
}
关于Perl语言的缓冲区溢出
一般来说,Perl脚本不易受到缓冲区溢出的影响,因为Perl会在需要时动态扩展其数据结构。Perl跟踪每个字符串的大小和分配长度。在每次写入字符串之前,Perl确保有足够的可用空间,并在必要时为该字符串分配更多空间。然而在一些较旧的Perl实现中存在一些已知的缓冲区溢出情况。值得注意的是,5.003版可以利用缓冲区溢出进行攻击。从早于5.004的Perl发行版构建的所有版本的suidperl(一个设计用于解决某些内核setuid脚本中的竞争条件的程序)都是可利用的(CERT Advisory CA--97.17)。
小结
在研究Perl的这些方面并查看一些特征性示例时,我们的目标是培养一种直觉,尽量一眼就看到Perl脚本中的安全问题,避免在程序中犯类似的错误。