0%
毅种循环

返回

APP安全测试:从客户端信息泄露到越权Blur image

APP安全测试:从客户端信息泄露到越权#


前言

最近在对某 APP 进行安全测试时,发现一个比较有意思的APP。从最开始的网络层暴力破解失败,到转向客户端逆向分析,最终发现隐藏在代码深处的逻辑后门,并成功利用报错信息泄露实现了未授权访问。

这篇文章记录了整个渗透测试的思路演变,轻喷。

免责声明:本文仅用于技术交流与安全教育。文中涉及的漏洞已提交给相关厂商修复,敏感信息均已脱敏处理。请勿利用文中技术进行非法攻击。


第一阶段:碰壁——失效的暴力破解#

起初,我试图对 APP 的登录接口进行传统的暴力破解测试。

目标接口POST /api/password

经过观察 Burp Suite 的流量,我发现请求头中包含几个动态字段:

  • timestamp: 毫秒级时间戳
  • sign: 一个 32 位的哈希值

img 1764212379640 3bb6f1
img 1764212379640 3bb6f1

再此登录请求的接口之前,还需要访问一个token接口生成对应的accesstoken参数。

img 1764212379713 def61d
img 1764212379713 def61d

逻辑等于是首先访问token接口,返回accesstoken参数,然后进入下一步登录的接口,携带accesstoken参数和singn参数作为登录请求的Header。

测试了一下应该是只有头部签名校验:签名只校验 Header 中的字段(如 appId, timestamp, nonce)。Body 的内容不参与签名计算。

在这种设计下,只要时间戳在有效期内,你可以随意修改 Body 里的密码,签名依然合法。

如果服务器采用的是这种策略,那么防重放机制其实是只校验了时间戳,但防篡改机制只保护了 Header,没有保护 Body。这属于 API 设计上的缺陷,正确的做法应该是连带body一起保护。

img 1764212379780 eb4425
img 1764212379780 eb4425


第二阶段:突破——APK 逆向与算法还原#

刚好这个APP没加壳,通过反编译 APK(使用 JADX),我开始寻找签名的生成逻辑。

1. 定位核心配置#

那么如何搜索这些特征点呢,我个人的思路是如下:

1. 搜索 HTTP 请求头中的关键词 (最有效)#

这是最快的方法。我举例如下,

刚开始我在Burp Suite 里看到了几个特殊的 Header,直接在反编译工具(如 JADX)中搜索这些字符串:

  • "sign" (搜字符串,注意带引号)
  • "timestamp"
  • "Id"
  • "access-token""accessToken"

搜索技巧: 如果搜 "sign" 结果太多(因为这是常用词),尝试搜 "sign": (带冒号) 或者 .addHeader("sign"

2. 搜索网络库拦截器 (Interceptor)#

现代 Android App (90%以上) 使用 OkHttpRetrofit 发送网络请求。开发者通常不会在每个页面单独写签名逻辑,而是写在一个全局拦截器里。

搜索以下关键词:

  • Interceptor (查看实现了这个接口的类)
  • addHeader
  • chain.proceed

典型代码长这样:

Java

public class SignInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) {
        Request original = chain.request();
        // ... 这里就是你要找的签名逻辑 ...
        String sign = MD5.encrypt(original.body() + timestamp + secret);
        
        Request newRequest = original.newBuilder()
                .addHeader("sign", sign) // 关键特征
                .build();
        return chain.proceed(newRequest);
    }
}
plain

3. 搜索加密相关的关键词#

如果以上都找不到,尝试搜索通用的加密类名或方法名:

  • MD5 (通常签名是 MD5 或 SHA256)
  • MessageDigest (Java 原生加密类)
  • Mac (HMAC 签名常用类)
  • SecretKey
  • sortedMap / TreeMap (签名通常需要对参数进行字母排序,搜索这个能找到排序逻辑)

4. 搜索 URL 路径#

直接搜索你在抓包里看到的 URL 路径片段:

  • "/api/token"
  • "/dPassword"

找到定义这些 URL 的地方,通常顺藤摸瓜就能找到是谁在使用它们,以及使用前做了什么处理

通过搜索抓包中看到的 clientId 字符串,我迅速定位到了一个名为 Getsign 的配置文件。:

2. 还原签名算法#

