安全开发-PHP-文件管理

token模块核心

注意:
1.浏览器的插件可能会影响token的使用,例如插件在你发出请求后重新刷新请求,导致数据包永远是不唯一的。
2.token使用的是session的会话维持
3.token是唯一的。

我的个人理解:

1.session的会话维持的特性,加入session的值在一定时间内是恒定不变的
2.利用生成算法等方式,创建一个唯一的token,并且传递给表单,让表单隐性提交过来
3.当表单提交的token值与服务器对不上时(!==),服务器更新token值。
4.当表单正确时,将token再次更新,确保数据包的唯一性。
作用:确保数据包的唯一性,相当于强制用户必须使用当前页面登录。
(反正每一次访问都会刷新token,并且想要破解token几乎不可能,除非程序员的设计有问题等。)

<?php
session_start();

$token = $_POST['token'] ?? '';

// 甄别条件从$_POST['submit']转换为甄别token是否正确
if ($token !== $_SESSION['token']) {
// token不匹配,禁止访问
header('HTTP/1.1 403 Forbidden');
$_SESSION['token'] = bin2hex(random_bytes(16));
echo 'Access denied';
exit;
}else{
$_SESSION['token'] = bin2hex(random_bytes(16));
if($_POST['username']=='admin' && $_POST['password']=='123456'){
echo '登录成功!';
echo '你是管理员可以访问文件管理页面!';
}else{
echo '登录失败!';
}
}

PHP文件管理

目的:为后期代码审计做铺垫。

界面设计

前端部分

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>文件列表</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<style type="text/css">
ul {
list-style: none;
padding: 0;
margin: 0;
}

li {
margin-bottom: 10px;
}

i {
margin-right: 10px;
}

a {
text-decoration: none;
color: #333;
}
</style>
</head>

<body>
<h1>当前目录下的文件列表</h1>
<ul></ul>
</body>
</html>

后端部分

<?php
// 取值,取get结果或者当前目录
$dir = $_GET['path'] ?? './';

//1.打开目录,读取文件列表 opendir
//2.循环读取文件列表 while readdir
//3.判断是文件还是文件夹 is_dir

//打开目录,读取文件列表 opendir
// 核心代码,也是核心函数
function filelist($dir)
{
if ($dh = opendir($dir)) {
//循环读取文件列表 while readdir
while (($file = readdir($dh)) !== false) {
//判断是文件还是文件夹 is_dir
if (is_dir($file)) {
echo "<li><i class='fa fa-folder'></i> <a href='?path=$file'>" . $file . '</a></li>';
} else {
echo '<li><i class="fa fa-file"></i> <a href="#">' . $file . '</a></li>';
}
}
}
}

filelist($dir);

function del($file)
{
if (!is_dir($file)) {
unlink($file);
echo "<script>alert('删除成功')</script>";
}
}

if (isset($_GET['del'])) {
del($_GET['del']);
}


function down($filepath)
{
$fileName = basename($filepath);
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"" . $fileName . "\"");
header("Content-Length: " . filesize($filepath));
readfile($filepath);
}

if (isset($_GET['down'])) {
down($_GET['down']);
}
?>

漏洞

php.ini文件:
如果不慎关闭这个设置,会导致全局目录遍历漏洞出现。导致本地的所有文件都会被攻击者浏览和下载。
关闭下面设置:形成文件遍历漏洞
open_basedir

防止被翻目录的方法:使用代码内语法限制,或者打开上面的open_basedir对访问目录进行限制

黑名单+白名单+文件类型限制

黑名单白名单

黑名单:写好不能上传的文件类型。
白名单:指定好能上传的文件类型(推荐)。

// 写好黑名单
// 缺点:变形文件后缀格式绕过,例如php改成php5
$black_ext = array('php','aps','jsp','aspx','js');
// 拆分上传的文件名
// 用‘ . ’来拆分上传的文件名
$chaifeng = explode('.',$_POST['file']['name']);
// 获取拆分的文件名后缀,当然,只能获取一层后缀
$exts = end($chaifeng);
// 测试后缀是否在黑名单内
if(is_array($exts,$black_ext)){
echo '非法后缀文件';
} else {
// 上传成功后,将文件存储到指定文件夹下
move_uploaded_file($tmp_name,'upload/'.$name);
}

