本文并不是系统的介绍如何进行PHP开发,而是从安全漏洞的视角来学习如何看懂大部分的php web开发代码,从而可以更方便的了解漏洞原理

基本概念

PHP(全称:PHP:Hypertext Preprocessor,即"PHP:超文本预处理器")是一种通用开源脚本语言,PHP 脚本一般在服务器上执行。

PHP脚本<?php 开始,以 ?> 结束。

1
2
3
<?php
// PHP 代码
?>

PHP 文件通常包含 HTML 标签和一些 PHP 脚本代码,其中PHP 脚本可以放在文件中的任何位置。

一个简单的PHP文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<body>

<h1>My first PHP page</h1>

<?php
echo "Hello World!";
?>

</body>
</html>

基本语法

一些基本的PHP语法参考:菜鸟PHP 教程

函数对象调用

使用字符串调用函数:直接用字符串变量存储函数名,并调用它。

1
2
3
4
5
6
function sayHello($name) {
return "Hello, $name!";
}

$func = 'sayHello';
echo $func('PHP'); // 输出: Hello, PHP!

超全局变量

参考:PHP 全局变量 - 超全局变量

PHP 中的许多预定义变量都是“超全局的”,这意味着它们在一个脚本的全部作用域中都可用。在函数或方法中无需执行 global $variable; 就可以访问它们,超全局变量包括以下几种:

1
2
3
4
5
6
7
8
$GLOBALS		用于在 PHP 脚本中的任意位置访问全局变量
$_SERVER 保存关于报头、路径和脚本位置的信息。
$_REQUEST 用于收集 HTML 表单提交的数据。
$_POST 用于收集提交 method="post" 的HTML表单后的表单数据。
$_GET 收集URL中的发送的数据,也可用于收集提交HTML表单数据(method="get") $_FILES 文件上传且处理包含通过HTTP POST方法上传给当前脚本的文件内容。
$_ENV 是一个包含服务器端环境变量的数组。
$_COOKIE 是一个关联数组,包含通过cookie传递给当前脚本的内容。
$_SESSION 是一个关联数组,包含当前脚本中的所有session内容。

html混编

通过echo函数,可以将内容在html上进行输出,如果输出的内容是js函数,则会使得JS在PHP语言中运行

例如:

1
2
3
<?php
echo '<script>alert('x');</script>'
?>

数据库操作

参考:『菜鸟教程』PHP 5 MySQLi 函数

PHP使用mysqli函数函数实现MySQL数据库的增删改查操作

操作数据库连接的php函数:

1
2
3
4
5
mysqli_connect() 			打开一个到MySQL的新的连接。
mysqli_select_db() 更改连接的默认数据库。
mysqli_query() 执行某个针对数据库的查询。
mysqli_fetch_row() 从结果集中取得一行,并作为枚举数组返回。
mysqli_close() 关闭先前打开的数据库连接。

执行的sql语句:

1
2
3
4
增:insert into 表名(列名1, 列名2) value(‘列1值1’, ‘列2值2’);
删:delete from 表名 where 列名 = ‘条件’;
改:update 表名 set 列名 = 数据 where 列名 = ‘条件’;
查:select * from 表名 where 列名=‘条件’;

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<form id="form1" name="form1" method="post" action="">

用户名:<input type="text" name="username" maxlength="2000"><br>

内容:

<textarea id="content" rows="10" cols="70" name="content" style="border:1px solid #E5E5E5;">
</textarea>
<script type="text/javascript">
UE.getEditor("content");

//实例化编辑器传参,id为将要被替换的容器。
</script>


<input type="submit" name="submit" id="submit" value="提交">

</form>

<?php
include 'config.php';


function add_gbook($con){
$u = @$_POST['username'];
if (isset($u)) {
$c = @$_POST['content'];
$i = @$_SERVER['REMOTE_ADDR'];
$ua = @$_SERVER['HTTP_USER_AGENT'];
$sql = "insert into gbook(`username`, `content`,`ipaddr`,`uagent`) value('$u', '$c','$i','$ua');";
if (mysqli_query($con, $sql)) {
echo "<script>alert('留言成功!')</script>";
}
}
}

