'代码审计学习笔记'

《代码审计 企业级Web代码安全架构》的学习笔记,一些已经知道的点没有记录。

1. PHP配置文件

一些影响PHP脚本安全的配置选项

1.1 register_globals

在设置为on的情况下,会将用户GET、POST方式提交的参数注册成全局变量并初始化为对应的值。

从PHP5.3.0起被废弃,在PHP5.4.0中移除了该选项。

1.2 allow_url_include

在设置为on的情况下,可以直接包含远程文件。

在PHP5.2.0后默认设置为off。

1.3 magic_quotes_gpc

在设置为on的情况下,会自动转义GET、POST、COOKIE变量中的单引号'、双引号"、反斜杠\、空字符NULL,不过在PHP5中magic_quotes_gpc不会转义$_SERVER

magic_quotes_gpc在PHP5.4后被移除。

1.4 magic_quotes_runtime

与magic_quotes_gpc相同,都是对单引号'、双引号"、反斜杠\、空字符NULL进行转义,但区别是magic_quotes_runtime对从数据库或者文件中获取的数据进行转义

magic_quotes_runtime在PHP5.4后被移除。

1.5 safe_mode

safe_mode,开启之后,Web服务启动用户只能使用自己的文件,若要调用其他用户的文件,需要使用safe_mode_include_dir;若要使用popen()、system()、exec()等函数,需要使用safe_mode_exec_dir指向脚本目录。

1.6 open_basedir

PHP可访问目录,指定的限制实际上是前缀,而不是目录名,若要将访问权限限制在指定的目录内,应用正斜杠结束路径名。如,open_basedir=/www/a/,设置该参数后,执行脚本访问其他文件时,都要验证文件路径,因此会影响执行效率。

1.7 disable_function

disable_functions,禁用敏感函数,使用逗号分隔函数名,同时禁用dl()函数,因为攻击者可以利用dl()函数来加载自定义的PHP扩展来突破disable_functions的限制。

disable_functions=phpinfo,eval,passthru,exec,system,dl

1.8 display_errors

display_errors决定是否显示PHP脚本错误,在调试PHP时,通常打开,但是在生产环境中,设置打开会带来安全隐患,例如可以通过报错获得网站的绝对路径。

2. 代码审计工具

2.1 Seay源代码审计系统

由Seay基于C#开发的针对PHP代码安全性审计的系统,通过正则匹配发现SQL注入、代码执行、命令执行、文件包含、文件上传、绕过转义防护、拒绝服务、XSS跨站、信息泄露、任意URL跳转漏洞,还可以定位函数位置、全局查找、自定义规则配置。

2.2 RIPS

通过Web页面对源代码进行自动化审计,扫描速度很快。能够发现SQL注入、XSS、文件包含、代码执行、文件读取,支持自动生成漏洞利用。

类似的源代码审计是为了让熟练的代码审计人员快速定位问题代码,刚开始学习代码审计,建议通读源码,自己去找有问题的代码,加强代码审计能力。

2.3 Burp Suite

Burp Suite有很多功能,可以截获并修改数据包来分析Web应用的工作流程,可以通过Intruder功能来fuzz一些Web功能。

3. 代码审计思路

3.1 敏感函数回溯参数过程

因为大多数漏洞是由于函数的使用不当造成的,所以根据敏感函数来逆向追踪参数的传递过程是当前使用最多的一种方式。对于非函数使用不当的漏洞,会有一些特征,如SQL注入会有select、insert。

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

缺点是没有通读代码,对程序地整体框架了解不够深入,在漏洞挖掘时定位利用点会花费一点时间,对逻辑漏洞挖掘覆盖不到。

3.2 通读全文代码

首先看程序的大体代码结构,如主目录有哪些文件,模块目录有哪些文件,插件目录有哪些文件,还要注意文件的大小、创建日期,根据文件的命名可以大致知道程序实现了哪些功能,核心文件是哪些。

重点关注文件:

  1. 函数集文件,通常命名中包含functions或common等关键字,由于包含公共函数提供给其他文件调用,所以大多数文件都会在文件头部包含这类文件。打开index.php或一些功能性文件,在头部一般都能找到。
  2. 配置文件,通常命名中包含config关键字,配置文件中包含数据库配置信息以及功能性配置选项,从配置文件中可以了解程序的小部分功能。另外,需要注意配置文件中参数值是用单引号还是双引号括起来的,如果是双引号,可能会存在代码执行漏洞。修改配置的时候利用PHP可变变量的特性即可执行代码。
  3. 安全过滤文件,通常命名中有filter、safe、check等关键字,这类文件主要对参数进行过滤,如SQL注入和XSS过滤,文件路径、执行的系统命令参数。
  4. index文件,index是Web程序的入口文件,通读一遍index文件,可以大致了解整个程序的架构、运行的流程、包含到的文件,核心的文件有哪些。不同目录的index文件有不同的实现方式,所以最好将几个核心目录的index文件都简单读一遍。

优点:了解程序的架构、业务逻辑,能够挖掘到更多、更高质量的逻辑漏洞

缺点:花费时间多,投入≠产出

3.3 根据功能点定向审计

  1. 文件上传功能

任意文件上传,后端程序没有严格地限制上传文件的格式,导致可以直接上传或存在绕过的情况。

