Task 3 完整漏洞链分析
一句话总结
App 用 java.util.Random 生成 session token,而 session
token 是用户登录后的"身份凭证"。因为 Random
可预测,攻击者可以绕过登录直接冒充任意用户。
漏洞链全景图
┌─────────────────────────────────────────────────────────────┐ │ 攻击者视角 │ │ 1. 反编译 APK → 看到 generateSessionToken() 算法 │ │ 2. 知道用了 new Random().nextInt(62) │ │ 3. 知道 Java Random 是 48-bit 种子 LCG │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 四步证据链 │ │ │ │ 第1步:KEY_SESSION_TOKEN = "sessionToken" │ │ → 这个值叫 session token,安全敏感 │ │ │ │ 第2步:checkCredentials() 验证成功 │ │ → 验证成功后才有资格获得 token │ │ │ │ 第3步:createSession() 执行 │ │ → generateSessionToken() 的结果存入 SharedPrefs │ │ │ │ 第4步:generateSessionToken() 里是 new Random() │ │ → 用了弱随机数生成器 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 攻击可行性 │ │ │ │ 48-bit 种子空间 = 2^48 ≈ 2.8×10^14 │ │ 已知算法 + 本地计算 → 几天内可预测有效 token │ │ 预测出 token → 直接进入用户账户(无需密码) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 修复方案 │ │ │ │ new Random() → new SecureRandom() │ │ 48-bit → 128-bit 熵 │ │ 暴力破解:从"几天"→"不可能" │ └─────────────────────────────────────────────────────────────┘
|
第一步:Session Token
的角色定义
文件: Login.java 第 26-29 行
private static final String KEY_SESSION_TOKEN = "sessionToken"; private static final String SESSION_PREF_NAME = "SessionPrefs"; private SharedPreferences.Editor editor; private SharedPreferences sharedPreferences;
|
含义:
- App 明确用
"sessionToken" 这个 key 存储一个值
- 存在
SessionPrefs 文件里
- 这个值的作用是代表用户的登录状态
为什么重要:
Session token
是安全敏感值——它的存在意味着"有一个合法用户刚刚成功登录了"。
第二步:Token 的生成时机
文件: Login.java 第 49-60 行
((Button) findViewById(R.id.button2)).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) throws Throwable { boolean zCheckCredentials = Login.this .checkCredentials(editText.getText().toString(), editText2.getText().toString()); if (!zCheckCredentials) { Toast.makeText(Login.this, "Wrong Credential", 0).show(); return; } Login.this.createSession(); Login.this.startActivity( new Intent(Login.this, (Class<?>) Profile.class)); } });
|
含义:
- 先验证用户名和密码
- 验证失败 → 停在登录页
- 验证成功 → 才调用 createSession()
为什么重要:
Token
不是随意生成的,而是在用户证明了自己是谁之后才生成的。这意味着
token 的存在 = 成功登录的证明。
第三步:Token 的存储动作
文件: Login.java 第 174-177 行
public void createSession() { this.editor.putString(KEY_SESSION_TOKEN, generateSessionToken()); this.editor.apply(); }
|
含义:
- 把
generateSessionToken() 的结果用
putString 存进 SharedPreferences
- Key 就是第一步定义的
"sessionToken"
为什么重要:
这个存储动作把"弱随机数生成器"和"认证状态"绑在了一起——生成的随机字符串直接成为了用户身份凭证。
第四步:弱随机数生成
文件: Login.java 第 183-189 行
private String generateSessionToken() { Random random = new Random(); StringBuilder sb = new StringBuilder(16); for (int i = 0; i < 16; i++) { sb.append("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" .charAt(random.nextInt(62))); } return sb.toString(); }
|
含义:
- 用
new Random() 生成 16 个随机字符
- 字符集是 A-Z, a-z, 0-9(62个字符)
为什么重要:
java.util.Random
是线性同余生成器(LCG),只有 48-bit
内部状态。这意味着: - 知道算法 + 少量观察 → 可以反推种子 -
种子已知 → 可以预测未来所有 token
攻击路径完整示例
攻击者条件: - 有一台 Android 设备(真机或模拟器) - 安装了该 App - 可以访问 SharedPreferences(需要 Root 或 ADB)
攻击步骤:
Step 1:反编译 APK → 用 jadx 打开 APK → 找到 Login.java → 看到 generateSessionToken() 的算法
Step 2:读取当前 session token → 通过 ADB 或 Root 访问: /data/data/com.example.mastg_test0016/shared_prefs/SessionPrefs.xml
Step 3:离线计算种子 → 把已知 token 的 16 个字符对应到 16 个 nextInt(62) 输出 → 反推 48-bit 种子(确定性计算,几秒完成)
Step 4:预测其他用户的 token → 用相同算法 + 反推的种子 → 计算出有效 token
Step 5:冒充用户 → 把计算出的 token 写入 SharedPreferences → 打开 App → 直接进入该用户账户
|
为什么这个漏洞链完整
| 证据点 |
内容 |
作用 |
| 1. 角色定义 |
KEY_SESSION_TOKEN |
说明这是安全敏感值 |
| 2. 时机证据 |
checkCredentials → createSession |
说明 token = 登录证明 |
| 3. 存储动作 |
putString in createSession |
把弱随机数和认证绑定 |
| 4. 弱随机数 |
new Random() |
漏洞的根因 |
| 5. 攻击可行性 |
48-bit 种子可破解 |
说明漏洞有实际影响 |
缺少任何一个环节,漏洞链就不完整。
量化对比
| 属性 |
当前(有漏洞) |
修复后 |
| 随机数生成器 |
java.util.Random |
java.security.SecureRandom |
| 种子大小 |
48-bit |
≥128-bit |
| 可能种子数 |
2^48 ≈ 2.8×10^14 |
2^128 ≈ 3.4×10^38 |
| 暴力破解时间 |
几天(可行) |
不可能 |
| 预测可行性 |
确定可预测 |
计算不可行 |
| 是否满足 CWE-338 |
❌ 是 |
✅ 否 |
修复代码
private String generateSessionToken() { Random random = new Random(); ... }
private String generateSessionToken() { SecureRandom secureRandom = new SecureRandom(); ... }
|
一行之差,从"可预测"变成"不可预测"。