目录

  1. 1. 前言
  2. 2. 审计思路
  3. 3. 系统分类
  4. 4. 审计方法
  5. 5. 前台漏洞
    1. 5.1. 寻找文件
      1. 5.1.1. 非框架
      2. 5.1.2. 框架
        1. 5.1.2.1. 模块化划分
        2. 5.1.2.2. 路由配置区分
        3. 5.1.2.3. 控制器命名规范
        4. 5.1.2.4. 中间件拦截(ThinkPHP6+)
        5. 5.1.2.5. 权限标识注解
        6. 5.1.2.6. 前端模板分离
        7. 5.1.2.7. 权限系统设计
    2. 5.2. 鉴权绕过
  6. 6. SQL注入
    1. 6.1. PHP原生SQL语句处理
    2. 6.2. 框架审计
  7. 7. 文件操作
    1. 7.1. 文件上传
    2. 7.2. 文件写入
    3. 7.3. 文件读取/下载
    4. 7.4. 文件删除
    5. 7.5. 文件包含
    6. 7.6. 审计思路

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

PHP代码审计

2025/4/17 Web PHP 代码审计
  |     |   总文章阅读量:

前言

参考:

https://mp.weixin.qq.com/s/y9d04tnRaKHQBfZubj1cKg


审计思路

  1. 敏感函数查找

    • 危险函数:evalsystem

    • SQL注入:selectfrom

    • XFF注入:HTTP_CLIENT_IPREMOTE_ADDR

  2. 通读全文代码

    • 函数集文件:functioncommon等关键词,一般是公共函数统一调用

    • 配置文件:config 等关键词,了解功能配置与数据库配置;观察参数值,如果存在双引号可能存在代码执行

    • 安全过滤文件:filtersafecheck 等关键词,waf

    • index文件:入口文件,可知 CMS 的架构,运行流程、包含到的文件、核心文件有哪些等

  3. 根据功能点定向审计

    • 文件上传:任意文件上传、sql注入(文件名可能记录到数据库)
    • 文件管理
    • 登入认证
    • 找回密码:重置管理员密码、验证码爆破

系统分类

原生非开发框架:WordPress、phpMyAdmin

  • 无额外抽象层
  • 传统 PHP 项目的路由访问一般是直接访问对应文件
  • 容易出洞
  • GitHub 上居多

框架二开CMS:ThinkPHP、Laravel、Yii

  • MVC 等标准化结构,多数不支持直接访问
  • 内置功能:路由、ORM、模板引擎等
  • 安全性:内置CSRF防护,XSS过滤等
  • 社区支持:文档
  • Gitee 上居多

自研框架系统


审计方法

黑白盒结合

黑盒:XSS、CSRF、越权、文件操作

白盒:SQL注入、SSRF、鉴权、XXE


前台漏洞

  • 无需授权即可访问
  • 后台文件/功能点,但存在鉴权但可绕过

寻找文件

非框架

寻找鉴权文件,关注哪些文件有包含鉴权文件

因为非框架类的路由通常是直接访问文件,所以可以考虑直接把所有 php 文件的文件名作为字典,丢 bp 或者 yakit 里面爆破看返回包

以 Seacms 为例

起一个脚本遍历目录并输出字典

import os

def find_php_files(directory):
    with open("dict.txt", "a", encoding='utf-8') as f:
        # 遍历目录及其子目录
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith('.php'):
                    abs_path = os.path.join(root, file)
                    rel_path = os.path.relpath(abs_path, directory)  # 计算相对路径
                    rel_path = rel_path.replace("\\", "/")  # 统一路径分隔符为/
                    f.write(f"{rel_path}\n")  # 每个路径单独占一行
    
if __name__ == "__main__":
    # 指定要遍历的目录(去掉首尾空白字符)
    target_directory = "D:\\phpstudy_pro\\WWW\\seacms.v13".strip()
    
    if os.path.isdir(target_directory):
        find_php_files(target_directory)
    else:
        print("指定的路径不是一个有效的目录。")

