PHP代码审计

为什么代码会有漏洞

CWE 项⽬中⼤约有 1000 种不同的软件缺陷。这些都是软件开发⼈员使⽤不安全的⽅式执⾏代码逻辑所引发的安全 问题。

软件开发没有学习过安全知识,⼤多数⼈也没有接受过任何关于软件安全的培训。

这些问题在最近⼏年变得⾮常重要,因为我们在以极快的速度增加互联⽅式,技术和协议。发明技术的能⼒已经远 远超过了保护技术的能⼒。今天使⽤的许多技术根本没有受到⾜够(或任何)的安全审计。

企业没有在安全上花费适当的时间有许多原因。最直观的原因是源于软件市场的⼀个潜在问题。 因为软件本质上是⼀个⿊盒,客户很难区分安全和不安全代码。 没有这种可⻅性, 就不⿎励供应商花费额外的精⼒来⽣产安全的代码。

什么是代码安全审计

代码审计旨在识别应⽤程序中与其特性和设计相关的安全缺陷,以及产⽣缺陷的根本原因。
随着应⽤程序的⽇益复杂和新技术的出现,传统的测试⽅法可能⽆法检测到应⽤程序中存在的所有安全缺陷。


⼈们必须理解应⽤程序、外部组件和配置的代码,这样才能更好地发现缺陷。深⼊地研究应⽤程序代码也有助于确定可⽤于避免安全缺陷的缓解技术。
审核应⽤程序源代码的过程是为了验证适当的安全和逻辑控制是否存在,它们是否按预期⼯作,以及它们是否在正确的位置被调⽤。


代码安全审计允许公司确保应⽤程序开发⼈员遵循安全开发技术。⼀般的经验法则是, 在应⽤程序经过适当的代码安全审计后,渗透测试不应发现与开发的代码相关的任何其他应⽤程序漏洞。或者发现很少的问题。

代码安全审计技术

代码安全审计与被审计的应⽤程序强相关。它们可能会突出⼀些新的或特定于应⽤程序代码实现的缺陷,如执⾏流的不安全终⽌、同步错误等。这些缺陷只有当我们理解了应⽤程序代码流及其逻辑后才能被发现。因此,代码安全审计不仅仅是扫描代码中的⼀组未知的不安全代码模式,还包括理解应⽤程序的代码实现和列举它独有的缺陷。


正在审计的应⽤程序可能已经设计了⼀些适当的安全控制,例如集中⿊名单、输⼊验证等。必须仔细研究这些安全控制措施,以确定它们是否可靠。根据控制的实施,必须分析攻击的性质或任何可⽤于绕过它的特定攻击向量。


列举现有安全控制中的弱点是代码安全审计的另⼀个重要⽅⾯。


应⽤程序中出现安全缺陷有多种原因,⽐如缺少输⼊验证或参数处理不当。在代码审计的过程中,缺陷的根本原因需要被暴露出来,要跟踪完整的数据流。确定应⽤程序(源)的所有可能的输⼊,以及它们是如何被应⽤程序(接收器)处理的。接收器可能是⼀种不安全的代码模式,如动态 SQL 查询、⽇志编写器或对客户端设备的响应。


考虑⼀个源是⽤户输⼊的场景。它流经应⽤程序的不同类/组件,最后落⼊⼀个拼接的SQL 查询(⼀个接收器)中,并且在路径中没有对它进⾏适当的验证。在这种情况下,应⽤程序将容易受到 SQL 注⼊攻击,这是由源到⽬的分析确定的。这种分析有助于理解哪些易受攻击的输⼊可能导致应⽤程序中的漏洞。


⼀旦发现缺陷,审计者必须列举应⽤程序中存在的所有可能的实例。这不是由代码变更发起的代码审计,这是由管理部⻔基于发现的缺陷发起的代码扫描,并且投⼊资源来查找该缺陷是否存在于产品的其他部分。例如,由于在不安全的显示⽅法中使⽤未经验证的输⼊,应⽤程序很容易在这些地⽅受到 XSS 漏洞的攻击。

