公司公众号要发文章,正好把以前的笔记整理一波。

0x01 PHP敏感配置项

register_globals(php版本小于5.4时存在)

当该配置项为ON时,会把用户通过GET、POST提交的参数自动注册成全局变量。当代码中存在有未初始化的变量时,可能会导致变量覆盖的问题;

(PS:其中参数覆盖的顺序受到配置文件中variables_order的参数影响,默认是EGPCS。按顺序,右边的参数来源会覆盖左边的的参数来源)

allow_url_include(php版本大于5.2默认为off)

当该配置项为ON时,可以通过include、require等函数进行远程文件包含

其中有个类似的配置项是allow_url_fopen,这个参数配置为on的时候可以函数中例如file_get_contents中打开url。

当两个配置项都为ON的时候,可以直接使用url进行远程包含,当include为ON,fopen为OFF时,只能通过php伪协议进行包含

magic_quato_gpc(php版本小于5.4存在)

此配置项为ON的时候会对GET、POST、COOKIE变量中的单引号(‘)、双引号(“)、反斜杠(\)、空字符(NULL)前添加反斜杠进行转义,注意:这个配置并不会对SERVER变量里的特殊字符进行转义,因此可能会导致referer、client-ip存在注入等漏洞

magic_quato_runtime(php版本小于5.4存在)

这个配置和magic_quato_gpc的区别就在于runtime是对从数据库或者文件中取出的数据进行转义,因此只对例如file()、fgets()、fread()、mysql_fetch_array()等很多对数据库查询和文件读取的函数产生影响

magic_quato_sybase(php版本小于5.4存在)

这个配置和magic_quato_gpc 的区别在于,sybase只会转义空字符,把单引号转为双引号,并且这个配置如果为ON会覆盖gpc的配置

open_basedir

这个配置用来设置限定php程序只能访问哪些目录。在windows下,多个目录用分号(;)分割,linux下用冒号(:)进行分割。注意的是配置的目录需要用斜杠(/)进行封尾,否则就变成了前缀匹配。例如,配置/var/test,那么/var/test和/var/test123都是可以进行访问的,如果指定一个确定的目录就要写成/var/test/

0x02 PHP常见敏感函数

  • 注入: select from 、mysql_connect、mysql_query、mysql_fetch_*、update、insert、delete
  • 宽字节注入: set names gbk、character_set_client=gbk mysql_set_charset(‘gbk’)、iconv
  • 二次编码注入: urldecode、rawurldecode、
  • 文件包含 : include、include_once 、require、require_once
  • 文件上传 : move_upload_file
  • 任意文件删除 : unlink、session_destory
  • 代码注入 : eval assert、preg_replace(/e)、 call_user_func、call_user_func_array、array_map等
  • 命令执行: system、exec、shell_exec、passthru 、pctnl_exec、popen、proc_exec、``
  • 变量覆盖: extract、parse_str、$$
  • 反序列化: unserialize
  • 随机数: rand、mt_rand

0x03 常见漏洞解析

  • 各种问题可以导致系统重装

    一般程序都是通过判断install文件下有没有安装过程中生成的以lock为后缀的文件或者config配置文件来判断有没有安装。

1.未对系统是否已经安装进行判断

例如PHPSHE B2C商城1.6(wooyun 2014-062047)


可以看到代码未对是否安装进行任何判断,直接进入安装流程

2.变量覆盖绕过

例如frcms (wooyun 2014-073244)

其中主要的漏洞代码是
1
2
3
foreach(Array('_GET','_POST','_COOKIE') as $_request){
foreach($$_request as $_k => $_v) ${$_k} = _runmagicquotes($_v);
}

他会把你从GET、POST、COOKIE中的变量注册为全局变量,因此我们直接通过GET参数提交$insLockfile变量即可绕过

3.判断已安装后未exit()退出程序

例如startbbs (wooyun-2013-045143)

1
2
3
4
5
6
7
8
9
10
11
class Install extends Install_Controller
{
function __construct ()
{
parent::__construct();
$this->load->library('myclass');
$file=FCPATH.'install.lock';
if (file_exists($file)){
$this->myclass->notice('alert("系统已安装过");window.location.href="'.site_url().'";');
}
}

可以看到其中判断install.lock文件存在后直接使用js代码将用户进行重定向,但是并没有die程序,直接从前端删除返回的js代码即可重装

4.还可以借助任意文件删除的漏洞来删除lock文件,然后进行重装(PS:这个就留在任意文件删除再分析)