还可能发生SQL注入漏洞,文件名没有进行过滤,又把文件名保存到数据库中。

  1. 文件管理功能

如果程序将文件名或文件路径直接在参数中传递,则很有可能存在任意文件操作的漏洞,如任意文件读取,在路径中使用../..\来进行跳转。

  1. 登录认证功能

登录认证功能不单指一个登录过程,而是整个操作过程中的认证,目前的认证方式大多基于Cookie和Session。

  1. 找回密码功能

重置管理员密码,可以间接控制业务权限甚至拿到服务器的权限。

验证码爆破,没有限制验证码错误次数和有效事件。

3.4 追踪用户输入

在每一个页面中,搜索用户可以输入的地方,追踪这些变量,查看与后台有什么交互,有没有输出用户输入。

Web安全的重点就是输入、输出、数据流。

4. 漏洞挖掘

4.1 SQL注入漏洞

在挖掘SQL注入等漏洞时,只要参数在拼接到SQL语句前,除非有宽字节注入或者其他特殊情况,否则使用了addslashes()函数就不能注入了。

SQL注入经常出现在登录页面、获取HTTP头(user-agent/client-ip)、订单处理,在订单系统中,由于订单涉及购物车等多个交互,所以经常会发生二次注入

4.1.1 普通注入

在代码中查找select frommysql_connectmysql_querymysql_fetch_row等关键字

4.1.2 宽字节注入

查看数据库连接配置文件,set character_set_client=gbkset names 'gbk'都是存在漏洞的。

若通过mysql_set_charset设置编码,后面没有合理地使用mysql_real_escape_string还是存在漏洞。

漏洞修复方法:

  1. 在执行查询之前,先执行set names 'gbk',character_set_client=binary设置character_set_clientbinary
  2. 使用mysql_set_charset('gbk')设置编码,然后使用mysql_real_escape_string()函数过滤参数。
  3. 使用PDO。

4.1.3 二次urldecode注入

搜索urldecode和rawurldecode函数

4.1.4 漏洞防范

增加过滤函数和类,不过如果过滤函数写得不够严谨,会出现绕过的情况,最好的解决方案还是利用预编译的方式。

过滤函数和类

  1. addslashes函数

单引号'、双引号"、反斜杠\、空字符NULL

  1. mysql_[real_]escape_string函数

mysql_escape_string和mysql_real_escape_string函数都是对字符串进行过滤,在PHP4.0.3以上版本存在,下列字符受影响\x00\n\r\'"\xla,两个函数唯一不一样的地方在于mysql_real_escap_string接受的是一个连接句柄并根据当前字符集转义字符串,所以推荐使用mysql_real_escape_string。

  1. intval函数

intval的作用是将变量转换成int类型。类似的还有floatval。

1
2
3
4
5
<?php
$id = intval("1 union select ");
echo $id;
?>
//输出:1

PDO prepare 预编译

在PHP版本<5.3.6之前,使用了POD的prepare仍然存在宽字节SQL注入漏洞,因为使用了PHP本地模拟prepare,再把完整的SQL语句发送给MySQL服务器,需要使用ATTR_EMULATE_PREPARES来禁用PHP本地模拟prepare。

4.2 XSS漏洞

前端页面能做的事,XSS都能做。

未过滤的输入点和未过滤的输出。

挖掘XSS漏洞的关键在于寻找没有被过滤的参数,并且这些参数被传入输出函数,常用的输出函数如下:print()、print_r()、echo()、printf()、sprintf()、die()、var_dump()、var_export()

XSS常出现在文章发表、评论回复、留言、资料设置等地方。发表文章的地方大多是富文本,有各种图片引用、文字格式设置,所以经常出现对标签事件过滤不严格导致的XSS。资料设置的地方,比如用户昵称、签名,有的应用可能不只一处设置资料的地方,不一定所有设置这个资料的地方都过滤严格。

XSS利用需要熟悉浏览器的解析顺序

防御

过滤字符,单引号、双引号、尖括号、反斜杠、冒号、&、#

在输出和二次调用时进行HTML实体化一类的转码

XSS的防范也需要熟悉浏览器的解析顺序

浅谈XSS—字符编码和浏览器解析原理

设置标签事件属性白名单,若事件不在白名单列表中,就直接拦截掉

4.3 CSRF

CSRF属于越权操作,所以存在于有权限控制的地方,像管理后台、会员中心、论坛帖子以及交易管理等场景中。

黑盒挖掘:打开几个有非静态操作的页面,抓包看有没有token,如果没有token,再不带referer直接请求这个页面,如果返回的数据还是一样的话,很有可能有CSRF漏洞。

白盒审计:读代码的时候看看几个核心文件里面有没有验证token和referer相关的代码,这里的核心文件指的是大量文件引用的基础文件;或者直接搜token关键字。如果核心文件中没有,再去看看比较关心的功能点的代码有没有验证。

防御:

  1. 增加token和referer验证避免img标签请求的水坑攻击
  2. 在敏感操作页面增加验证码

4.4 文件包含漏洞

4.4.1 本地文件包含LFI

搜索include()、include_once()、require()、require_once(),回溯看有没有可控的变量,可以将路径写在括号内,也可以函数名加空格后跟路径

1
2
include( $dir );
include $dir;

本地文件包含大多需要截断