然后进行爆破,按返回相应的大小确定前台文件

image-20250425094907203


框架

先关注鉴权部分

模块化划分

以 OneBase 为例,项目结构大致如下

image-20250426005212507

挑出主要的结构,和 thinkphp 差不多

app(lication)/
├─ admin/ # 后台模块
|	├─ controller/
|	├─ view/
|
├─ index/ # 前台模块
|	├─ controller/
|	├─ view/
|
├─ common.php # 公共函数

那么前台访问路径就是 index/controller/action

后台访问路径就是 admin/controller/action


路由配置区分

路由文件 route.php,vscode 下 ctrl+P 搜索此文件

<?php

return [
    '__pattern__' => [
        'name' => '\w+',
    ],
    '[hello]'     => [
        ':id'   => ['index/hello', ['method' => 'get'], ['id' => '\d+']],
        ':name' => ['index/hello', ['method' => 'post']],
    ],

];

一般此处路由前缀标识可以区分


控制器命名规范

前台控制器:

namespace app\home\controller;

class IndexController {
    public function index() {
        return '前台首页';
    }
}

后台控制器:

通常会有在 action 中存在一些 Auth 认证相关的方法,大致如下:

// 后台用户管理控制器
namespace app\admin\controller;

class UserController {
    public function list() {
        return '用户列表';
    }
}

中间件拦截(ThinkPHP6+)

大致位置会在 app/admin/middleware 下

通过后台控制器绑定中间件,使所有方法自动继承中间件验证

namespace app\admin\middleware;
use think\facade\Session;
use think\facade\View;

class AuthCheck {
    public function handle($request, \Closure $next) {
        // 检查是否登录(排除登录页面)
        if (!Session::has('admin') && !preg_match('/login/', $request->pathinfo())) {
            return redirect('/admin/login');
        }
        return $next($request);
    }
}

权限标识注解

自定义注解检查:@Permission("admin:access")


前端模板分离

视图目录结构


权限系统设计

RBAC 权限节点


鉴权绕过

关注身份认证的逻辑是否存在缺陷

身份认证的凭据是否能够伪造


SQL注入

PHP原生SQL语句处理

mysql_query

mysqli

PDO


框架审计

永远优先使用查询构造器而不是原生 SQL

关注是否使用参数绑定处理用户输入


框架二开但是原生写法

框架的非预编译写法:

  • 直接拼接 sql 字符串:

    $username = input('username');
    Db:query("SELECT * FROM user WHERE username = '".$username."'")
  • 使用 where() 时直接拼接

    $map = 'id = '.input('id');
    Db::name('user')->where($map)->select();

    正确写法:

    // 参数绑定
    Db::name('user')->where('id = ?', [input('id')])->select();
    
    // 强制类型转换
    $id = (int) input('id');
    Db::name('user')->where('id', $id)->select();
    
    // 数组条件,ThinkPHP会自动处理参数
    Db::name('user')->where(['id' => input('id')])->select();
    
    // 框架的输入过滤
    $id = input('id/d');  // 强制转为整数
    Db::name('user')->where('id', $id)->select();

文件操作

文件上传

  • 未校验文件类型
  • 黑名单过滤不严
  • 解析漏洞利用

文件写入

  • 未过滤用户输入
  • 目录穿越

文件读取/下载

危险函数列表:

include()
require()
file_get_contents()
readfile()
fopen()
file()
show_source()
highlight_file()

文件删除

危险函数列表:

unlink()
rmdir()
array_map('unlink',glob())
system("rm -rf $path")
Filesystem::deleteDirectory
remove_dir
Filesystem::delete

文件包含

危险函数列表:

include()
include_once()
require()
require_once()
    
// 视图渲染方法
assign()
fetch()
display()
view()

// 类加载方法
Loader::import()

// 配置文件加载
config()

审计思路

黑盒 + 白盒