PHP编码安全:请求伪造攻击

  • A+
所属分类:安全开发

来自公众号:计算机与网络安全


如果Web系统中存在服务器请求伪造漏洞,不仅会影响系统本身,而且会影响到与其相关的其他系统服务。一个被忽视的服务器请求伪造漏洞,很容易引起蝴蝶效应,可能给整个系统带来长期的巨大危害。


1、服务器请求伪造


服务器请求伪造(Server-Side Request Forgery,SSRF)漏洞是一种由攻击者利用某服务器请求来获取内网或外网系统资源,从服务器发起请求的一个安全漏洞。正因为它是由服务器发起的,所以它能够请求到与它相连而与外网隔离的内部系统。一般情况下,SSRF攻击的目标是企业的内网系统。


SSRF可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息。攻击运行于内网或本地的应用程序(比如攻击内部数据库系统),并通过扫描默认Web文件对内网Web应用进行指纹识别,同时可以利用file协议读取本地文件。


SSRF形成的原因大多是由于服务器提供了从其他服务器应用中获取数据的功能且没有对目标地址进行过滤与限制,比如从指定URL地址中获取网页文本内容、加载指定地址的图片、下载等。


SSRF漏洞流程如图1所示,即:

PHP编码安全:请求伪造攻击

图1  SSRF攻击流程


1)攻击者构造请求;


2)服务器根据攻击者构造的请求对内网服务器进行请求;


3)内网服务器将请求反馈给服务器;


4)服务器将获取到的内网资源返回给攻击者。


2、SSRF漏洞的危害


SSRF漏洞的主要危害是使服务器资源泄露,内网服务任意扫描泄露内网信息。很多网站提供了通过用户指定的URL上传图片和文件的功能,可以将第三方的图片和文件直接保存在当前的Web系统上,如果用户输入的URL是无效的,大部分的Web应用会返回错误信息。


攻击者可以输入一些不常见但有效的URL,比如以下URL。


http://127.0.0.1:8080/dir/images/

http://127.0.0.1:22/dir/public/image.jpg

http://127.0.0.1:3306/dir/images/


然后根据服务器的返回信息来判断端口是否开放。大部分应用并不会去判断端口,只要是有效的URL,就会发出请求。而大部分的TCP服务,在建立socket连接时就会发送banner信息。banner信息是使用ASCII编码的,能够作为原始的HTML数据展示。当然,服务端在处理返回信息的时候一般不会直接展示,但是不同的错误码,返回信息的长度以及返回时间都可以作为依据来判断远程服务器的端口状态。


以下是一段未经过安全编码的代码,有很大概率被攻击者利用进行端口扫描。


<?php

if(isset($_GET['url']))

{

$url=$_GET['url'];

$filename='/tmp/'.rand().'txt';

$ch=curl_init();

$timeout=5;

curl_setopt($ch,CURLOPT_URL,$url);

curl_setopt($ch,CURLOPT_FOLLOWLOCATION,1);

curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);

curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,$timeout);

$content=curl_exec($ch);

$fp_in=fopen($filename,'w');

fwrite($fp_in,$content);

fclose($fp_in);

$fp_out=fopen($filename,"r");

$result=fread($fp_out,filesize($filename));

fclose($fp_out);

echo $result;

} else {

echo "请输入资源地址";

}


由于上面不安全编码的代码,容易被攻击者利用对内网Web应用进行指纹识别,识别内网应用所使用的框架、平台、模块以及cms,这实际上为潜在的攻击提供了很多“便利”。大多数Web应用框架有一些独特的文件和目录,通过这些文件可以识别出应用的类型,甚至是详细的版本。根据这些信息就可以有针对性地搜集漏洞进行攻击。比如可以通过访问下列文件来判断phpMyAdmin是否安装。


http://127.0.0.1:8080/phpMyAdmin/themes/original/img/b_tblimport.png

http://127.0.0.1:8081/wp-content/themes/default/images/audio.jpg