大多出现在模块加载、模板加载和cache调用的地方

本地文件包含利用方式,上传一个允许上传的文件格式,再包含执行代码,包含PHP上传的临时文件,在请求URL或者UA里面加入要执行的代码,WebServer记录到日志后再包含WebServer的日志,还有Linux下包含/proc/self/environ文件

4.4.2 远程文件包含RFI

需要设置allow_url_include = On

4.4.3 文件包含截断

  1. %00截断

受限于GPC、addslashes等函数的过滤,在PHP5.3之后的版本全面修复了文件名%00截断的问题,所以在5.3之后的版本不能用这个方法截断。

  1. 利用多个英文句号和正斜杠来截断

不受限于GPC,但也在PHP5.3版本之后被修复

Windows下240个连接的点能够截断,同样的点加斜杠也是240个能够截断,Linux下测试时2038个./组合才能截断

  1. 远程文件包含时利用问号来伪截断

不受限于GPC和PHP版本,只要能返回代码给包含函数,就能执行

1
/test.php?a=http://remote/test.php?

4.5 文件读取(下载)漏洞

filename参数直接在请求里面传递,且该参数用户可控,后台程序获得这个文件路径后直接读取返回。

挖掘方式

  1. 先黑盒看功能点对应的文件,再去读文件
  2. 搜索文件读取的函数,查看有没有直接或间接控制的变量,文件读取函数有,file_get_contents()、highlight_file()、fopen()、readfile()、fread()、fgetss()、fgets()、parse_ini_file()、show_source()、file(),除了这些正常的读取文件的函数之外,其他一些功能函数也可以用来读取文件,例如文件包含函数include,php输入输出流php://filter

4.6 文件上传漏洞

搜索move_uploaded_file()函数,查看调用这个函数上传文件的代码存不存在未限制上传格式或者可以绕过

常见问题:黑名单限制文件格式以及未更改文件名,没有更改文件名的情况下,在Apache利用其向前寻找解析格式和IIS6的分号解析bug都可以执行代码。

4.6.1 未过滤或本地过滤

在服务器端未过滤,没有限制任何格式的文件上传。

4.6.2 黑名单扩展名过滤

  1. 限制的扩展名不够全
  2. 验证扩展名的方式存在问题可以直接绕过,结合PHP和系统的特性,导致了可以截断文件名来绕过黑名单限制。
  3. 文件头、content-type验证绕过

4.6.3 漏洞防范

  1. 使用白名单形式过滤文件扩展名,使用in_array或者强等于(===)来对比扩展名
  2. 保存上传的文件时,采用时间戳拼接随机数加MD5的方式重命名”md5(time+rand(1,10000))”上传文件

4.7 文件删除漏洞

文件删除漏洞出现在文件管理功能的应用上比较多,这些应用一般也都有文件上传和读取的功能,漏洞原理跟文件读取漏洞是差不多的,删除的文件名可以用../跳转,或者没有限制当前用户只能删除他该有权限删除的文件。常出现这个漏洞的函数是unlink(),不过老版本下session_destory()函数也可以删除文件

挖掘文件删除漏洞可以先去找相应的功能点,先黑盒测试一下看能不能删除某个文件,如果删除不了,再从执行流程去追提交的文件名参数的传递功能。纯白盒挖的话,可以去搜索带有变量参数的unlink(),依然采用回溯变量的方式。

4.8 通用文件操作防御

文件操作漏洞利用的共同点:

  1. 由越权操作引起可以操作未授权操作的文件
  2. 要操作更多文件需要跳转目录
  3. 大多都是直接在请求中传入文件名

从上面三点来思考防御手段:

  1. 对权限的管理要合理,平级用户在未授权的情况下不能对其他用户的文件进行查看、删除等文件操作,特殊的操作文件行为限定特定用户才有权限,比如后台删除文件的操作,限制管理员才能操作
  2. 在满足业务需求的情况下,可以使用数据库存储文件路径,有些情况需要传入文件路径时,可以固定文件操作目录,然后禁止参数中含有.../\,来禁止目录跳转,检测到参数中包含上述字符,直接提示非法操作,并停止脚本
  3. 使用更安全的方法来替代直接以文件名为参数下载的操作,在上传文件时,将文件名、文件路径、文件ID(随机MD5)以及文件上传人存储在数据库中,下载的时候直接根据文件ID和当前用户名去判断当前用户有没有权限下载该文件,若有则读取路径指向的文件并返回下载

4.9 代码执行漏洞

该漏洞主要由eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()、array_map()等函数的参数过滤不严格导致,还有PHP的动态函数($a($b))也是出现问题比较多

preg_replace()函数的代码执行需要存在/e参数

1
mixed preg_replace(mixed $pattern, mixed $replacement, mixed $subject [, int $limit = -1 [, int &$count]])

查找$subject中匹配$pattern的部分,用$replacement进行替换,当存在/e参数时,$replacement的值会被当成PHP代码来执行

call_user_func()和call_user_func_array()函数的功能时调用函数,多用在框架里动态调用函数,较小的程序出现这种方式的代码执行比较少。

array_map()函数的作用时调用函数并且除第一个参数外其他参数为数组,通常会写死第一个参数,即调用的函数。