接着,通过搜索 .addHeader("sign" 关键字,我在 App.kt 中找到了签名的计算逻辑:

// App.kt 伪代码
fun getSign(timestamp: String, hasToken: Boolean): String {
    val raw = if (hasToken) {
        FULL_KEY + token + timestamp
    } else {
        FULL_KEY + timestamp
    }
    return MD5(raw).toUpperCase()
}
plain

算法逻辑明确了: Sign = MD5(SecretKey + [Token] + Timestamp)

有了这个算法和密钥,我编写了一个 Python 脚本,能够实时生成合法的签名。


第三阶段:发现——代码中的逻辑后门#

在审计 App.kt 的网络拦截器(Interceptor)代码时,一段奇怪的逻辑引起了我的注意:

img 1764212379859 a214fb
img 1764212379859 a214fb

// 拦截器逻辑
if (url.contains("/queryUser")) {
    //高危逻辑:如果是查询用户接口,强制将 userid 设为 system1
    request.addHeader("userid", "system1");
} else {
    request.addHeader("userid", currentUser.uid);
}
plain

漏洞点分析:

这意味着有一个接口 /XXX/queryUser,它大概率是用来获取用户列表的。且服务器可能存在逻辑漏洞:只要 Header 里的 userid system1 ,它就允许查看所有数据,而不校验 Token 是否真的是管理员的。

由于 API 签名(Sign)和时间戳(Timestamp)是动态的,我们需要先生成一个合法的token和sign,然后修改userid信息来测试system1发送请求。


第四阶段:利用——报错信息泄露与 Fuzzing#

我利用 Python 脚本获得了新的sign信息,目标直指 /queryUsers 接口,并在 Header 中伪造了 userid: system1

1. Fuzz1#

我发送了一个空的 JSON Body {}。

img 1764212379939 00ba4f
img 1764212379939 00ba4f

服务器响应:

{
    "code": 500,
    "message": "请输入PageSize",
    "result": null
}
plain

突破口!

  1. 服务器没有返回“403 Forbidden”或“权限不足”,说明伪装 system1 成功绕过了鉴权
  2. 服务器报错信息泄露了必需参数名:PageSize

2. Fuzz2#

既然知道了 PageSize,肯定是代表某个页码,根据开发经验,分页参数通常是成对出现的。

在编程开发中(特别是 Java 后端),分页永远需要两个参数:

  1. 一页显示多少条 (刚刚服务器泄露了,叫 PageSize)
  2. 当前是第几页 (这个我们需要猜)

既然参数1叫 PageSize,那么参数2通常会遵循相同的命名风格。根据经验,常见的组合只有以下几种:

  • pageSize + page
  • pageSize + pageIndex (常见于 C# 或某些前端框架)
  • pageSize + current
  • pageSize + pageNum (常见于 Java 的 PageHelper 插件)

虽然报错信息显示的是 PageSize (大写开头,PascalCase),但这个 App 是安卓应用,后端大概率是 Java (Spring Boot)。 Java 的标准变量命名规范是 小驼峰 (camelCase) ,即首字母小写。

img 1764212380014 8bd60b
img 1764212380014 8bd60b

我构建了一个 Fuzzing 列表来猜测另一个参数:

  • {"PageSize": 10, "Page": 1} -> 失败
  • {"pageSize": 10, "page": 1} -> 失败
  • {"pageSize": 10, "pageIndex": 1} -> 失败
  • {"pageSize": 10, "pageNum": 1} -> 成功 (200 OK)

服务器返回了:"code": 0, "message": "接口调用成功",并吐出了大量的用户列表数据。

img 1764212380106 413119
img 1764212380106 413119


第五阶段:危害扩大#

通过这个漏洞,我编写了自动化脚本,利用 pageNum 遍历,成功拉取了全量的用户信息。

img 1764212380184 676e95
img 1764212380184 676e95

泄露数据包括:

  • 真实的用户 ID (userId)
  • 用户昵称
  • 手机号 (部分脱敏)

后续危害链 (Kill Chain):

  1. 精准撞库:拥有了 100% 准确的用户 ID 列表后,配合弱口令(如 123456)进行撞库,成功率极高。

img 1764212380255 89e6f1
img 1764212380255 89e6f1


总结与防御建议#

这次实战展示了一个典型的 API 漏洞组合拳:硬编码密钥 + 签名校验逻辑缺陷+APP后门

给开发者的防御建议:#

  1. 严禁硬编码密钥
    • 不要将 Client Secret 写死在客户端代码中,APK 反编译有成本,真遇到高手了也是秒了。
    • 建议使用动态密钥交换或将签名逻辑放在服务端(如网关层)。
  2. 修复鉴权逻辑 (Broken Access Control)
    • 永远不要相信客户端提交的 UserID
    • 后端必须从验证过的 AccessToken 中解析用户身份(Subject),以此作为权限判断的依据。Header 里的 userid 只能作为参考,不能作为凭证。
  3. 统一错误处理
    • 生产环境应屏蔽详细的错误堆栈和具体的参数提示,统一返回模糊的错误码,防止攻击者利用报错信息进行 Fuzzing。
  4. API 签名不是万能药
    • 签名只能保证传输安全,不能保证业务逻辑安全。不要因为有了签名就忽略了越权检测。