这篇笔记讲什么
代码审计(Code Audit)就是拿到一套网站的源码,逐行读代码找漏洞。黑盒测试是”在门外瞎敲门试锁”,代码审计是”拿到大楼的建筑图纸找哪扇窗没装防盗网”——效率完全不是一个量级。
这篇笔记不讲具体某个漏洞怎么挖(那是后面各专项笔记的事),而是讲方法论:拿到源码后第一步干什么、第二步干什么、往哪个方向看最出活。先建立流程,再往里填技术细节。
先修知识
阅读本篇前,这些术语需要先混个脸熟(每个一句话):
- 漏洞(Vulnerability):代码里能被攻击者利用的缺陷,比如”把用户输入直接拼进 SQL”。
- 源码(Source Code):网站程序员写的原始代码文件(
.php、.java、.py),区别于黑盒只能看到的网页。 - CMS:Content Management System,内容管理系统,就是”现成的建站程序”,比如 WordPress、织梦。审计练习常拿开源 CMS 开刀。
- PoC(Proof of Concept):漏洞的”演示视频”——一段能证明漏洞真实存在的请求或代码,不是完整武器。
- RCE(Remote Code Execution):远程代码执行,最高危的漏洞——攻击者能在你服务器上跑任意命令,等于拿到服务器钥匙。
- Webshell:上传到服务器上的一个恶意脚本文件(比如一个
.php),访问它就能执行命令,相当于在对方电脑上留了个遥控后门。 - Source / Sink:审计的核心概念。Source 是”脏水进水口”——用户可控的数据从哪进来(GET 参数、POST 表单、Cookie);Sink 是”危险出水口”——数据最终流入哪个危险函数(
eval、SQL 执行)。审计就是沿着水管看:脏水从进水口到出水口之间,有没有被净化(过滤)。 - 数据流(Data Flow):一个变量从”用户输入”到”危险函数”之间经过的完整路径,中间可能经过赋值、拼接、过滤。
- 预编译 / 参数化查询:防 SQL 注入的标准手段。类比”先印好表格模板,再把用户填的内容贴到固定格子里”——用户填的内容永远只能是”数据”,不会变成表格结构(SQL 语法)的一部分。
- MVC:一种代码组织方式,把代码分三层——Model(管数据/数据库)、View(管页面)、Controller(管收请求、调度)。类比餐厅:Controller 是服务员接单,Model 是后厨做菜,View 是摆盘上桌。
- 路由(Route):URL 和代码函数的对应表。类比写字楼的楼层索引牌:
/user/login这个地址对应到哪个类的哪个方法处理,路由说了算。 - 过滤器 / 拦截器(Filter / Interceptor):请求到达业务代码之前统一经过的关卡,常用来做登录校验。类比小区门口的保安亭:不管你去几号楼,先过保安这关。
- 鉴权 / 认证:认证(Authentication)是”证明你是谁”(登录);鉴权/授权(Authorization)是”确认你能干什么”(权限)。越权漏洞就是认证过了但授权没查。
- 越权:水平越权 = 你登录后看到了别人的订单(同级别平移);垂直越权 = 普通用户用上了管理员的功能(向上爬)。类比酒店:认证是发房卡,水平越权是拿自己房卡开了隔壁同房型的门,垂直越权是住客刷卡进了机房。
- IDOR:不安全的直接对象引用,水平越权的技术学名——URL 里
?orderId=1001改成1002就能看到别人的订单,因为后端只查了”登录没”,没查”这单是不是你的”。 - 白名单 / 黑名单:两种校验思路。白名单 = “只放行名单上的人”,安全;黑名单 = “记住坏人长相拦着”,永远记不全,总有漏网的坏人。审计看到黑名单过滤要本能地兴奋。
- POP 链:PHP 反序列化利用术语,把多个类里的魔术方法像多米诺骨牌一样串起来,第一张牌推倒最后一张牌执行命令。
- 伪协议:PHP 的
php://、file://、phar://这类特殊开头的”假文件名”,可以让文件操作函数干出读源码、写木马之类的骚操作。
审计整体流程
拿到一套源码后的标准动作链:环境准备 → 结构通读 → 选审计法 → 数据流追踪 → 漏洞验证。
先记一个总原则:审计不是从头读到尾,而是”带着问题找答案”。就像查监控找小偷不是从月初看到月末,而是先看案发时间段的录像。
第一步:环境准备
为什么要把环境跑起来?因为能跑起来的系统会给你白送信息:路由列表(访问一下就知道有没有这个接口)、报错信息(故意输错参数看报错,直接告诉你框架和路径)、调试断点(让代码停在关键行看变量值)。干读代码是”看地图”,跑起来是”实地踩点”,两个结合才快。
- 源码完整性:一定要拿到完整源码,包括配置文件和 SQL 建表脚本。缺了 SQL 文件,很多业务逻辑和数据表结构只能靠猜——比如你不知道
user表里到底有没有role字段,越权漏洞的判断就没依据。 - 可运行环境:尽量把系统本地装起来。PHP 用 phpstudy(一键集成 Apache+PHP+MySQL),Java 用 IDEA + 本地起 Tomcat 或直接跑 SpringBoot。
- IDE 就位:PHP 用 VSCode/PhpStorm,Java 用 IDEA。必须会两个快捷键:全局搜索(IDEA 里是
Ctrl+Shift+F,全项目搜关键字)、追踪调用链(Ctrl+Alt+H,看一个函数被谁调用、它内部又调用了谁)。命令行 grep 用于批量初筛,比如在 Linux 下:
# 在整个项目里找所有调用 eval 的 PHP 文件,-rn 表示递归+显示行号
grep -rn "eval(" --include="*.php" .第二步:通读目录结构与配置
这一步的目标是建立全局地图,就像进商场先看楼层导览图,而不是直接扎进第一家店。花半小时通读,能省后面几个小时的瞎撞。
- 目录结构:认清楚哪个目录是入口、哪个是 controller/model、第三方组件放在哪(PHP 的
vendor、Java 的WEB-INF/lib)。 - 路由配置:MVC 项目先找路由表——Spring 看注解(
@RequestMapping/@GetMapping)、ThinkPHP 看 route 目录、Flask 看@app.route装饰器。找到了路由表,你就有了全站接口清单,攻击面一目了然。 - 配置文件:数据库连接配置(确认用了什么框架和驱动版本)、过滤器/拦截器注册在哪(
web.xml、Spring 的WebMvcConfigurer、Flask 的中间件)、有没有开鉴权拦截——很多系统的过滤器配了但没拦全,这就是洞。 - 三方组件版本:看
composer.json(PHP)、pom.xml(Java)、package.json(Node)、requirements.txt(Python)。组件版本过旧可能直接带着公开的历史漏洞,比如 fastjson 1.2.24 的反序列化 RCE——版本号一对上,漏洞就是白捡的。
三种审计法
读代码有三种基本姿势,没有绝对优劣,看项目大小和时间预算选:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 通读全文法 | 代码量小(个人站、小程序)、对系统不熟需要整体学习 | 理解系统全貌,能发现逻辑漏洞、隐藏功能、隐藏后门 | 最慢,大项目不可行 |
| 敏感函数回溯法(推荐) | 中大型项目快速出洞 | 效率最高,直奔高危点 | 只能发现已知的、有明显 Sink 的漏洞类型;漏掉逻辑漏洞 |
| 定向功能审计法 | 时间有限、需要按功能点逐个击破 | 深钻一个模块,能挖到业务逻辑漏洞 | 视野窄,模块间交互的风险看不到 |
新手建议:先精通敏感函数回溯法(出洞最快,建立正反馈),再补通读全文法(理解系统全貌),最后按需用定向功能审计法。
通读全文法
是什么:从入口文件开始,按一次请求的完整生命周期顺序读代码:index.php / Application.java → 路由分发 → 过滤器/拦截器 → 控制器 → 业务逻辑 → 数据层。
类比:像坐飞机时完整走一遍流程——值机(入口)→ 安检(过滤器)→ 登机口(路由)→ 座位(控制器)。每个环节你都要问一句:“这里查证件了吗?”
怎么读:每读一个文件,在笔记上记三条:
- 这个文件对外提供什么接口?
- 它校验权限了吗?(没校验的就是可疑点)
- 它把哪些变量带进了危险操作(SQL、文件读写、命令执行)?
敏感函数回溯法(Sink → Source)
是什么:反过来读——先全局搜索危险函数(Sink),再往回追溯这个函数的参数是不是来自用户输入(Source)。这是最出洞的方法,务必练熟。
为什么有效:漏洞的本质几乎都是”用户可控的数据没经过滤就进了危险函数”。危险函数的数量是有限的(就那几十个),把每个危险函数的参数来源查清楚,等于把全站的高危点排查了一遍。
典型 Sink 对照表(背下来):
| 漏洞类型 | 典型 Sink(PHP / Java 各举一例) |
|---|---|
| 命令执行 | system / Runtime.exec |
| 代码执行 | eval / ScriptEngine.eval |
| SQL 注入 | mysqli_query / createStatement、MyBatis 的 ${} |
| 文件包含/读取 | include / new FileInputStream |
| 反序列化 | unserialize / ObjectInputStream.readObject |
具体操作(以 PHP 找命令执行为例):
# ① 全局搜危险函数
grep -rn "system(" --include="*.php" .
# ② 命中一行:system("ping " . $ip);
# ③ 打开该文件,找 $ip 哪来的——是写死的,还是 $_GET['ip'] 来的?
# ④ 如果是用户输入,再找中间有没有过滤函数;有过滤就审计过滤能不能绕第一轮粗筛的搜索清单(新手上手直接抄这张表,在 IDE 全局搜索里逐个过):
| 搜什么 | 语言 | 可能挖出什么 |
|---|---|---|
$_GET / $_POST / $_REQUEST / $_COOKIE | PHP | 所有 Source 入口,先看哪些参数被收进来了 |
eval( / assert( / system( / exec( / shell_exec( / popen( | PHP | 代码执行、命令执行 |
unserialize( / include / require / file_get_contents | PHP | 反序列化、文件包含、任意文件读 |
Runtime.getRuntime().exec / ProcessBuilder | Java | 命令执行 |
readObject / XMLDecoder / parseObject | Java | 反序列化(fastjson 的 parseObject 尤其高危) |
${ (只在 .xml 文件里搜) | Java/MyBatis | SQL 拼接——#{} 是参数化安全的,${} 是直接拼接 |
render_template_string / pickle.loads / yaml.load( / subprocess | Python | SSTI、反序列化、命令执行 |
exec( / eval( / child_process | Node.js | 代码执行、命令执行 |
搜完别急着高兴——命中 Sink 只是嫌疑,Source 可控才算实锤。每个命中点都要回答三个问题:参数从哪来?中间过了哪些函数?过滤能不能绕?
Warning
回溯中途遇到自定义过滤函数不要直接放弃——停下来审计这个过滤函数本身的实现。经验上 90% 的自写过滤函数是不完整的:黑名单不全(只拦了
cat没拦tac)、只替换一次(str_replace把;删掉一个,写两个;;就剩一个)、宽字节编码问题(吃掉转义符)。看到str_replace、preg_replace黑名单、自写checkXxx函数,都要蹲下来细看。
定向功能审计法
是什么:不做全量扫描,挑高风险功能模块,一个模块一个模块地深钻整条链。
为什么有这个玩法:有些漏洞没有明显的危险函数(比如”找回密码的验证码不过期”、“订单金额可以改成负数”),敏感函数回溯法扫不出来,只能盯着功能逻辑看。
模块优先级(按历史出洞率排序):文件上传 > 登录/注册/找回密码 > 支付/订单 > 文件管理(删除/下载/重命名)> 数据导出 > 后台接口。
操作示例:专门审计上传功能时,从上传点进来到落盘,整条链走一遍——文件落盘路径谁定的?文件名有没有被用户控制?类型校验是在前端还是后端、白名单还是黑名单?上传后能不能被直接访问执行?每一环都是潜在漏洞点。
白盒 vs 黑盒视角差异
初学者容易混淆:渗透测试(黑盒)和代码审计(白盒)到底啥关系?一句话:同一个漏洞,黑盒靠猜,白盒靠看。
| 维度 | 黑盒(测试者视角) | 白盒(代码视角) |
|---|---|---|
| 攻击面发现 | 靠目录爆破、爬虫、JS 提取 API | 直接读路由,一个不漏 |
| 漏洞判断 | 盲测,靠回显/报错/时间盲注 | 直接看拼接代码,确定性判断 |
| 过滤情况 | 看不到,只能盲试绕过 | 过滤代码可见,可针对性绕过 |
| 信息泄露 | 挖到 .git、.bak 才有源码 | 源码在手,.env、密钥、历史漏洞一览无余 |
| 适用 | 无源码、快速打点 | 有源码、深度挖掘、合规审计 |
完整 CMS 审计思路(从安装包到出洞)——这是新人第一个完整练手项目该走的流程:
- 下载安装包,本地搭环境跑起来
- 观察安装流程:安装过程往往暴露数据库配置、后台路径、版本信息(装的时候别一路点”下一步”,截图记录每一步)
- 从后台登录后抓一遍所有功能点的请求(Burp 挂着点一遍),建立接口清单
- 回代码侧,从路由映射到 Controller,通读目录结构
- 敏感函数回溯法扫一遍全项目,标记可疑点
- 对标记点逐个做数据流闭环验证,把”拼接 + 可控”点按可利用性排序
- 高危点用黑盒方式构造请求验证一次(拿到实际 PoC 再写报告——代码里看着像漏洞和实际能打是两回事,中间可能隔着 php.ini 配置、WAF、框架默认过滤)
- 不出洞就转入定向功能审计:文件上传、任意文件读取、SQL 注入(搜索/排序场景)、登录逻辑
MVC vs 非 MVC 审计流程差异
| 对比项 | MVC 项目 | 非 MVC 项目(平铺 PHP 等) |
|---|---|---|
| 审计起点 | 路由:先找全部路由到 Controller 的映射 | 入口文件:从 index.php、各功能页面文件直接读 |
| 分层定位 | Controller 接收参数 → Service 处理逻辑 → Model/DAO 拼 SQL | 一个文件内混着 HTML、业务逻辑、SQL |
| 权限控制位置 | 拦截器(Interceptor)、过滤器(Filter)、注解(@PreAuthorize) | 每个文件顶部是否 include 了权限校验文件 |
| 关键风险点 | 分层传参中变量丢失上下文,Model 层忘了参数化 | 大量重复代码,可能有的页面加了过滤有的没加 |
| 审计策略 | 路由全覆盖 → Controller 逐个回溯到 DAO | 逐文件通读,重点关注没做过滤的页面 |
为什么非 MVC 项目更容易漏鉴权:权限校验靠每个文件开头手动 include 'check.php',一百个文件有一个忘了 include,这个文件就是未授权访问。类比:MVC 是大楼统一门禁,非 MVC 是每个房间自己挂锁——挂漏一个就完。
漏洞类型优先级
审计时间永远不够,所以按危害程度分配注意力,高危优先出洞:
RCE(命令/代码执行)> SQL 注入 > 反序列化 > 文件类(任意文件读写/上传)> SSRF/XXE > XSS > 业务逻辑
| 优先级 | 漏洞类型 | 为什么是这个位置 |
|---|---|---|
| 1 | RCE | 直接拿到服务器权限,危害无上限,一个洞结束战斗 |
| 2 | SQL 注入 | 可拖库(全站用户数据泄露),可配合 --os-shell 拿权限,覆盖面最广、存量最多 |
| 3 | 反序列化 | 在 PHP/Java 里基本等于 RCE,出洞率高 |
| 4 | 文件类 | 任意文件读(拿配置/源码/密钥)、任意文件写(写 webshell)、文件上传(直接 getshell) |
| 5 | SSRF / XXE | 打内网、读文件、探测服务,常作为组合拳的一环 |
| 6 | XSS | 需要配合钓鱼诱导管理员点击才能提权,单独危害有限 |
| 7 | 业务逻辑 | 越权、支付逻辑、并发——出洞率高但危害依业务而定,众测里很值钱 |
各语言审计侧重
不同语言的”常见病”不一样,审计关注点也不同:
| 语言 | 重点漏洞类型 | 典型框架 | 独有审计点 |
|---|---|---|---|
| PHP | SQL 注入、文件包含、反序列化(POP 链)、文件上传 | ThinkPHP / Laravel | 宽字节注入、extract 变量覆盖、$$ 可变变量、unserialize POP 链 |
| Java | 反序列化(readObject)、SpEL/OGNL 表达式注入、JNDI、SQL 注入 | Spring / Struts2 / Shiro | 反射调用链、MyBatis ${} 拼接、Filter 链 URI 解析差异绕过鉴权 |
| Python | SSTI(Jinja2)、pickle 反序列化、SSRF、格式化字符串 | Flask / Django / FastAPI | pickle.loads、模板引擎沙箱逃逸、__globals__ 链 |
| JavaScript/Node | 原型链污染、SSRF、命令注入、Mass Assignment | Express / Koa | __proto__ 污染、动态 require、eval/Function 调用 |
| Go | 命令注入、路径穿越、SSRF、goroutine 竞争 | Gin / Echo | exec.Command 参数拼接、filepath.Join 未校验、unsafe 包 |
各语言审计流程与切入点
三种主流语言各自的标准动作链与框架切入点,拿到对应项目时对号入座。
PHP
环境与工具:
- PHPStudy/WampServer 一键搭环境。注意 PHP 5.x / 7.x / 8.x 行为差异很大,版本必须与目标对齐——比如
strcmp(数组)在 PHP 7 返回NULL能绕过校验,在 PHP 8 直接抛TypeError异常,用错版本你复现不出漏洞会怀疑人生。 - Seay 源代码审计系统:Windows 下的老牌 PHP 审计工具,原理是关键字匹配粗筛危险函数。它能帮你快速定位 Sink,但只做匹配不做数据流分析,定位 Sink 后必须人工回溯 Source,别指望它直接报漏洞。
- PHPStorm + Xdebug:下断点动态验证,代码停在危险函数那一行时,你能直接看到变量的真实值,判断过滤有没有生效。
入口文件与路由分析:
| 类型 | 切入点 | 高频问题 |
|---|---|---|
| 原生 PHP | 每个 .php 文件即入口;配合 .htaccess/nginx rewrite 确定真实路由;重点看 inc/、include/ 公共文件里的过滤与鉴权函数是否被所有页面 include | 鉴权只 header() 跳转不 die()、过滤函数覆盖不全、LFI |
| ThinkPHP | 路由看 route.php/注解;公共基类 BaseController 的鉴权与 I() 取参过滤;3.2.3 where 注入、5.0/5.1 系列 Request 类 RCE(注意补丁版本分界) | 历史 RCE、I('get.') 数组注入 |
| Laravel | 路由 routes/web.php;APP_DEBUG=true(Ignition RCE)、Model::create($request->all()) 未设 $fillable 的批量赋值、{!! !!} 不转义、APP_KEY 泄露 | 调试模式 RCE、Mass Assignment 越权 |
| WordPress | 主程序较稳,重心在插件/主题:wp_ajax_nopriv_* 未登录可达入口、nonce/current_user_can() 校验、$wpdb->prepare()、esc_* 转义 | 插件 SQL 注入、未授权 AJAX、CSRF |
案例带读:原生 PHP 的”假鉴权”
这是原生 PHP 项目里最高频的鉴权失效模式,逐行看:
<?php
// admin/delete_user.php —— 后台删除用户页面
session_start();
if (!isset($_SESSION['admin'])) { // 检查 session 里有没有管理员标记
header('Location: login.php'); // ❌ 没有就跳转登录页——但只是"告诉浏览器去登录页"
}
// ⚠️ 上面没有 die()/exit,代码会继续往下执行!
$id = $_GET['id']; // 直接接收用户传入的 id
mysqli_query($conn, "DELETE FROM user WHERE id=$id"); // 执行删除数据流分析(按编号走一遍):
- 攻击者直接请求
admin/delete_user.php?id=1,根本没登录,$_SESSION['admin']不存在 - 代码进入 if 分支,调用
header('Location: login.php')——但这个函数只是往 HTTP 响应头里写一行”请跳转”,它不终止程序 - PHP 继续往下执行,
$id被取到,DELETE 语句照样执行 - 浏览器收到响应后确实跳去了登录页,但数据库里的用户已经被删了——攻击者用 Burp 发请求根本不看跳转
为什么防线失效:开发者以为 header() 跳转 = 把人拦在门外,实际上它只是”递了一张写着’请去登录’的纸条”,门本身没锁。正确写法是跳转后立刻 die() 或 exit:
if (!isset($_SESSION['admin'])) {
header('Location: login.php');
die(); // ✅ 拦下之后必须终止后续代码
}审计时怎么找:全局搜 header('Location,逐个检查跳转后有没有 die/exit。看到没有的基本就是未授权访问漏洞。
特有关注点(PHP 的”地方病”,每个都值得单独学):
- 弱类型松散比较:
==会做隐式类型转换('1abc' == 1为真);strcmp传数组在 PHP<8 返回NULL,NULL == 0为真;in_array不传第三个参数true就不做严格比较;两个 MD5 以0e开头后跟全数字的字符串,==比较时被当成科学计数法的 0 而相等(Magic Hash)。 - 变量覆盖:
extract($_GET)会把用户传的键值对直接注册成变量,$$可变变量同理——用户传个_SESSION[admin]=1可能就把自己变成管理员。 - 伪协议:
php://filter可以 base64 读出 PHP 源码(防止被执行)、php://input配合文件包含写入 webshell、phar://可以触发反序列化。 - GPC 机制:PHP<5.4 的 magic_quotes_gpc 会自动给 GET/POST/Cookie 里的单引号加转义符。绕点在于:来自数据库、文件、
$_SERVER的数据不受它保护(→ 二次注入),宽字节编码可以吃掉它加的转义符(→ 宽字节注入)。
Java
标准动作链(四步走):
- 依赖识别:先把
pom.xml/build.gradle过一遍——fastjson、Jackson、Shiro、Struts2、log4j2 这些组件的版本号直接决定能不能打已知 CVE。但注意:版本只是嫌疑,不是结论,必须确认代码里真的用了危险特性(比如 fastjson 版本老旧,但代码里根本没parseObject外部输入,那就打不了)。 - 路由梳理:传统 Servlet 项目看
web.xml的<servlet-mapping>;Spring 项目全局搜@RequestMapping/@GetMapping/@RestController;Struts2 看struts.xml。产出一张「URL → 入口方法 → 是否需登录」的清单,这是后续所有工作的底表。 - 权限链梳理:Filter 的声明顺序即执行顺序(SpringBoot 看
@Order);HandlerInterceptor在 Filter 之后、Controller 之前执行;鉴权的url-pattern是否覆盖了全部敏感路由。 - Sink 回溯:对每个危险 Sink(
Runtime.exec、readObject、createStatement、${}等)反推 Source 是否可控、过滤是否可绕过。
案例带读:Java 拦截器的”假拦截”
HandlerInterceptor(处理器拦截器)就是前面说的”保安亭”,请求进 Controller 之前先过它。它有一个经典翻车点:
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("user"); // 从 session 取登录用户
if (user == null) { // 没登录
response.sendRedirect("/login"); // ❌ 让浏览器跳去登录页
// ⚠️ 这里没有 return false!
}
return true; // ❌ 返回 true = 放行,请求继续进 Controller
}
}数据流分析:
- 攻击者未登录,直接请求
/admin/userList - 拦截器
preHandle执行,发现没登录,调了sendRedirect——和 PHP 的header()一样,这只是写了个响应头,不终止流程 - 方法走到末尾
return true,Spring 认为”拦截器放行了”,请求被送进AdminController.userList() - 管理员接口在未登录状态下被执行
为什么防线失效:preHandle 的返回值就是”放行/拦截”的开关——return true 放行、return false 拦截。开发者调了跳转却忘了 return false,等于保安把访客拦下登记完,挥挥手说”进去吧”。正确写法:sendRedirect 后立即 return false。审计时全局搜 preHandle,逐个检查每个拦截分支的返回值。
框架切入点:
| 框架 | 审计切入点 | 高频问题 |
|---|---|---|
| Spring MVC / SpringBoot | 注解路由、HandlerInterceptor、Actuator 暴露端点(/env、/heapdump) | Actuator 未鉴权、SpEL 注入、参数绑定覆盖(Spring4Shell)、redirect: 可控跳转 |
| Struts2 | struts.xml action 配置、struts.devMode 常量 | OGNL 表达式注入(S2-045 等)、devMode 调试接口 |
| Shiro | filterChainDefinitionMap 的 URL 匹配规则、rememberMe 配置 | URL 匹配与应用路径归一化差异绕鉴权(..;/、%2e)、rememberMe AES 硬编码密钥 |
| Filter 链通用 | web.xml filter-mapping 顺序、路径归一化 | getServletPath 与 getRequestURI 解析差异、大小写/分号/双斜杠绕过鉴权 |
“路径归一化差异”是什么(Shiro 和 Filter 链那两行都在说它):鉴权组件和 Web 容器对同一个 URL 的”理解”可能不一样。比如鉴权规则是”/admin/** 需要管理员”,攻击者请求 /xxx/..;/admin/user——Shiro 看到的路径不含 /admin/ 就放行了,但 Tomcat 把 ..; 归一化后实际进的是 /admin/user。两边对路径的解释不一致,鉴权就被架空了。审计思路:对比鉴权组件拿路径的方法(getRequestURI)和容器实际分发用的方法(getServletPath),找解析差异。
Python
标准动作链:
- 先读
requirements.txt/pyproject.toml确定框架与组件版本(依赖版本决定哪些漏洞天然存在) - 梳理路由:Flask 搜
@app.route(、app.add_url_rule(、Blueprint 注册;Django 读urls.py的urlpatterns(注意include()的层层分发和re_path的正则写法) - 以
request.args/request.form/request.json等为 Source,按 Sink 表全局 grep 后做数据流回溯 - 最后看配置面:
DEBUG、SECRET_KEY、模板与中间件配置
特有关注点:
| 关注点 | 审计要点 |
|---|---|
| SSTI | 模板本体是否拼接了用户输入——render_template_string("Hello " + name) 危险,render_template("hello.html", name=name) 安全。区别就在于用户输入进的是”模板”还是”模板的数据”。利用链 __class__→__mro__→__subclasses__()→__init__.__globals__ |
| pickle 反序列化 | 搜 pickle.loads/load、yaml.load(未用 SafeLoader);__reduce__ 协议等于任意函数调用;数据来源不止请求参数——Cookie、session 后端、缓存、消息队列里的数据都可能进 pickle |
| Flask debug PIN | app.run(debug=True) 会开启 Werkzeug 交互式调试台(网页版 Python 控制台);控制台有 PIN 码保护,但 PIN 由用户名/模块名/MAC 地址/machine-id 等六个要素哈希派生——如果同时存在任意文件读取漏洞,把这几样读出来就能本地算出 PIN,进控制台 = RCE |
| session 密钥 | Flask 默认 session 存在客户端 Cookie 里,内容经 TaggedJSON 序列化(JSON 风格的带类型标记格式)后 base64,再仅靠 SECRET_KEY 做 HMAC 签名防伪。密钥一旦泄露或被弱口令爆破(工具 flask-unsign),攻击者就能伪造任意 session 完成提权。Django 若使用 signed_cookies 会话后端且配置了 PickleSerializer,泄露 SECRET_KEY 更严重——伪造的 session 会直接触发 pickle 反序列化 RCE |
水平越权 / 垂直越权的代码审计点
越权是业务逻辑漏洞中最高频的,也是扫描器扫不出来、只能靠人脑的漏洞类型。代码层审计盯四个位置:
1. 权限校验注解/拦截器位置
- Java:
@PreAuthorize("hasRole('ADMIN')")是否遗漏在某个敏感接口上;拦截器addPathPatterns是否漏掉了某条路径(尤其是/admin/**用了通配但漏了特定子路径) - PHP:后台文件头部是否
include了权限校验文件,有没有文件漏 include(见前面 PHP 的”假鉴权”案例) - Python:装饰器
@login_required/@permission_required是否遗漏——Flask/Django 项目里逐个视图函数对一遍
2. 对象归属校验缺失(水平越权/IDOR)
这是越权里最高频的代码缺陷,对照案例看:
// ❌ 危险:只校验登录,没校验订单归属
@GetMapping("/order/{id}") // 接口:查看订单详情
public Order getOrder(@PathVariable Long id) { // id 来自 URL 路径,用户可控
return orderMapper.selectById(id); // 拿着 id 直接查库——传别人的 id 也能看
}
// ✅ 安全:校验对象属于当前用户
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id, @AuthenticationPrincipal User user) {
// 查询条件同时带上订单 id 和当前登录用户 id——不是自己的订单查不出来
return orderMapper.selectByIdAndUserId(id, user.getId());
}数据流对比:
- 危险版:攻击者请求
/order/1002→id=1002进selectById→ 数据库返回 1002 号订单 → 后端全程没问”这单是谁的” → 别人订单内容泄露 - 安全版:
selectByIdAndUserId(1002, 当前用户7号)→ SQL 变成WHERE id=1002 AND user_id=7→ 订单不是 7 号用户的就查不到 → 越权被堵死
为什么会出现这个漏洞:开发者写代码时的思维是”登录的人才能调这个接口”,但登录只证明了”你是用户”,没证明”这条数据是你的”。归属校验必须在每次取数据时显式做,框架不会替你做。
3. IDOR 参数识别
URL、Body、JSON 中出现的 id、userId、orderId、fileId、docId 这类参数,只要后端没用”当前登录人”这个维度做校验,全部逐个手工遍历测试(把 1001 改成 1002、1003 试试)。
4. 垂直越权常见代码缺陷
- 前端隐藏了管理菜单,但接口本身没鉴权(经典中的经典——菜单看不见 ≠ 接口调不了)
- 角色判断写在前端 JS 里做,后端不验证(Burp 改包直接绕过)
- 普通用户和管理员接口写在同一个 Controller,但只给部分方法加了权限注解——审计时按方法逐个核对注解,不要看类上有注解就以为全覆盖
甲方代码审计 vs 红队漏洞挖掘
同样是读代码找洞,两种立场的玩法完全不同:
| 维度 | 甲方/合规审计 | 红队/众测挖掘 |
|---|---|---|
| 目标 | 全面排查,不留死角,重在修复 | 快速出洞,拿到最高权限证明危害 |
| 深度 | 要求覆盖所有接口,低危也要报 | 只关心可利用的高危,低危直接跳过 |
| 时间 | 充足,可以通读全文法 | 有限,敏感函数回溯为主 |
| 产出 | 完整审计报告、修复建议、修复后回归验证 | PoC + 漏洞证明,不重修复细节 |
| 范围 | 整个应用、包括后台、包括内部接口 | 优先暴露面(登录前接口、公开 API) |
| 合规 | 必须授权、在生产副本上测 | 众测平台规则内操作 |
一句话记忆:甲方是”体检”,红队是”一击必杀”。面试时说清楚你做的是哪一种,以及为什么选对应的审计法,立刻显得专业。
「审计经验/印象深刻的漏洞」答题框架
面试被问”讲一个你挖过的漏洞”,按五步走,逻辑清晰、细节拉满:
- 项目背景:什么系统、什么技术栈、多少代码量
- 审计思路:选了哪种审计法、为什么选它
- 数据流:Source 在哪、经过了哪些中间处理、最终到达哪个 Sink
- 漏洞细节:漏洞类型、触发条件、危害、PoC 思路
- 修复建议:参数化/白名单/权限校验补充——能讲修复的人才像防御侧的人
虚构案例:某 CMS 搜索功能 SQL 注入
项目背景:一套 PHP 中小型 CMS(ThinkPHP 5.1 框架),代码约 2 万行,有完整源码。
审计思路:敏感函数回溯法——全局搜索 ->query(、Db::query 这类原生 SQL 执行点,发现一处手工拼接。
案例带读(逐行注释):
// application/index/controller/Search.php
public function index()
{
$keyword = input('get.keyword'); // Source①:URL 里的 ?keyword=,用户完全可控
$order = input('get.order', 'id'); // Source②:排序字段,默认 'id',用户完全可控
// ❌ 危险:$keyword 和 $order 都通过字符串插值直接拼进 SQL
$sql = "SELECT id, title FROM article
WHERE title LIKE '%{$keyword}%'
ORDER BY {$order} DESC";
$list = Db::query($sql); // Sink:ThinkPHP 原生 SQL 执行,不会做任何转义
return json($list);
}数据流分析(编号步骤):
- 用户请求
/search?keyword=test&order=xxx,input('get.order')把xxx收进$order,默认过滤为空,没有任何校验 $order通过{$order}字符串插值,一字不差地拼进 SQL 的ORDER BY子句Db::query($sql)把整条 SQL 原样发给数据库执行——它执行的是”字符串”,不区分哪些是语句哪些是数据- 数据库把攻击者注入的内容当成 SQL 语法执行,注入成立
为什么防线失效:这里两道常见防线恰好都不管用——
- 预编译防不住 ORDER BY:参数化查询的占位符只能替换”数据值”,不能替换列名/排序关键字这种”语法结构”(类比:表格模板里贴照片的格子能防注入,但”按哪一列排序”是表格本身的结构,没法用占位符)。所以
ORDER BY场景的正确做法是白名单,而不是预编译。 - 开发者没做任何校验:
input()取到啥就拼啥,连黑名单都没有。
漏洞验证:
GET /search?keyword=test&order=(select updatexml(1,concat(0x7e,user()),1))
触发报错注入,页面回显数据库当前用户名。逐段拆解这个 payload:
order=后面的内容会被拼进ORDER BY子句(select ...):用括号包一个子查询,让数据库执行它而不是把它当列名updatexml(1, ..., 1):MySQL 的 XML 修改函数,正常用法是updatexml(XML文档, XPath路径, 新值)。这里第一个参数故意给1(不是合法 XML),逼迫函数报错,而 MySQL 的报错信息会把第二个参数的执行结果带出来——这就是”报错注入”的核心:借错误消息当显示器concat(0x7e, user()):要借报错带出来的内容。user()返回当前数据库用户(如root@localhost);0x7e是十六进制的~符号,拼在前面当标记——回显内容里一眼就能认出”~ 后面的就是我注出来的数据”- 整体效果:数据库报错
XPATH syntax error: '~root@localhost',当前数据库用户被读出来了
修复建议:
// ✅ 安全:ORDER BY 用白名单——用户输入不在名单里就用默认值
$allowOrder = ['id', 'title', 'create_time'];
$order = in_array($order, $allowOrder, true) ? $order : 'id'; // 第三个参数 true = 严格比较
// keyword 用框架的参数绑定(底层就是预编译),不再手写拼接
$list = Db::name('article')
->where('title', 'like', "%{$keyword}%")
->order($order, 'desc')
->select();修复逻辑:白名单把 $order 的可能值锁死在三个合法列名里,用户传什么都会被校验或替换;keyword 走框架的 where 绑定,用户输入永远是”数据”。两条路都堵死。
新手常见误区(避坑)
这几个坑几乎每个初学者都会踩一遍,提前知道能省很多时间:
- 看到危险函数就报漏洞。
system()本身不是漏洞,system($_GET['cmd'])才是。必须走完”Source → 数据流 → Sink”的完整闭环,确认参数用户可控,才算实锤。 - 看到过滤函数就放弃。自写过滤函数恰恰是最该细看的地方——黑名单不全、只替换一次、编码问题,几乎必有绕过空间。
- 只看 Controller 不看 Model。MVC 项目里参数经常穿过三四层才拼 SQL,Controller 层干干净净不代表 DAO 层也干净。追踪必须跟到底。
- 信了代码里的注释。注释写着”已过滤""安全”不算数,以代码实际逻辑为准。很多注释是复制粘贴来的,过滤代码早被改没了。
- 纸上谈兵不做验证。代码里看着能打的洞,实际可能被 php.ini 配置、框架默认转义、前置 WAF 挡住。任何结论都要构造真实请求验证一次再写进报告。
- 忽略配置文件。很多致命问题不在
.php/.java里,而在配置里:debug=true、APP_KEY泄露、Filter 没配全路径、Actuator 端点没关。通读配置是第二步规定的动作,别跳。
面试题眼/速答
| 题目 | 一句话答案 | 展开两三句 |
|---|---|---|
| 代码审计的整体流程? | 环境准备 → 通读目录/路由/配置 → 选审计法 → 数据流追踪 → 漏洞验证与报告 | 先把系统跑起来拿到路由和报错信息,再按项目特点选通读/回溯/定向,核心是 Source 到 Sink 的数据流闭环,最后必须黑盒验证拿到 PoC 再下结论。 |
| 三种审计方法的区别? | 通读全文法全面但慢;敏感函数回溯法从 Sink 反推 Source 最快;定向功能审计法深钻高风险模块 | 通读适合小项目和学习系统全貌,能挖到逻辑漏洞;回溯法效率最高但只能发现有明显 Sink 的已知漏洞类型;定向法适合时间有限时按模块优先级(上传>登录>支付)逐个击破。 |
| 白盒和黑盒挖掘的区别? | 白盒源码在手可确定性判断,黑盒靠盲测 | 白盒能直接看拼接代码和过滤实现,漏洞判断是确定性的,还能发现黑盒测不到的深层漏洞;黑盒更贴近真实攻击者视角,二者结合:白盒定位、黑盒验证。 |
| MVC 和非 MVC 审计差异? | MVC 先找路由映射到 Controller 分层审,非 MVC 逐文件通读 | MVC 的权限控制在拦截器/注解,风险在分层传参丢上下文;非 MVC 每个文件头部手动 include 鉴权文件,风险在”一百个文件漏 include 一个”的未授权访问。 |
| 审计时优先关注什么漏洞? | RCE > SQL 注入 > 反序列化 > 文件类 > SSRF/XXE > XSS > 业务逻辑 | 按可利用性和危害排序分配时间。RCE 直接拿服务器权限排第一;SQL 注入存量最大可拖库;XSS 需要配合钓鱼所以排后面;逻辑漏洞危害依业务而定但众测很值钱。 |
| 越权漏洞在代码层怎么找? | 找权限注解/拦截器遗漏、对象归属校验缺失(IDOR)、前端隐藏但接口未鉴权 | 四个位置:注解是否逐个方法覆盖;取数据时有没有带”当前用户”维度(IDOR 核心);菜单隐藏不等于接口鉴权;角色判断是否写在前端 JS 里。 |
| 甲方审计和红队挖掘的区别? | 甲方求全覆盖重修复,红队求快速出高危 | 甲方是”体检”:时间充裕用通读全文法,低危也报,产出审计报告和修复建议并回归验证;红队是”一击必杀”:时间有限用敏感函数回溯,只关心可利用高危,产出 PoC。 |
| 怎么描述一个印象深刻的漏洞? | 背景 → 思路 → 数据流 → 漏洞细节与 PoC → 修复建议,五步走 | 关键在数据流环节讲清 Source 经过什么处理到达哪个 Sink、防线为什么失效;结尾一定要给修复方案——能讲修复才像防御侧出身的人。 |