同类函数有:

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_udiff_assoc()、array_udiff_uassoc()、array_intersect_assoc()、array_intersect_assoc()、array_intersect_uassoc()、array_uintersect()、array_walk()、array_walk_recursive()、
xml_set_character_data_handler()、xml_set_end_namespace_decl_handler()、xml_set_external_entity_ref_handler()、xml_set_notation_decl_handler()、xml_set_processing_instruction_handler()、xml_set_start_namespace_decl_handler()、xml_set_unparsed_entity_decl_handler()、stream_filter_register()、set_error_handler()、register_shutdown_function()、register_tick_function()

动态函数的代码执行

1
$_GET($_POST['xx'])

这种写法一般被用来当作web后门使用,在不少知名程序中也用到了动态函数的写法,用来更简单、更方便地调用函数,但是一旦过滤不严格就会造成代码执行漏洞

4.9.1 漏洞防御

在满足正常业务需求下,结合正则表达式,设置参数白名单过滤

4.10命令执行漏洞

执行命令的函数有system()、exec()、shell_exec()、passthru()、pcntl_exec()、popen()、proc_open()、反引号

PHP执行命令继承WebServer用户的权限

system()、exec()、shell_exec()、passthru()以及反引号是可以直接传入命令并返回执行结果,system()函数直接回显结果打印输出

pcntl是PHP的多进程处理扩展,在处理大量任务的情况下会使用到,需要额外安装

1
void penctl_exec(string $path [, array $args [, array $envs]])

$path为可执行程序路径,$args表示传递给$path程序的参数,$envs是执行程序的环境变量

popen()、proc_open()函数不会直接返回执行结果,而是返回一个文件指针,

1
2
popen(string $command,string $mode):resource
proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd = NULL [, array $env = NULL [, array $other_options = NULL ]]] ) : resource

漏洞防范

  1. 使用PHP自带的命令防注入函数,escapeshellcmd()、escapeshellarg()
  2. 参数白名单,根据业务需求限定某些参数,匹配参数在不在这个白名单列表中,不在白名单中则直接显示错误提示

4.11 变量覆盖漏洞

变量覆盖是指可以用自定义的参数值替换程序原有的变量值,变量覆盖漏洞通常需要结合程序的其他功能来实现攻击。这个漏洞想象空间非常大,比如在文件上传页面,限制文件扩展名白名单列表卸载配置文件中的变量中,通过变量覆盖漏洞将任意扩展名覆盖掉原来的白名单,就可以上传一个PHP的shell

变量覆盖大多由函数使用不当导致,经常引发变量覆盖漏洞的函数有:extract()、parse_str(),import_request_variables()函数则是用在没有开启全局变量注册的时候,调用这个函数相当于开启了全局变量注册,在PHP5.4之后这个函数被取消

另外部分应用利用$$的方式注册变量没验证已有变量导致覆盖,在外部传递进来的参数没用类似$_GET[‘key’]这样原始的数组变量,而是把里面的key注册成一个变量$key,注册过程中由于没有验证该变量是否存在就直接赋值,导致已有变量被覆盖掉

4.11.1 挖掘经验

由于变量覆盖漏洞通常要结合应用其他功能代码来实现完整攻击,所以挖掘一个可用的变量覆盖漏洞不仅要考虑能否实现变量覆盖,还要考虑后续代码能不能让这个漏洞利用起来。挖掘可用的变量覆盖漏洞,一定要看漏洞代码行之前存在哪些变量可以覆盖并且后面有被使用到

由函数导致的变量覆盖比较好挖掘,只要搜寻参数带有变量的extract()、parse_str()函数,然后去回溯变量是否可控,extract()还要考虑它的第二个参数。import_request_variables()函数则相当于开启全局变量注册,只要找哪些变量没有初始化并且操作之前没有赋值,然后就去提交这个变量作为参数,另外只要卸载import_request_variables()函数前面的变量,不管师傅以经初始化都可以覆盖,这个还是只能在PHP4~4.1.0和PHP5~5.4.0版本可用

$$符号注册变量导致变量覆盖,可以通过搜索$$关键字去挖掘,不过建议挖掘之前还是先把核心文件通读一遍,了解程序的大致框架

4.11.2 函数使用不当

  1. extract函数

extract()函数覆盖变量需要一定条件,官方功能说明为“从数组中将变量导入到当前的符号表”,通俗讲就是将数组中的键值对注册成变量

1
int extract(array &$var_array [, int $extract_type = EXTR_OVERWRITE [,string $prefix =NULL]])

有三种情况会导致变量覆盖,第一种是第二个参数为EXTR_OVERWRITE,表示如果有冲突,则覆盖已有的变量;第二种情况是只传入第一个参数,这时候默认为EXTR_OVERWRITE,第三种是第二个参数为EXTR_IF_EXISTS,表示仅在当前符号表中已有同名变量时,覆盖他们的值,其他都不注册新变量

1
2
3
4
5
<?php 
$b = 3;
extract([]'b' => '1']);
print_r($b);
?>
  1. parse_str函数

parse_str()函数的作用是解析字符串并注册成变量,在注册变量之前不会验证当前变量是否以经存在,所以会直接覆盖掉已有变量

1
void parse_str(string $str [,array &$arr])

当第二个参数存在时,注册的变量会放到这个数组里面,但是如果这个数组原来就存在相同的键(key),则会覆盖掉原有的键值