应⽤程序功能和业务规则

审计⼈员应了解应⽤程序当前提供的所有功能,并获取与这些功能相关的所有业务限制规则。还有⼀种情况是,要注意潜在的计划中的功能,这些功能可能会出现在应⽤程序的路线图上,从⽽在当前的代码审计过程中对安全决策进⾏提前验证。这个系统失败的后果是什么?如果应⽤程序不能按预期执⾏其功能,企业会受到很⼤影响吗?

上下⽂


所有的安全都在我们试图保护的范围内。在苹果的应⽤程序上推荐军事安全标准机制将是矫枉过正不恰当的。什么类型的数据被操纵或处理,如果这些数据被泄露会对公司造成什么损害?上下⽂是安全代码审计和⻛险评估的“圣杯”。


敏感数据


审计⼈员还应记录对应⽤程序敏感的数据实体,如账号和密码。根据敏感度对数据实体进⾏分类将有助于审计者确定应⽤程序中任何类型的数据丢失的影响。


⽤户⻆⾊和访问权限


了解被允许访问应⽤程序的⽤户类型很重要。是⾯向外部还是内部给“信任”的⽤户? ⼀般来说,只有组织内部⽤户才能访问的应⽤程序可能与互联⽹上任何⼈都能访问的⾯临不同的威胁。因此,了解应⽤程序的⽤户及其部署的环境将允许审计者正确认识威胁。除此之外,还必须了解应⽤程序中存在的不同权限级别。这将有助于审计者列举适⽤于应⽤程序的不同安全违规/权限提升攻击。


应⽤类型


这是指了解应⽤程序是基于浏览器的应⽤程序、基于桌⾯的独⽴应⽤程序、⽹络服务、移动应⽤程序还是混合应⽤程序。不同类型的应⽤程序⾯临不同类型的安全威胁,了解应⽤程序的类型将有助于审计者查找特定的安全缺陷,确定正确的威胁代理,并突出适合应⽤程序的必要控制。


代码


使⽤的语⾔,从安全⻆度看该语⾔的特点和问题。从安全性和性能的⻆度来看,程序员需要注意的问题和语⾔最佳实践。


设计


⼀般来说,如果使⽤ MVC 设计原则开发,⽹络应⽤程序有⼀个定义良好的代码布局。应⽤程序可以有⾃⼰的定制设计,也可以使⽤⼀些著名的设计框架,如 Struts/Spring 等。
应⽤程序属性/配置参数存储在哪⾥?
如何为任何功能/URL 识别业务类别?
什么类型的类被执⾏来处理请求(例如。集中式控制器、命令类、视图⻚⾯等)?
对于任何请求,视图是如何呈现给⽤户的?

确定攻击⾯

通过分析输⼊、数据流和事务来确定攻击⾯。实际执⾏代码安全审计的主要部分是对攻击⾯进⾏分析。应⽤程序接
受输⼊并产⽣某种输出。第⼀步是识别代码的所有输⼊。
应⽤程序的输⼊可能包括以下要点:
l 浏览器输⼊
l Cookie
l ⽂件
l 命令⾏参数
l 环境变量

PHP代码审计思路

敏感函数⽅法回溯(反向审计)

查找项⽬中的敏感函数⽅法,查找传⼊的参数判断⽤户是否可控;


⽤户可控参数正向查找

查找项⽬中的⽤户输⼊ 追踪⽤户输⼊ 判断是否得到有效的过滤/调⽤敏感函数/存在逻辑问题


关键业务功能分析(功能审计)

专⻔审计易出现漏洞的关键功能点
如 头像上传 系统登陆 ⽂件下载 等功能


审计所有代码

⽤户可控参数

来⾃⽤户可控的输⼊, 安全审计中永远不要相信⽤户的输⼊

