PHP代码审计(一)——基础知识

工具准备

手工审计工具

VS CODE+PHP Intelephense扩展用于辅助审计

快捷键帮助提高审计效率

关闭当前窗口: Ctrl+W
文件之间切换: Ctrl+T_a_b
移动到行首: Home
移动到行尾: End
移动到文件开头: Ctrl+Home
移动到文件结尾: Ctrl+End
查找/转到定义: F12
查找/转到引用: Shift+F12
代码格式化: Shift+Alt+F
查找: Ctrl+F
F9 : 打断点
F5 : 开始调试
在项目文件中搜索: Ctrl+Shift+F

PHP Intelephense插件主要用到函数跳转追踪功能,按住ctrl 点击被引用对象可以快速跳转定义位置(或把光标放在查找位置 点击F12)

按住ctrl 点击定义可以快速查找项目中所有的引用位置(或把光标放在查找位置 点击Shift+F12)

审计思路

其他语言可借鉴审计思路

1、敏感函数方法回溯(反向审计)

查找项目中的敏感函数方法——回溯传入的参数——判断用户是否可控——是否得到有效的过滤

● 缺点:很难发现越权等逻辑漏洞

2、用户可控参数追踪(正向审计)

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

PHP敏感函数速查表

参考:https://www.yuque.com/burpheart/phpaudit/php-shen-ji-ji-chu_cui-ruo-han-shu-su-cha-biao

系统命令执行

函数/语法 描述 举例
system 执行命令并输出结果 system('whoami');
exec 执行命令 只可获取最后一行结果 exec('whoami',$a); print_r($a);
passthru 执行命令并输出结果 passthru('id');
shell_exec ` (反引号) 执行命令并返回结果 $a=shell_exec('whoami');print_r($a);$a=`whoami`;print_r($a);
popen 执行命令并建立管道 返回一个指针 使用fread等函数操作指针进行读写 $a=popen("id", "r"); echo fread($a, 2096);
proc_open 同popen,进程控制能力更强大 见php手册
pcntl_exec 执行命令,只返回是否发生错误 pcntl_exec('id');

注意,id是Linux中的命令

代码注入/文件包含

函数/语法 描述 举例
eval 将传入的参数内容作为PHP代码执行 eval 不是函数 是一种语法结构 不能当做函数动态调用 eval('phpinfo();');
assert 将传入的参数内容作为PHP代码执行 版本在PHP7以下是函数 PHP7及以上为语法结构 assert('phpinfo();');
preg_replace 当preg_replace使用/e修饰符且原字符串可控时时 有可能执行php代码 echo preg_replace("/e","{${PHPINFO()}}","123");
call_user_func 把第一个参数作为回调函数调用 需要两个参数都完全可控才可利用 只能传入一个参数调用
call_user_func_array 同call_user_func 可传入一个数组带入多个参数调用函数 call_user_func_array ('file_put_contents', ['1.txt','6666']);
create_function 根据传递的参数创建匿名函数,并为其返回唯一名称 利用需要第二个参数可控 且创建的函数被执行 $f = create_function('','system($_GET[123]);'); $f(); 它通过create_function函数动态创建了一个匿名函数,该函数执行了system函数来运行一个外部命令。这个外部命令来自于一个GET请求的参数($_GET[123])。这意味着,任何人只要通过构造恰当的URL,就能在你的服务器上执行任意命令。
include 包含并运行指定文件 执行出错会抛出错误 include 'vars.php'; (括号可有可无)
require 同include 执行出错会抛出警告 require('somefile.php'); (括号可有可无)
require_once 同require 但会检查之前是否已经包含该文件 确保不重复包含
include_once 同include 但会检查之前是否已经包含该文件 确保不重复包含

SQL/LDAP注入

SQL注入:允许攻击者通过向Web应用的输入字段提交恶意SQL语句,从而在后端数据库上执行未授权的数据库命令。
LDAP注入:利用应用程序中对LDAP(轻量级目录访问协议)查询的不当处理。LDAP是一种在网络上查询和修改目录服务信息的协议,常用于身份验证和组织信息的存储。当应用程序将用户输入不加验证和清理直接拼接到LDAP查询中时,就可能发生LDAP注入。

函数/方法 备注
mysql_query
odbc_exec 执行SQL语句,通过ODBC接口与数据库进行交云
mysqli_query
mysql_db_query
mysql_unbuffered_query
mysqli::query 用法:$mysqli = new mysqli("localhost", "my_user", "my_password", "world");$mysqli->query(); 最后一句传入sql语句,$result = $mysqli->query("SELECT * FROM tablename");
pg_query pg_query() 是 PHP 中用于执行 PostgreSQL 数据库查询的函数。它允许你向 PostgreSQL 服务器发送SQL语句并执行
pg_query_params
pg_send_query
pg_send_query_params
sqlsrv_query
pdo::query 用法: $pdo=new PDO("mysql:host=localhost;dbname=phpdemo","root","1234"); $pdo->query($sql); new PDO(...) 创建PDO实例,然后使用PDO对象($pdo)执行一个SQL查询。$sql 是一个包含SQL语句的字符串变量。这个查询可以是任何有效的SQL语句,比如SELECT、INSERT、UPDATE或DELETE。$pdo->query($sql) 方法执行SQL语句并返回一个PDOStatement对象,如果查询执行成功的话。这个对象可以用来获取查询结果。
SQLite3::query 用法:SQLite3::exec $db = new SQLite3('mysqlitedb.db'); $db->query('SELECT bar FROM foo'); $db->exec('CREATE TABLE bar (bar STRING)'); $db->exec(...) 方法用于执行那些不返回数据的SQL语句,如CREATE TABLE、DROP TABLE、INSERT、UPDATE、DELETE等。如果语句执行成功,它通常返回TRUE;如果执行失败,返回FALSE
$mongo = new mongoclient(); $data = $coll->find($data); https://wooyun.js.org/drops/Mongodb注入攻击.html
$ld = ldap_connect("localhost");…. $lb = @ldap_bind($ld, "cn=test,dc=test,dc=com", "test");
Db::query Thinkphp
Db::execute

1、PDO:PDO(PHP Data Objects)扩展来创建一个与MySQL数据库的连接,并执行一个SQL查询。PDO是PHP中用于访问数据库的一个轻量级、一致的接口,它提供了一个数据访问抽象层,这意味着不论使用什么数据库,你都可以使用相同的函数来查询和获取数据。示例中,代码未显示$sql变量的内容。在实际应用中,应该确保SQL语句是安全的,尤其是当它包含用户输入的数据时。为了防止SQL注入,建议使用PDO的预处理语句(prepare)和参数绑定(bindValue或bindParam)。
2、SQLite:SQLite是一个轻量级的、自包含的、高可靠的SQL数据库引擎。PHP的SQLite3扩展提供了一个面向对象的接口,用于执行SQLite数据库操作。
3、ODBC:Open Database Connectivity,开放数据库互联。ODBC 是一个标准的 API,用于访问数据库管理系统(DBMS),使得客户端程序能够以统一的方式访问不同的数据库系统。使用 odbc_exec() 时需要确保 PHP 的 ODBC 扩展已经启用。

<?php
// 创建到数据库的连接
$connection = odbc_connect("Driver={Microsoft Access Driver (*.mdb)};Dbq=C:\\database.mdb", "user", "password");

// 执行SQL查询
$result = odbc_exec($connection, "SELECT * FROM table_name");

// 处理查询结果
while($row = odbc_fetch_array($result)){
    print_r($row);
}

// 关闭连接
odbc_close($connection);
?>

文件读取/SSRF

函数 描述 举例
file_get_contents 读入文件返回字符串 echo file_get_contents("flag.txt"); echo file_get_contents("https://www.bilibili.com/
");
curl_setopt curl_exec Curl访问url获取信息 function curl($url){ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_exec($ch); curl_close($ch); } $url = $_GET['url']; curl($url); https://www.php.net/manual/zh/function.curl-exec.php
fsockopen 打开一个套接字连接(远程 tcp/udp raw) https://www.php.net/manual/zh/function.fsockopen.php
readfile 读取一个文件,并写入到输出缓冲 同file_get_contents
fopen/fread/fgets/fgetss /fgetc/fgetcsv/fpassthru/fscanf 打开文件或者 URL 读取文件流 $file = fopen("test.txt","r"); echo fread($file,"1234"); fclose($file);
file 把整个文件读入一个数组中 echo implode('', file('https://www.bilibili.com/'));
highlight_file/show_source 语法高亮一个文件 highlight_file("1.php");
parse_ini_file 读取并解析一个ini配置文件 print_r(parse_ini_file('1.ini'));
simplexml_load_file 读取文件作为XML文档解析

文件上传/写入/其他

函数 描述 例子
file_put_contents 将一个字符串写入文件 file_put_contents("1.txt","6666");
move_uploaded_file 将上传的临时文件移动到新的位置 move_uploaded_file($_FILES["pictures"]["tmp_name"],"1.php")
rename 重命名文件/目录 rename($oldname, $newname);
rmdir 删除目录
mkdir 创建目录
unlink 删除文件
copy 复制文件 copy($file, $newfile);
fopen/fputs/fwrite 打开文件或者 URL https://www.php.net/manual/zh/function.fwrite.php
link 创建文件硬链接 link($target, $link);
symlink 创建符号链接(软链接) symlink($target, $link);
tmpfile 创建一个临时文件 (在临时目录存放 随机文件名 返回句柄) $temp = tmpfile(); fwrite($temp, "123456"); fclose($temp);
request()->file()->move() request()->file()->file() Thinkphp 文件上传 $file = request()->file($name); $file->move($filepath);
extractTo 解压ZIP到目录
DOMDocument loadXML simplexml_import_dom 加载解析XML 有可能存在XXEE 漏洞 file_get_contents获取客户端输入内容 new DOMDocument()初始化XML解析器 loadXML($xmlfile)加载客户端输入的XML内容 simplexml_import_dom($dom)获取XML文档节点,如果成功则返回SimpleXMLElement对象,如果失败则返回FALSE。 <?php $xmlfile=file_get_contents('php://input'); $dom=new DOMDocument(); $dom->loadXML($xmlfile); $xml=simplexml_import_dom($dom); $xxe=$xml->xxe; $str="$xxe \n"; echo $str; ?>来自 https://xz.aliyun.com/t/6887
simplexml_load_string 加载解析XML字符串 有可能存在XXE 漏洞 $xml=simplexml_load_string($_REQUEST['xml']); print_r($xml);
simplexml_load_file 读取文件作为XML文档解析 有可能存在XXE 漏洞
unserialize 反序列化

PHP用户可控输入速查表

参考:https://www.yuque.com/burpheart/phpaudit/php-shen-ji-ji-chu_yong-hu-ke-kong-shu-ru-su-cha-biao

变量/常量/函数/其他 描述
$_SERVER 包含 服务器信息 环境变量 用户传入的http头和uri路径等信息
$_GET $HTTP_GET_VARS 包含 用户传入的URL参数
$_POST$HTTP_POST_VARS 包含 用户传入的POST BODY的参数 (当 HTTP头中Content-Type 值为 application/x-www-form-urlencoded 或 multipart/form-data时才会被传入)
$_FILES$HTTP_POST_FILES 包含 用户上传文件信息 文件内容 原文件名 临时文件名 大小 等信息
$_COOKIE$HTTP_COOKIE_VARS 包含 用户传入的HTTP头中的Cookies kv值
$_REQUEST 同时包含 $_GET $_POST $_COOKIE
php://input$HTTP_RAW_POST_DATA 包含 用户POST请求中BODY 的完整数据 常见用法:file_get_contents('php://input');
apache_request_headers()getallheaders() 包含 用户传入的http头 (Apache ONLY)
下方为PHP框架常见输入
Request::instance()->get();input('get.');input('变量类型.变量名/修饰符')详见官方文档 获取用户传入的URL参数 可用过滤器和类型转换 THINKPHP框架5 例子:获取url参数中的id值 Request::instance()->get('id'); 调用时如不传入参数默认获取全部 Request::instance()->get(); input('get.id'); 调用时如传入get.则获取全部 input('get.'); input('get.id/d');// 强制变量转换为整型 Request::instance()->get('name','','htmlspecialchars'); //过滤器 input('get.name/s');// 强制转换变量为字符串 input('get.ids/a');// 强制变量转换为数组 默认为/s
Request::instance()->post();input('post.'); 获取用户传入的POST参数 THINKPHP框架5 例子:获取post请求body中的name值 Request::instance()->post('name'); input('post.name'); 同get
Request::instance()-> param();input('param.');input(''); 自动判断用户提交方法(POST GET PUT)获取参数 THINKPHP框架5 用法同get 除此之外 可直接调用input('');获取全部参数 或使用 input('name');获取单个参数 注:input方法默认获取param
Request::instance()->request(); input('request.'); 用法同get 获取$_REQUEST 变量 THINKPHP框架5
Request::instance()->server(); input('server.'); 用法同get 获取$_SERVER 变量THINKPHP框架5
Request::instance()->cookie(); input('cookie.'); Cookie::get('name'); cookie('name'); 用法同get 获取$_COOKIE 变量THINKPHP框架5
Request::instance()->header(); input('header.'); 用法同get 获取用户传入的HTTP头THINKPHP框架5
Request::instance()->file(); 用法同get 获取$_FILES 变量THINKPHP框架5
I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则']) 获取变量 THINKPHP框架3.* 例子 I('get.id'); I('get.'); 使用方法同input
request(); 实例化request对象THINKPHP框架5 例$req=request(); 相当于$req=Request::instance() 这种使用方法比较常见 还可以获取用户传入的请求信息 可将前面的Request::instance()直接替换成request() 例 request()->post();
{$Request.变量类型.变量名} THINKPHP框架 在模板中获取参数
路由传入值 (Action参数绑定)
Request::instance() 其他用户变量 https://www.kancloud.cn/manual/thinkphp5/158834THINKPHP框架5 见官方文档 略
$this->input->post() $this->input->get() $this->input->cookie() $this->input->server() $this->input->user_agent(); $this->input->request_headers(); $this->input->get_request_header('some-header', TRUE); $this->input->ip_address(); $this->input->raw_input_stream; $this->input->input_stream('key'); (POST BODY)
$request->getGet() $request->getPost() $request->getServer() $request->getCookie() $request->getPostGet()- 先 $_POST, 后 $_GET $request->getGetPost()- 先 $_GET, 后 $_POST $request->getJSON(); $this->request->getFiles(); $this->request->getFile('123'); 获取post body json数据 $request->getRawInput() 同php://input Codeigniter4框架 https://codeigniter.org.cn/user_guide/incoming/incomingrequest.html?highlight=post#id4
$this->request->getQuery('page'); (GET参数) $this->request->getQueryParams(); 全部get参数 $this->request->getData('title'); (POST 参数) $this->request->getParsedBody(); 全部post参数 $this->request->getUploadedFile('attachment'); $jsonData=$this->request->input('json_decode'); $request->getUploadedFiles(); $data=$this->request->input('Cake\Utility\Xml::build',['return'=>'domdocument']); $userAgent=$this->request->getHeaderLine('User-Agent');// Get an array of all values.$acceptHeader=$this->request->getHeader('Accept'); $this->request->getCookie('remember_me'); Cakephp 4.* 框架 文件上传 https://book.cakephp.org/4/en/controllers/request-response.html#file-uploads
Yii 2.0框架
Laravel 框架

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

专门审计易出现漏洞的关键功能点

如:头像上传 系统登陆 文件下载 等功能

● 优点:可以快速完成审计,减少审计面 可发现越权等逻辑漏洞

● 缺点:审计不完全

4、完全审计源代码

● 优点:完全覆盖源代码,可发现一些特殊条件的漏洞

● 缺点:耗时长,容易遗漏

PHP原生过滤方法

1、escapeshellarg 传入参数添加单引号并转义原有单引号 用于防止命令注入

例:传入 ' id # 处理后 '\' id #' 处理后的字符串可安全的添加到命令执行参数

2、escapeshellcmd 转义字符串中的特殊符号 用于防止命令注入

反斜线\会在以下字符之前插入: &#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 '和 " 仅在不配对儿的时候被转义

3、addslashes 在单引号(')、双引号(")、反斜线(\)与 NUL前加上反斜线
可用于防止SQL注入

4、
在PHP中,用于防止SQL注入攻击的常用函数包括mysqli::real_escape_string, mysqli::escape_string, mysqli_real_escape_string, mysql_real_escape_string, 以及 SQLite3::escapeString。这些函数的主要目的是对输入字符串进行转义处理,使之可以安全地用在SQL语句中。

mysqli::real_escape_string 和 mysqli::escape_string
这两个函数实际上是相同的,mysqli::escape_string 是 mysqli::real_escape_string 的别名。它们属于面向对象风格的 mysqli 类方法。
用法示例(面向对象风格):

$mysqli = new mysqli("localhost", "my_user", "my_password", "my_db");
$safeString = $mysqli->real_escape_string($userInput);

mysqli_real_escape_string是mysqli::real_escape_string的过程化版本。
用法示例(过程化风格):

$link = mysqli_connect("localhost", "my_user", "my_password", "my_db");
$safeString = mysqli_real_escape_string($link, $userInput);

mysql_real_escape_string: 这是一个较旧的函数,用于转义字符串以用在 mysql 扩展的SQL语句中。注意,mysql 扩展自PHP 5.5.0起已经被废弃,并在PHP 7.0.0中被完全移除。使用这个函数的旧代码应该更新为使用mysqli或PDO_MySQL扩展。
用法示例(已废弃):

$safeString = mysql_real_escape_string($userInput);

SQLite3::escapeString: 这是用于SQLite3数据库的方法,用于转义字符串中的特殊字符。
用法示例:

$safeString = SQLite3::escapeString($userInput);

注意:经过以上函数处理后的字符串不可直接用于sql查询拼接 需要使用引号包裹后拼接到sql语句中 否则仍可导致sql注入

5、PDO::quote 转义特殊字符 并添加引号

PDO::prepare 预处理SQL语句 有效防止SQL注入 (推荐)

htmlspecialchars 和 htmlentities 将特殊字符转义成html实体 可用于防止XSS

intval($input) floatval() floatval() floor() (int)$input num+0 将输入强制转换为整数/浮点 常见于防止SQL注入

防止SQL注入的最佳实践: 预处理语句确保了数据和SQL命令的分离,使得攻击者难以通过输入数据来改变SQL命令的结构。

  • 对于mysqli,使用prepare方法和bind_param方法。
  • 对于PDO,使用prepare方法和bindParam或bindValue方法。

参考文章

https://www.yuque.com/burpheart/phpaudit

posted @ 2024-03-28 16:22  smile_2233  阅读(51)  评论(0编辑  收藏  举报