Nginx/PHP 文件类型错误解析漏洞:fix_pathinfo

转载时请标明文章原始出处和作者信息, 作者: lostsnow.
http://www.lsproc.com/blog/nginx_php_pathinfo_securit/

漏洞介绍:nginx是一款高性能的web服务器,使用非常广泛,其不仅经常被用作反向代理,也可以非常好的支持PHP的运行。80sec发现其中存在一个较为严重的安全问题,默认情况下可能导致服务器错误的将任何类型的文件以PHP的方式进行解析,这将导致严重的安全问题,使得恶意的攻击者可能攻陷支持php的nginx服务器。

漏洞分析:nginx默认以cgi的方式支持php的运行,譬如在配置文件当中可以以

location ~ \.php$ {
    root html;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
    include fastcgi_params;
}

的方式支持对php的解析,location对请求进行选择的时候会使用URI环境变量进行选择,其中传递到后端Fastcgi的关键变量 SCRIPT_FILENAME由nginx生成的$fastcgi_script_name决定,而通过分析可以看到$fastcgi_script_name是直接由URI环境变量控制的,这里就是产生问题的点。而为了较好的支持PATH_INFO的提取,在PHP 的配置选项里存在cgi.fix_pathinfo选项,其目的是为了从SCRIPT_FILENAME里取出真正的脚本名。
那么假设存在一个http://www.lsproc.com/a.jpg,我们以如下的方式去访问


http://www.lsproc.com/a.jpg/xxx.php

将会得到一个URI

/a.jpg/xxx.php

经过location指令,该请求将会交给后端的fastcgi处理,nginx为其设置环境变量SCRIPT_FILENAME,内容为

/scripts/a.jpg/xxx.php

而在其他的webserver如lighttpd当中,我们发现其中的SCRIPT_FILENAME被正确的设置为

/scripts/a.jpg

所以不存在此问题。
后端的fastcgi在接受到该选项时,会根据fix_pathinfo配置决定是否对SCRIPT_FILENAME进行额外的处理,一般情况下如果不对fix_pathinfo进行设置将影响使用PATH_INFO进行路由选择的应用,所以该选项一般配置开启。Php通过该选项之后将查找其中真正的脚本文件名字,查找的方式也是查看文件是否存在,这个时候将分离出SCRIPT_FILENAME和PATH_INFO分别为

/scripts/a.jpg和xxx.php

最后,以/scripts/a.jpg作为此次请求需要执行的脚本,攻击者就可以实现让nginx以php来解析任何类型的文件了。

PHP为什么会接受这样的参数, 并且把a.jpg解析呢?
这就要说到PHP的cgi SAPI中的参数, fix_pathinfo了:

; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's
; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok
; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting
; this to 1 will cause PHP CGI to fix it's paths to conform to the spec. A setting
; of zero causes PHP to behave as before. Default is 1. You should fix your scripts
; to use SCRIPT_FILENAME rather than PATH_TRANSLATED.
cgi.fix_pathinfo=1

如果开启了这个选项, 那么就会触发在PHP中的如下逻辑:

/*
 * if the file doesn't exist, try to extract PATH_INFO out
 * of it by stat'ing back through the '/'
 * this fixes url's like /info.php/test
 */
