CGI(Perl)的漏洞及防范措施-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

CGI(Perl)的漏洞及防范措施

简介: CGI(Perl)的漏洞及防范措施(1) 一般来说,编程语言本身并不是导致安全隐患的主要因素,事实上,软件的整体安全性仍然大部分取决于软件制造者的知识面、理解能力和安全意识。

CGI(Perl)的漏洞及防范措施(1)

一般来说,编程语言本身并不是导致安全隐患的主要因素,事实上,软件的整体安全性仍然大部分取决于软件制造者的知识面、理解能力和安全意识。

在这一小节里,我们将来介绍一下Perl中一些最普遍被误用和忽视的属性,以及这些属性被误用后对系统及数据的安全有什么样的威胁,并且还讲解了如何去发现并挖掘出这些隐患来进行修改。

1. 用户输入上的弱点

Perl脚本中产生的安全问题中,有一个是提交信息的脚本在运行时对用户输入的信息不经过确认而引起的对服务器的威胁。每次Perl程序要从一个不信任的用户那里获取输入的信息时,即使采用的是非直接的方式,对系统安全仍然是有威胁的。开发者在用Perl写CGI脚本时,要考虑到可能会有恶意用户输入恶意信息,如果不加以确认就认可并使用了这些信息,就很可能会导致系统多方面出错。目前最常见和明显的错误是可能使系统没有经过确认就去执行用户提交信息中定义的其他程序。

2. syetem()和exec()函数

Perl以能被用作一种“粘合”语言而著称——它能够通过如下方式完成一个出色的工作:在调用其他程序来为它工作的时候,通过采集一个程序的输出,将它重新格式成一种特定的方式后再传递给其他程序并协调其他程序的运行,从而使各个程序能更好的运行。

正如Perl的发布者告许我们的:我们有不止一种方法可以做同样的事。

一种执行一个外部程序和一个系统命令的方法是通过调用 exec()函数。当 Perl 遇到一个 exec()语句的时候,它将审视 exec()被调用处的参数,然后启动一个新的进程来执行这条特定的命令。Perl 从不会返回控制
给调用exec()的原来的那个进程。

另一个相似的函数是system(),system()的运行方式非常象exec(),它们之间的唯一的大的区别是Perl会首先从父进程中分叉出一个子进程,子进程作为提供给 system()的一个参数,父进程等到子进程结束运行后
再接着运行程序的其余部分。我们将会在下面更详细的讨论 system()调用,不过这些讨论大部分内容也适用
于exec()。

传递给 system()的参数是一个列表——列表里的第一个元素是要被执行的这个程序的程序名,其他元素是传给这个程序的参数。然而,如果只有一个参数的的话,system()的执行方式会发生差异。在这种情形下,Perl将会扫描这个参数看它是不是包含任何shell转换字符,如果有的话,它就要把这些字符通过shell来解释,并产生一个shell命令行来工作。不然,Perl会先将字符串拆成单词然后调用效率更高的C库函数execvp(),而这个函数不能理解特殊的shell字符。

现在假设我们有一张CGI表单,它要询问用户名,然后显示包含这个用户统计信息的一个文件。我们可以如下使用system()来调a用“c t”实现那种要求:

system ("cat /usr/stats/$username");

用户名来自这样的一个表单:

$username = param ("username");

举个例子,当用户在表单里添上username = jdimov提交后,Perl在字符串“cat /usr/stats/jdimov'”中没有找到任何转换字符创,所以它就调用execvp()函数运行“cat”后返回到我们的脚本中。这个脚本也许看起来没有任何隐患,但是它很容易被一个恶意的攻击者所利用。

问题是这样的,通过在表单的“username”域内使用特殊的字符,一个攻击者可以通过 shell 来执行任何命令。举个例子,如果攻击者传递这样的一个字符串“jdimov ; cat /etc/passwd”,Perl会把分号当作一个转换字符,然后把它传递到shell中,这样一来就变成了下面的结果:

cat /usr/stats/jdi /mov; cat etc/passwd