function show_gbook($con,$del){
$sql1="select * from gbook";
$data=mysqli_query($con,$sql1);
while ($row=mysqli_fetch_row($data)) {
echo '<hr>';
echo '用户名:'.$row[0].'<br>';
echo '内容:'.$row[1].'<br>';
echo 'IP地址:'.$row[2].'<br>';
echo 'UA浏览器:'.$row[3].'<br>';
if($del=='del'){
echo "<a href='gbook-admin.php?del=$row[0]'>删除</a>";
}
}
}

// 关闭数据库连接
mysqli_close($con);
?>

身份验证(Cookie、Session和Token)

参考:一文讲透Token与Cookie、Session的区别

文件操作

文件上传

超全局变量 $_FILES

$_FILES 是PHP中一个预定义的超全局变量,用于在上传文件时从客户端接收文件,并将其保存到服务器上。它是一个 关联数组,存储了上传文件的相关信息,如文件名、类型、大小、临时存储位置等。

1
2
3
4
5
$_FILES[“表单值”][“name”] 获取上传文件原始名称
$_FILES[“表单值”][“type”] 获取上传文件MIME类型
$_FILES[“表单值”][“size”] 获取上传文件字节单位大小
$_FILES[“表单值”][“tmp_name”] 获取上传的临时副本文件名
$_FILES[“表单值”][“error”] 获取上传时发生的错误代码

文件上传过滤

  1. 无过滤机制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $name=$_FILES['f']['name'];
    $type=$_FILES['f']['type'];
    $size=$_FILES['f']['size'];
    $tmp_name=$_FILES['f']['tmp_name'];
    $error=$_FILES['f']['error'];

    echo $name."<br>";
    echo $type."<br>";
    echo $size."<br>";
    echo $tmp_name."<br>";
    echo $error."<br>";
    if(move_uploaded_file($tmp_name,'upload/'.$name)){
    echo "文件上传成功!";
    }
  2. 黑名单过滤机制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 定义上传文件后缀的黑名单
    $black_ext = array('php','asp','jsp','aspx');

    $fenge = explode('.',$name);
    $exts = end($fenge);
    if(in_array($exts,$black_ext)){
    echo '非法后缀文件'.$exts;
    }else{
    move_uploaded_file($tmp_name,'upload/'.$name);
    echo '<script>alert("上传成功")</script>';
    }
    • explode 函数分割文件名 $name 并返回数组 $fenge
    • end() 函数用于返回数组的最后一个元素,即拓展名
  3. 白名单过滤机制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 定义上传文件后缀的白名单
    $allow_ext = array('png','jpg','gif','jpeg');

    $fenge = explode('.',$name);
    $exts = end($fenge);
    if(!in_array($exts,$allow_ext)){
    echo '非法后缀文件'.$exts;
    }else{
    move_uploaded_file($tmp_name,'upload/'.$name);
    echo '<script>alert("上传成功")</script>';
    }

  4. 文件类型过滤机制

    直接根据数据包的上传文件 MIME 类型 Content-Type 判断

    前面几种方法都是根据文件名称后缀进行判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $allow_type = array('image/png', 'image/jpg', 'image/jpeg', 'image/gif');

    if (!in_array($type, $allow_type)) {
    echo '非法后缀文件';
    } else {
    move_uploaded_file($tmp_name, 'upload/' . $name);
    echo '<script>alert("上传成功")</script>';
    }

文件上传的安全性

  1. 文件上传的存储

    1)上传至服务器本身的存储磁盘,和源码放在一起
    2)通过云产品OSS存储对象存储文件

    • 优点:无脚本执行环境,降低安全风险

    • 缺点:如果前端源码泄露ak/sk,可以利用OSS浏览器、行云管家等工具进行bucket接管

目录浏览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 功能:
// 1.打开目录读取文件列表
// 2.递归循环读取文件列表
// 3.判断是文件还是文件夹


