一、前言
php因天生支持web应用的开发,以其简单易学,开发效率高而备受喜爱。使其占据了大片的市场。但是php本身的安全问题却一直不曾消停,以及不规范的php代码编写规范,使得web应用漏洞百出。这篇文章从配置文件和代码编写角度出发,总结记录php相关安全。新手上路,向前辈致敬。
二、请充分了解你的php
1. 基本信息
注意到以下的文件结构在新版本php或者不同的发行版中略有不同,就好比在ubuntu18.04中安装php7就和下面的文件结构有较大的差别,所以下面的文件仅仅作为一个apache的架构参考。
2. 敏感配置
以下是一些常见的配置举例,更多请查看:
http://php.net/manual/zh/ini.core.php#ini.variables-order。
不在请求头中泄露php信息:
- expose_php=Off
不回显php错误(包括运行错误时和启动时错误),但是进行错误记录:
- play_errors=Off
- display_startup_errors=off
- log_errors=On
- error_log=/var/log/httpd/php_scripts_error.log
文件上传开启与否和最大上传文件限制:
- file_uploads=On
- upload_max_filesize=1M
控制最大post数据:
- post_max_size=1M
注意:到要比upload_max_filesize大,否则后者失效。
关闭远程代码执行:
- allow_url_fopen=Off
- allow_url_include=Off
关闭全局注册变量,不过默认5.x版本的php是off:
- register_globals=off
关于安全模式和粗暴的魔术引号过滤,注意到save_mode模式在php5.3以上版本,safe_mode被弃用,在php5.4以上版本,则将此特性完全去除了:
- safe_mode=On
- safe_mode_include_dir = D:/phpstudy/www/include/
- magic_quotes_gpc=Off #如果开启了这个,然后在php应用中使用addslashes()过滤输入会造成双重转义,使得过滤无济于事,遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。
- magic_quotes_runtime
资源管理防止过分消耗服务器资源:
- max_execution_time = 30
- max_input_time = 30
- memory_limit = 40M
禁用危险函数:
- disable_functions =
- phpinfo,eval,passthru,assert,exec,system,ini_set,ini_get,get_included_files,
- get_defined_functions,get_defined_constants,get_defined_vars,
- glob,``,chroot,scandir,chgrp,chown,shell_exec,proc_open,proc_get_status,
- ini_alter,ini_restore,dl,pfsockopen,openlog,syslog,readlink,
- symlink,popepassthru,stream_socket_server,fsocket,fsockopen
限制php访问文件系统:
- open_basedir='/var/www/html/';......;......
session保存路径:
- session.save_path="/var/lib/php/session"
上传文件默认路径:
- upload_tmp_dir="/var/lib/php/upload"
3. 关于危险函数
特殊符号:
“:反引号运算符在激活了安全模式或者关闭了 shell_exec() 时是无效的,同时与其它某些语言不同,反引号不能在双引号字符串中使用。否则将会当作shell命令执行,执行效果等同于shell_exec()。
三、不要过分相信php
1. 弱类型
前人之述备矣,仅仅做个汇总。同样还可以参看官网给出的类型表(PHP 类型比较表)。
- 0=='0' //true
- 0 == 'abcdefg' //true
- 1 == '1abcdef' //true
- null==false //true
- 123=='123' //true
- //哈希比较
- "0e132456789"=="0e7124511451155" //true
- "0e123456abc"=="0e1dddada" //false
- "0e1abc"=="0" //true
- "0x1e240"=="123456" //true
- "0x1e240"==123456 //true
- var_dump(intval('2')) //2
- var_dump(intval('3abcd')) //3
- var_dump(intval('abcd')) //0
- //任意两个array,MD5相等
- var_dump(md5($array1)==var_dump($array2)); //true
- //case 自转换,以下代码输出i is less than 3 but not negative
- $i ="2abc";
- switch ($i) {
- case 0:
- case 1:
- case 2:
- echo "i is less than 3 but not negative";
- break;
- case 3:
- echo "i is 3";
- }
- //in_array的缺陷,array_search
- $array=[0,1,2,'3'];
- var_dump(in_array('abc', $array)); //true
- var_dump(in_array('1bc', $array)); //true
- //strcmp在php5.x个版本后有些特性不太同,所以遇到的时候具体讨论
2. 全局注册变量
如果已经弃用的 register_globals 指令被设置为 on 那么局部变量也将在脚本的全局作用域中可用。例如, $_POST['foo'] 也将以 $foo 的形式存在。这将会造成一些变量覆盖,条件判断绕过。以下是简化的全局变量认证绕过模型:
- if(authenticated_user()){
- $authorized=true;
- }
- if($authorized){
- do something......
- }
对于以上的绕过,我们可以有以下的规避措施:(1) php.ini register_globals=off(2) 在每次判断前初始化变量,如下:
- $authorized=false;
- if(authenticated_user()){
- $authorized=true;
- }
- if($authorized){
- do something......
- }
3. php伪协议
伪协议在很多绕过场景下发挥着举足轻重的作用,如后面提到的文件包含file://协议绕过,以及最近才提出的phar协议反序列化对象注入,我们可以在不存在可控unserialization()函数的情况下利用phar反序列化对象,实现对象注入。所以在web应用中不要忽视他们的存在,千里之堤,溃于蚁穴。
四、向DVWA学习php安全的代码编写
以下样例来自于DVWA v1.9版本
1. sql注入
(1) Low level
- if( isset( $_REQUEST[ 'Submit' ] ) ) {
- // Get input
- $id = $_REQUEST[ 'id' ];
- // Check database
- $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
- $result = mysql_query( $query ) or die( '
' . mysql_error() . '' );- // Get results
- $num = mysql_numrows( $result );
- $i = 0;
- while( $i < $num ) {
- // Get values
- $first = mysql_result( $result, $i, "first_name" );
- $last = mysql_result( $result, $i, "last_name" );
- // Feedback for end user
- echo "
ID: {$id}";
First name: {$first}
Surname: {$last}- // Increase loop count
- $i++;
- }
- mysql_close();
- }
- ?>
在这个例子中,是最low安全等级的php代码编写样例,可以看到,代码中并没有对用户输入的id变量进行检查和过滤,同时使用的是$_REQUEST全局数组的方式,如果不是特别需要,我们编程的时候尽量不要使用$_REQUEST获取用户的参数,因为$_REQUEST的参数比较杂,包括$_GET,$_POST,$_COOKIE等超全局变量,并且二者还存在变量获取顺序的不一致,受配置文件中variables_order的约定,在存在waf的环境下,容易造成绕过。未经处理的用户输入直接与sql语句拼接交互,造成sql注入漏洞,十分危险。
(2) Medium level
- if( isset( $_POST[ 'Submit' ] ) ) { // Get input $id = $_POST[ 'id' ]; $id = mysql_real_escape_string( $id );
- // Check database
- $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
- $result = mysql_query( $query ) or die( '
' . mysql_error() . '' );- // Get results
- $num = mysql_numrows( $result );
- $i = 0;
- while( $i < $num ) {
- // Display values
- $first = mysql_result( $result, $i, "first_name" );
- $last = mysql_result( $result, $i, "last_name" );
- // Feedback for end user
- echo "
ID: {$id}";
First name: {$first}
Surname: {$last}- // Increase loop count
- $i++;
- }
- //mysql_close();
- }
- ?>
这个版本的代码,与之前的相比只是多了个mysql_real_escape_string函数的过滤,但是要知道这里的$id在sql语句中是数字类型,这样mysql_real_escape_string的转义就会形同虚设,注入仍旧是一马平川。当然不恰当的字符编码,可能会造成宽字节注入。
(3) High leval
- if( isset( $_SESSION [ 'id' ] ) ) {
- // Get input
- $id = $_SESSION[ 'id' ];
- // Check database
- $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
- $result = mysql_query( $query ) or die( '
Something went wrong.' );- // Get results
- $num = mysql_numrows( $result );
- $i = 0;
- while( $i < $num ) {
- // Get values
- $first = mysql_result( $result, $i, "first_name" );
- $last = mysql_result( $result, $i, "last_name" );
- // Feedback for end user
- echo "
ID: {$id}";
First name: {$first}
Surname: {$last}- // Increase loop count
- $i++;
- }
- mysql_close();
- }
- ?>
在高级版本中只是把注入点隐匿在了$_SESSION全局变量里面,而session中的id值的注册是通过用户输入$_POST全局变量传入,所以是完全可控的,这样一来,就和之前的注入没有什么不一样。这段代码是要提醒我们对于session,只要注册值是用户可控的,也是可能存在sql注入的风险的。另外需要注意到的是,在这个High级别的注入中,回显和传参页面不是同一个,是一个二阶注入,如果使用工具注入,如sqlmap,别忘了加上自定义回显–second-order参数。
(4) Impossible level
- if( isset( $_GET[ 'Submit' ] ) ) {
- // Check Anti-CSRF token
- checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
- // Get input
- $id = $_GET[ 'id' ];
- // Was a number entered?
- if(is_numeric( $id )) {
- // Check the database
- $data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
- $data->bindParam( ':id', $id, PDO::PARAM_INT );
- $data->execute();
- $row = $data->fetch();
- // Make sure only 1 result is returned
- if( $data->rowCount() == 1 ) {
- // Get values
- $first = $row[ 'first_name' ];
- $last = $row[ 'last_name' ];
- // Feedback for end user
- echo "
ID: {$id}";
First name: {$first}
Surname: {$last}- }
- }
- }
- // Generate Anti-CSRF token
- generateSessionToken();
- ?>
根据DVWA的说法,这样写出来的应用,是不存在sql注入的。也即这是个十分安全的php代码编写规范。why?首先,我们可以看到它使用Anti-CSRF token的方法来避免csrf攻击(具体细节会在下文csrf防御谈到),然后在sql语句的编写中,使用的是预处理语句,所谓的预处理就是通过php的pdo预处理机制PDO::prepare,先往数据库送出语句模板,进行解析,编译,然后第二次向数据库传入查询参数,在第二次的查询过程中可以理解为不再进行语义解析,所以即使传入sql语句,也会因为不进行语义解析而失效。所以这是一种比较推荐的数据库交互sql语句编写规范。现在很多主流的数据库已经支持预处理,即使不支持,PHP的PDO也会进行预处理模拟实现,这样对于程序员接口一致,不需了解不同数据库对预处理支持的方式差异。
2. CSRF
完整的攻击过程,可以看这篇前辈的文章:http://www.freebuf.com/articles/web/118352.html。
(1) Low level
- if( isset( $_GET[ 'Change' ] ) ) {
- // Get input
- $pass_new = $_GET[ 'password_new' ];
- $pass_conf = $_GET[ 'password_conf' ];
- // Do the passwords match?
- if( $pass_new == $pass_conf ) {
- // They do!
- $pass_new = mysql_real_escape_string( $pass_new );
- $pass_new = md5( $pass_new );
- // Update the database
- $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
- $result = mysql_query( $insert ) or die( '
' . mysql_error() . '' );- // Feedback for the user
- echo "
Password Changed.";- }
- else {
- // Issue with passwords matching
- echo "
Passwords did not match.";- }
- mysql_close();
- }
- ?>
所谓的CSRF(Cross-site request forgery)直白的翻译就是跨站点请求伪造。说人话就是攻击者通过诱使victim访问其精心构造的url或者访问其精心构造的页面,来使得攻击者可以以victim的身份做诸如发邮件,发消息,改密码等骚操作。在DVWA这个系列里面,模拟的是修改密码的界面。先来看下low等级的代码,可以说是没有进行仍和的再认证,试下为啥是“再认证”?其实我们在访问到这个修改密码界面的时候,已经登陆过一次,服务器会在每次访问时检查session。所以这是第一道认证。但是这种会话级别的认证对csrf是没有抵抗力的。具体的过程可以参看之前提到的链接。我们可以直接构造url:
http://localhost/dvwa/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#。
让victim访问,或者使用更加隐匿的:
构造无迹表单,结合js发送请求,或者:
来实现欺骗隐匿行踪,达到修改密码的目的。顺便盗用两个别人的poc方便展示:
图片形式诱导
404
file not found.
隐藏表单的形式
- New password:
- Confirm new password:
- /body>
(2) Medium level
- if( isset( $_GET[ 'Change' ] ) ) {
- // Checks to see where the request came from
- if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
- // Get input
- $pass_new = $_GET[ 'password_new' ];
- $pass_conf = $_GET[ 'password_conf' ];
- // Do the passwords match?
- if( $pass_new == $pass_conf ) {
- // They do!
- $pass_new = mysql_real_escape_string( $pass_new );
- $pass_new = md5( $pass_new );
- // Update the database
- $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
- $result = mysql_query( $insert ) or die( '
' . mysql_error() . '' );- // Feedback for the user
- echo "
Password Changed.";- }
- else {
- // Issue with passwords matching
- echo "
Passwords did not match.";- }
- }
- else {
- // Didn't come from a trusted source
- echo "
That request didn't look correct.";- }
- mysql_close();
- }
- ?>
在这个级别的CSRF漏洞中,服务端多了一句eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ]校验,ereg()函数是模式匹配,通过超全局数组获取了请求头referer值(也就是访问者向host发起请求时所在的页面)和host值,并且检查host的值是否在referer中出现。根据权威
(https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name),这两个值无法以编程的方式修改,抓包除外,因为在csrf中无法通过抓取客户端的包进行修改,所以按理来说是安全的。实则不然,通过公网服务器,诱使victim访问名字包含host的html文件就可以实现绕过。
(3) High level
- if( isset( $_GET[ 'Change' ] ) ) {
- // Check Anti-CSRF token
- checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
- // Get input
- $pass_new = $_GET[ 'password_new' ];
- $pass_conf = $_GET[ 'password_conf' ];
- // Do the passwords match?
- if( $pass_new == $pass_conf ) {
- // They do!
- $pass_new = mysql_real_escape_string( $pass_new );
- $pass_new = md5( $pass_new );
- // Update the database
- $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
- $result = mysql_query( $insert ) or die( '
' . mysql_error() . '' );- // Feedback for the user
- echo "
Password Changed.";- }
- else {
- // Issue with passwords matching
- echo "
Passwords did not match.";- }
- mysql_close();
- }
- // Generate Anti-CSRF token
- generateSessionToken();
- ?>
在高级别中的代码,主要是使用了Anti-csrf机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。我们可以按F12来看看这个token:
可以看到不同的用户会返回一个不同的token,这个token在hidden栏里面,这样一来,迫于同源策略,攻击者无法获取victim的token,也就无法实现CSRF攻击。但是真的无法实现吗?配合xss我们还是可以盗取token的,但是这难度无疑增大,我们必须要有服务器的一个xss漏洞来盗取token,然后再使用CSRF。攻击成本也增大。
(4) Impossible level
- if( isset( $_GET[ 'Change' ] ) ) {
- // Check Anti-CSRF token
- checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
- // Get input
- $pass_curr = $_GET[ 'password_current' ];
- $pass_new = $_GET[ 'password_new' ];
- $pass_conf = $_GET[ 'password_conf' ];
- // Sanitise current password input
- $pass_curr = stripslashes( $pass_curr );
- $pass_curr = mysql_real_escape_string( $pass_curr );
- $pass_curr = md5( $pass_curr );
- // Check that the current password is correct
- $data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
- $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
- $data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
- $data->execute();
- // Do both new passwords match and does the current password match the user?
- if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
- // It does!
- $pass_new = stripslashes( $pass_new );
- $pass_new = mysql_real_escape_string( $pass_new );
- $pass_new = md5( $pass_new );
- // Update database with new password
- $data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
- $data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
- $data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
- $data->execute();
- // Feedback for the user
- echo "
Password Changed.";- }
- else {
- // Issue with passwords matching
- echo "
Passwords did not match or current password incorrect.";- }
- }
- // Generate Anti-CSRF token
- generateSessionToken();
- ?>
在high的基础上,直接进行了密码再认证,这样一来,即使盗取了token,没有原始密码,也无法进行修改密码的操作,这样CSRF就可以完全避免了。所以通过这个CSRF系列,我们可以知晓,在csrf防御中,采用关键操作的原子性认证,是避免这一漏洞攻击的不二办法。其实我们只关注了CSRF部分,在之前的level中,也还存在了sql注入,在这个impossible版本里,还使用了之前提到的预操纵来进行数据库交互,降低了sql注入的风险。
3. Command Injection
(1) Low level
- if( isset( $_POST[ 'Submit' ] ) ) {
- // Get input
- $target = $_REQUEST[ 'ip' ];
- // Determine OS and execute the ping command.
- if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
- // Windows
- $cmd = shell_exec( 'ping ' . $target );
- }
- else {
- // *nix
- $cmd = shell_exec( 'ping -c 4 ' . $target );
- }
- // Feedback for the end user
- echo "
{$cmd}";- }
- ?>
过分相信用户的输入,直接拼接到ping 命令中,会造成命令注入。注意到常用的bash命令拼接的方式有||.&&,|,&,;这五个,所以由于没有过滤完全,我们直接进行命令拼接,然后执行任意命令,如127.0.0.1;cat /etc/passwd。
(2) Medium level
- if( isset( $_POST[ 'Submit' ] ) ) {
- // Get input
- $target = $_REQUEST[ 'ip' ];
- // Set blacklist
- $substitutions = array(
- '&&' => '',
- ';' => '',
- );
- // Remove any of the charactars in the array (blacklist).
- $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
- // Determine OS and execute the ping command.
- if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
- // Windows
- $cmd = shell_exec( 'ping ' . $target );
- }
- else {
- // *nix
- $cmd = shell_exec( 'ping -c 4 ' . $target );
- }
- // Feedback for the end user
- echo "< 分享标题:浅谈PHP安全规范
文章起源:http://www.shufengxianlan.com/qtweb/news9/446009.html网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联