$_SERVER['HTTP_ACCEPT_LANGUAGE']//浏览器语⾔
$_SERVER['REMOTE_ADDR'] //当前⽤户 IP 。
$_SERVER['REMOTE_HOST'] //当前⽤户主机名
$_SERVER['REQUEST_URI'] //URL
$_SERVER['REMOTE_PORT'] //端⼝。
$_SERVER['SERVER_NAME'] //服务器主机的名称。
$_SERVER['PHP_SELF']//正在执⾏脚本的⽂件名
$_SERVER['argv'] //传递给该脚本的参数。
$_SERVER['argc'] //传递给程序的命令⾏参数的个数。
$_SERVER['GATEWAY_INTERFACE']//CGI 规范的版本。
$_SERVER['SERVER_SOFTWARE'] //服务器标识的字串
$_SERVER['SERVER_PROTOCOL'] //请求⻚⾯时通信协议的名称和版本
$_SERVER['REQUEST_METHOD']//访问⻚⾯时的请求⽅法
$_SERVER['QUERY_STRING'] //查询(query)的字符串。
$_SERVER['DOCUMENT_ROOT'] //当前运⾏脚本所在的⽂档根⽬录
$_SERVER['HTTP_ACCEPT'] //当前请求的 Accept: 头部的内容。
$_SERVER['HTTP_ACCEPT_CHARSET'] //当前请求的 Accept-Charset: 头部的内容。
$_SERVER['HTTP_ACCEPT_ENCODING'] //当前请求的 Accept-Encoding: 头部的内容
$_SERVER['HTTP_CONNECTION'] //当前请求的 Connection: 头部的内容。例如:“Keep-Alive”。
$_SERVER['HTTP_HOST'] //当前请求的 Host: 头部的内容。
$_SERVER['HTTP_REFERER'] //链接到当前⻚⾯的前⼀⻚⾯的 URL 地址。
$_SERVER['HTTP_USER_AGENT'] //当前请求的 User_Agent: 头部的内容。
$_SERVER['HTTPS']//如果通过https访问,则被设为⼀个⾮空的值(on),否则返回off
$_SERVER['SCRIPT_FILENAME'] #当前执⾏脚本的绝对路径名。
$_SERVER['SERVER_ADMIN'] #管理员信息
$_SERVER['SERVER_PORT'] #服务器所使⽤的端⼝
$_SERVER['SERVER_SIGNATURE'] #包含服务器版本和虚拟主机名的字符串。
$_SERVER['PATH_TRANSLATED'] #当前脚本所在⽂件系统(不是⽂档根⽬录)的基本路径。
$_SERVER['SCRIPT_NAME'] #包含当前脚本的路径。这在⻚⾯需要指向⾃⼰时⾮常有⽤。
$_SERVER['PHP_AUTH_USER'] #当 PHP 运⾏在 Apache 模块⽅式下,并且正在使⽤ HTTP 认证功能,这个变量便是⽤户输⼊的⽤户名。
$_SERVER['PHP_AUTH_PW'] #当 PHP 运⾏在 Apache 模块⽅式下,并且正在使⽤ HTTP 认证功能,这个变量便是⽤户输⼊的密码。
$_SERVER['AUTH_TYPE'] #当 PHP 运⾏在 Apache 模块⽅式下,并且正在使⽤ HTTP 认证功能,这个变量便是认证的类型

可以通过 Request 对象完成全局输⼊变量的检测、获取和安全过滤,⽀持包括$_GET 、 $_POST 、 $_REQUEST 、 $_SERVER 、 $_SESSION 、 $_COOKIE 、 $_ENV 等系统变量,以及⽂件上传信息。

ThinkPHP框架

概述

ThinkPHP是⼀个免费开源的,快速、简单的⾯向对象的轻量级PHP开发框架,是为了敏捷WEB应⽤开发和简化企业应⽤开发⽽诞⽣的。ThinkPHP从诞⽣以来⼀直秉承简洁实⽤的设计原则,在保持出⾊的性能和⾄简的代码的同时,也注重易⽤性。遵循 Apache2 开源许可协议发布,意味着你可以免费使⽤ThinkPHP,甚⾄允许把你基于ThinkPHP开发的应⽤开源或商业产品发布/销售。