$dir = $_GET['path'] ?: './';

function filelist($dir){
if($dh = opendir($dir)){
while(($file=readdir($dh) )!== false){
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);

  • 通过 $_GET['path'] 获取用户传递的目录路径,如果为空,则默认为当前目录 ./
  • opendir($dir):打开指定目录。
  • readdir($dh):循环读取目录中的文件和子目录。
  • is_dir($file)
    • 如果是目录,显示文件夹图标并提供超链接(?path=$file)。
    • 如果是文件,显示文件图标,但只是普通文本。

显示效果:

image-20250219164443192

文件删除

1
2
3
4
5
6
7
8
9
10
function del($file){
if(!is_dir($file)){
unlink($file);
echo "<script>alert('删除成功')</script>";
}
}
if(isset($_GET['del'])){
del($_GET['del']);
}

文件删除方法:

  • unlink() :删除 $_GET['del'] 传入的文件路径。
  • 调用 system shell_exec exec 命令

文件下载

1
2
3
4
5
6
7
8
9
10
11
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']);
}

  • $_GET['down'] 传入文件路径,使用 readfile() 读取并发送给浏览器下载。

文件编辑

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

文件包含

PHP 文件包含(File Inclusion) 即:允许一个 PHP 文件在执行时引入另一个 PHP 文件的内容,从而重用代码,避免重复编写相同的功能模块。

PHP 提供了四种主要的文件包含方式:

  • include() :如果文件不存在或路径错误,PHP 会发出警告,但脚本会继续执行

  • require() :如果包含的文件不存在或出错,脚本会终止执行

  • include_once() 和 require_once() :作用与 includerequire 类似,但它们确保文件不会被重复包含。适用于 避免重复加载相同文件,尤其是包含函数定义或类定义的文件

    1
    2
    3
    4
    <?php
    include_once 'header.php'; // 只包含一次
    include_once 'header.php'; // 第二次不会再包含
    ?>

模板引用

自定义模板引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
// 引入配置文件
include 'config.php';
// 从文件中读取HTML模板内容
$template=file_get_contents('new.html');

$id=$_GET['id'] ?' ':'1';
$sql="select * from news where id=$id";
$data=mysqli_query($con,$sql);

// 循环获取数据并赋值
while ($row=mysqli_fetch_row($data)) {
$page_title=$row['1'];
$heading=$row['2'];
$subheading=$row['3'];
$content=$row['4'];
$item=$row['5'];
//echo $page_title;
}
echo "$page_title<br>$page_title";

// 替换模板中的占位符
$template=str_replace('{page_title}',$page_title,$template);
$template=str_replace('{heading}',$subheading,$template);
$template=str_replace('{subheading}',$subheading,$template);
$template=str_replace('{content}',$content,$template);
$template=str_replace('{$item}',$item,$template);

// 使用 eval 函数将模板字符串作为PHP代码执行
eval('?>' . $template);
?>
  • file_get_contents 函数读取名为 new.html 的HTML文件,并将文件的内容存储到 $template 变量中。该HTML文件将用作页面的模板,动态替换其中的一些占位符
  • 通过 mysqli_fetch_row 逐行获取查询结果。此处假设每行的字段按照顺序存储在 $row 数组中。
  • 使用 str_replace 函数将模板中的占位符替换为从数据库中获取到的数据。

安全问题

因为后面使用了eval函数将模板字符串作为PHP代码执行,模板字符串基于数据库中提取,如果数据库中的字段存在异常代码,会对其代码执行。

如果在html模板源码中加入<?php phpinfo();?> ,在执行HTML时并不会显示,因为没有以PHP的形式对其执行。

第三方模版引用Smarty

  1. 下载https://github.com/smarty-php/smarty/releases

  2. 使用

    1)创建一个文件夹,命名为smarty-demo
    2)下载Smarty对应版本并解压缩到该文件夹中。
    3)创建一个PHP文件,命名为index.php,并在文件中添加以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?php
    // 引入 Smarty 类文件
    require('smarty-demo/libs/Smarty.class.php');
    // 创建 Smarty 实例
    $smarty = new Smarty;
    // 设置 Smarty 相关属性
    $smarty->template_dir = 'smarty-demo/templates/';
    $smarty->compile_dir = 'smarty-demo/templates_c/';
    $smarty->cache_dir = 'smarty-demo/cache/';
    $smarty->config_dir = 'smarty-demo/configs/';
    // 赋值变量到模板中
    $smarty->assign('title', '欢迎使用 Smarty');
    // 显示模板
    $smarty->display('index.tpl');
    ?>

    4)创建一个名为index.tpl的模板文件,并将以下代码复制到上述点定义文件夹中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!DOCTYPE html>
    <html>
    <head>
    <title>{$title}</title>
    </head>
    <body>
    <h1>{$title}</h1>
    <p>这是一个使用 Smarty 的例子。</p>
    </body>
    </html>
  3. 安全隐患

    CVE-2017-1000480参考:https://blog.csdn.net/qq_33020901/article/details/79150260