http://127.0.0.1:8082/profiles/minimal/translations/README.txt


3、在PHP中容易引起SSRF的函数


容易出现SSRF漏洞的功能代码通常是为了获取远程或本地内容,例如使用file_get_contents()、fsockopen()、curl()等函数。


如使用file_get_contents()函数从用户指定的URL中获取文件,然后把它输出到浏览器端用于下载。


<?php

$content=base64_decode(file_get_contents($_GET['url']));

echo $content;


正常情况下,用户的下载地址是通过服务端提供的base64编码后的路径回传到服务端进行下载操作的,不会造成SSRF漏洞,但是攻击者很容易识别base64的内容,如果攻击者将下载地址替换成自己伪造的编码内容,则可以下载服务器上的任意文件,以及对内网地址进行扫描。如将/etc/password进行base64编码之后放在URL参数中,会直接将/etc/password的内容展示在页面上,使得攻击者能直接获取服务器的所有用户列表,从而危害服务器的安全。


使用fsockopen()函数实现获取用户指定URL中的数据。这个函数会使用socket与服务器建立tcp连接,传输原始数据。在下面的示例代码中,由于研发人员的疏忽,用户一旦传入“主机名+端口+地址”之后,攻击者可以对内网服务器进行任意扫描。


<?php

function GetFile($host,$port,$link)

{

$fp = fsockopen($host, intval($port), $errno, $errstr, 30); 

if (!$fp) {

echo "$errstr (error number $errno) n";

} else {

$out = "GET $link HTTP/1.1rn";

$out .= "Host: $hostrn";

$out .= "Connection: Closernrn";  $out .= "rn";

fwrite($fp, $out);

$contents='';

while (!feof($fp)) {

$contents.= fgets($fp, 1024);

}

fclose($fp);

return $contents;

}

}

echo GetFile($_GET['host'],$_GET['port'],$_GET['link']);

?>


curl函数使用不当获取数据也会造成SSRF漏洞。


<?php

if (isset($_POST['url']))

{

$link = $_POST['url'];

$curlobj = curl_init();

curl_setopt($curlobj, CURLOPT_POST, 0);

curl_setopt($curlobj,CURLOPT_URL,$link);

curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);

$result=curl_exec($curlobj);

curl_close($curlobj);

echo $result;

}

?>

4、容易造成SSRF的功能点


从上面的流程可以看出,SSRF都是由于服务端需要获取其他服务器的相关服务的功能而造成的,因此可以列举几种在Web应用中常见的从服务端获取其他服务器信息的功能。


(1)页面分享


如图2所示,某网站的页面分享。用户输入希望分享的URL,该网站会通过自己的后端服务器获取URL中的内容返回到用户页面中。

PHP编码安全:请求伪造攻击

图2  页面分享


通过目标URL地址获取了网页标题、图片和相关文本内容。如果在此功能中没有对目标地址的范围进行过滤与限制,则就存在着SSRF漏洞。国内多家知名互联网企业曾因为分享功能而被曝出SSRF漏洞。


(2)页面转码


移动互联网刚刚兴起的时候,大部分网站不提供专门的移动端页面,而手机端的移动浏览器不能很好地支持PC浏览器的页面展示(包括昂贵的流量费用因素)。


很多搜索公司为了抢占移动端的市场入口,纷纷推出了页面转码服务,通过搜索,将PC端的页面转成适应移动端浏览器可查看的页面。一旦控制不当,很容易产生SSRF漏洞。


(3)翻译服务


为了不懂外语的用户可以正常浏览外语页面,翻译服务通过URL地址翻译对应页面中文本的内容,以方便用户阅读。如果不做好内网穿透限制,则很容易被攻击者利用。


(4)图片加载与下载


通过URL地址加载或下载图片。比如,加载图片服务器上的图片展示给用户,为了有更好的用户体验,通常对图片进行尺寸比例调整、水印添加、图片格式转换、压缩等,就可能造成SSRF漏洞。