thinkphp框架⽬录如下,可以看到初始的⽬录结构:

project 应⽤部署⽬录


├─application 应⽤⽬录(可设置)
│ ├─common 公共模块⽬录(可更改)
│ ├─index 模块⽬录(可更改)
│ │ ├─config.php 模块配置⽂件
│ │ ├─common.php 模块函数⽂件
│ │ ├─controller 控制器⽬录
│ │ ├─model 模型⽬录
│ │ ├─view 视图⽬录
│ │ └─ ... 更多类库⽬录
│ ├─command.php 命令⾏⼯具配置⽂件
│ ├─common.php 应⽤公共(函数)⽂件
│ ├─config.php 应⽤(公共)配置⽂件
│ ├─database.php 数据库配置⽂件
│ ├─tags.php 应⽤⾏为扩展定义⽂件
│ └─route.php 路由配置⽂件
├─extend 扩展类库⽬录(可定义)
├─public WEB 部署⽬录(对外访问⽬录)
│ ├─static 静态资源存放⽬录(css,js,image)
│ ├─index.php 应⽤⼊⼝⽂件
│ ├─router.php 快速测试⽂件
│ └─.htaccess ⽤于 apache 的重写
├─runtime 应⽤的运⾏时⽬录(可写,可设置)
├─vendor 第三⽅类库⽬录(Composer)
├─thinkphp 框架系统⽬录
│ ├─lang 语⾔包⽬录
│ ├─library 框架核⼼类库⽬录
│ │ ├─think Think 类库包⽬录
│ │ └─traits 系统 Traits ⽬录
│ ├─tpl 系统模板⽬录
│ ├─.htaccess ⽤于 apache 的重写
│ ├─.travis.yml CI 定义⽂件
│ ├─base.php 基础定义⽂件
│ ├─composer.json composer 定义⽂件
│ ├─console.php 控制台⼊⼝⽂件
│ ├─convention.php 惯例配置⽂件
│ ├─helper.php 助⼿函数⽂件(可选)
│ ├─LICENSE.txt 授权说明⽂件
│ ├─phpunit.xml 单元测试配置⽂件
│ ├─README.md README ⽂件
│ └─start.php 框架引导⽂件
├─build.php ⾃动⽣成定义⽂件(参考)
├─composer.json composer 定义⽂件
├─LICENSE.txt 授权说明⽂件
├─README.md README ⽂件
├─think 命令⾏⼊⼝⽂件

⼊⼝⽂件

⽤户请求的PHP⽂件,负责处理⼀个请求(注意,不⼀定是URL请求)的⽣命周期,最常⻅的⼊⼝⽂件就是 index.php ,有时候也会为了某些特殊的需求⽽增加新的⼊⼝⽂件,例如给后台模块单独设置的⼀个⼊⼝⽂件admin.php 或者⼀个控制器程序⼊⼝ think 都属于⼊⼝⽂件。

MVC模式

MVC的全名是Model View Controller,是模型(Model)-视图(view)-控制器(controller)的缩写,是⼀种设计模式。它是⽤⼀种业务逻辑、数据与界⾯显示分离的⽅法来组织代码,将众多的业务逻辑聚集到⼀个部件⾥⾯,在需要改进和个性化定制界⾯及⽤户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间,提⾼代码复⽤性。


使⽤的MVC的⽬的:它将这些对象、显示、控制分离以提⾼软件的的灵活性和复⽤性,MVC结构可以使程序具有对象化的特征,也更容易维护。


模型层(Model):指从现实世界中抽象出来的对象模型,是应⽤逻辑的反应;它封装了数据和对数据的操作,是
实际进⾏数据处理的地⽅(模型层与数据库才有交互)
视图层(View):是应⽤和⽤户之间的接⼝,它负责将应⽤显示给⽤户 和 显示模型的状态。
控制器(Controller):控制器负责视图和模型之间的交互,控制对⽤户输⼊的响应、响应⽅式和流程;它主要负责
两⽅⾯的动作,⼀是把⽤户的请求分发到相应的模型,⼆是吧模型的改变及时地反映到视图上。