if (script_path_translated &&
     (script_path_translated_len = strlen(script_path_translated)) > 0 &&
     (script_path_translated[script_path_translated_len-1] == '/' ||
//....以下省略.

到这里, PHP会认为SCRIPT_FILENAME是a.jpg, 而xxx.php是PATH_INFO, 然后PHP就把a.jpg当作一个PHP文件来解释执行… So…

POC: 访问一个nginx来支持php的站点,在一个任何资源的文件如robots.txt后面加上/xxx.php,这个时候你可以看到如下的区别:

访问http://www.lsproc.com/robots.txt

HTTP/1.1 200 OK
Server: nginx/0.6.32
Date: Thu, 20 May 2010 10:05:30 GMT
Content-Type: text/plain
Content-Length: 18
Last-Modified: Thu, 20 May 2010 06:26:34 GMT
Connection: keep-alive
Keep-Alive: timeout=20
Accept-Ranges: bytes

访问http://www.lsproc.com/robots.txt/xxx.php

HTTP/1.1 200 OK
Server: nginx/0.6.32
Date: Thu, 20 May 2010 10:06:49 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Keep-Alive: timeout=20
X-Powered-By: PHP/5.2.6

其中的Content-Type的变化说明了后端负责解析的变化,该站点就可能存在漏洞。

漏洞厂商:http://www.nginx.org

解决方案:

1. 修改php.ini中的cgi.fix_pathinfo为0 (即使你在php.ini中没有找到,也要设置,默认为1)
2. 把nginx的判断正则修改为去除/

if ( $fastcgi_script_name ~ \..*\/.*php ) {
    return 403;
}

3. 如果上传的文件类型为图片, 使用 gd/imagemagick 等进行处理后再保存, 原始文件务必删除
4. 上传的文件放到一个静态文件server, 此 server 不允许php 文件执行

本文参考链接

-- EOF --

nginx userid 模块客户端 cookie 解码

转载时请标明文章原始出处和作者信息, 作者: lostsnow.
http://www.lsproc.com/blog/nginx_userid_decode/

在网上看到用 ruby 解码的一段程序
http://forum.nginx.org/read.php?2,52592,52592

> cookie_uid = "0Cvz4ktwVPEdbRcMAwMFAg=="; cc = cookie_uid.unpack('m*').first; rr = cc.split("").map{|c| c[0].to_i}.inject([]) {|v,s| v.push sprintf("%02X", s); v; }.values_at(3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12).join("")
=> "E2F32BD0F154704B0C176D1D02050303"

我用 PHP 写了一个

<?php
function nginx_userid_decode($str)
{
    $str_unpacked =  unpack('h*', base64_decode(str_replace(' ', '+', $str)));
    $str_split = str_split(current($str_unpacked), 8);
    $str_map = array_map('strrev', $str_split);
    $str_dedoded = strtoupper(implode('', $str_map));

    return $str_dedoded;
}

// uid=8380A8C09A7E8C4B0A112CC202030303
echo nginx_userid_decode('wKiAg0uMfprCLBEKAwMDAg==');

update:

如果 base64 后的编码中含有 '+' , 在 url 传递中或是 $_COOKIE 数组读取中会被转换为空格

-- EOF --

nginx+factcgi 下使用 ob_flush

转载时请标明文章原始出处和作者信息, 作者: lostsnow.
http://www.lsproc.com/blog/use_ob_flush_on_nginx_fastcgi/

Nginx与php-cgi是两个独立的程序,通过TCP或Unix套接字通信,不像Apache那样是集成在一起的。所以,Nginx有fastcgi 缓冲区,数据超出缓冲区大小、或程序执行完,才会将内容输出到客户端。如果要使用ob_flush,不能开启gzip压缩输出。

nginx.conf:

fastcgi_buffer_size 4k;
fastcgi_buffers 8 4k;
gzip off;

php.ini:

output_buffering = Off
<?php
set_time_limit(0);
ob_end_clean();
ob_implicit_flush(1);

for($i = 0; $i < 10; $i++)
{
    echo $i . "<br />\n";
    echo str_repeat(' ', 1024*4);
    sleep(1);
}

其中 echo str_repeat(' ', 1024*4);
使得fastcgi_buffer_size 4k; 的缓冲区满,从而输出内容到浏览器

参考: http://blog.s135.com/nginx_php_v6/2/1/

-- EOF --

lighttpd + PHP(fastcgi) 配置

转载时请标明文章原始出处和作者信息, 作者: lostsnow.
http://www.lsproc.com/blog/lighttpd_phpfastcgi_config/

php(fastcgi) 5.2.6 编译参数

./configure  --prefix=/usr/local/php --with-mysql=/usr/local/mysql --with-gd=/usr --enable-calendar --with-zlib --with-bz2 --with-curl --with-mysqli=/usr/local/mysql/bin/mysql_config --enable-mbstring --with-openssl --enable-zend-multibyte --with-gettext --enable-exif --with-png-dir=/usr --with-jpeg-dir=/usr --with-libxml-dir=/usr --enable-gd-native-ttf --enable-dom --with-freetype-dir=/usr --with-iconv-dir=/usr --enable-fastcgi

lighttpd 1.5.0 编译参数

./configure --prefix=/usr/local/lighttpd --with-mysql=/usr/bin/mysql_config --with-openssl --with-pcre --with-bzip2 --enable-lfs --with-linux-aio

添加fastcgi 及lighttpd执行用户

groupadd www
useradd -g www -s /sbin/nologin -d /dev/null www

复制lighttpd源码包内 doc/spawn-php.sh 并修改如下

#!/bin/bash

## ABSOLUTE path to the spawn-fcgi binary
SPAWNFCGI="/usr/local/lighttpd/bin/spawn-fcgi"

## ABSOLUTE path to the PHP binary
FCGIPROGRAM="/usr/local/php/bin/php-cgi"

## TCP port to which to bind on localhost
FCGIPORT="1026"

## SOCKET to which to bind on localhost
FCGISOCKET="/tmp/php-fastcgi1.sock"

## number of PHP children to spawn
PHP_FCGI_CHILDREN=16

## maximum number of requests a single PHP process can serve before it is restarted
PHP_FCGI_MAX_REQUESTS=1000

## IP addresses from which PHP should access server connections
FCGI_WEB_SERVER_ADDRS="127.0.0.1"

# allowed environment variables, separated by spaces
ALLOWED_ENV="HOME PATH USER"

## if this script is run as root, switch to the following user
USERID=www
GROUPID=www

################## no config below this line

if test x$PHP_FCGI_CHILDREN = x; then
  PHP_FCGI_CHILDREN=5
fi

export PHP_FCGI_MAX_REQUESTS
export FCGI_WEB_SERVER_ADDRS

ALLOWED_ENV="$ALLOWED_ENV PHP_FCGI_MAX_REQUESTS FCGI_WEB_SERVER_ADDRS"

# port
#if test x$UID = x0; then
#  EX="$SPAWNFCGI -p $FCGIPORT -f $FCGIPROGRAM -u $USERID -g $GROUPID -C $PHP_FCGI_CHILDREN"
#else
#  EX="$SPAWNFCGI -p $FCGIPORT -f $FCGIPROGRAM -C $PHP_FCGI_CHILDREN"
#fi

# socket
if test x$UID = x0; then
  EX="$SPAWNFCGI  -s $FCGISOCKET -f $FCGIPROGRAM -u $USERID -g $GROUPID -C $PHP_FCGI_CHILDREN"
else
  EX="$SPAWNFCGI  -s $FCGISOCKET -f $FCGIPROGRAM -C $PHP_FCGI_CHILDREN"
fi

# copy the allowed environment variables
E=

for i in $ALLOWED_ENV; do
  E="$E $i=${!i}"
done

# clean the environment and set up a new one
env - $E $EX

保存为 spawn-php1.sh 放到 /etc/lighttpd 下, 多个fastcgi实例可复制此文件并修改相关端口号及socket

lighttpd 相关配置 lighttpd.conf 如下

server.use-ipv6 = "disable" # 缺省为禁用
server.event-handler = "linux-sysepoll" # Linux环境下epoll系统调用可提高吞吐量
#server.max-worker = 10 # 如果你的系统资源没跑满,可考虑调高  lighttpd进程数
server.max-fds = 4096 # 默认的,应该够用了,可根据实际情况调整
server.max-connections = 4096 # 默认等于 server.max-fds
server.network-backend = "linux-sendfile"
server.max-keep-alive-requests = 0 # 在一个keep-alive会话终止连接前能接受处理的最大请求数。0为禁止

# 设置要加载的module
server.modules = (
    "mod_rewrite",
    "mod_redirect",
#    "mod_alias",
    "mod_access",
#    "mod_cml",
#    "mod_trigger_b4_dl",
    "mod_auth",
    "mod_expire",
#    "mod_status",
#    "mod_setenv",
    "mod_proxy_core",
    "mod_proxy_backend_http",
    "mod_proxy_backend_fastcgi",
#    "mod_proxy_backend_scgi",
#    "mod_proxy_backend_ajp13",
#    "mod_simple_vhost",
    "mod_evhost",
#    "mod_userdir",
#    "mod_cgi",
    "mod_compress",
#    "mod_ssi",
#    "mod_usertrack",
#    "mod_secdownload",
#    "mod_rrdtool",
    "mod_accesslog" )

# 网站根目录
server.document-root = "/var/www/"

# 错误日志位置
server.errorlog = "/var/log/lighttpd/error.log"

# 网站Index
index-file.names = ( "index.php", "index.html",
                                 "index.htm", "default.htm" )

# 访问日志, 以及日志格式 (combined), 使用X-Forwarded-For可越过代理读取真实ip
accesslog.filename = "/var/log/lighttpd/access.log"
accesslog.format = "%{X-Forwarded-For}i %v %u %t \"%r\" %s %b  \"%{User-Agent}i\" \"%{Referer}i\""

# 设置禁止访问的文件扩展名
url.access-deny = ( "~", ".inc", ".tpl" )

# 服务监听端口
server.port = 80

# 进程id记录位置
server.pid-file = "/var/run/lighttpd.pid"

# virtual directory listings 如果没有找到index文件就列出目录。建议disable。
dir-listing.activate = "disable"

# 服务运行使用的用户及用户组
server.username = "www"
server.groupname = "www"

# gzip压缩存放的目录以及需要压缩的文件类型
compress.cache-dir = "/tmp/lighttpd/cache/compress/"
compress.filetype = ("text/plain", "text/html")

# fastcgi module
# for PHP don't forget to set cgi.fix_pathinfo = 1 in the php.ini
$HTTP["url"] =~ "\.php$" {
    proxy-core.balancer = "round-robin"
    proxy-core.allow-x-sendfile = "enable"
#    proxy-core.check-local = "enable"
    proxy-core.protocol = "fastcgi"
    proxy-core.backends = ( "unix:/tmp/php-fastcgi1.sock","unix:/tmp/php-fastcgi2.sock" )
    proxy-core.max-pool-size = 16
}

# 权限控制
auth.backend = "htpasswd"
auth.backend.htpasswd.userfile = "/var/www/htpasswd.userfile"

# 基于 evhost 的虚拟主机 针对域名
$HTTP["host"] == "a.lostk.com" {
    server.document-root = "/var/www/lostk/"
    server.errorlog = "/var/log/lighttpd/lostk-error.log"
    accesslog.filename = "/var/log/lighttpd/lostk-access.log"

    # 设定文件过期时间
    expire.url = (
        "/css/" => "access 2 hours",
        "/js/" => "access 2 hours",
    )

    # url 跳转
    url.redirect = (
        "^/$" => "/xxx/index.html",
    )

    # url 重写 (cakephp可用)
    url.rewrite = (
        "^/(css|js)/(.*)$" => "/$1/$2",
        "^/([^.]+)$" => "/index.php?url=$1",
    )

    # 权限控制
    auth.require   = ( "" =>
         (
            "method" => "basic",
            "realm" => "admin only",
            "require" => "user=admin1|user=admin2"  # 允许的用户, 用户列表文件 在上面配置的auth.backend.htpasswd.userfile 里
        ),
    )
}

# 针对端口的虚拟主机
$SERVER["socket"] == "192.168.0.1:8000" {
    server.document-root        = "/var/www/xxx/"
    server.errorlog = "/var/log/lighttpd/test-error.log"
    accesslog.filename = "/var/log/lighttpd/test-access.log"

   # ...
}

# fastcgi 以及 lighttpd 启动脚本

#!/bin/sh
startphp(){
    /etc/lighttpd/spawn-php1.sh
    /etc/lighttpd/spawn-php2.sh
}

starthttpd(){
    /usr/local/lighttpd/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf
}

start(){
    startphp
    starthttpd
}

stopphp(){
    killall php-cgi
}

stophttpd(){
    killall lighttpd
}

stop(){
    killall php-cgi lighttpd
}

case "$1" in
    start)
        start
        ;;
    startphp)
        startphp
        ;;
    starthttpd)
        starthttpd
        ;;
    stop)
        stop
    ;;
    stopphp)
        stopphp
        ;;
    stophttpd)
        stophttpd
        ;;
    *)
        echo "Usage: lighttpd {start|stop|startphp|starthttpd|stopphp|stophttpd}"
        RETVAL=1