我们在前面提到 system()有一个参数表,并且将第一个元素看作命令来执行,而将其余的元素作为参数来传递。所以我们可以稍微改变一下脚本,使我们想让它执行的程序能够被执行:

system ("cat", "/usr/stats/$username");

如果攻击者想要搞破坏的话,只要发送“; rm rf /*”就可以了,这样攻击者既可以获得源文件,又可以获得密码文件。不过,既然我们分开来指定程序的参数,那么shell就永远也不会被调用了。所以发送“;rm -rf/*”也就不会起作用了,因为攻击符串将只会被解释成一个文件名字而已。

这种方法比单个参数的版本要好多了,因为它避免了使用shell命令,但是仍然有潜在的缺陷。在特别的情况下,我们要考虑到$username 的值会不会被攻击者利用。比如一个攻击者可以利用我们重写的代码,通过把$username设置成字符串“../../etc/passwd”来获得系统的密码文件。

有些时候,一些应用程序将特殊的字符序列解释成执行一条shell命令的请求,因而会导致很多地方出错。一个普遍的问题是对有些版本的Unix邮件工具来说,当它们在一定的上下文背景下发现到有“~!…”等字符序列的时候将会执行一个shell命令,所以一个包含“~!rm -rf *”的用户输入信息将会导致问题出现。
CGI(Perl)的漏洞及防范措施(2)

3. Open()函数

在Perl中open()函数被用来打开文件。这个函数最为常见的使用形式是这样的:

open (FILEHANDLE, "filename");

在上面的语句中,文件“filename”是以只读方式打开的。如果“filename”是含有“>”标志的前缀,那么它是为输出而打开的,并且如果文件已经存在据就会覆盖原文件;如果含有”>>”前缀,那么是为追加打开的;前缀“<”打开文件来进行输入操作,这也是不含前缀的时候的默认方式。用未经确认的用户输入信息作为文件名的一部分时,所产生的问题应该总是比较明显的。举例来说,向后回溯浏览目录的骗招在这里仍然能用。

另外,还有其他值得担忧的问题,现在我们使用open()替换“cat”来修改我们的脚本文件。命令如下:

open (STATFILE, "/usr/stats/$username");

然后我们从文件中读取代码并显示,Perl文档告许我们:如果文件名是以“-|”开始的,文件名将会被解释成一个输出管道命令;反之,如果文件名以“|-”结束的话,文件名将会被解释成让我们进行输出的管道。

于是,只要加上一个“|”前缀,用户就可以在/usr/stats目录下运行任何命令了。向后回溯目录的操作能够让用户在这个系统里执行任何程序。

有一种解决这个问题打方法是:对于你想要打开并向其中输入的文件总是通过加“<”标识指明。

有时我们确实要调用一个外部的程序,比如,我们想要修改某个脚本文件来让该脚本文件能够读取旧的纯文本文件/usr/stats/username,但是在给用户显示之前要先通过一个HTML过滤器。我们有一个便利的方法来马上实现这个意图,这个方法如下所示:

open (html, "/usr/bin/txt2html /usr/stats/$username|");
print while ;
不幸的是,这依然要通过shell层。然而我们可以采用调用open()的另一个形式来避免牵涉到shell:方法如下:
open (HTML, "-|")
or exec ("/usr/bin/txt2html", "/usr/stats/$username");
print while ;

当我们打开一个管道命令,或者是为了读(“-|”)或写(“|-”)的时候,Perl 在当前进程中产生分支,并且返回子进程的 PID 给父进程,返回 0 给子进程。“or”语句用来决定我们是在父进程中还是在子进程中。如果我们在父进程(返回值为非零),就继续执行print()语句;如果我们在子进程中,就执行txt2html程序,使用包含一个以上参数的 exec()的安全版本来避免传递任何命令到 shell 层。子进程答应 txt2html 产生的STDOUT 输出,然后就默默的消亡了(记住:exec()从不返回),同时父进程从 STDIN 中读取结果。这种技术可以被用来通过管道将结果输到一个外部程序,相应的代码如下所示:

 

open (PROGRAM, "|-")
or exec ("/usr/bin/progname", "$userinput");
print PROGRAM, "This is piped to /usr/bin/progname";
在需要管道的时候,以上 open()函数的使用方法应该总是比直接的管道 open()命令优先采用,因为它们不通过shell层。现在来设想我们要将静态文本转化成HTML页面,基于方便考虑,要将转换后的HTML文件存放在与显示这些页面的Perl脚本相同的目录下。那么open ()语句的形式可能如下所示:
open (STATFILE, "<$username.html");
比如当用户通过表单中传递username=jdimo的时候,脚本显示的将是jdimov.html,这里仍然有被攻击的可能。不同于C++和C语言,Perl不用空字节来结束字符串,这样的话字符串jdimov/jdimov/lo/bah在绝大数C库调用中解释为“jdimo”,但是在Perl中却是“jdimov/0blah”。当Perl传递一个含空字符的字符串给用C语言程序的时候,这个问题就更突出了。当用户使用如下的方式调用脚本时,会发生什么情况呢?
statscrit.plusername=jdimov/%00

这个程序将传递字符串“jdimov/%.Html”到对应的系统调用里以打开它,但是因为那些系统调用是用C语言的程序,接受的是空字节的字符串方式,结果是这样的,如果有文件“jdimov”的话就会显示这个文件。如果用“statscript./pusername=statscript.p/%”来调用脚本,会发生什么呢?如果脚本和HTML文件在同一个目录下,就可以使用这个输入的数据来欺骗脚本以显示所有的内容。在这种情况下或许没有什么大的安全危险,但是它可以被其他的程序使用,因为它允许攻击者分析其他可利用的缺陷的来源。

4. 单引号

在Perl中,另一种读取外部程序的输出的方法是把命令放在单引号里。所以如果我们想在分等级的$stats的文件中保存我们stats文件的内容的话,我们可以这样做:

$stats=’cat/user/stats/$username’;
这确实要通过shell层来实现。任何把用户输入包含在一对单引号内的脚本都有发生前面讨论的所有安全问题的危险。有很多方法可试图使shell不要误解一切可能的转换字符,但是最安全的就是不要用但引号。取而代之的是,打开一个通到STDIN的管道,然后分叉执行外部程序。

7.7  CGI(Perl)的漏洞及防范措施(3)

5. Eval()和/e 修饰符

函数eval()可以在运行时间执行一个 Perl 代码块,并返回上一次经评估语句的值。这种函数功能经常用于配置文件,它可以写成 Perl 代码,除非你绝对相信输进 eval()的源代码,否则不要做诸如 eval/$userinput,之类的事,这也适用于一个常规表述中的/e修饰符,用来使Perl在执行之前解释该表述。

6. 过滤用户输入

用于本节我们所讨论的过滤用户输入数据的一个通用方法(FU In OCA )就是过滤任何不需要的转换字符和有问题的数据。例如我们可以通过在任何时段过滤来避免向后的目录查看。同样,我们一旦看见非法的字符,就让程序运行失败,这种策略被称为“黑名单”,意思是说如果某种东西没有被明确禁止,那它肯定是好的。一个更好的策略就“白名单”,它指的是如果某种东西没有被明确的认可,那么它必须禁止。黑明单的最重要的问题是它非常难保持并得到完整性。你也许会忘掉过滤某一特定的字符,或者你的程序或许不得不随着不同的转换字符集合转到一个不同的shell中,不过滤掉不需要的转换字符和其他危险的数据输入,而是只过滤进合法的输入数据。下面就是一个例子,如果有户输入的数据中包含了除字母、数字、点和@符号外任何东西(@经常用于用户的电子邮件地址),就会停止执行一个安全性有问题的操作,其代码如下。

unless($useradress=~/^[-@/w。]+)$/)
{
print”secrity error。/n”exit(1)   ;
}

基本的思想是不去编译一个特定值的列表来进行保护,而是产生一个安全列表来接受那些可接受的输入值的选择当然会随着不同的应用程序而变公,可接受的真应该根据能将破坏的可能性降到最小的方式来选择。

7. 避免shell

如果你调用一个有特定序列的编辑器,你必须确认这些特定序列是不被允许使用的。一般通过使用现存Perl模块,你可以避免使用外部程序来执行一个外部函数,CPAN是一个可以完成所有标准UNIX工具集能做的而且经过测试的函数模块来源,CPAN 或许会包含一个模块并且调用它,而不是调用一个外部程序,模块的方法一般来说更安全和灵活,为解释清楚这一点,使用Net:SMTP,而不用exec()’ing sendmail/--T会帮c()’ing sendmail/--T会帮你少一些使用shell的麻烦,并能防止你的用户在sendmail代理中寻求已知的弱点。

8. 其他安全问题的来源(不安全环镜变量)

除了用户数据输入的安全问题,Perl还有其他因素是写Perl源代码时所必须考滤的,经常在shell下运行脚本时容易受攻击的环镜变量,最通常的是PATH/变量。当你在代码内部使用一个外部应用程序或功能,而且仅仅指明了一条相对路径的时候,就可能使这个程序和运行它的系统处于危险中。比如下面所示的一个system()调用。

system(" txt2html","user/stats/jdiov" );

对于这种调用,假设 txt2htm 文件包含在 PATH 的某处目录下,如果有一个攻击者改变了你的路径,并把路径指向了含有相同名字的其他带恶意程序中,那么系统的安全性就得不到保证。为了避免类似的事情发生,每个考虑到安全问题的程序都应该这样开始写:

#!/usr/bin/perl -wT
require 5.001;
use strict;
$ENV{PATH} = join ':' => split (" ", << '__EOPATH__');
/usr/bin
/bin
/maybe/something/else
__EOPATH__

如果程序依赖于其他的环境变量,也要在这些变量使用前明确的定义出来。

另一个Perl的危险变量是@INC距阵变量,有关@INCR的问题和PATH是非常相似,只不过@INCR指明了Perl到哪里去找要包含在程序中的模块。有人可能会把Perl指向一个具有相同名字的模块,除了完成预定功能外,背地里还做一些坏事,因此@INC同PATH都是不值得信任的。在包括任何外部模块之前都必须完全重新定义。

9. Setuid脚本

通常一个Perl程序是以可执行用户的权限来运行的。通过产生一个setuid脚本,其有效用户ID可以设定成更高的权限,通过该权限使这个用户可以访问他实际没有访问权限的资源,比如passwd程序使用setuid来获取对系统 password 文件的写的权限,这样就允许用户来更改自已的密码,因为执行程序是通过 CGI 界而来执行的,该界面是在使用网络服务器的用户权限下运行的,CGI程序员经常试图使用setuid技巧来让自己编写的脚本执行一些恶作剧。这也可能是十分危险的,如果某个攻击者发现有一个方法可以利用脚本的弱点,不仅能获得访问系统的权限,而且会用有效的UID的特权来获得存在着另外几种类似的种族状况,在一个程序中这类缺陷是比较容易监控的,尤其是对有经验的程序员。关于这个问题,目前还没有一个既容易些而又完全有效的解决方法。

另外我们所作的一种标准模式是使用SYSOPEN来确定一种只读模式,不必再设定删减标志。

通过这种方法,即使我们的文件名确实已经形成,当我们打开文件进行写操作的时候 ,我们也不会破坏文件。注意: Fcnt1模快必须包含进来,以便让sysopen()函数起作用,因为这个模块是如下常数O_RDONLY、O_WRONLY、O_CREAT等被定义的地方。

10. 缓冲区溢出和Perl

一般来说,Perl脚本是不容易发生缓冲区溢出的,因为Perl能在需要的时候动态地扩展它的数据结构。Perl跟踪的是每个字符串分配的大小,在一个字符串每次被赋值之前,Perl 保证有足够的空间可以利用,如果需要的话,也可以为那个字符串分配更多的空间。

然而,在一些较老的Perl实现方法中,有几种缓冲区溢出的情况,一个明显的例子是5.03版本会因为缓冲区溢出而崩溃。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: