各位老铁,大家下午好。坐得端正一点,别把那杯咖啡洒在键盘上,刚才那位穿格子衬衫的大哥,把你脚从桌子上拿下来,你的脚趾头都快戳到主机箱散热孔了。
今天我们不聊什么“PHP 是世界上最好的语言”这种毫无营养的口水话,我们来聊点硬核的。聊聊怎么让你的代码不变成黑客的提款机。我们的话题是:利用大模型(LLM)给 PHP 代码做全身 CT 扫描,专门找那些藏在逻辑里的“越权”漏洞。
你知道越权漏洞有多烦人吗?这是安全界的“幽灵”。它不像 SQL 注入那么露骨,不像 XSS 那么狂野,它就像是电影里的刺客,悄无声息,把你后台里那个叫“王总”的数据偷走,你还以为他在前台逛商品呢。
传统的安全扫描器,说白了就是个只会读死书的复读机。你给它看代码,它只会喊:“哎呀,这里有个参数没过滤!”“哎呀,这里有个 SQL 拼接!”至于这个参数会不会影响别人的数据?它会吗?它不会。它只有眼睛,没有脑子。
而今天我们要讲的主角——LLM,它有脑子,而且是大脑皮层很发达的那种。我们要把它变成一个“逻辑越权审计员”。这就好比以前你要把代码找遍,还要手动分析数据流,现在你把代码往 LLM 面前一扔,让它去思考,去猜,去发现那些“隐秘的角落”。
好,废话不多说,我们直接上代码,上干货。如果你听不懂,那是我的锅,不是你的锅。
第一部分:为什么 PHP 里的越权像是在玩“大家来找茬”
首先,咱们得搞清楚,PHP 的代码逻辑有多乱。很多老项目,那简直就是意大利面,面条 spaghetti。
举个例子,你要查订单。代码大概是这个样子:
// order.php
$orderId = $_GET['id'];
$user = getUserFromSession(); // 假设这是当前登录用户
// 获取订单详情
$order = $db->query("SELECT * FROM orders WHERE id = $orderId")->fetch();
// 输出结果
echo json_encode($order);
这时候,传统的扫描器跑一遍,拍拍屁股走了:“这里有个 $_GET['id'],参数不安全,打钩。” 然后它不管,反正它没发现 SQL 注入,就算过了。
但如果你是个黑客,你看到了什么?你看到了两个变量:$orderId 和 $user。这个 $user 在这行代码里根本没被用到!这就是漏洞。这就是逻辑越权(IDOR)。我可以随便改 $_GET['id'],比如改成 99999,后台数据库里根本没这个订单,或者是个超级管理员的数据,但我能查到。
所以,我们的 LLM 审计系统要做的事情,就是盯着这个 $user,然后问它:“嘿,哥们,你拿着用户 ID 99 去查订单,但你查出来的订单里明明有用户 ID 1 的余额,你知不知道?” LLM 会回答:“哦,它没比对,它确实不知道。”
这就需要 AST(抽象语法树)和语义分析。但我们要用 LLM 来做这件事,怎么用?
第二部分:搭建“LLM 警探”的工作台
我们要写一个 PHP 脚本,这个脚本的作用就是:把 PHP 代码“翻译”给 LLM 听,然后把 LLM 的回答“翻译”回可执行的漏洞报告。
为了让 LLM 听懂,我们不能把整个几十万行的项目扔给它,太贵了,而且它也容易糊涂。我们得搞“切片”。
假设我们要审计 api/orders.php 文件。我们的核心流程是这样的:
- 代码提取:把相关逻辑段的代码抠出来。
- 上下文注入:告诉 LLM 这是一个“获取订单”的 API。
- Prompt 编写:用一种诱导性的语言问它。
- 结果解析:看它到底发现了什么。
来,看代码。我们用 Python 脚本来调用 LLM,毕竟 PHP 做网络请求处理起来稍微有点重,而且 Python 的 LLM 库多。
import openai
def audit_php_code(code_snippet, context):
# 构造 Prompt
system_prompt = """
你是一名资深的 PHP 安全审计专家。你的任务是根据提供的 PHP 代码片段,分析是否存在逻辑越权漏洞。
请特别关注:
1. 输入参数是否直接用于查询或操作数据库,而未经过身份验证或权限检查。
2. 查询结果是否包含当前用户无权访问的数据(如其他用户的信息)。
3. 代码中是否存在依赖 Session 或 Cookie 的变量,却未与请求参数进行关联性检查。
如果发现漏洞,请用 JSON 格式输出,包含漏洞类型、严重程度和利用思路。
"""
user_prompt = f"""
代码上下文:{context}
待审计代码:
```php
{code_snippet}
请分析是否存在 IDOR(不安全的直接对象引用)或其他逻辑越权风险。
"""
# 调用 LLM API (这里以 OpenAI 为例,你可以换成国内的 Claude 或者本地模型)
response = openai.ChatCompletion.create(
model="gpt-4-turbo-preview",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
)
return response.choices[0].message.content
模拟一段“坏”代码
bad_code = “””
$userId = $_GET[‘uid’]; // 用户传来的 ID
$query = “SELECT * FROM users WHERE id = $userId”;
$result = $db->query($query);
echo json_encode($result);
“””
audit_result = audit_php_code(bad_code, “这是一个查看用户详情的接口”)
print(audit_result)
你看,这行代码一跑,LLM 看到了 `$userId = $_GET['uid']`。它会想:“哎?这个 `$userId` 没有验证它是否属于当前登录用户啊!这就像你给了小偷一把钥匙,让他去偷邻居家的东西。” 它会返回类似这样的 JSON:
```json
{
"vulnerability": "IDOR (Insecure Direct Object Reference)",
"severity": "High",
"exploit_logic": "攻击者可以通过修改 uid 参数查看任意用户信息,无需登录或无需验证当前用户身份。"
}
这时候,你的扫描器就活了。
第三部分:从“能看”到“能懂”——处理复杂的业务逻辑
但是,光查个 ID 查不到什么大不了的东西。真正的越权,往往藏在复杂的业务逻辑里。比如,转账限制。
假设你有这样一个函数,处理转账:
public function transferMoney($fromId, $toId, $amount) {
$fromUser = $this->getUserById($fromId);
$toUser = $this->getUserById($toId);
// 只有管理员能操作
if (!$this->isAdmin()) {
die("Access Denied");
}
if ($fromUser->balance < $amount) {
die("Insufficient Funds");
}
$fromUser->balance -= $amount;
$toUser->balance += $amount;
$this->db->save($fromUser);
$this->db->save($toUser);
}
如果有人绕过了 isAdmin() 的检查呢?或者 isAdmin() 检查在某个地方失效了?
这时候,单纯的正则匹配就废了。我们得让 LLM 读懂业务逻辑。我们的脚本需要更智能。
我们要给 LLM 提供“调用栈”信息。比如,这个函数是被哪个 Controller 调用的,Controller 是否做了权限校验。
修改一下我们的 Prompt:
def audit_complex_logic(code, controller_check):
system_prompt = "你是一个逻辑漏洞挖掘机。"
user_prompt = f"""
业务逻辑代码:
{code}
前置控制器中的权限检查代码:
{controller_check}
请分析:
1. 如果前端传来的 $fromId 或 $toId 没有被控制器验证为当前登录用户,是否会导致越权转账?
2. 如果控制器里的 $this->isAdmin() 检查存在拼写错误(例如写成了 $this->isAdmins),攻击者是否可以绕过?
"""
# ... API 调用 ...
这就很厉害了。LLM 能看到 $this->isAdmin()。虽然它不会真的去改你的代码拼写错误,但它能敏锐地捕捉到:“嘿,这行代码如果少个 ‘s’,或者如果这里的条件永远为真,那就完蛋了。”
这就是深度语义分析。它不像静态分析工具(比如 SonarQube)那样死板地报错,它能产生“猜想”。
第四部分:实战演练——构建自动化审计链
好了,理论讲完了,咱们来搭个架子。假设我们要做一个 PHP 代码库的自动化审计。
我们要解决两个问题:
- 数据流追踪:怎么知道
$userId是从哪里来的? - 幻觉防御:LLM 偶尔会胡说八道(幻觉),我们不能把它的话当圣旨。
步骤 1:AST 解析器
我们用 PHPStan 或者类似的工具来生成 AST,或者直接用 PHP 内置的 token_get_all。
<?php
// parse_code.php
$code = file_get_contents('vulnerable_code.php');
$tokens = token_get_all($code);
$dataFlow = [];
$contextStack = [];
foreach ($tokens as $token) {
if (is_array($token)) {
// 检测函数调用,如 $_GET, $_POST
if ($token[0] == T_VARIABLE && $token[1] == '$_GET') {
$contextStack[] = 'HTTP_PARAM';
}
// 简单的变量赋值模拟数据流
if ($token[0] == T_VARIABLE && $contextStack) {
$lastContext = array_pop($contextStack);
$dataFlow[] = [
'source' => $lastContext,
'variable' => $token[1],
'line' => $token[2]
];
}
}
}
// 打印数据流
echo "Data Flow Trace:n";
foreach ($dataFlow as $flow) {
echo "Line {$flow['line']}: {$flow['source']} -> {$flow['variable']}n";
}
这段代码会把 $_GET['id'] 捕捉到变量 $id 上。我们把这个数据流信息作为 Prompt 的一部分发给 LLM。
步骤 2:Prompt 优化与上下文注入
我们把 AST 追踪的结果和源代码拼在一起,发给 LLM。
def generate_audit_prompt(ast_trace, source_code):
prompt = f"""
你正在审计一个 PHP Web 应用。
数据流追踪记录:
{ast_trace}
核心源代码:
{source_code}
问题:
1. 请检查上述数据流中的变量(如 $id, $uid)是否被用于数据库查询或对象访问。
2. 请检查代码中是否存在用户可以直接控制该变量的入口点(如 URL 参数)。
3. 如果用户可以控制该变量,请判断攻击者是否可以通过修改该变量访问其他用户的数据。
请输出分析结果,如果存在漏洞,请详细描述攻击步骤。
"""
return prompt
步骤 3:自动化执行
我们写一个循环,遍历项目中的 PHP 文件。
import os
import subprocess
def scan_project(directory):
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.php'):
filepath = os.path.join(root, file)
# 1. 获取代码
with open(filepath, 'r', encoding='utf-8') as f:
code = f.read()
# 2. 解析 AST (这里简化处理,实际可用 PHP-Parser 库)
# 假设我们有个函数叫 parse_ast,返回数据流
ast_trace = parse_ast(code)
# 3. 生成 Prompt
prompt = generate_audit_prompt(ast_trace, code)
# 4. 发给 LLM
result = call_llm_api(prompt)
# 5. 处理结果
if "漏洞" in result or "Vulnerability" in result:
print(f"发现潜在风险: {filepath}")
print(result)
print("-" * 50)
scan_project('./my_php_project')
第五部分:处理“高级”越权——水平与垂直越权
光查查用户信息不够,我们得学会区分“水平越权”(Level 1 能看 Level 2 的)和“垂直越权”(Level 1 管理员能看 Level 2 的)。
LLM 需要一点“身份”上下文。我们要告诉它,当前的请求是什么角色发的。
def audit_with_roles(code, current_role, target_roles):
prompt = f"""
当前请求的 User Role: {current_role}
目标系统中存在的角色: {target_roles}
代码片段:
{code}
分析:
1. 如果 $userId 被传递到代码中,代码是否会根据 current_role 拦截 $userId != current_user_id 的情况?
2. 对于 {current_role},是否被允许访问 {target_roles} 的数据?
"""
# ... API call ...
举个例子,如果是“普通用户”角色去访问“管理员”的数据,LLM 会盯着权限检查那几行代码看。如果发现只有一行注释说“TODO: 加权限检查”,那 LLM 就会高兴地报警:“嘿!这里少了个 if 语句!”
第六部分:LLM 的局限性——我们得留一手
别把 LLM 神化了。它不是神,它也是个容易犯错的实习生。
- 幻觉:它有时候会一本正经地胡说八道。比如它可能会说“第 50 行有个漏洞”,但实际上根本没那行。所以,我们的脚本必须有“置信度评分”机制。
- 代码缩进和格式:LLM 很讨厌缩进不对的代码。如果你的代码全是制表符和空格混用,LLM 会晕头转向。所以在喂给 LLM 之前,必须用
phpcbf(PHP Code Beautifier)标准化一下格式。 - Token 限制:如果文件太大,LLM 会截断。这时候就要切文件,或者用 RAG(检索增强生成),只把相关的片段喂给它。
第七部分:模拟攻击——让 LLM 变成攻击者
除了审计,我们还能让 LLM 变成攻击者。这就是“对抗性生成”。
如果我们发现了一个 IDOR 漏洞,我们能不能让 LLM 自动构造 POC(概念验证)脚本?
def generate_exploit_payload(code_snippet):
prompt = f"""
代码片段显示了以下逻辑漏洞:通过修改 URL 参数可以获取任意用户信息。
请用 Python 编写一个简单的脚本,利用 Python requests 库,向目标 API 发送请求,获取当前登录用户的 ID,然后尝试修改参数获取管理员 ID 的数据。
"""
# 获取脚本
exploit_script = call_llm_api(prompt)
# 执行脚本 (注意:在实际环境中要小心,最好在沙箱里跑)
# exec(exploit_script)
return exploit_script
这就从“被动审计”变成了“主动攻击模拟”。如果你能自动生成脚本并跑通,那这个漏洞基本就实锤了。
第八部分:部署与监控
光有个脚本还不行,得部署起来。你可以把它集成到 CI/CD 流水线里。
每当开发人员提交代码,git commit 一触发,Jenkins 就跑一下我们的 LLM 审计脚本。如果 LLM 报了错,代码就别想合并。
这就叫“安全左移”。别等到上线被黑了才哭爹喊娘。
结束语:代码是写给人看的,漏洞是写给黑客看的
最后,我想说点掏心窝子的话。
PHP 代码为什么容易有漏洞?因为它是胶水语言,它是为了快为了方便。很多老程序员,为了赶进度,eval() 一把梭,include 一把梭,extract($_GET) 一把梭。
用 LLM 来审计,其实就是利用 AI 的通用性,去填补人类对复杂业务逻辑理解的盲区。人类程序员容易疲劳,容易想当然,容易以为“这个检查肯定在前面了”。但 LLM 可以不知疲倦地盯着每一行代码。
当然,工具只是工具。最安全的防线,永远是那个坐在显示器前,手里拿着咖啡,眼神犀利,正在检查代码的你。LLM 只能告诉你哪里有坑,能不能跳过去,还得你自己决定跳不跳。
好了,今天的讲座就到这里。大家回去记得把代码格式化一下,别让 LLM 看不懂你的代码而发火。如果你们项目里还有那种“用户 ID 直接传给后台”的接口,赶紧改了。散会!