Perl CGI 安全编程点滴
2014-11-27 17:20:27 阿炯

本站赞助商链接,请多关照。 CGI在现在的互联网应用越来越广泛,CGI编程的安全问题也得到越来越多的重视。Perl作为CGI编程的主要语言之一,其安全性也受到很大的关注。在 W3C组织的 "WWW Security FAQ" 之 "CGI Scripts"一章中,Perl安全编程就整整占了一节。由此可见 Perl CGI 安全编程的重要性。

这里我不会重复 "WWW Security FAQ" 的内容,而是根据一直以来对 Perl免费/商业程序包的源代码的研究,得出一些较易被忽略的编程漏洞,在此将几个最常见的(主要是变量字符过滤方面)写出来,供广大程序员作为参考。如果你对本文的内容有不同意见或任何建议,请告诉我。如果你发现了其它Perl CGI编程漏洞,也请告诉我。如果你也发表了关于这方面的文章,当然也请告诉我。

---------------------
1、“有毒”的NULL字符
---------------------
如果我说:"root"=="root",相信没有什么人反对。但同时我也这样说:"root"!="root"!还有多少人会认为我是个“正常人”?

但在各种不同的编程语言中,确实存在着这种情况。对于每一个希望发现CGI漏洞的安全专家或黑客来说,最常用的方法之一是通过传递特殊字符(串),绕过CGI限制以执行系统级调用或程序。如果你仔细留意的话,或许也会发现NULL字符确实有它的“妙用”。

阅读以下例子:
# parse $user_input
$database="$user_input.db";
open(FILE "<$database");

这个例子用于打开客户端指定的数据库文件。例如客户端输入"backend",则系统将打开"backend.db"文件考只读方式)。(注:在这里我们暂且不讨论"../"的安全问题。)这种处理方式在互联网中是很常见的。

现在,让我们在客户端输入"backend%00",在该PERL程序中$database="backend.db",然后调用open函数打开该文件。但结果是什么呢?系统会打开"backend"文件(如果该文件存在)!

出现这种情况的原因是由于PERL允许在字符串变量中使用NULL空字符,而在C语言中字符串则不允许包含空字符。因此也就有了"root"!="root"(在PERL中)和"root"="root"(在C语言中)。由于系统内核/调用等都是使用C语言编写,因此当PERL将"backend.db"字符串传递到(C语言的)链接库/程序时,空字符以后的字符将被忽略?(或许还有利用价值?我还没发现。:))

这种编程缺陷的影响可大可小。试想一下,如果利用以上编程原理编写一个给系统其他管理员修改除了root外的其他用户口令的PERL程序:
$user=$ARGV[1] # user the jr admin wants to change
if ($user ne "root"){
# do whatever needs to be done for this user }

那么,聪明的你应该知道如何绕过这个限制修改root用户口令了吧?对了,只要使 $user="root",则PERL会执行上面程序中花括号内的语句。除非所有处理过程均使用PERL,否则一旦该变量传递给系统,则会造成安全问题。如修改root用户口令等。也许你认为很难遇到这种会造成严重安全问题的情况,那么我们能否将它作为一种寻找网站源程序漏洞的间接手段呢?不知你有没有经常遇到这种类型的CGI程序,该程序用于打开客户端(提交的表单中)要求的页面?如:
page.cgi?page=1

然后网站是否返回页面"1.html"呢?;-) 好,现在将其改为:
page.cgi?page=page.cgi%00 (%00 == '' escaped)
这样,我们就可以得到我们感兴趣的文件内容了!这种方法连PERL的"-e"参数也可绕过:
$file="/etc/passwd.txt.whatever.we.want";
die("hahaha! Caught you!) if($file eq "/etc/passwd");
if (-e $file){
open (FILE, ">$file");}

绕过这段程序的后果你应该想像得到吧? 解决方法?最简单地,过滤NULL空字符。在PERL程序中,
$insecure_data=~s///g;

------------------------
2、漏网之鱼--反斜杠()
------------------------
对于每一个关心CGI安全的人,也许都看过 W3C 的 WWW Security FAQ 中关于CGI安全编程一节。其中列出了建议过滤的字符:
&;`'"|*?~<>^()[]{}$nr
但我在很多时候发现反斜杠()往往被遗忘了。以下是正确的过滤表达式:
s/([&;`'\|"*?~<>^()[]{}$nr])/\$1/g;
但在很多商业的CGI程序中反斜杠却没有被包含进去,这可能是程序员们写程序时被这些过滤用的匹配表达式搞迷糊了?

那么,没有过滤反斜杠会造成安全问题吗?试想一下,如果向你的程序中发送如下一行内容:user data `rm -rf /`
大多数情况下,程序员编写的程序会将以上内容过滤为:
user data `rm -rf /`

从而保护了系统。但如果PERL程序中忘记过滤了反斜杠,当客户端向该程序提交如下内容时:
user data `rm -rf / `
经过匹配表达式后为:user data \`rm -rf / \`
怎么样,看出危险了吗?由于两个反斜杠经系统解释后为一个字符"",但`字符却因此没有被过滤掉,`rm -rf / `将被系统执行!不过,由于其中还含有一个反斜杠字符,执行时系统会出错。你自己想办法绕过这个限制吧?利用反斜杠的另一个应用--绕过系统目录进入限制。请看以下表达式:
s/..//g;
这个匹配表达式的作用非常简单,就是过滤字符串中的".."。当输入为:
/usr/tmp/../../etc/passwd
将被过滤为:
/usr/tmp///etc/passwd
这样,你将无法访问/etc/passwd文件。(注:*nix系统允许///,试一下'ls -l
/etc////passwd'命令就知道了。)
 现在,让我们的“好伙伴”反斜杠来帮忙。将输入改为:/usr/tmp/../../etc/passwd
则由于反斜杠的存在而不符合过滤表达式。当PERL中存在如下程序段时,
$file="/usr/tmp/.\./.\./etc/passwd";
$file=s/..//g;
system("ls -l $file");

当运行到执行系统调用时,执行的命令会是"ls -l /usr/tmp/../../etc/passwd"。想知道会得到什么输出吗?自己在机器上试试吧。然而,以上方法只适用于系统调用或``命令中。无法绕过PERL中的'-e'命令和open函数(非管道)。如下程序:
$file="/usr/tmp/.\./.\./etc/passwd";
open(FILE, "<$file") or die("No such file");
执行时将显示"No such file"并退出。我还没有找出绕过这个限制的方法。
解决方法:只要别忘了过滤反斜杠字符(),就已足够了。

--------------------------------
3、畅通无阻的“管道”--字符"|"
--------------------------------
在PERL的open函数中,如果在文件名后加上"|",则PERL将会执行这个文件,而不是打开它。即:
open(FILE, "/bin/ls")
将打开并得到/bin/ls的二进制代码,但
open(FILE, "/bin/ls|")
将执行/bin/ls命令!
 以下过滤表达式
s/(|)/\$1/g
可以限制这个方法。PERL会提示"unexpected end of file"。如果你找到绕过这个限制的方法,请告诉我。

综合应用
现在让我们综合以上几种编程安全漏洞加以利用。先举个例子,$FORM是客户端需要提交给CGI程序的变量。而在CGI程序中有如下语句:
open(FILE, "$FORM")
那我们可以将"ls|"传递给$FORM变量来获得当前目录列表。现在让我们考虑如下程序段:
$filename="/safe/dir/to/read/$FORM"
open(FILE, $filename)
如何再执行"ls"命令呢?只要能使$FORM="../../../../bin/ls|"即可。如果系统对目录操作加入了".."过滤,则可利用反斜杠的漏洞绕过它。
在这段程序中,我们还可以在命令中加入参数。如"touch /backend|",将建立/backend文件。(但我不会使用这个文件名,因为它是我的名字。)现在,让我们在程序段中加入更多的安全限制:

$filename="safe/dir/to/read/$FORM"
if(!(-e $filename)) die("I don't think so!")
open(FILE, $filename)

这样我们还需要绕过"-e"的限制。由于我们在$FORM变量中使用了"|"字符,当"-e"运算符检查"ls|"文件时,因为不存在此文件而退出程序。如何当"-e"检查时去掉管道符,而调用open函数时又含有管道符呢?回忆一下在前面谈到的NULL字符的利用,我们就知道应该如何做了。只要使$FORM="ls|"(注:在客户端提交的表单中为"ls%00|")即可。其中的原理复习一下前面提到的内容就会明白了。

需要说明的是,以上程序段中,我们无法象再上一段程序那样执行带参数的命令,这是因为"-e"运算符的限制所致。举例如下:
$filename="/bin/ls /etc|"
open(FILE, $filename)
将显示/etc目录下文件列表。
$filename="/bin/ls /etc|"
if(!(-e $filename)) exit;
open(FILE, $filename)
将导致因不存在文件而退出。
$filename="/bin/ls /etc|"
if(!(-e $filename)) exit;
open(FILE, $filename)
将只显示当前目录下文件列表。

PerlCGI的性能
Perl CGI可能在开发网站的初期是一种常用方式。但在实际使用后采用Perl CGI,性能问题就开始变得很明显。这种情况在原型应用程序应用在实际环境中经常碰到,表现为较慢的响应速度、不寻常的行为和系统失败等。在一个站点遭遇Perl CGI脚本产生的性能问题之后,不可能再次允许其出现在实际环境中。

Perl CGI在测试一个Web程序的初期工作很好,但随着站点使用得越来越多,应用程序负载也更大,CGI进程产生越来越多的开销,并最终超过服务器的能力。处理器过载,内存被占满,数据库没有可用连接,系统将比预期地更早终止。

Perl 的随机编译器在这里是一个最大缺陷,执行Perl CGI进程时CPU的许多负载都来源于编译和初始化程序。最明显的结论是Perl相对当前任务响应太慢了,当然不会有任何公司会鼓吹Perl CGI的速度,在实际使用中,经过优化的Perl程序的例子更少。

一次性任务
Perl CGI进程在原型测试环境下工作得很好,这时通常每次只有一个用户访问Web应用程序,并且所有界面延迟时间只是由于用户请求的处理引起的。这与Perl 程序在Web处理之外的其他情况下的使用方式很类似,所以,那些情况的假设在此都成立,也就是每次只有一个请求,在请求完成后,程序和数据就不再需要了。

这些假定在演示原型时也是正确的。这时演示者是访问系统的唯一用户,通常处理过程产生的延迟很小并且也被解释过程给掩盖了。结果,CGI进程响应的缓慢性不可能在演示或可用性测试阶段觉察出来。即使单一用户注意到运行缓慢,也可能注重其他考虑而被忽视。

把原型投入实际使用就会截然不同。关于一个用户、很少的请求和没有对资源竞争的假设不再成立,应用程序的负载会按指数规律增长,这是由于Web请求的性质,他们不像单一用户应用程序中那样在同意时刻输入,也不像大多数用户应用程序(如数据库或群件)那样固定在一定范围之内。Web请求可能同时输入,并且数量可能不断增加,而不采取定单处理过程中排队请求那样简单的方式。相反,要求Web服务器存在和请求一样多的进程,附加进程的负载似的现有进程更加缓慢,因此,在处理以前进程的时候,会有更多的进程输入进来。在有限负载下,这种情况很容易失控,从而导致所有的Perl CGI进程阻塞停止,在处理过程中的所有请求都失败了。

Perl进程的内存痕迹
一个Perl CGI进程可能占用1MB到15MB的RAM空间。占用这些内存的进程如果单独处理很容易管理,甚至不能使最一般的Web服务器崩溃。然而,CGI进程处理过程中使用的Perl编译器可能是静态链接而不是动态链接,导致一个不在进程间共亨的Perl解释器。如果许多CGI进程在同时运行,Perl解释器就可能复制无数次,将占用更多的内存开销。

如果新进程在旧进程没结束之前就开始了,新进程会站用更多的系统内存。随着站点的经常光顾的访问者的增多,将会产生更多的覆盖和系统资源的更大消耗。例如,如果一个站点的一个CGI应用程序每秒接到20个请求,那么最少需要2秒钟来处理,可能要 40个CGI进程不停地开始和结束,这样会占用500M的RAM空间,随着请求率的增加,对一个实际的Web应用程序来说,100个请求每秒仍是一个非常保守的数字,内存请求增加不是只简单相加,而是呈几何基数地增加。每个进程通过增加CPU负载从而减慢其他进程,但这样又会增加必须的进程数,并且会恶性循环下去。这样即使用最昂贵的Web服务器,可用RAM的限制也将很快突破。

当可用RAM被CGI进程需求超过后,大多数服务器开始使用磁盘上的交换空间来为进程产生虚拟内存。这时,进程开始明显变得缓慢,处理一个进程的时间成10倍地增加,同时,活动进程的数量也同样增加了。进程总负载一旦超过系统的极限,所有进程都会终止。同样,请求失败,连接的系统可能会处在不确定的状态下,并产色怀念感不寻常的行为和管理上的混乱。

编译的费用
Perl CGI的大部分开销来自于编译成字节码和初始化程序数据。每个Perl程序在执行前都要经过编译,每个程序(不管采用什么语言)都需要出事内存结构和运行时系统库函数。编译过程对任何在系统环境中不许要解释器就运行的程序都是必须的。

编译过程在运行一个单用户程序时很少能看得到,如 C 程序,编译过程需要几分钟甚至几小时。编译步骤也是检查系统相关错误的时候,所以如果第一个编译步骤没成功,就需要更多的调试,也就需要更多的时间。因此,编译过程通常在程序提交给用户之前就已完成了。即使开放源代码程序通常也只在目标系统上第一次安装时编译一次。

而Perl程序要在执行前进行编译,这使Perl程序可以更迅速地对系统环境变化做出响应,也比编译过的执行程序更容易跨平台移植。Perl runtime在几秒(或几毫秒)内编译每个程序,包括所有库函数和核心程序。Perl程序开始时同时开始编译和执行步骤,这就给人Perl是解释不是编译的印象。而Perl runtime(有时叫Perl解释器)需要在编译和执行每个程序时激活,这种印象也就更深了。

对有些 Perl程序,编译时间不止几秒钟。程序更大时更是如此,因此要添加大量的模块到程序中。初始变量、系统连接和更大的数据结构都可能增加系统初始阶段的时间开销。例如,把较大的XML文档分析Perl数据结构,可能会占用相当的时间,这些额外的编译和初始时间造成了PerlCGI使用范围的限制,因为更复杂的过程可能会包含不允许的性能损失。

除了大多数集中处理器操作的Perl程序,编译一个Perl程序会比简单执行编译过程的程序占用更多的CPU时间,因为编译程序需要于Perl语言本身同样的文本操作、系统集成和磁盘输入/输出,但更复杂一些。实际在大多数情况下,一个Perl CGI进程占用比相同的经过预编译的程序多20倍的CPU处理器时间。

小结

建立Web应用程序的原型是Perl的用武之地,许多Perl程序开始是作为Perl原型的。Perl的灵活性和强大的系统集成模块使原型设计和实施特别容易。Perl CGI的性能在短时间内受到挫折,并且Perl CGI进程占用更多内存和CPU时间,最后可能使系统过载,完全终止Web服务器进程。这主要是由于Perl进程的实时编译的开销引起的,虽然是一、两秒钟,但这比程序本身还需要更多的处理能力。

PerlCGI环境变量列表
Perl CGI 环境变量会因为服务器的不同而有所区别,所以本内容不一定和你的 WEB 服务器相完全符合。

以下的内容以字母排序:
1. AUTH_TYPE
如果服务器支持基本的认证并且如果脚本被保护,此变量提供认证类型,此信息是特定于协议和服务器的。

2. CONTENT_LENGTH
如果通过 POST 方法的请求中包括数据,此变量的值为此请求中合法数据的长度。

3. CONTENT_TYPE
如果请求中包括数据,此变量指定数据类型的类别。

4. DOCUMENT_ROOT
网络提供的文件服务所在的路径。

5. GATEWAY_INTERFACE
被服务器支持的 CGI 接口的版本数,其格式为 CGI/版本号。

6. HTTP_ACCEPT
提供由逗号分开的并被客户服务器可接受的 MIME 类型的列表。

7. HTTP_ACCEPT_CHARSET
客户机能接受的语言代码设置。

8. HTTP_ACCEPT_ENCODING
客户机能接受的编码形式。

9. HTTP_ACCEPT_LANGUAGE
客户机能接受的语言类型。

10. HTTP_COOKIE
客户机内的 COOKIE 内容。

11. HTTP_FORM
使用者发出请求的电子邮件讯息。

12. HTTP_REFERER
在读取 CGI 程式前,客户端所指的 URL。

13. HTTP_USER_AGENT
提供包含了版本数或其他专有数据的客户浏览器信息。

14. PATH_INFO
显示由客户提供并附在虚拟路径尾的任何附加的路径信息。它通常被用作脚本的参数。

15. PATH_TRANSLATED
仅由部分服务器支持,此变量包含由虚拟路径到被执行脚本的转换(即虚拟路径到物理路径的映射)。例如,如果到你WEB 服务器根的绝对路径为 /apache/httpd/htdocs,一个具有/cgi-bin/test.cgi 的脚本会将变量 PATH_TRANSLATED 设置为/apache/httpd/htdocs/cgi-bin/test.cgi。

16. QUERY_STRING
显示由客户提供的附在URL尾并用一个问号与脚本名分开的任何附加信息。

17. REMOTE_ADDR
提供发请求客户的 IP 地址。

18. REMOTE_HOST
提供已分解的发请求客户的主机名。

19. REMOTE_IDENT
如果服务器和客户支持 RFC931,此变量将包含由远程用户的计算机提供的识别信息。

20. REMOTE_USER
如果AUTH_TYPE被设置,此变量将包含用户提供并由服务器确认的用户名。

21. REQUEST_METHOD
提供脚本被调用的方法。对于使用 HTTP/1.0 协议的脚本,仅 GET 和 POST 有意义。

22. SERVER_ADMIN
显示服务器网络管理员。

23. SCRIPT_NAME
这是被调用脚本文件的名字,它对于自引用脚本很有用。

24. SERVER_NAME
这是你的 WEB 服务器的主机名、别名或IP地址。

25. SERVER_PORT
这是你的 WEB 服务器的端口号。

26. SERVER_PORT_SECURE
接受Http请求的服务器安全、加密端口。

27. SERVER_PROTOCOL
这是本请求所用协议的名字/版本。

28. SERVER_SOFTWARE
这是运行脚本的 HTTP 服务器的名字/版本。  

环境变量    说明 
QUERY_STRING 传递给程式的 query 资讯 
REMOTE_HOST  使用者发出 request 的远端 host 名称 
REMOTE_ADDR  使用者发出 request 的远端 IP 位址 
AUTH_TYPE    用来确定使用者合法性的监定方法 
REMOTE_USER  使用者的合法名称 
REMOTE_IDENT 发出 request 的使用者 
CONTENT_TYPE query 资料中的 MIME 型别 
CONTENT_LENGTH    资料长度,以 byte 或字元数来计算 
HTTP_FORM    使用者发出 request 的电子邮件讯息 
HTTP_ACCEPT  client 可以接受的 MIME 型别列表 
HTTP_USER_AGENT   client 用来发出 request 的浏灠器 
GATEWAY_INTERFACE Server 使用的 CGI 版本 
SERVER_NAME  Server 的 host 名称或 IP 位址 
SERVER_SOFTWARE   回应 client request 的 Server 软体名称和版本 
SERVER_PROTOCOL   传递资讯所用的协定名称或版本 
SERVER_PORT  Server 正在执行的 port number 
REQUEST_METHOD    发出 request 的方法 
PATH_INFO    传递给 CGI 程式的额外路径 
PATH_TRANSLATED   存在 PATH_INFO 中的给定路径的传递版本 
SCRIPT_NAME  程式执行时的 virtual path 
DOCUMENT_ROOT     网路提供的文件服务所在路径 
HTTP_REFERER 在读取 CGI 程式前,client 所指的文件 URL