1
2
3
4
5
<?php
$b = 1;
parse_str('b=2');
print_r($b);
?>
  1. import_request_variables函数

import_request_variables()函数作用是把GET、POST、COOKIE的参数注册成变量,用在register_globals被禁止的时候,需要PHP4.1至5.4版本

1
bool import_request_variables(string $type [, string $prefix])

其中$type代表要注册的变量,G代表GET、P代表POST、C代表COOKIE,当$type为GPC时,会注册GET、POST、COOKIE参数为变量,第二个参数$prefix为要注册的变量前缀

1
2
3
4
5
6
<?php 
$b = 1;
import_request_variables('GP');
print_r($b);
?>
//?b=2

4.11.3 $$变量覆盖

1
2
3
4
5
6
7
8
9
10
11
<?php
$a = 1;
foreach(array('_COOKIE','_POST','_GET') as $_request){
foreach($$_request as $_key => $_value){
echo $_key. '<br />';
$$_key = addslashes($_value);
}
}
echo $a;
?>
//a=2

4.11.4 漏洞防范

变量覆盖漏洞最常见漏洞点在做变量注册以及赋值给变量时,没有验证变量是否存在,所以推荐使用原始的变量数组,如$_GET$_POST,或在注册变量前验证变量是否存在

使用原始变量

不进行变量注册,直接使用原生的$_GET$_POST等数组变量进行操作,如果需要注册个别变量,可以直接在代码中定义变量,然后再把请求中的值赋值给它

验证变量存在

在注册变量以及赋值前,判断变量是否存在。使用extract()函数,则可以配置第二个参数为EXTR_SKIP。使用parse_str()函数注册变量前需要先自行通过代码判断变量师傅存在。不推荐使用import_request_variables()函数注册全局变量,会导致变量不可控。最重要的一点,自行申明的变量一定要初始化,不然即使注册变量代码在执行流程最前面也能覆盖掉这些未初始化的变量。

4.12 业务逻辑漏洞

常见漏洞位置,支付、找回密码、程序安装

4.12.1 挖掘经验

业务逻辑漏洞大多存在与逻辑处理以及业务流程中,没有特别明显的关键字可以用来快速定位,通常这类漏洞的挖掘技巧是通读功能点源码,先熟悉这套程序的业务流程,后面挖掘起来就会比较顺畅,值得关注的点是程序是否可重复安装,修改密码处是否可越权修改其他用户密码,找回密码验证码师傅可暴力破解以及修改其他用户密码,cookie是否可预测或cookie验证是否可绕过

4.12.2 等于与存在判断绕过

  1. in_array函数

in_array()函数用来判断一个值是否在某一个数组中,但由于该函数在判断前会自动做类型转换,所以会出现问题

1
2
3
4
5
6
7
<?php 
if(in_array($_GET['typeid'],array(1,2,3,4))){
$sql = "select ... where typeid = '" . $_GET['typeid'] . "'";
echo $sql;
}
?>
//?typeid=1' union select

请求的typeid并不在数组中,但是依然成功执行了

  1. is_numeric函数

is_numeric()函数用来判断一个变量是否为数字,该函数对于十六进制数字也会返回true,MySQL是可以使用十六进制来代替字符串的,所以虽然不能直接注入SQL语句,但可以插入脏数据,若有其他地方调用插入的脏数据,可以造成二次注入和XSS漏洞

1
2
3
4
5
<?php 
if(is_numeric($_GET['var'])){
$sql = "insert into xx values('xx',{$_GET['var']})";
echo $sql;
}
  1. 双等于和三等于

双等于会在判断之前先做变量类型转换,而三等于不会,由于数据类型被改变,所以等于在判断时可能存在安全风险

双等于

1
2
3
4
<?php 
var_dump($_GET['var'] == 2)
?>
//?var=2aaa True

三等于

1
2
3
4
<?php 
var_dump($_GET['var'] === 2)
?>
//?var=2aaa False

4.12.3 账户体系中的越权漏洞

越权分为水平越权和垂直越权

都是账户体系在判断权限时不严格导致存在绕过漏洞,通常发生在cookie验证不严、简单判断用户提交的参数,归根结底,都是因为这些参数是在客户端提交,服务的未严格校验

4.12.4 未exit或return

在if条件判断之后,有两个操作,一种是继续执行if后面的代码,另一种是在if体内退出当前操作,但未写return、die()或exit(),导致程序还会继续执行

1
2
3
4
5
6
<?php 
if($_GET['var'] === 'aa'){
header("Location: ../");
}
echo $_GET['var'];
?>

请求?var=aa,使用BurpSuite可以看到输出的aa

4.12.5 漏洞防范

逻辑漏洞,是由于开发者对业务逻辑或者代码逻辑理解不清楚导致。每一种业务功能都有可能导致逻辑漏洞的产生,解决这类逻辑漏洞需要注意以下两点:

  1. 深入熟悉业务逻辑,只有熟悉了业务逻辑,才能根据业务需求编写满足需求而又不画蛇添足的代码
  2. 注意多熟悉函数的功能和差异,注意代码执行逻辑上考虑不周全的地方

4.13 会话认证漏洞

问题多出现在cookie上,由于cookie存储在浏览器中,所以可以被用户修改。若服务端直接取用cookie而没有校验,或cookie加密数据存在可预测的情况。