esac

-- EOF --

lamp 相关配置 [Debian]

转载时请标明文章原始出处和作者信息, 作者: lostsnow.
http://www.lsproc.com/blog/configure_lamp/

编译环境

Debian (Ubuntu)

apt-get install build-essential
apt-get install libncurses5-dev
sudo apt-get install libxml2-dev libcurl3-dev libpng-dev libmhash-dev libmcrypt-dev libxslt-dev libpspell-dev

Mysql编译安装参数

CHOST="i686-pc-linux-gnu" CFLAGS="-O3 -msse2 -mmmx -mfpmath=sse -mcpu=pentium4 -march=pentium4 -pipe -fomit-frame-pointer" CXXFLAGS="-O3 -msse2 -mmmx -mfpmath=sse -funroll-loops -mcpu=pentium4 -march=pentium4 -pipe -fomit-frame-pointer" ./configure --prefix=/usr/local/mysql --localstatedir=/var/lib/mysql --with-comment=Source --with-server-suffix=-Community-Server --with-mysqld-user=mysql --without-debug --with-big-tables --with-charset=utf8 --with-collation=utf8_general_ci --with-extra-charsets=all --with-pthread --enable-static --enable-thread-safe-client --with-client-ldflags=-all-static --with-mysqld-ldflags=-all-static --enable-assembler --without-ndb-debug --without-isam --with-unix-socket-path=/usr/local/mysql/tmp/mysql.sock

