最近一段时间参与了公司的年度安全测试,我负责对接测试方,来修复安全测试发现的漏洞那么安全一顿操作下来,也是 发现了两个重大的漏洞,第一个就是重放,第二个就是参数篡改
简单介绍一下重放和参数篡改
重放?
就是别有用心的人呢,对你的请求进行抓包,拿到通信包后,伪装一下,拿着包不断地进行请求,这样就耗费掉很多服务器资源,比如恶意地区刷你的短信服务,或者如果是电商业务地话,恶意攻击者就可以恶意地去下单
我们怎么解决呢?
每个请求带个时间戳(加密后的时间戳),后端拿到时间戳之后去缓存中进行比对,如果没有发现这个时间戳,认为这个请求有效,若发现有这个时间戳,那么就认为请求无效。
这样有什么问题么?
对的,这样的话会导致缓存中存储的时间戳越来越多,
怎么办?
让缓存中存储的时间戳有个过期时间比如60s,请求到来之后先验时间戳和服务器时间,如果时间戳超时了,那么认为这个请求无效,如果时间戳和现在的服务器时间相比在60s 内,则继续去缓存中验证,走刚刚的逻辑。那么下面就是具体的流程图。
参数篡改
什么是参数篡改
就是比刚刚的重放更狠,拿到数据包之后,如果发现数据包没有加密,那么改改参数,1000的鞋子付款时改一下变成0.01,麻烦了,这不完蛋了么?
我们怎么解决的呢?
有种加密算法叫做杂凑算法,比如MD5 或者SM3,他们的特点就是同样的数据杂凑之后的结果相同,不同的数据杂凑之后的结果一定不同,
于是我们把数据(json串)进行杂凑,然后把杂凑后的结果再进行加密,最后把这个结果放到json串中,这样后端拿到请求后,先对json串去掉一个key(杂凑结果的那个key),然后进行杂凑,接着用同样的密钥进行加密,最后比较一下后端计算的结果和json中携带的结果是否一致?
这样如果攻击者修改了请求体(json串)中的数据,因为杂凑算法的特性,后端在对请求体进行杂凑时获取的结果就和前端携带的不一致,有人说攻击者可以在修改json后,对json串进行重新杂凑啊!,不好意思,我的杂凑结果是加过密的,他不知道密钥。
流程图长下面这样子
哇,好麻烦!
第一个坑来了!
我们的前端是个手机端的应用,在app内套个浏览器,所有的请求都用让app(我们称之为壳子)去发,而不是a壳子里的浏览器发。
我们的安卓应用呢,是Java写的,json用的是阿里巴巴的fastJson,原来好好的json串,给到fastJson 后,json中key的顺序就变了!原来可能是
{
"A-name": "BeJson",
"B-url": "",
"C-page": 88
}
但是给到壳子之后,壳子发请求的时候呢,可能就变成了
{
"B-url": "hello.com",
"C-page": 88,
"A-name": "BeJson"
}
那么,根据杂凑算法的特点呢,顺序不同,杂凑的值肯定也是不同的,于是,就是后端校验不通过了,
怎么办?
排序!
前端对接送串的key 按照字母顺序进行排序,然后再杂凑,后端拿到json后也进行排序,然后再杂凑,这样就规避掉了fastJson乱序的问题
上代码,
主要的思想就是利用TreeMap里面的树结构进行自动的排序,排序后再转为json串
package com.cloud.serviceapp.util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.util.*;
/**
* @author bigbo
* @description json key 排序工具类 按照 a-z排序
* @createTime 2023/10/26
*/
public class JsonKeySorter {
public static Object sortJsonKeys(String json) {
try {
Object obj = new JSONTokener(json).nextValue();
if (obj instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) obj;
Map<String, Object> sortedMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
Object value = jsonObject.get(key);
// 如果是个jsonObject,递归调用自己
if (value instanceof JSONObject) {
value = sortJsonKeys(value.toString());
// 如果是个Josn中的列表,那么调用sortJsonArray
} else if (value instanceof JSONArray) {
value = sortJsonArray((JSONArray) value);
}
sortedMap.put(key, value);
}
com.alibaba.fastjson.JSONObject result = (com.alibaba.fastjson.JSONObject)com.alibaba.fastjson.JSONObject.toJSON(sortedMap);
return result;
}
// 因为我们业务的特殊性,json串是一个对象而不是列表
// else if (obj instanceof JSONArray) {
// JSONArray jsonArray = (JSONArray) obj;
// return sortJsonArray(jsonArray);
// }
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
// 对json列表进行排序
private static ArrayList<Object> sortJsonArray(JSONArray array) {
ArrayList<Object> list = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Object value = array.get(i);
if (value instanceof JSONObject) {
value = sortJsonKeys(value.toString());
} else if (value instanceof JSONArray) {
value = sortJsonArray((JSONArray) value);
}
list.add(value);
}
list.sort(new JsonValueComparator());
return list;
}
//重写compare方法实现按照字母大小写排序
private static class JsonValueComparator implements Comparator<Object> {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof JSONObject && o2 instanceof JSONObject) {
return sortJsonKeys(o1.toString()).toString().compareTo(sortJsonKeys(o2.toString()).toString());
} else if (o1 instanceof JSONArray && o2 instanceof JSONArray) {
return sortJsonArray((JSONArray) o1).toString().compareTo(sortJsonArray((JSONArray) o2).toString());
} else {
return o1.toString().compareTo(o2.toString());
}
}
}
}
这样有什么问题呢?
我忘了,后来我们采取的是整个请求体全加密,一步到位,完美解决参数篡改,直接规避掉了json串的key乱序。