测试版本:PHPYUN 3.1 GBK beta 20140728
PHPYUN使用了两套waf,一套自己写的,一套360的,从第一套开始。
\data\db.safety.php:
code 区域 quotesGPC(); // 效果:addslashes
if($config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey'])
{
foreach($_POST as $id=>$v){
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
}
}
foreach($_GET as $id=>$v){
safesql($id,$v,"GET",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
if(!is_array($v))
$v=substr(strip_tags($v),0,80);
$_GET[$id]=common_htmlspecialchars($v);
}
foreach($_COOKIE as $id=>$v){
safesql($id,$v,"COOKIE",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$v=substr(strip_tags($v),0,52);
$_COOKIE[$id]=common_htmlspecialchars($v);
}
首先通过quotesGPC()对输入进行addslashes,然后分别对$_GET/$_POST/$_COOKIE做同样的过滤($_POST为例):
code 区域 safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
但对$_POST做过滤时多了一个判断:
code 区域 if($config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey'])
{
foreach($_POST as $id=>$v){
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
$_POST[$id]=common_htmlspecialchars($v);
}
}
当判断为真时进行过滤,为假则不过滤,而判断中的两个条件:
code 区域 $config['sy_istemplate']!='1' || md5(md5($config['sy_safekey']).$_GET['m'])!=$_POST['safekey']
默认$config['sy_istemplate']=1,因此只要第二个条件为false就可以跳过对$_POST参数的过滤。
而第二个条件最关键的参数$config['sy_safekey']未知,它是在安装时随机生成的:
\install\index.php
code 区域 $r=rand(10000000,99999999);
mysql_query("update $table_config set `config`='$r' where `name`='sy_safekey'");
懒得穷举,可不可以用更优雅的方法拿到safekey?
搜索一下,看看有没有可能从其他地方泄露这个值,结果发现了好玩的东西:
\templates_c\%%8F^8F9^8F951B06%%admin_web_config.htm.php
code 区域 <tr>
<th width="160">系统安全码:</th>
<td><input class="input-text tips_class" type="text" name="sy_safekey" id="sy_safekey" value="<?php echo $this->_tpl_vars['config']['sy_safekey']; ?>
" size="40" maxlength="255"/><font color="gray" style="display:none">系统部分功能使用的加密串,请自定义修改,如:986jhgyutw.*x</font></td>
<td width="160">sy_safekey</td>
</tr>
此处模板里会输出safekey,但是如果直接访问这个模板的话,会因为$this未定义而报错。
按照模板编译的风格,这个命名方式的模板应该是编译后的模板,来找到其对应的编译前模板:
\template\admin\admin_web_config.htm
code 区域 <tr>
<th width="160">系统安全码:</th>
<td><input class="input-text tips_class" type="text" name="sy_safekey" id="sy_safekey" value="{yun:}$config.sy_safekey{/yun}" size="40" maxlength="255"/><font color="gray" style="display:none">系统部分功能使用的加密串,请自定义修改,如:986jhgyutw.*x</font></td>
<td width="160">sy_safekey</td>
</tr>
如果能让PHPYUN编译这个模板,拿到输出,safekey就到手了,因此需要找到可控的编译点:
\company\model\index.class.php:
code 区域 function index_action(){
if($this->uid!=$_GET['id']&&$_COOKIE['usertype']=='1'){
... ... ...
}
if($_POST['submit'])
{
... ... ...
}
if($_POST['submit2'])
{
... ... ...
}
... ... ...
$tp=$_GET['tp']?$_GET['tp']:"index";
$this->seo("company_".$tp);
$this->yunset("com_style",$this->config['sy_weburl']."/template/company/".$tplurl."/");
$this->yunset("comstyle","../template/company/".$tplurl."/");
$this->yunset("defaultstyle","../template/default/");
$this->yuntpl(array('company/'.$tplurl."/".$tp));
}
在这里$_GET['tp']被传入到$tp然后进入$this->yuntpl(array('company/'.$tplurl."/".$tp)),yuntpl函数的主要作用是加载模板并编译输出它,所以只要控制$_GET['tp']为../../admin/admin_web_config就可以编译想要的模板了,试试:
官方demo试试:
看到可爱的safekey了。
拿到safekey后,就能让前面判断的第二个条件为false,因此第一个waf就跳过了。
来到第二个waf,360的webscan:
code 区域 if(is_file(LIB_PATH.'webscan360/360safe/360webscan.php')){
require_once(LIB_PATH.'webscan360/360safe/360webscan.php');
}
在360webscan.php中还有又一轮的过滤: \include\webscan360\360safe\360webscan.php
code 区域 //get拦截规则
$getfilter = "<[^>]*?=[^>]*?&#[^>]*?>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//post拦截规则
$postfilter = "<[^>]*?=[^>]*?&#[^>]*?>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b[^>]*?>|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
//cookie拦截规则
$cookiefilter = "\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
这些正则会被用在:
code 区域 if ($webscan_switch&&webscan_white($webscan_white_directory,$webscan_white_url)) {
if ($webscan_get) {
foreach($_GET as $key=>$value) {
webscan_StopAttack($key,$value,$getfilter,"GET"); // 对GET进行过滤
}
}
if ($webscan_post) {
foreach($_POST as $key=>$value) {
webscan_StopAttack($key,$value,$postfilter,"POST"); // 对POST进行过滤
}
}
if ($webscan_cookie) {
foreach($_COOKIE as $key=>$value) {
webscan_StopAttack($key,$value,$cookiefilter,"COOKIE"); // 对COOKIE进行过滤
}
}
if ($webscan_referre) {
foreach($webscan_referer as $key=>$value) {
webscan_StopAttack($key,$value,$postfilter,"REFERRER"); // 对REFERER头进行过滤
}
}
}
而检测点的第一个if判断是可以跳过的:
code 区域 if ($webscan_switch&&webscan_white($webscan_white_directory,$webscan_white_url)) {
$webscan_switch默认是1.
webscan_white($webscan_white_directory,$webscan_white_url)用来检查当前URL请求在不在白名单范围中,存在的话就不检查: \include\webscan360\360safe\360webscan.php:215
code 区域 /**
* 拦截目录白名单
*/
function webscan_white($webscan_white_name,$webscan_white_url=array()) {
$url_path=$_SERVER['PHP_SELF'];
$url_var=$_SERVER['QUERY_STRING'];
if (preg_match("/".$webscan_white_name."/is",$url_path)==1) {
return false;
}
foreach ($webscan_white_url as $key => $value) {
if(!empty($url_var)&&!empty($value)){
if (stristr($url_path,$key)&&stristr($url_var,$value)) {
return false;
}
}
elseif (empty($url_var)&&empty($value)) {
if (stristr($url_path,$key)) {
return false;
}
}
}
return true;
}
webscan_white先判断请求url是否在白名单里,接下来判断请求的参数对是否在白名单里,白名单:
code 区域 //后台白名单,后台操作将不会拦截,添加"|"隔开白名单目录下面默认是网址带 admin /dede/ 放行
$webscan_white_directory='admin|\/dede\/|\/install\/';
//url白名单,可以自定义添加url白名单,默认是对phpcms的后台url放行
//写法:比如phpcms 后台操作url index.php?m=admin php168的文章提交链接post.php?job=postnew&step=post ,dedecms 空间设置edit_space_info.php
$webscan_white_url = array('index.php' => 'admin_dir=admin','post.php' => 'job=postnew&step=post','edit_space_info.php'=>'');
这个白名单检测知道怎么绕过的人应该很多,只要让传入参数存在白名单目录或参数即可。 比如利用白名单目录: http://**.**.**.**/index.php/dede/?m=foo&c=bar&id=1' and 1=2 union select xxx 由于请求中包含了白名单目录/dede/,所以放行。 利用白名单参数: http://**.**.**.**/index.php?m=foo&c=bar&admin_dir=admin&id=1' and 1=2 union select xxx 同理,请求中包含了白名单参数所以放行。
绕过360waf后,接下来就进入程序逻辑,没有什么需要绕的了。
虽然绕了两个waf,但是还有一个quotesGPC()函数是生效的,quotesGPC()的作用:
code 区域 function quotesGPC() {
if(!get_magic_quotes_gpc()){
$_POST = array_map("addSlash", $_POST);
$_GET = array_map("addSlash", $_GET);
$_COOKIE = array_map("addSlash", $_COOKIE);
}
}
function addSlash($el) {
if (is_array($el))
return array_map("addSlash", $el);
else
return addslashes($el);
}
等同于一个addslashes_deep,想要绕过这个得结合具体漏洞点:
\wap\model\login.class.php:30
code 区域 function index_action()
{
$this->get_moblie(); // 通过UA判断是否是手机端
if($this->uid || $this->username)
{
$this->wapheader('member/index.php'); //登陆用户跳转
}
if($_POST['submit'])
{
if($_POST['wxid'])
{
$wxparse = '&wxid='.$_POST['wxid'];
}
$usertype=$_POST['usertype']?intval($_POST['usertype']):1;
$username = str_replace('\\','',$_POST['username']); // 漏洞点:过滤\
if($usertype>0 && $username!='')
{
$userinfo = $this->obj->DB_select_once("member","`username`='".str_replace('\\','',$_POST['username'])."' and usertype='".$usertype."'","username,usertype,password,uid,salt");
... ... ...
由于str_replace('\\','',$_POST['username']);过滤了\,直接导致quotesGPC函数失效。
quotesGPC失效,单引号就可逃脱
safekey拿到,第一套过滤就可绕过
360webscan用白名单绕过
所以该处注入漏洞存在并无视防御