Task 3 完整漏洞链分析

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^482.8×10^14
│ 已知算法 + 本地计算 → 几天内可预测有效 token │
│ 预测出 token → 直接进入用户账户(无需密码) │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ 修复方案 │
│ │
new Random() → new SecureRandom() │
48-bit128-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(); // ❌ 48-bit 种子
...
}

// 修改后
private String generateSessionToken() {
SecureRandom secureRandom = new SecureRandom(); // ✅ 128-bit+ 熵
...
}

一行之差,从"可预测"变成"不可预测"。