控制器

每个模块拥有独⽴的 MVC 类库及配置⽂件,⼀个模块下⾯有多个控制器负责响应请求,⽽每个控制器其实就是⼀个独⽴的控制器类。
控制器主要负责请求的接收,并调⽤相关的模型处理,并最终通过视图输出。严格来说,控制器不应该过多的介⼊业务逻辑处理。
事实上,5.0中控制器是可以被跳过的,通过路由我们可以直接把请求调度到某个模型或者其他的类进⾏处
理。
5.0的控制器类⽐较灵活,可以⽆需继承任何基础类库。
⼀个典型的 Index 控制器类如下:

操作 

⼀个控制器包含多个操作(⽅法),操作⽅法是⼀个URL访问的最⼩单元。
下⾯是⼀个典型的 Index 控制器的操作⽅法定义,包含了两个操作⽅法:

操作⽅法可以不使⽤任何参数,如果定义了⼀个⾮可选参数,则该参数必须通过⽤户请求传⼊,如果是URL请求,则通常是 $_GET 或者 $_POST ⽅式传⼊。 

检测变量是否设置

可以使⽤ has ⽅法来检测⼀个变量参数是否设置,如下:

Request::instance()->has('id','get');

Request::instance()->has('name','post');

或者使⽤助⼿函数:

input('?get.id');
input('?post.name');
 

获取PARAM变量

PARAM变量是框架提供的⽤于⾃动识别 GET 、 POST 或者 PUT 请求的⼀种变量获取⽅式,是系统推荐的获取请求参数的⽅法,⽤法如下:

// 获取当前请求的name变量
Request::instance()->param('name');
// 获取当前请求的所有变量(经过过滤)
Request::instance()->param();
// 获取当前请求的所有变量(原始数据)
Request::instance()->param(false);
// 获取当前请求的所有变量(包含上传⽂件)
Request::instance()->param(true);

使⽤助⼿函数实现:

input('param.name');
input('param.');
或者
input('name');
input('');
因为 input 函数默认就采⽤PARAM变量读取⽅式。

获取GET变量

Request::instance()->get('id'); // 获取某个get变量
Request::instance()->get('name'); // 获取get变量
Request::instance()->get(); // 获取所有的get变量(经过过滤的数组)
Request::instance()->get(false); // 获取所有的get变量(原始数组)

或者使⽤内置的助⼿函数 input ⽅法实现相同的功能:

input('get.id');
input('get.name');
input('get.');

获取POST变量

Request::instance()->post('name'); // 获取某个post变量
Request::instance()->post(); // 获取经过过滤的全部post变量
Request::instance()->post(false); // 获取全部的post原始变量

使⽤助⼿函数实现:

input('post.name');
input('post.');

获取PUT变量

Request::instance()->put('name'); // 获取某个put变量
Request::instance()->put(); // 获取全部的put变量(经过过滤)
Request::instance()->put(false); // 获取全部的put原始变量

使⽤助⼿函数实现:

input('put.name');
input('put.');

获取REQUEST变量

Request::instance()->request('id'); // 获取某个request变量
Request::instance()->request(); // 获取全部的request变量(经过过滤)
Request::instance()->request(false); // 获取全部的request原始变量数据
使⽤助⼿函数实现:

input('request.id');
input('request.');

获取SERVER变量

Request::instance()->server('PHP_SELF'); // 获取某个server变量
Request::instance()->server(); // 获取全部的server变量

使⽤助⼿函数实现:

input('server.PHP_SELF');
input('server.');

变量过滤

框架默认没有设置任何过滤规则,你可以是配置⽂件中设置全局的过滤规则:

// 默认全局过滤⽅法 ⽤逗号分隔多个
'default_filter' => 'htmlspecialchars',

