本文原创作者:bt0sea
0×00、业务需求
移动应用产业以爆炸性的速度呈指数增长,但移动应用的安全问题落后于应用的发展速度。由于黑客事件变得越来越公开,对企业的影响比以往任何时候都更大。黑客渗透获取用户敏感数据后,出于商业目的销售,或者公开。所以,在移动APP端对企业数据的保护尤为重要。
P2P网站、金融等相关的网站、APP保存着用户的身份信息(真实姓名、身份证号、手机号、征信证明、银行流水等)。目前趋势是移动APP漏洞层出不穷,今天的移动安全相当于十年前web服务器安全的级别。基本上可以得出这样的结论:根据权威机构评估,平均每一个移动APP应用大约有3.5个安全漏洞,Apk 就是open source code代名词。
但是,单纯研究Mobile APP安全性问题,无法利用高危,获得服务器端的数据。所以,在分析移动APP漏洞的同时,要重点分析连接移动APP的RESTful API Web service。为什么要提RESTful API Web service呢?普通web架构+JSON不就成了么? 经过分析大部分有价值数据的网站都是使用RESTful API Web service。这是热需求。
咱们先看看哪些大咖用这种设计架构,github/fackbook等。不了解RESTful API Web Service的同学请参考:
http://www.ruanyifeng.com/blog/2014/05/restful_api.html
0×01、研究手段
一、破解思路:
其实重点突破RESTful API Web Service 还是通过iOS/Android App。由于很多高大上的网站为了安全性,把Web网站和移动APP隔离,发现使用http(s)://api(OpenApi).xxx.com/login/v1?phone=13811110000&sgin=022BEC9EE1B935B50D1193A07A3AD7B2。都是通过移动APP抓到的包。所以,我们先看看移动端安全风险主要有哪些,我们如何下手,其实OWASP Mobile Top 10 Risks给我们指引了方向:
M1: 不安全的服务器端访问控制 M2: 不安全的数据存储 M3: 传输层保护不足 M4: 非预期的数据泄露 M5: 脆弱的认证和授权 M6: 已知的弱加密算法 M7: 客户端注入 M8: 通过不被信任的输入改变安全设定 M9: 会话处理不当 M10: 缺乏二进制保护
虽然说官网总结的很好,但是对于服务器端渗透从字面上只有M1。但是个人觉得需要配合APP破解才能达到最佳效果。那么,具体应该怎么操作呢?
M1:不安全的服务器端访问控制
(1)通过android移动APP程序攻击服务器端业务逻辑。
(2)验证服务器端对输入校验。
(3)通用SQL注入 XSS跨站脚本,不安全的认证检查。
(4)通过代理方式操纵伪造数据提交。
当然要配合M2相关内容。
M2:不安全的数据存储
(1)敏感信息:用户名、密码、加密key、信用卡信息、session识别码、token等。
(2)个人信息:手机号、家庭住址、电子邮件、位置信息等。
(3)还有一些可能包含敏感信息的文件存储:SQLite databases 、Log Files 、XML Data Stores or Manifest Files 、Binary data stores 、SD Card、Cloud
二、具体攻击方法
2.1 使用安卓手机应用程序攻击服务器业务逻辑
通过上述分析,可以发现,如果想获取服务器端数据库的数据,主要是集中在对Web平台的安全检测,同时,结合这期的主题RESTful API Web Service,渗透测试重点主要表现在:
(1)、对手机APP客户端做身份认证(Authentication)
(2)、对手机APP身份认证之后的授权(Authorization)
(3)、对手机APP和服务器端传输的数据加密级别判定
首先,我们要了解目前的针对RESTful API Web Service防护的技术手段。
方法论研究
1、对手机APP客户端做身份认证
1.1、在APP请求中增加签名参数 1.2、http auth (HTTP Basic/HTTP Digest)身份认证机制,这个一般都由安全架构来承担(例如Spring Security) 1.3、OAuth 1/2协议做身份认证(一般都是第三方接入时使用,例如:weibo、微信等)
攻击思路:
寻找Android App存储在本地token信息,比如说在database、s_Prefs;等地方。
破解Android App的加固,寻找和服务器通讯URL签名算法。知道生成算法,就可以伪造请求了。。。
测试用例:撞库分析。。。(分析后可以通过已经泄露的数据库,碰撞测试):账号和密码产生机制,以及签名产生机制。
第一步:首先观察不同两次登录 URL是否存在URL签名。
POST http://wdapi.wandafilm.com/filmserver/user/login HTTP/1.1 Host: wdapi.wandafilm.com Connection: close Accept-Encoding: gzip Content-Type: application/x-www-form-urlencoded; charset=utf-8 Content-Length: 312 Connection: close User-Agent: 万达电影 3.9.0 rv:95475 (iPhone; iPhone OS 8.4.1; zh_CN) clientversion=30900&clientid=1&verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0SpI2Olm3oacqAG%252BxXBrCAm49QWQqztOFLvu5MuecuPzg%253D%253D&password=f73177f13486f55bcd8bea88d1610f58&mobile=186183xx220&imei=&version=2&clienttype=1
POST http://wdapi.wandafilm.com/filmserver/user/login HTTP/1.1 Host: wdapi.wandafilm.com Connection: close Accept-Encoding: gzip Content-Type: application/x-www-form-urlencoded; charset=utf-8 Content-Length: 308 Connection: close User-Agent: 万达电影 3.9.0 rv:95475 (iPhone; iPhone OS 8.4.1; zh_CN) clientversion=30900&clientid=1&verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0TyLPqEc9hZzGdykXLBNTChe8Cq3c6zqbo2I03zxSZDbA%253D%253D&password=f73177f13486f55bcd8bea88d1610f58&mobile=186183xx220&imei=&version=2&clienttype=1
发现verifycode没有改变。
第二步:分析登录各个参数的含义,同时参考破解APK文件。
POST http://wdapi.wandafilm.com/filmserver/user/login HTTP/1.1 Host: wdapi.wandafilm.com Connection: close Accept-Encoding: gzip Content-Type: application/x-www-form-urlencoded; charset=utf-8 Content-Length: 316 Connection: close Cookie: jsessionid|JSESSIONID=4151ab5036290f18ef079cf0f0e43281 User-Agent: 万达电影 3.9.0 rv:95475 (iPhone; iPhone OS 8.4.1; zh_CN) clientversion=30900& clientid=1& verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0SrYk%252FpfqrZZAbA%252BwmJphBLFeaxqAU3tAC5Du9C9W4Yyw%253D%253D& password=f73177f13486f55bcd8bea88d1610f58& mobile=186183xx220& imei=& version=2& clienttype=1
apk破解后找到登陆处理函数。
public UserAPILogin(String paramString1, String paramString2, String paramString3, UserModel paramUserModel) { super("/user/login"); this.mMobile = paramString1; this.mPassWord = Md5Utils.md5(paramString2); this.mUserModel = paramUserModel; this.mTerminal = paramString3; }
查看源代码后,发现没有加salt,当然你可以使用MD5工具加密对比,但是绝大部分的程序是加salt的。
public static String md5(String paramString) { sMd5MessageDigest.reset(); sMd5MessageDigest.update(paramString.getBytes()); paramString = sMd5MessageDigest.digest(); sStringBuilder.setLength(0); int i = 0; for (;;) { if (i >= paramString.length) { return sStringBuilder.toString(); } int j = paramString[i] & 0xFF; if (j < 16) { sStringBuilder.append('0'); } sStringBuilder.append(Integer.toHexString(j)); i += 1; } }
在源代码中搜索verifycode、clientversion、clientid等关键字,定位到WandafilmServerAPI类中的描述:
public String getUrl() { String str = super.getUrl(); StringBuilder localStringBuilder = new StringBuilder(); localStringBuilder.append(str); if (!str.contains("?")) { localStringBuilder.append("?"); } for (;;) { localStringBuilder.append("clientid="); localStringBuilder.append(Integer.toString(NetConstants.CLIENT_ID)); localStringBuilder.append("&clienttype="); localStringBuilder.append(Integer.toString(2)); localStringBuilder.append("&verifycode="); str = getVerifyCodeAes(NetConstants.VERIFY_CODE + System.currentTimeMillis()); if (!TextUtils.isEmpty(str)) {} try { localStringBuilder.append(URLEncoder.encode(str.trim(), "utf-8")); return localStringBuilder.toString(); localStringBuilder.append("&"); } catch (UnsupportedEncodingException localUnsupportedEncodingException) { for (;;) { localUnsupportedEncodingException.printStackTrace(); } } } }
然后定位getVerifyCodeAes。
private String getVerifyCodeAes(String paramString) { if (mAes == null) {} try { mAes = new EncryptionAES(NetConstants.AES_KEY); } catch (NoSuchAlgorithmException localNoSuchAlgorithmException) { for (;;) { try { paramString = mAes.encrypt(paramString.getBytes()); return paramString; } catch (InvalidKeyException paramString) { paramString.printStackTrace(); return null; } catch (NoSuchAlgorithmException paramString) { paramString.printStackTrace(); continue; } catch (NoSuchPaddingException paramString) { paramString.printStackTrace(); continue; } catch (InvalidAlgorithmParameterException paramString) { paramString.printStackTrace(); continue; } catch (IllegalBlockSizeException paramString) { paramString.printStackTrace(); continue; } catch (BadPaddingException paramString) { paramString.printStackTrace(); continue; } localNoSuchAlgorithmException = localNoSuchAlgorithmException; localNoSuchAlgorithmException.printStackTrace(); } } catch (InvalidKeySpecException localInvalidKeySpecException) { for (;;) { localInvalidKeySpecException.printStackTrace(); } } }
static { int i; if (CURRENT_APP_SOURCE == AppSource.CINEMA) { i = 1; CLIENT_ID = i; if (CURRENT_APP_SOURCE != AppSource.CINEMA) { break label89; } str = "lhdV+wj177YyjirsxeikaNestM/YafgOxrpiGbMVxzvefarFNbQfGHXauqmTIJk+upexqehjv8xK+mlx2SbnDg=="; label34: VERIFY_CODE = str; if (CURRENT_APP_SOURCE != AppSource.CINEMA) { break label96; } } label89: label96: for (String str = "wandayuanxian100008";; str = "wanhui20008qweasd") { AES_KEY = str; FACE_PACKAGE_LOCAL_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/WandaFacePackage"; return; i = 2; break; str = "vcj3smUdw7H47eqDg++qQUR4P52JMZolcamSazRZAQP6h6H9C4vusOkCdt/a15ofuLwGgt+lsgCyzD+ph1WyNg=="; break label34; } }
private byte[] encrypt(SecretKey paramSecretKey, IvParameterSpec paramIvParameterSpec, byte[] paramArrayOfByte) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); localCipher.init(1, paramSecretKey, paramIvParameterSpec); return localCipher.doFinal(paramArrayOfByte); }
System.currentTimeMillis()产生一个当前的毫秒,这个毫秒其实就是自1970年1月1日0时起的毫秒数
没有对手机唯一性的判断,那么,导致verifycode每次登陆都一样,那么,完全可以撞库。
设置8次提交。JSON数据返回值都一样({"msg":"手机号或密码错误!","status":13007}),那证明,服务器端java应用程序没有次数限制判断。
那么,具体怎么撞库,请参考:
2.2 常见的服务器端漏洞,如SQL注入
通过代理方式遍历APP抓取所有的URL连接,然后使用SQLMAP检查:
GET http://wdapi.wandafilm.com/filmserver/info/popularize?verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0S1UEUY0GOe3b2gBZ6jI19NjRz%252FMbY2H1MJ%252BG%252FUT6LPPg%253D%253D&clientid=1&imei=&clientversion=30900&version=2&clienttype=1 HTTP/1.1 Host: wdapi.wandafilm.com Connection: keep-alive Accept-Encoding: gzip User-Agent: 万达电影 3.9.0 rv:95475 (iPhone; iPhone OS 8.4.1; zh_CN) Connection: keep-alive Cookie: UID=20130211223622356711; SESSIONID=7d4c8c04397e5542e890004d2100f904; jsessionid|JSESSIONID=ecc029e6326c8a7c319c7aa6506c7369
开启kali linux
开启终端:sqlmap –r sqlinj12.txt
提交的时候可以提交User-Agent:等相关参数为移动App提交,如果服务器端有相关的判断,可以绕过。
同时,建议通过wireshark观察一下返回值的情况。
经过测试发现没有sql注入漏洞。
2.3 服务器端输入验证的缺失
是否越权获取其他用户数据。
GET http://wdapi.wandafilm.com/filmserver/user/userdetail?clientversion=30900&type=1&userid=20130211223622356711&clientid=1&verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0RQz5UQQ8aiPpE15TIQiIOSUIaJWMCXwVDD5lGOpgY6gg%253D%253D&imei=&version=2&clienttype=1 HTTP/1.1 Host: wdapi.wandafilm.com Connection: keep-alive Accept-Encoding: gzip User-Agent: 万达电影 3.9.0 rv:95475 (iPhone; iPhone OS 8.4.1; zh_CN) Connection: keep-alive Cookie: UID=20130211223622356711; SESSIONID=7d4c8c04397e5542e890004d2100f904; jsessionid|JSESSIONID=ecc029e6326c8a7c319c7aa6506c7369
http://wdapi.wandafilm.com/filmserver/user/userdetail? clientversion=30900& type=1& userid=20130211223622356711& clientid=1& verifycode=26FZaoBW4leyKOPjrsruo%252FidC3w3S8ETmXUfiepYaAa6dwp1rqTh%252FLqFHsVxVnP8okfw%252BZaUOJ0ONHxYgOO2%252BrDmYygAmW874ZEZabzDK0RQz5UQQ8aiPpE15TIQiIOSUIaJWMCXwVDD5lGOpgY6gg%253D%253D& imei=& version=2& clienttype=1
((StringBuffer)localObject5).append("UserId"); ((StringBuffer)localObject5).append(" =? "); Object localObject1 = CinemaGlobal.getInst().mUserModel.getUser().getUid();
0×02、测试结论:
(1)不能相信任何用户的输入,服务器端必须做校验。为了防止提交URL篡改。设计URL时:建议使用用户名密码 + https + url签名(url+时间戳+随机字串)链接。服务器返回token:使用约定的AES算法secret key随机
(2)为了防止为重放攻击,请求中增加一次性的Token,或者短时间内有效的Token。需要提取用户手机特征,但是iphone最好不要提取IMEI,因为苹果权限控制,很多时候为失效状态,导致URL签名没有变化。
(3)客户端最好不要存储任何有价值的数据。存储也必须加密。因为很多时候,你无法判断你的APP是否运行在一个被root的手机上。
* 作者:bt0sea,本文属FreeBuf原创奖励计划文章,未经许可禁止转载。