// 白名单和它相反,代码和黑名单差不多
$allow_ext = array('php','aps','jsp','aspx','js');
// 拆分上传的文件名
// 用‘ . ’来拆分上传的文件名
$chaifeng = explode('.',$_POST['file']['name']);
// 获取拆分的文件名后缀,当然,只能获取一层后缀
$exts = end($chaifeng);
// 测试后缀是否在黑名单内
if(is_array($exts,$allow_ext)){
move_uploaded_file($tmp_name,'upload/'.$name);
} else {
echo '非法后缀文件';
}

文件类型限制

下面数数据包

POST /PHPStorm/demo02-file/upload.php HTTP/1.1
Host: localhost:63342
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------5435133931587131946420401364
Content-Length: 82957
Origin: http://localhost:63342
Connection: close
Referer: http://localhost:63342/PHPStorm/demo02-file/upload.html
Cookie: trc_cookie_storage=taboola%2520global%253Auser-id%3Df4eeb98c-1ca4-4407-9a33-806360ecfab3-tuctade20dc; Phpstorm-b8aba78c=ded8652c-119a-412b-aa29-d33a3f081b89; PHPSESSID=0sheefdllfhlpouaupbvm9jlka
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

-----------------------------5435133931587131946420401364
Content-Disposition: form-data; name="f"; filename="腾讯会议图片_20230408214013.jpeg"
Content-Type: image/jpeg

下面是文件内容,省略了

经过观察,可以发现Content-Type指定上传文件的文件类型是image/jpeg。

安全风险:改下文件类型的数据包就可以了。需要验证逻辑严谨些才能显得安全些。

$type = $_POST['file']['type'];
// 白名单mime类型表
$allow_type=array('image/png','image/jpg','image/jpeg','image/gif');
if(in_array($type,$allow_type)){
move_uploaded_file($tmp_name,'upload/'.$name);
echo '<script>alert("上传成功")</script>';
}else{
echo '非法文件类型';

}

文件包含:

include() 在错误发生后脚本继续执行
功能:'包含'哪个文件就执行那个文件
把任意一个文件调用过来执行。
=> 任意脚本执行漏洞:如下
include($_GET['page']);
require() 在错误发生后脚本停止执行
include_once() 如果已经包含,则不再执行
require_once() 如果已经包含,则不再执行

文件删除:

unlink() 文件删除函数
调用命令删除:system shell_exec exec等

php调用WindowsCMD删除文件
system('del 1.txt');
安全问题:任意命令执行漏洞。

文件下载:

修改HTTP头实现文件读取解析下载:

header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename="");
header("Content-Length: " . filesize($file));
readfile($file);

文件编辑:

1、file_get_contents() 读取文件内容
2、fopen() fread() 文件打开读入

文件编辑下载删除实现

switch ($action){
case 'del':
echo "<script>console.log('$fname')</script>";
unlink("$path/$fname");
header("Location: http://127.0.0.1:8028/all_file_read-x.php?path=$path");
echo "<script>window.location.href='http://127.0.0.1:8028/all_file_read-x.php?path=$path'</script>";
break;
case 'down':
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment;filename=\"".$fname."\"");
header("Content-Length: ".filesize($fname));
break;
case 'edit':
$content = file_get_contents("$path/$fname");
echo '<form name="form1" method="post" action="all_file_read-x.php">';
echo "文件名:".$fname."<br>";
echo "文件内容:<br>";
echo '<textarea name="codes" style="resize:none;" rows="100" cols="100"">'.$content.'</textarea><br>';
echo '<input type="submit" name="submit" id="submit" value="提交">';
echo "<input type='hidden' value='$path/$fname' name='pf'>";
echo '</form>';
break;
}
if (isset($_POST['submit'])){
if(isset($_POST['codes'])){
file_put_contents($_POST['pf'],$_POST['codes']);
}
}