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方法。