4.13.1 挖掘经验

挖掘登录认证漏洞时,可以先看一下程序的登录功能代码,看看整个登录过程的业务逻辑有没有可以控制session值或直接绕过密码验证的漏洞;另外需要关注程序验证是否为登录的代码,验证cookie,而不是直接去取cookie里面的值,怎么去判断这个值来验证是否登录。

4.13.2 漏洞防范

在防御认证漏洞前,先了解认证的业务逻辑,严格限制输入的异常字符以及避免使用客户端提交上来的内容去直接进行操作。cookie和session结合起来使用,不直接从cookie中获取参数值进行操作,使用cookie时进行校验,设置session时,保证客户端不能操作session参数

敏感数据不放在cookie中,不要将用户名和密码放在cookie中

4.14 二次漏洞审计

可以通过相关关键字去定位,比如根据数据库字段、数据库表名等去代码中搜索。为了更好地挖掘到二次漏洞,最好把全部代码读一遍,这样能更好地了解程序的业务逻辑和全局配置

业务逻辑越是复杂的地方越容易出现二次漏洞,可以重点关注购物车、订单、引用数据、文章编辑、草稿等这些跟数据库交互的地方

5. PHP代码审计小技巧

5.1 不受GPC保护的$_SERVER变量

在PHP5之后用$_SERVER取header字段不受GPC影响,所以当GPC开启时,$_SERVER中的特殊字符不会被转义掉。常见的header注入有user-agent、referer以及client-ip/x-forward-for,同样的$_FILES变量也不受GPC保护

getenv()得到的变量不受GPC影响

$HTTP_RAW_POST_DATA与PHP输入流不受GPC影响

数据库操作容易忘记加'的地方:in()/limit/order by/group by

在PHP4~PHP5.2.1的版本中,GPC不处理数组第一维变量的key

5.2 iconv函数字符编码转换截断

iconv()函数用来做字符编码转换,由于字符集的编码转换存在一定的差异性,导致部分编码不能被成功转换,遇到不能处理的字符,后续字符串不会被处理

1
2
3
4
5
6
<?php
$a = '1' .chr(130) . '2';
echo $a;
echo '<br />';
echo iconv("UTF-8","gbk",$a);
?>

能否截断成功和PHP版本有关系
PHP5.2.17可以直接截断,并且没有报错提示
PHP5.3.29-nts可以截断,但会有notice提示
PHP5.4后,会直接报错,无法截断

5.3 php://输入输出流

php://stdin、php://stdout、php://stderr、php://input、php://output、php://fd、php://memory、php://temp、php://filter

5.4 PHP代码解析标签

  1. 脚本标签
1
<script language = "php">code</script>
  1. 短标签
1
<? code ?>

需要在php.ini中设置short_open_tag=on,默认是on

  1. asp标签
1
<% code %>

在PHP3.0.4后可用,需要在php.ini中设置asp_tags=on,默认是off

5.5 不严谨的正则表达式

  1. 没有使用^$限定匹配位置
  2. 特殊字符未转义

5.6 Windows FindFirstFile利用

大多数程序会对上传的文件名加入时间戳等字符再进行MD5,这时无法直接得到上传的webshell文件路径,但是在Windows下,只需要知道文件所在目录,然后利用Windows特性就可以访问到文件,因为Windows在搜索文件的时候使用FindFirstFile这一个winapi函数。

只要将文件名不可知部分之后的字符使用<代替即可,使用一个代表一个字符,如果文件名更长,需要两个字符,代表继续往下搜索。

在同目录下创建文件2.txt

1
2
3
4
<?php 
include($_GET['file']);
?>
//?file=2<<

可用函数

函数 功能
include() 包含文件
include_once() 包含文件
require() 包含文件
require_once() 包含文件
fopen() 打开文件
ziparchive::open() 打开文件
copy() 复制文件
file_get_contents() 读取文件
parse_ini_file() 读取文件
readfile() 读取文件
file_put_contents() 写入文件
mkdir() 创建文件夹
tempnam() 创建文件
touch() 创建文件
move_uploaded_file() 移动文件
opendir() 文件夹操作
readdir() 文件夹操作
rewinddir() 文件夹操作
closedir() 文件夹操作

5.7 PHP可变变量

PHP中,单引号代表纯字符串,双引号会解析中间的变量,所以当使用双引号时会存在代码执行漏洞

1
2
3
<?php 
$a = "${@phpinfo()}";
?>

除了@符号外,还有其他的写法也可以执行

  1. 花括号内第一个字符为空格:
1
$a = "${ phpinfo()}";
  1. 花括号内第一个字符为TAB:
1
$a = "${	phpinfo()}";
  1. 花括号内第一个字符为注释符:
1
$a = "${/**/phpinfo()}";
  1. 花括号内第一个字符为回车:
1
2
$a = "${
phpinfo()}";
  1. 花括号内第一个字符为加号:
1
$a = "${+phpinfo()}";
  1. 花括号内第一个字符为减号:
1
$a = "${-phpinfo()}";
  1. 花括号内第一个字符为感叹号:
1
$a = "${!phpinfo()}";

除此之外,还有~\

5.8 PHP函数的溢出漏洞