(5)图片、文章收藏功能


早期,有好的个人博客,可以将其他互联网服务商的内容,例如图片、文字内容等,通过爬虫或者单个URL获取页面中的内容,并复制到自己的服务器上。通常不会限制访问路径,很容易造成SSRF漏洞。


5、SSRF漏洞防御


防止SSRF漏洞的主要方式是合理控制访问权限,尽可能地控制PHP的访问权限,防止穿透到内网以及访问非授权的资源。通常需要做到以下几点。


1)无特殊需要的情况下,不要从用户那里接收要访问的URL,防止用户自行构建URL地址进行穿透访问。


2)在没有对服务器本身文件访问需求的情况下,建议开启PHP的open_basedir配置,将PHP的访问限制在特定目录下,禁止PHP随意访问服务器任意路径。


在PHP配置中进行如下设置开启open_basedir。


open_basedir=/home/web/php/

; PHP配置中限定PHP的访问目录为/home/web/php/


或在PHP项目中使用ini_set()函数进行开启。


ini_set('open_basedir','/home/web/php/');

; PHP项目中限定PHP的访问目录为/home/web/php/


开启open_basedir可以有效地防止file_get_content、curl、fopen等函数对服务器敏感文件进行访问。


3)如果必须接收用户传递的URL,建议使用白名单机制。如下面的代码示例中,只允许用户请求特定的网址,限制请求的端口为HTTP常用的端口,比如80、443,同时只允许用户访问特定的文件类型,如只允许访问静态文件。并且统一系统返回的错误信息,避免请求的错误信息直接返回给用户,避免用户可以根据错误信息来判断远端服务器的端口状态。禁用不需要的协议,仅仅允许HTTP和HTTPS请求,可以防止类似于file:///、gopher://、ftp://等引起的安全问题。


<?php

$url=$_GET['url'];

$schemeWhiteList=array("http","https");

$hostWhiteList=array("www.ptpress.com.cn");

$portWhiteList=array("80","443");

$typeWhiteList=array("html","gif","png","jpeg");

$urlInfo=parse_url($url);

// 只允许HTTP和HTTPS请求

if(!in_array($urlInfo['scheme'],$schemeWhiteList)) {

die("访问地址类型错误!");

}

// 只允许访问白名单内的域名

if(!in_array($urlInfo['host'],$hostWhiteList)) {

die("访问地址错误!");

}

// 只允许访问白名单内的端口

if(!in_array($urlInfo['port'],$portWhiteList)) {

die("访问地址端口错误!");

}

$type=pathinfo($url,PATHINFO_EXTENSION);

// 只允许访问白名单内的文件类型

if(!in_array($type,$typeWhiteList)) {

die("访问文件类型错误!");

}

$info=file_get_contents($url);

if(empty($info)) {

die("访问失败");

} else {

echo $info;

}

?>


4)如果在项目中需要获取外网资源,建议使用黑名单屏蔽内网,以避免应用被用来获取内网数据,攻击内网。


<?php

$url=$_GET['url'];

$schemeWhiteList=array("http","https");

$blackHostList=array("172.","10.","localhost","127.","192.");

$blackIpList=array("172.","10.","127.","192.");

$urlInfo=parse_url($url);

// 只允许HTTP和HTTPS请求

if(!in_array($urlInfo['scheme'],$schemeWhiteList)) {

die("访问地址类型错误!");

}

// 拒绝访问黑名单内的地址

foreach($blackHostList as $blackHost) {

if(strpos($urlInfo['host'],$blackHost)===0) {

die("访问地址错误!");

}

}

$ip=gethostbyname($urlInfo['host']);

// 拒绝访问黑名单内的IP

foreach($blackIpList as $ipHost) {

if(strpos($ip,$ipHost)===0) {

die("访问地址错误!");

}

}

$info=file_get_contents($url);

if(empty($info)) {

die("访问失败");

} else {

echo $info;

}

?>


发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: