|
PHP 安全
http://netkiller.github.com/article/phpsecurity.html
Mr. Neo Chen (netkiller), 陈景峰(BG7NYT)
<openunix@163.com>
版权 © 2011, 2012 http://netkiller.github.com
摘要
下面是我多年积累下来的经验总结,整理成文档供大家参考:
Netkiller Architect 手札 | Netkiller Linux 手札 | Netkiller Developer 手札 | Netkiller Database 手札 | Netkiller Debian 手札 | Netkiller CentOS 手札 | Netkiller FreeBSD 手札 | Netkiller Shell 手札 | Netkiller Web 手札 | Netkiller Monitoring 手札 | Netkiller Storage 手札 | Netkiller Mail 手札 | Netkiller Security 手札 | Netkiller Multimedia 手札 | Netkiller Writer 手札 | Netkiller Version 手札 | Netkiller PostgreSQL 手札 | Netkiller MySQL 手札 | Netkiller Cryptography 手札 | Netkiller Cisco IOS 手札 | Netkiller LDAP 手札 | Netkiller Intranet 手札 | | |
目录
1. Apache mod_php / php-fpm
1.1. 用户权限
1.1.1. Apache
1.1.2. Nginx / lighttpd + fastcgi
1.2. web server 版本信息
2. php.ini
2.1. 危险PHP函数
2.1.1. chdir()函数安全演示
2.2. 隐藏PHP版本信息
2.3. 隐藏PHP出错信息
3. 开发于安全
3.1. 彻底解决目录于文件的安全
3.2. Session / Cookie安全
3.3. 注入安全
1. Apache mod_php / php-fpm
目录权限安全
1.1. 用户权限
web server 启动用户不能于运行用户为同一个用户
web server 运行用户与php程序不能为同一个用户
root 1082 0.0 0.1 11484 2236 ? Ss Mar01 0:00 nginx: master process /usr/sbin/nginx
www-data 13650 0.0 0.0 11624 1648 ? S 09:44 0:00 nginx: worker process
www-data 13651 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker process
www-data 13652 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker process
www-data 13653 0.0 0.0 11624 1132 ? S 09:44 0:00 nginx: worker process
- 夫进程
root 启动 web server, 此时web server 父进程应该是 root,同时父进程监听80端口
- 子进程
父进程派生许多子进程,同时使用setuid,setgid将子进程权限切换为非root
子进程用户可以通过httpd.conf设置
User nobody
Group nobody
nginx.conf
$ cat /etc/nginx/nginx.conf
user www-data;
- fastcgi 进程
root 13082 0.0 0.1 19880 2584 ? Ss 09:28 0:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)
www-data 13083 0.0 0.1 20168 3612 ? S 09:28 0:00 php-fpm: pool www
www-data 13084 0.0 0.1 20168 2808 ? S 09:28 0:00 php-fpm: pool www
www-data 13085 0.0 0.1 20168 2812 ? S 09:28 0:00 php-fpm: pool www
www-data 13086 0.0 0.1 20168 2812 ? S 09:28 0:00 php-fpm: pool www
php-fpm 于apache类似,都是root父进程,然后派生子进程,由于fastcgi 使用 9000 所有我们可以不使用root启动php-fpm
现在我们开始讲解安全配置问题
我们目的是避免用户通过漏洞提升权限,或者由于权限配置不当产生漏洞
1.1.1. Apache
Apache 案例
- Apache : root
- Apache 子进程 : nobody
- HTDOCS 目录 : /var/www
/var/www
|--include
|--image
|--temp
|--...
很多人会将/var/www用户与组设置为 nobody:nogroup / nobody:nobody, 同时因为images会上传文件需要设置777, 很多书本于教程上面也是这样讲的, 这样配置会有什么问题呢?我们来分析一下:
我们假设,一个用户上传一个文件到images目录,会有几种情况:
- 上传一个.php文件,我们可以通过程序禁止上传.php文件
- 我们上传一个.jpg文件,OK 通过了,通过某种手段将他重命名位.php扩展名的文件,然后通过http://www.example.com/images/your.php 运行它,your.php 可以做什么呢? 它可以查看所有文件,修改所有文件,创建其他php文件,去你可include目录下看config.php然后下载数据库。
- 内部开发人员偷偷将一个程序植入到系统中,这个做code review 可以避免
如何避免这样问题出现,有一个办法,我们新建一个用户www, webserver 进程是nobody,程序目录/var/www中的代码是www用户,nobody可能读取但不能修改。/var/www/images 目录所有者是nobody可以上传图片
chown www /var/www/
chown nobody /var/www/images
find /var/www/ -type d -exec chmod 555 {} \;
find /var/www/ -type f -exec chmod 444 {} \;
chmod 755 /var/www/images
使所有可能目录允许运行.php文件,http://www.example.com/images/your.php 将被拒绝. include 也是同样处理方式,只允许使用include_once,require_one 包含,不允许http://www.example.com/include/your.php运行
<Location ~ "/((js/)|(css/)|(images/)).*\.php">
Order Deny,Allow
Deny from all
</Location>
<Location /includes/>
Order allow,deny
Deny from all
</Location>
<Location /library/>
Order allow,deny
Deny from all
</Location>
<Directory /var/www/themes/>
<Files *.php>
Order allow,deny
Deny from all
</Files>
</Directory>
1.1.2. Nginx / lighttpd + fastcgi
Nginx / lighttpd 案例分析
- nginx / lighttpd: root
- web server 子进程 : nobody
- php-fpm: root
- php-fpm 子进程 : nobody
- HTDOCS 目录 : /var/www
/var/www
|--include
|--image
|--temp
|--...
fastcgi 遇到的问题与上面apache案例中遇到的问题类似,不同是的fastcgi把动态于静态完全分开了,这样更容易管理,我们可以这样入手
- nginx / lighttpd: root
- web server 子进程 : nobody
- php-fpm: root
- php-fpm 子进程 : www
chown nobody /var/www/
chown www /var/www/images
find /var/www/ -type d -exec chmod 555 {} \;
find /var/www/ -type f -exec chmod 444 {} \;
chmod 755 /var/www/images
/var/www所有权限给nobody, images权限给www, 同时保证www用户可以读取/var/www下的程序文件
location ~ ^/upload/.*\.php$
{
deny all;
}
location ~ ^/static/images/.*\.php$
{
deny all;
}
location ~ /include/.*\.php$ {
deny all;
}
location ~ .*\.(sqlite|sq3)$ {
deny all;
}
vim /etc/php5/fpm/pool.d/www.conf
user = www
group = www
/etc/php5/fpm/pool.d/www.conf
chdir = /
改为
chdir = /var/www
chroot可以彻底解决cd跳转问题,单配置比较繁琐
chroot = /var/www
这样当用户试图通过chdir跳转到/var/www以外的目录是,将被拒绝
1.2. web server 版本信息
Apache:
ServerTokens ProductOnly
ServerSignature Off
Nginx:
server_tokens off;
2. php.ini
2.1. 危险PHP函数
这些函数应该尽量避免使用它们
exec, system, ini_alter, readlink, symlink, leak, proc_open, popepassthru, chroot, scandir, chgrp, chown, escapeshellcmd, escapeshellarg, shell_exec, proc_get_status, max_execution_time, opendir,readdir, chdir ,dir, unlink,delete,copy,rename
2.1.1. chdir()函数安全演示
$ cat chdir.php
<pre>
<?php
echo "current:".getcwd();
echo '<br />';
chdir('/');
echo "chdir:".getcwd();
echo '<br />';
$lines = file('etc/passwd');
foreach ($lines as $line_num => $line) {
echo "Line #<b>{$line_num}</b> : " . htmlspecialchars($line) . "<br />\n";
}
?>
</pre>
运行结果
current:/www
chdir:/
Line #0 : root:x:0:0:root:/root:/bin/bash
Line #1 : daemon:x:1:1:daemon:/usr/sbin:/bin/sh
Line #2 : bin:x:2:2:bin:/bin:/bin/sh
Line #3 : sys:x:3:3:sys:/dev:/bin/sh
Line #4 : sync:x:4:65534:sync:/bin:/bin/sync
Line #5 : games:x:5:60:games:/usr/games:/bin/sh
2.2. 隐藏PHP版本信息
expose_php Off
2.3. 隐藏PHP出错信息
display_errors = Off
同时开启error_log日志
error_log = php_errors.log
3. 开发于安全
3.1. 彻底解决目录于文件的安全
选择一个MVC开发框架,它们的目录结构一般是这样的:
/www
/www/htdocs/index.phphtdocs目录下只有一个index.php文件,他是MVC/HMVC框架入口文件
/www/htdocs/static这里防止静态文件
/www/app/这里放置php文件
然后放行index.php文件,在URL上不允许请求任何其他php文件,并返回404错误
3.2. Session / Cookie安全
session.save_path 默认session 存储在/tmp, 并且一明文的方式将变量存储在以sess_为前缀的文件中
$ cat session.php
<?php
session_start();
if(isset($_SESSION['views']))
$_SESSION['views']=$_SESSION['views']+1;
else
$_SESSION['views']=1;
echo "Views=". $_SESSION['views'];
?>
http://www.example.com/session.php 我们刷新几次再看看sess_文件中的变化
$ cat /tmp/sess_d837a05b472390cd6089fc8895234d1a
views|i:3;
经过侧记你可以看到session文件中存储的是明文数据,所以不要将敏感数据放到Session中,如果必须这样作。建议你加密存储的数据
有一个办法比较好,就是封装一下session.不再采用$_SESSION方式调用
Class Encrype{
}
Class Session extend Encrype {
function set($key,$value,$salt){
$value = Encrype($value)
$_SESSION[$key] = $value
}
function get($key){
return $_SESSION[$key]
}
}
Class Cookie extend Encrype {
function set($key,$value,$salt){
$value = Encrype($value)
$_COOKIE[$key] = $value
}
function get($key){
return $_COOKIE[$key]
}
}
Cookie
cookie 也需要作同样的处理,上面代码仅供参考,未做过运行测试
3.3. 注入安全
SQL 注入
<?php
$mysql_server_name="172.16.0.4";
$mysql_username="dbuser";
$mysql_password="dbpass";
$mysql_database="dbname";
$conn=mysql_connect($mysql_server_name, $mysql_username,
$mysql_password);
$strsql="";
if($_GET['id']){
$strsql="select * from `order` where id=".$_GET['id'];
}else{
$strsql="select * from `order` limit 100";
}
echo $strsql;
$result=@mysql_db_query($mysql_database, $strsql, $conn);
$row=mysql_fetch_row($result);
echo '<font face="verdana">';
echo '<table border="1" cellpadding="1" cellspacing="2">';
echo "\n<tr>\n";
for ($i=0; $i<mysql_num_fields($result); $i++)
{
echo '<td bgcolor="#000F00"><b>'.
mysql_field_name($result, $i);
echo "</b></td>\n";
}
echo "</tr>\n";
mysql_data_seek($result, 0);
while ($row=mysql_fetch_row($result))
{
echo "<tr>\n";
for ($i=0; $i<mysql_num_fields($result); $i++ )
{
echo '<td bgcolor="#00FF00">';
echo "$row[$i]";
echo '</td>';
}
echo "</tr>\n";
}
echo "</table>\n";
echo "</font>";
mysql_free_result($result);
mysql_close();
SHELL 命令注入
<?php
system("iconv -f ".$_GET['from']." -t ".$_GET['from']." ".$_GET['file']) |
|