  • 文件包含漏洞(未做过滤+截断)

例如: 用thinkphp 改造的hdcms (wooyun-2015-092061)
我们先跟着框架走一遍,首先查看入口文件index.php

首先定义了一些基本的框架目录,然后就直接开始引入框架文件,我们进入框架初始化文件hdphp.php


前面还是检测一次是否成功初始化常量,然后检测是否进行过编译,否则就载入文件进行首次编译,然后进行boot的run方法,查看该方法内容。

前面依然是定义了各种常量,我们可以直接跳过,来到最后的应用初始化,查看该方法

可以看到module_path常量是通过将get形式提交的var_group参数进行拼接的,然后又将module_path拼接入module_config_path,最后使用require进行了文件包含。(PS:因为这里后面制定了config.php,所以需要用到%00进行截断)

但是我们分析到目前为止只能说是疑似存在文件包含漏洞,我们还要看GET参数接收时有没有进行过滤,于是我们进入之前的解析路由方法route::parseurl(),代码较长我就不贴图了,里面就是将url中的参数进行截取解析,没有进行任何的过滤和检测,因此可以确定此处存在文件包含漏洞。因此我们接下来需要确定输入点,可以发现变量是通过thinkphp中获取参数的C方法进行获取的,而C方法获取的变量在config.php中,于是我们查看文件中可以看到var_group对应的变量是g


因此我们可以先上传一个文件然后index.php?g=../test.php%00

  • 注入漏洞

之前的文件包含漏洞我们是通过index.php这个入口文件一步步搞懂cms框架然后进行审计。除了这种方法,我们还可以直接定位数据库查询语句或者功能附近,看看传入的数据有没有被进行清洗。

注入漏洞这里我们就用两个有意思的骚操作来分析一下

1.Ecshop 支付宝插件全局转义绕过导致sql注入

其实在ECshop中的init.php中对用户输入的参数进行了全局转义


但是我们来到来到案发现场看看,巧妙的运用str_replace的替换功能来帮助单引号进行逃逸

其中的核心漏洞代码是$order_sn = str_replace($_GET['subject'], '', $_GET['out_trade_no']);其中代码对用户提交的out_trade_no参数中将subject替换为空,然后送入check_money函数中的sql查询语句.
这个漏洞的关键在于str_replace函数中的替换内容和源字符串都是可控的。
理解这个漏洞之前我们再来回顾一下php中addslashes中的转义处理机制[\-->\\,"-->\",'-->\',null-->\0]
于是我们可以提交out_trade_no参数为%00’后面再跟上我们的payload,提交的subject参数为0,我们来看一下数据转换过程.
out_trade_no=%00' ————>经过全局gpc转义————> out_trade_no=\0\'————>送入str_replace函数处理,将0替换————>out_trade_no=\\' 也就是等于了’,最终也就成功在sql语句中引入了一个单引号,从而可以进行注入

2.格式化字符串导致的单引号逃逸

再来分析一下前段时间出来的wordpress格式化字符导致的注入

具体的代码分析在这就不贴图了,我们直接来分析一下格式化字符串漏洞的核心原理,其中一个关键点就是sprintf的padding特性

printf()和sprintf()函数中可以通过使用%接一个字符来进行padding功能

例如%10s 字符串会默认在左侧填充空格至长度为10,还可以 %010s 会使用字符0进行填充,但是如果我们想要使用别的字符进行填充,需要使用\’单引号进行标识,例如 %’#10s 这个就是使用#进行填充(百分号不仅会吃掉’单引号,还会吃掉\ 斜杠)

同时sprintf()可以使用指定参数位置的写法

%后面的数字代表第几个参数,$后代表格式化类型

于是当我们输入的特殊字符被放到引号中进行转义时,但是又使用了sprintf函数进行拼接时,例如%1$’%s’ 中的 ‘%被当成使用%进行padding,导致后一个’逃逸了

还有一种情况就是’被转义成了\’,例如输入%’ and 1=1#进入,存在SQL过滤,’被转成了\’
于是sql语句变成了select * from user where username = '%\' and 1=1#’;
如果这个语句被使用sprintf函数进行了拼接,%后的\被吃掉了,导致了’逃逸

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%\' and 1=1#';";
$args = "admin";
echo sprintf( $sql, $args ) ;
//result: select * from user where username = '' and 1=1#'
?>

不过这样容易遇到 PHP Warning: sprintf(): Too few arguments的报错
这个时候我们可以使用%1$来吃掉转移添加的\

1
2
3
4
5
6
<?php
$sql = "select * from user where username = '%1$\' and 1=1#' and password='%s';";
$args = "admin";
echo sprintf( $sql, $args) ;
//result: select * from user where username = '' and 1=1#' and password='admin';
?>