在以往的PHP版本中,有很多函数都曾出现过溢出漏洞,所以在审计应用程序漏洞时,要根据测试目标使用的PHP版本信息测试函数。

5.9 PHP函数的其他漏洞

unset()–Zend_Hash_Del_Key_Or_Index Vulnerability

版本要求:PHP 4 < 4.3 PHP 5 < 5.14

5.10 rand()和mt_rand()

rand()函数的最大值是32767

mt_rand()函数的最大值是2147483647

使用rand()函数容易被暴力破解

5.11 关注变量本身

有的程序会把变量本身的kye当作变量提交给函数处理。

1
2
3
4
5
<?php 
foreach ($_GET as $key => $value){
print $key . "\n";
}
?>

上面的代码会将变量本身输出,如果提交下面的URL,则会产生XSS

1
?<script>alert(1);</script)=1&test=2

6. 业务功能安全设计

6.1 验证码

6.1.1 验证码绕过

  1. 不刷新直接绕过

验证码和Session绑定在一起,为了保证验证码能够正常使用,会昂验证码明文或加密后放在Cookie或POST数据包中,所以只要每次同一个数据包里面的两个验证码对上即可绕过

  1. 暴力破解

程序没有设置验证码错误次数和超时,导致能够不断进行尝试

  1. 机器识别

有两种情况,一种是不是实时生成的验证码,生成了部分验证码在服务端保存,前端直接加载验证,这类只要把全部的验证码文件保存回来,做一个图片MD5库,然后直接匹配服务端返回的图片MD5即可识别。另一种是动态生成的验证码,这类需要做图片文字识别或语音识别

  1. 打码平台

廉价的人工资源打码

合理的验证码:

  1. 设置验证码错误次数,一个验证码只能错误一次,错误就直接刷新,避免暴力破解问题
  2. 不把验证码放在HTML页面或cookie中
  3. 验证码设置只能请求一次,一次之后不管是否错误,都在后端程序强制刷新
  4. 短信或邮件验证码必须6位以上字母和数字混合,图片文字变形,增加干扰斑点,语音验证码增加背景噪声
  5. 验证码动态生成,不能同一生成多次调用

6.1.2 验证码资源滥用

短信验证码接口设置获取验证码次数和时间间隔,限制单个手机号在一个时间段内接收短信的次数,限制某个IP在一个时间段内请求接收短信的次数

6.2 用户登录

6.2.1 撞库漏洞

没有对登录次数进行限制

  1. 用户名和密码错误次数都无限制
  2. 单时间段内用户的密码错误次数限制
  3. 单时间段内IP登录错误次数限制

6.2.2 API登录

  1. 登录密钥(clientkey)需要不可预测并且不固定,生成key的算法中加入随机字符
  2. API接口禁止搜索引擎收录
  3. 登录密钥当次绑定当前主机,换机器不可用,防止QQ木马和嗅探key

6.3 用户注册

安全设计思路:

  1. 设计验证码
  2. 采集用户机器唯一识别码,拦截短时间内多次注册
  3. 根据账号格式自学习识别垃圾账号
  4. 防止SQL注入漏洞与XSS漏洞

6.4 密码找回

6.4.1 输入用户名/邮箱/手机阶段

提交时进行抓包,直接修改手机或邮箱参数,如果后端没有验证,邮箱会发送到篡改的手机或邮箱上

6.4.2 填写验证码和新密码阶段

  1. 验证凭证较简单,可以被暴力破解
  2. 验证凭证算法简单,凭证可预测
  3. 验证凭证直接保存在源码里

6.4.3 发送新密码阶段

请求链接时,只在第一步验证uid和key,在提交新密码时,可以修改uid或email指定的用户密码

密码找回安全风险点:

  1. 接收验证码的邮箱和手机号用户不可控,应直接从数据库中读取出来
  2. 加强验证凭证复杂度,防止被暴力破解
  3. 限制验证凭证错误次数,单个用户在半个小时内验证码错误三次,半小时内禁止找回密码
  4. 验证凭证设置失效时间
  5. 验证凭证不要保存在页面中
  6. 输入用户邮箱或ID、手机号取验证凭证的地方需要设置验证码防止短信炸弹和批量找回
  7. 验证凭证跟用户名、用户ID、用户邮箱绑定,找回密码时验证当前凭证是否是当前用户的

6.5 资料查看与修改

可能存在一下问题:

  1. 未验证用户权限
  2. 未验证当前登录用户

安全设计思路:

用户资源ID绑定到用户,只允许有权限的用户查看

用户信息存储到session中,不放到request中,避免攻击者修改当前用户ID

6.6 投票/积分/抽奖

  1. cookie或POST请求正文绕过

有的应用会将验证是否抽奖或领取积分的凭证放在cookie或POST请求中,通过修改数据可以绕过验证。

  1. 基于IP验证,若通过client-ip或x_forward_for获取IP,可以伪造IP绕过
  2. 基于用户认证。尝试批量注册,或修改POST包或cookie里面的当前uid、用户名看能否随意修改绕过限制

安全设计思路:

  1. 机器识别码验证,每台机器都可以根据硬件信息生成唯一的识别码
  2. 操作需要登录,当前用户信息从session中读取

6.7 充值支付

修改单价、总价、购买数量、利用时间差多次购买

