前言
参考:
https://mp.weixin.qq.com/s/y9d04tnRaKHQBfZubj1cKg
审计思路
敏感函数查找
危险函数:
eval
、system
SQL注入:
select
,from
XFF注入:
HTTP_CLIENT_IP
、REMOTE_ADDR
通读全文代码
函数集文件:
function
、common
等关键词,一般是公共函数统一调用配置文件:
config
等关键词,了解功能配置与数据库配置;观察参数值,如果存在双引号可能存在代码执行安全过滤文件:
filter
、safe
、check
等关键词,wafindex文件:入口文件,可知 CMS 的架构,运行流程、包含到的文件、核心文件有哪些等
根据功能点定向审计
- 文件上传:任意文件上传、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("指定的路径不是一个有效的目录。")
然后进行爆破,按返回相应的大小确定前台文件
框架
先关注鉴权部分
模块化划分
以 OneBase 为例,项目结构大致如下
挑出主要的结构,和 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()
审计思路
黑盒 + 白盒