TP(Thinkphp)框架

参考:ThinkPHP5.1完全开发手册

基本信息

  1. 未启用路由情况下的URL访问

    在没有启用路由的情况下,TP框架典型的URL访问规则是:

    http://serverName/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值…]

    例如:在application/index/controller下新建cc.php

    访问url:localhost/thinkphp5.1/public/index.php/index/cc/sayhi

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php
    namespace app\index\controller;
    use think\Controller;
    use think\Db;
    class cc extends Controller
    {
    public function sayhi()
    {
    return 'hi';
    }
    }
    ?>

数据库操作

使用TP框架操作数据库,默认受到框架内置过滤保护

  • 规矩写法:使用 ThinkPHP 提供的 查询构造器(Query Builder)

    • 相对安全,如果TP版本存在漏洞可能被绕过
  • 部分安全写法

    • 这里使用了 Db::query() 执行 字符串拼接的 SQL 语句
    • 如果 $id 来自用户输入(未过滤),攻击者可以通过 id=1 OR 1=1 绕过限制,导致 SQL 注入。
  • 不安全写法(原生写法):完全没有使用TP语法

    • SQL 注入漏洞:攻击者可以构造恶意 SQL 代码(同上 id=1 OR 1=1)。

      不使用 ThinkPHP 的数据库安全机制,直接操作 mysqli_query(),必须手动处理 SQL 安全(如 mysqli_real_escape_string())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function testsql()
{

//1. 规矩写法
$id=request()->param('x');
$data=Db::table('news')->where('id',$id)->find();

//2. 部分安全写法
$id=request()->param('x');
$data=Db::query("select * from news where id=$id");

//3. 纯原生写法
$id=$_GET['id'] ?? '1';
$sql="select * from news where id=$id";
$data=mysqli_query($con,$sql);
while ($row=mysqli_fetch_row($data)) {
$username = request()->get('username/a');
db('admin')->where("id")->update(['username' => $username]);
//return json($data);
}

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function upload(){
// 获取表单上传文件,例如上传了001.jpg
$file = request()->file('image');

// 移动到框架应用根目录/uploads/ 目录下
$info = $file->validate(['ext'=>'jpg,png,gif'])->move('../uploads');

if($info){
// 成功上传后,获取上传信息

// 输出文件扩展名,例如 jpg
echo $info->getExtension();
echo '<br>';

// 输出文件保存路径,例如 20160820/42a79759f284b767dfcb2a0197904287.jpg
echo $info->getSaveName();
echo '<br>';

// 输出文件名,例如 42a79759f284b767dfcb2a0197904287.jpg
echo $info->getFilename();
} else {
// 上传失败,获取错误信息
echo $file->getError();
}
}

框架版本安全

确定目标所使用的Thinkphp版本

  • 看报错页面
  • 看THINK_VERSION全局变量
  • 看url地址构造

然后通过版本找历史漏洞