安全设计思路:

  1. 保证数据可信,商品单价及总价不可从客户端获取
  2. 购买数量不能小于等于0
  3. 账户支付锁定机制,当一个支付操作开始就应该立马锁定当前账户,不能同时两个后端请求对余额进行操作

6.8 私信及反馈

和XSS防御方法一样,对特殊字符进行过滤,使用白名单和黑名单结合

6.9 远程地址访问

限制填写,注意短链接的限制

6.10 文件管理

安全设计思路:

  1. 禁止写入可在服务端执行的文件

如服务器能解析PHP,那么要限制PHP扩展名文件和PHP标签的代码

  1. 限制文件管理功能操作的目录

目录直接在代码中写死,禁止提交../..\,避免目录穿越

  1. 限制文件管理功能访问权限
  2. 禁止上传特殊字符文件名的文件

一些应用会展示文件名,所以禁止文件名中有尖括号、单双引号等特殊字符,避免攻击者用文件名进行XSS攻击

6.11 数据库管理

安全设计思路:

  1. 限制可以操作的数据库表,数据库备份可以直接在代码里写死能操作的表,或创建新的MySQL用户,限制可以操作的表和字段
  2. 限制备份到服务器上的文件名,需要系统随机生成类似MD5并且长度不低于16位,扩展名不能自定义,防止攻击者导出WebShell,防止猜解文件名直接下载文件

6.12 命令/代码执行

通常出现在系统后台,命令执行在类似Zabbix、WDCP等运维系统上存在。

安全设计思路:

  1. 严格控制该功能访问权限,高权限才能访问
  2. 满足业务需求的情况下,设置命令白名单,可使用escapeshellcmd()和escapeshellarg()函数进行过滤,命令直接在代码中写死更好
  3. 给命令及代码执行功能设置独立密码
  4. 代码执行功能限制脚本可访问的路径
  5. 在满足需求的情况下限制当前执行命令的系统用户权限

6.13 文件/数据库备份

容易出问题的情况:

  1. 未授权访问和越权访问
  2. 备份文件名可预测
  3. 生成的文件可利用Web中间件解析漏洞执行代码

安全设计思路:

  1. 进行权限控制,限制高权限才能使用
  2. 文件名随机生成,不可预测,md5(时间戳+6位以上随机字母和数字)

6.14 API

出现最多的问题是未授权访问以及数据遍历漏洞

安全设计思路:

  1. 访问权限控制
  2. 防止敏感信息泄露,API可能会泄露用户资料
  3. SQL注入等常规漏洞

7. 应用安全体系建设

7.1 横向细化策略

依靠规则量来填补空洞,规则做的越细,拦截的攻击越多,提升攻击者的攻击成本,但同时提升了防御成本

7.2 纵深防御策略

假设上一层防御策略失效而设计的内网防御策略

7.3 用户密码安全策略

  1. 强制密码使用8位以上的大小写字母+数字+特殊字符的组合
  2. 禁止使用各种类型弱口令
  3. 禁止用户名和密码相同,或存在较大相似度

7.4 前后台用户分表

前台用户不需要后台数据,若将前台用户和管理员的数据放在同一个数据库或同一张表中,可能存在越权修改管理员信息的情况,而且前后台数据库分配不同MySQL用户,可以避免前台SQL注入获取后台管理用户名、密码的情况

7.5 后台地址隐藏

用户登录后台页面自定义设置,或修改后台文件夹

7.6 密码加密存储方式

服务方使用高强度加密方式存储用户密码,避免拖库后用户密码泄露

7.7 登录限制

  1. 限制登录IP
  2. 双因素认证,手机验证码,动态令牌

7.8 API站库分离

通过API调用数据库服务器,建立API接口监控,设置一个阈值,当接口被频繁调用,说明可能存在刷库行为

7.9 敏感操作多因素验证

在进行敏感操作,如登录、删除、修改操作时,进行多因素验证:手机短信验证、手机语音验证、手机APP动态令牌、邮箱验证码、实体令牌卡、电子图片令牌卡、硬件令牌

8. 学习资源

8.1 学习过程

先了解常规Web漏洞的原理,然后按照别人的审计案例去分析漏洞成因,跟踪变量,函数调用过程,直到能够说出为什么会出现这个漏洞,对不同类型的漏洞都重复上述过程,在弄明白所有的常规漏洞后,自己去独立审计一个CMS。这样就算入门了,平时多积累经验,收集哪里容易产生漏洞,收集大佬的审计思路,收集各种函数、语言的特性,再加以练习。

学习代码审计的前期不要去读开源框架或使用开源框架的应用,GitHub上找一些小应用来读,多找几套程序通读全文代码,这样才能总结经验。总结了一定的经验,对PHP也比较熟悉的时候,再去读thinkphp、Yii、Zend Framework、等开源框架,才能快速地挖掘高质量的漏洞。

分析和学习别人发现的漏洞和EXP,总结漏洞类型和字典;学习PHP手册或官方文档,挖掘新的有危害的函数或利用方式;fuzz PHP的函数,找到新的有问题的函数;分析PHP源代码,发现新的漏洞函数特性或漏洞

8.2 代码审计案例

其中包含代码审计和ThinkPHP5漏洞分析

8.3 关注点

代码审计中,除了关注代码,还应该了解服务器的操作系统,Web中间件,PHP版本信息。