这篇笔记讲什么

代码审计(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" .

第二步:通读目录结构与配置

这一步的目标是建立全局地图,就像进商场先看楼层导览图,而不是直接扎进第一家店。花半小时通读,能省后面几个小时的瞎撞。

  1. 目录结构:认清楚哪个目录是入口、哪个是 controller/model、第三方组件放在哪(PHP 的 vendor、Java 的 WEB-INF/lib)。
  2. 路由配置:MVC 项目先找路由表——Spring 看注解(@RequestMapping/@GetMapping)、ThinkPHP 看 route 目录、Flask 看 @app.route 装饰器。找到了路由表,你就有了全站接口清单,攻击面一目了然。
  3. 配置文件:数据库连接配置(确认用了什么框架和驱动版本)、过滤器/拦截器注册在哪(web.xml、Spring 的 WebMvcConfigurer、Flask 的中间件)、有没有开鉴权拦截——很多系统的过滤器配了但没拦全,这就是洞。
  4. 三方组件版本:看 composer.json(PHP)、pom.xml(Java)、package.json(Node)、requirements.txt(Python)。组件版本过旧可能直接带着公开的历史漏洞,比如 fastjson 1.2.24 的反序列化 RCE——版本号一对上,漏洞就是白捡的。

三种审计法

读代码有三种基本姿势,没有绝对优劣,看项目大小和时间预算选:

方法适用场景优点缺点
通读全文法代码量小(个人站、小程序)、对系统不熟需要整体学习理解系统全貌,能发现逻辑漏洞、隐藏功能、隐藏后门最慢,大项目不可行
敏感函数回溯法(推荐)中大型项目快速出洞效率最高,直奔高危点只能发现已知的、有明显 Sink 的漏洞类型;漏掉逻辑漏洞
定向功能审计法时间有限、需要按功能点逐个击破深钻一个模块,能挖到业务逻辑漏洞视野窄,模块间交互的风险看不到

新手建议:先精通敏感函数回溯法(出洞最快,建立正反馈),再补通读全文法(理解系统全貌),最后按需用定向功能审计法。

通读全文法

是什么:从入口文件开始,按一次请求的完整生命周期顺序读代码:index.php / Application.java → 路由分发 → 过滤器/拦截器 → 控制器 → 业务逻辑 → 数据层。

类比:像坐飞机时完整走一遍流程——值机(入口)→ 安检(过滤器)→ 登机口(路由)→ 座位(控制器)。每个环节你都要问一句:“这里查证件了吗?”

怎么读:每读一个文件,在笔记上记三条:

  1. 这个文件对外提供什么接口?
  2. 它校验权限了吗?(没校验的就是可疑点)
  3. 它把哪些变量带进了危险操作(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 / $_COOKIEPHP所有 Source 入口,先看哪些参数被收进来了
eval( / assert( / system( / exec( / shell_exec( / popen(PHP代码执行、命令执行
unserialize( / include / require / file_get_contentsPHP反序列化、文件包含、任意文件读
Runtime.getRuntime().exec / ProcessBuilderJava命令执行
readObject / XMLDecoder / parseObjectJava反序列化(fastjson 的 parseObject 尤其高危)
${ (只在 .xml 文件里搜)Java/MyBatisSQL 拼接——#{} 是参数化安全的,${} 是直接拼接
render_template_string / pickle.loads / yaml.load( / subprocessPythonSSTI、反序列化、命令执行
exec( / eval( / child_processNode.js代码执行、命令执行

搜完别急着高兴——命中 Sink 只是嫌疑,Source 可控才算实锤。每个命中点都要回答三个问题:参数从哪来?中间过了哪些函数?过滤能不能绕?

Warning

回溯中途遇到自定义过滤函数不要直接放弃——停下来审计这个过滤函数本身的实现。经验上 90% 的自写过滤函数是不完整的:黑名单不全(只拦了 cat 没拦 tac)、只替换一次(str_replace 把 ; 删掉一个,写两个 ;; 就剩一个)、宽字节编码问题(吃掉转义符)。看到 str_replace、preg_replace 黑名单、自写 checkXxx 函数,都要蹲下来细看。

定向功能审计法

是什么:不做全量扫描,挑高风险功能模块,一个模块一个模块地深钻整条链。

为什么有这个玩法:有些漏洞没有明显的危险函数(比如”找回密码的验证码不过期”、“订单金额可以改成负数”),敏感函数回溯法扫不出来,只能盯着功能逻辑看。

模块优先级(按历史出洞率排序):文件上传 > 登录/注册/找回密码 > 支付/订单 > 文件管理(删除/下载/重命名)> 数据导出 > 后台接口。

操作示例:专门审计上传功能时,从上传点进来到落盘,整条链走一遍——文件落盘路径谁定的?文件名有没有被用户控制?类型校验是在前端还是后端、白名单还是黑名单?上传后能不能被直接访问执行?每一环都是潜在漏洞点。

白盒 vs 黑盒视角差异

初学者容易混淆:渗透测试(黑盒)和代码审计(白盒)到底啥关系?一句话:同一个漏洞,黑盒靠猜,白盒靠看。

维度黑盒(测试者视角)白盒(代码视角)
攻击面发现靠目录爆破、爬虫、JS 提取 API直接读路由,一个不漏
漏洞判断盲测,靠回显/报错/时间盲注直接看拼接代码,确定性判断
过滤情况看不到,只能盲试绕过过滤代码可见,可针对性绕过
信息泄露挖到 .git、.bak 才有源码源码在手,.env、密钥、历史漏洞一览无余
适用无源码、快速打点有源码、深度挖掘、合规审计

完整 CMS 审计思路(从安装包到出洞)——这是新人第一个完整练手项目该走的流程:

  1. 下载安装包,本地搭环境跑起来
  2. 观察安装流程:安装过程往往暴露数据库配置、后台路径、版本信息(装的时候别一路点”下一步”,截图记录每一步)
  3. 从后台登录后抓一遍所有功能点的请求(Burp 挂着点一遍),建立接口清单
  4. 回代码侧,从路由映射到 Controller,通读目录结构
  5. 敏感函数回溯法扫一遍全项目,标记可疑点
  6. 对标记点逐个做数据流闭环验证,把”拼接 + 可控”点按可利用性排序
  7. 高危点用黑盒方式构造请求验证一次(拿到实际 PoC 再写报告——代码里看着像漏洞和实际能打是两回事,中间可能隔着 php.ini 配置、WAF、框架默认过滤)
  8. 不出洞就转入定向功能审计:文件上传、任意文件读取、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 > 业务逻辑
优先级漏洞类型为什么是这个位置
1RCE直接拿到服务器权限,危害无上限,一个洞结束战斗
2SQL 注入可拖库(全站用户数据泄露),可配合 --os-shell 拿权限,覆盖面最广、存量最多
3反序列化在 PHP/Java 里基本等于 RCE,出洞率高
4文件类任意文件读(拿配置/源码/密钥)、任意文件写(写 webshell)、文件上传(直接 getshell)
5SSRF / XXE打内网、读文件、探测服务,常作为组合拳的一环
6XSS需要配合钓鱼诱导管理员点击才能提权,单独危害有限
7业务逻辑越权、支付逻辑、并发——出洞率高但危害依业务而定,众测里很值钱

各语言审计侧重

不同语言的”常见病”不一样,审计关注点也不同:

语言重点漏洞类型典型框架独有审计点
PHPSQL 注入、文件包含、反序列化(POP 链)、文件上传ThinkPHP / Laravel宽字节注入、extract 变量覆盖、$$ 可变变量、unserialize POP 链
Java反序列化(readObject)、SpEL/OGNL 表达式注入、JNDI、SQL 注入Spring / Struts2 / Shiro反射调用链、MyBatis ${} 拼接、Filter 链 URI 解析差异绕过鉴权
PythonSSTI(Jinja2)、pickle 反序列化、SSRF、格式化字符串Flask / Django / FastAPIpickle.loads、模板引擎沙箱逃逸、__globals__ 链
JavaScript/Node原型链污染、SSRF、命令注入、Mass AssignmentExpress / Koa__proto__ 污染、动态 require、eval/Function 调用
Go命令注入、路径穿越、SSRF、goroutine 竞争Gin / Echoexec.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");  // 执行删除

数据流分析(按编号走一遍):

  1. 攻击者直接请求 admin/delete_user.php?id=1,根本没登录,$_SESSION['admin'] 不存在
  2. 代码进入 if 分支,调用 header('Location: login.php')——但这个函数只是往 HTTP 响应头里写一行”请跳转”,它不终止程序
  3. PHP 继续往下执行,$id 被取到,DELETE 语句照样执行
  4. 浏览器收到响应后确实跳去了登录页,但数据库里的用户已经被删了——攻击者用 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 的数据不受它保护(→ 二次注入),宽字节编码可以吃掉它加的转义符(→ 宽字节注入)。

相关:PHP 代码审计、命令执行审计、文件类漏洞审计

Java

标准动作链(四步走):

  1. 依赖识别:先把 pom.xml / build.gradle 过一遍——fastjson、Jackson、Shiro、Struts2、log4j2 这些组件的版本号直接决定能不能打已知 CVE。但注意:版本只是嫌疑,不是结论,必须确认代码里真的用了危险特性(比如 fastjson 版本老旧,但代码里根本没 parseObject 外部输入,那就打不了)。
  2. 路由梳理:传统 Servlet 项目看 web.xml 的 <servlet-mapping>;Spring 项目全局搜 @RequestMapping/@GetMapping/@RestController;Struts2 看 struts.xml。产出一张「URL → 入口方法 → 是否需登录」的清单,这是后续所有工作的底表。
  3. 权限链梳理:Filter 的声明顺序即执行顺序(SpringBoot 看 @Order);HandlerInterceptor 在 Filter 之后、Controller 之前执行;鉴权的 url-pattern 是否覆盖了全部敏感路由。
  4. 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
    }
}

数据流分析:

  1. 攻击者未登录,直接请求 /admin/userList
  2. 拦截器 preHandle 执行,发现没登录,调了 sendRedirect——和 PHP 的 header() 一样,这只是写了个响应头,不终止流程
  3. 方法走到末尾 return true,Spring 认为”拦截器放行了”,请求被送进 AdminController.userList()
  4. 管理员接口在未登录状态下被执行

为什么防线失效:preHandle 的返回值就是”放行/拦截”的开关——return true 放行、return false 拦截。开发者调了跳转却忘了 return false,等于保安把访客拦下登记完,挥挥手说”进去吧”。正确写法:sendRedirect 后立即 return false。审计时全局搜 preHandle,逐个检查每个拦截分支的返回值。

框架切入点:

框架审计切入点高频问题
Spring MVC / SpringBoot注解路由、HandlerInterceptor、Actuator 暴露端点(/env、/heapdump)Actuator 未鉴权、SpEL 注入、参数绑定覆盖(Spring4Shell)、redirect: 可控跳转
Struts2struts.xml action 配置、struts.devMode 常量OGNL 表达式注入(S2-045 等)、devMode 调试接口
ShirofilterChainDefinitionMap 的 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),找解析差异。

相关:Java 代码审计、反序列化审计、JNDI 注入审计

Python

标准动作链:

  1. 先读 requirements.txt/pyproject.toml 确定框架与组件版本(依赖版本决定哪些漏洞天然存在)
  2. 梳理路由:Flask 搜 @app.route(、app.add_url_rule(、Blueprint 注册;Django 读 urls.py 的 urlpatterns(注意 include() 的层层分发和 re_path 的正则写法)
  3. 以 request.args / request.form / request.json 等为 Source,按 Sink 表全局 grep 后做数据流回溯
  4. 最后看配置面: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 PINapp.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

相关:Python 代码审计、模板注入审计、反序列化审计

水平越权 / 垂直越权的代码审计点

越权是业务逻辑漏洞中最高频的,也是扫描器扫不出来、只能靠人脑的漏洞类型。代码层审计盯四个位置:

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());
}

数据流对比:

  1. 危险版:攻击者请求 /order/1002 → id=1002 进 selectById → 数据库返回 1002 号订单 → 后端全程没问”这单是谁的” → 别人订单内容泄露
  2. 安全版: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)
合规必须授权、在生产副本上测众测平台规则内操作

一句话记忆:甲方是”体检”,红队是”一击必杀”。面试时说清楚你做的是哪一种,以及为什么选对应的审计法,立刻显得专业。

「审计经验/印象深刻的漏洞」答题框架

面试被问”讲一个你挖过的漏洞”,按五步走,逻辑清晰、细节拉满:

  1. 项目背景:什么系统、什么技术栈、多少代码量
  2. 审计思路:选了哪种审计法、为什么选它
  3. 数据流:Source 在哪、经过了哪些中间处理、最终到达哪个 Sink
  4. 漏洞细节:漏洞类型、触发条件、危害、PoC 思路
  5. 修复建议:参数化/白名单/权限校验补充——能讲修复的人才像防御侧的人

虚构案例:某 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);
}

数据流分析(编号步骤):

  1. 用户请求 /search?keyword=test&order=xxx,input('get.order') 把 xxx 收进 $order,默认过滤为空,没有任何校验
  2. $order 通过 {$order} 字符串插值,一字不差地拼进 SQL 的 ORDER BY 子句
  3. Db::query($sql) 把整条 SQL 原样发给数据库执行——它执行的是”字符串”,不区分哪些是语句哪些是数据
  4. 数据库把攻击者注入的内容当成 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 绑定,用户输入永远是”数据”。两条路都堵死。

新手常见误区(避坑)

这几个坑几乎每个初学者都会踩一遍,提前知道能省很多时间:

  1. 看到危险函数就报漏洞。system() 本身不是漏洞,system($_GET['cmd']) 才是。必须走完”Source → 数据流 → Sink”的完整闭环,确认参数用户可控,才算实锤。
  2. 看到过滤函数就放弃。自写过滤函数恰恰是最该细看的地方——黑名单不全、只替换一次、编码问题,几乎必有绕过空间。
  3. 只看 Controller 不看 Model。MVC 项目里参数经常穿过三四层才拼 SQL,Controller 层干干净净不代表 DAO 层也干净。追踪必须跟到底。
  4. 信了代码里的注释。注释写着”已过滤""安全”不算数,以代码实际逻辑为准。很多注释是复制粘贴来的,过滤代码早被改没了。
  5. 纸上谈兵不做验证。代码里看着能打的洞,实际可能被 php.ini 配置、框架默认转义、前置 WAF 挡住。任何结论都要构造真实请求验证一次再写进报告。
  6. 忽略配置文件。很多致命问题不在 .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、防线为什么失效;结尾一定要给修复方案——能讲修复才像防御侧出身的人。