配置成功会提示:

MySQL has a Web site athttp://www.mysql.com/which carries details on the
latest release, upcoming features, and other information to make your
work or play with MySQL more productive. There you can also find
information about mailing lists for MySQL discussion.

Remember to check the platform. specific part of the reference manual for
hints about installing MySQL on your platform. Also have a look at the
files in the Docs directory.

Thank you for choosing MySQL!

make
make install

groupadd mysql                     //增加mysql组
useradd -g mysql mysql         //增加mysql用户,这个用户属于mysql组
cd /usr/local/mysql
bin/mysql_install_db --user=mysql
chown -R root:mysql . //设置权限,注意后面有一个 "."
chown -R mysql /var/lib/mysql //设置 mysql 目录权限
chgrp -R mysql . //注意后面有一个 "."
cp share/mysql/my-medium.cnf /etc/my.cnf
cp share/mysql/mysql.server /etc/init.d/mysqld //开机自动启动 mysql。
chmod 755 /etc/init.d/mysqld
rcconf    //开启启动服务设置
/etc/init.d/mysqld start //启动 MySQL
bin/mysqladmin -u root password "password_for_root"

查看mysql编译参数
cat /usr/local/mysql/bin/mysqlbug |grep ./configure

把 mysql 加入环境变量
export PATH="$PATH:/usr/local/mysql/bin"