也⽀持使⽤ Request 对象进⾏全局变量的获取过滤,过滤⽅式包括函数、⽅法过滤,以及PHP内置的Types of filters,我们可以设置全局变量过滤⽅法,例如:

Request::instance()->filter('htmlspecialchars');
⽀持设置多个过滤⽅法,例如:

Request::instance()->filter(['strip_tags','htmlspecialchars']);

也可以在获取变量的时候添加过滤⽅法,例如:

Request::instance()->get('name','','htmlspecialchars'); // 获取get变量 并⽤htmlspecialchars函数过滤
Request::instance()->param('username','','strip_tags'); // 获取param变量 并⽤strip_tags函
数过滤
Request::instance()->post('name','','org\Filter::safeHtml'); // 获取post变量 并⽤org\Filter类的safeHtml⽅法过滤

原⽣查询

Db 类⽀持原⽣ SQL 查询操作,主要包括下⾯两个⽅法:

query⽅法

query ⽅法⽤于执⾏ SQL 查询操作,如果数据⾮法或者查询错误则返回false,否则返回查询结果数据集(同 select ⽅法)。

使⽤示例:
Db::query("select * from think_user where status=1");

execute⽅法

execute⽤于更新和写⼊数据的sql操作,如果数据⾮法或者查询错误则返回false ,否则返回影响的记录数。

使⽤示例:

Db::execute("update think_user set name='thinkphp' where status=1");

参数绑定

⽀持在原⽣查询的时候使⽤参数绑定,包括问号占位符或者命名占位符,例如:

Db::query('select * from think_user where id=?',[8]);
Db::execute('insert into think_user (id, name) values (?, ?)',[8,'thinkphp']);


也⽀持命名占位符绑定,例如:

Db::query('select * from think_user where id=:id',['id'=>8]);
Db::execute('insert into think_user (id, name) values (:id, :name)',
['id'=>8,'name'=>'thinkphp']);

基本查询

查询⼀个数据使⽤:

// table⽅法必须指定完整的数据表名
Db::table('think_user')->where('id',1)->find();

查询数据集使⽤:

Db::table('think_user')->where('status',1)->select();

如果设置了数据表前缀参数的话,可以使⽤:

Db::name('user')->where('id',1)->find();
Db::name('user')->where('status',1)->select();
 

PHP敏感函数

命令执⾏函数

 <?php
 exec('ping 127.0.0.1',$output,$return_var);
 
 system('ping -c 127.0.0.1',$return_var);
 
 passthru('ping 12.0.0.1',$return_var);
 passthru('id');
 
 shell_exec("ping 127.0.0.1");
 shell_exec(`ping 127.0.0.1`);
 
 popen("id", "r");
 pcntl_exec('id');

mail 函数


php的mail函数声明如下:
其参数含义分别表示如下:
to,指定邮件接收者,即接收⼈
subject,邮件的标题
message,邮件的正⽂内容
additional_headers,指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
additional_parameters,指定传递给发送程序sendmail的额外参数。
在Linux系统上,mail函数在底层实现中,默认调⽤Linux的sendmail程序发送邮件。在sendmail程序的参数中,有⼀个 -X 选项,⽤于记录所有的邮件进出流量⾄log⽂件中。
通过 -X 指定log⽂件记录邮件流量,实际可以达到写⽂件的效果。
例如,如下php代码:

$to = 'Alice@example.com';
$subject = 'Hello Alice!';
$message=‘<?php phhpinfo(); ?>’;
$headers = "CC: somebodyelse@example.com";
$options = '-OQueueDirectory=/tmp -X/var/www/html/rce.php';
mail($to, $subject, $message, $headers, $options);

代码注⼊/⽂件包含函数

<?php
 eval('phpinfo();');
 assert('phpinfo();');
 echo preg_replace("/e","{${PHPINFO()}}","123");
 call_user_func('assert', 'phpinfo();');
 call_user_func_array('file_put_contents', ['1.txt','6666']);
 $f = create_function('','system($_ GET[123]);'); $f();
 include 'vars.php';
 require('somefile.php');