apache 编译

./configure   //配置源代码树
--prefix=/usr/local/apache2  //体系无关文件的顶级安装目录PREFIX ,也就Apache的安装目录。
--enable-module=so   //打开 so 模块,so 模块是用来提 DSO 支持的 apache 核心模块
--enable-deflate=shared   //支持网页压缩
--enable-expires=shared   //支持 HTTP 控制
--enable-rewrite=shared   //支持 URL 重写
--enable-cache  //支持缓存
--enable-file-cache  //支持文件缓存
--enable-mem-cache  //支持记忆缓存
--enable-disk-cache //支持磁盘缓存
--enable-static-support   //支持静态连接(默认为动态连接)
--enable-static-htpasswd   //使用静态连接编译 htpasswd - 管理用于基本认证的用户文件
--enable-static-htdigest  //使用静态连接编译 htdigest - 管理用于摘要认证的用户文件
--enable-static-rotatelogs  //使用静态连接编译 rotatelogs - 滚动 Apache 日志的管道日志程序
--enable-static-logresolve   //使用静态连接编译 logresolve - 解析 Apache 日志中的IP地址为主机名
--enable-static-htdbm   //使用静态连接编译 htdbm - 操作 DBM 密码数据库
--enable-static-ab  //使用静态连接编译 ab - Apache HTTP 服务器性能测试工具
--enable-static-checkgid   //使用静态连接编译 checkgid
--disable-cgid  //禁止用一个外部 CGI 守护进程执行CGI脚本
--disable-cgi   //禁止编译 CGI 版本的 PHP
--disable-userdir  //禁止用户从自己的主目录中提供页面
--with-mpm=worker // 让apache以worker方式运行
--enable-authn-dbm=shared // 对动态数据库进行操作。Rewrite时需要。

make
make install

建立一个符号连接:

ln -s /usr/local/apache2/bin/apachectl /etc/init.d/httpd
rcconf    //加入自动启动

php 编译

CHOST="i686-pc-linux-gnu" CFLAGS="-O3 -msse2 -mmmx -mfpmath=sse -mcpu=pentium4 -march=pentium4 -pipe -fomit-frame-pointer" CXXFLAGS="-O3 -msse2 -mmmx -mfpmath=sse -funroll-loops -mcpu=pentium4 -march=pentium4 -pipe -fomit-frame-pointer" ./configure --prefix=/usr/local/php5 --with-mysql=/usr/local/mysql --with-gd --enable-calendar --with-zlib --with-curl --with-mysqli=/usr/local/mysql/bin/mysql_config --enable-mbstring --with-apxs2=/usr/local/apache2/bin/apxs --with-openssl --enable-zend-multibyte --with-gettext --with-mcrypt --enable-exif --with-png-dir=/usr/local/lib --enable-ftp --with-mhash --with-libxml-dir=/usr/local/lib --with-xsl --with-pspell

配置完成提示
+--------------------------------------------------------------------+
| License: |
| This software is subject to the PHP License, available in this |
| distribution in the file LICENSE. By continuing this installation |
| process, you are bound by the terms of this license agreement. |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point. |
+--------------------------------------------------------------------+

Thank you for using PHP.

make
make install

修改/usr/local/apache2/conf/httpd.conf,在AddType部分加入如下内容
AddType application/x-httpd-php .php

-- EOF --