版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/songyanfei1205/article/details/80885658
背景:
使用SSM做的web系统,在进行渗透测试的时候暴露出漏洞:设计缺陷:修改响应包,扰乱系统正常逻辑
设计缺陷/逻辑漏洞包括:
1 修改数值;
2 验证码爆破;
3 修改相应包;
4 修改密码;
5 服务端无有效性校验;
整改方案:(此方案还是有缺陷)
1 首先保证所有功能在服务器端做有效性校验。例如,应该填写数字的地方不允许保存别的字符等。
2 在所有请求发送之前,统一追加一个随机码;此随机码需要经过一定的算法及排序生成。随机码的作用是确保返回值是由后台发送的。
此处楼主将随机码分成长短不同的4段,使用4个自定义请求头携带发送。
3 后台在返回时,使用过滤器进行拦截所有返回请求,对所有返回值进行加密处理,类似于简单的数字签名
3 在请求返回之前,将获取到的数据进行预处理。好在jquery已经具备这个方法: dataFilter.
具体思路是
前台:
基于系统大部分功能都是jquery异步请求,在所有页面引入的公共js文件中加入方法,
即$.ajaxSetup({
beforeSend: function(xhr) {//x-hhkj是自定义的header头名称
xhr.setRequestHeader("x-hhkj", '随机码');
},
headers: {'x-hhkj':'随机码'},//由于未知原因,有些时候beforeSend这个方法不会被调用,故增加方法header。
dataFilter:function(data,type){//返回值data为原始数据 ,type是数据类型
//处理data
return data;
}
});
后台:
使用代理将返回值获取到,使用过滤器对返回值进行处理,然后重写返回值到前台。
注:
此方案的问题症结是随机码可能被破解。
所有涉及加密解密的js文件全部进行混淆加密。
楼主定义的随机码位19位长度,字母加数字的格式
注:
此方案的问题症结是随机码可能被破解。
所有涉及加密解密的js文件全部进行混淆加密。
楼主定义的随机码位19位长度,字母加数字的格式
可使用Fiddler工具或者Burb suite工具进行测试。如果需要这两个工具,可联系楼主
楼主整改的案例demo:需要将ResponseFilter.java在web.xml中配置或者使用注解注册
楼主整改的案例demo:需要将ResponseFilter.java在web.xml中配置或者使用注解注册
前台引入的js:
var random_num = '';//定义一个随机数
$.ajaxSetup({
beforeSend: function(request) {
random_num = createRandom();
var st = random_num.split(',');
request.setRequestHeader("x-agent-with", st[0]);
request.setRequestHeader("hhkj-Language", st[1]);
request.setRequestHeader("fildpath", st[2]);
request.setRequestHeader("ranne-x", st[3]);
random_num = st[2] + st[1] + st[0] + st[3];
},
headers: createHeaderRandom(),
dataFilter:function(data,type){
if(!data){
return null;
}
//此处的变量值是为了识别后台返回的数据是否加密。目前楼主的只有返回值是json串的才进行加密处理
var siencrpty = '0e5a0c2cca384a1b';
if(data.indexOf(siencrpty) == -1){//如果没有加密,则直接返回值,不进行下一步的处理
return data;
}
//此处是为了防止有个别返回值是页面或者脚本漏掉的,也需要放行
if(type == 'html'
|| type == 'script'){
return data;
}
//开始进行鉴别
var res = data;
res = res.replace(siencrpty,'');//先将验证返回值是否加密的表示移除掉
var arr = res.split("|"); //后台返回的值得格式是:摘要|数据 。所以需要进行拆分
var orginData = arr[1];//原始数据
var priKey = $.trim(arr[0]);//摘要
if(random_num){//首先判断随机码是否存在,如果存在,则处理
//先将随机码反向输出
random_num = random_num.split('').reverse().join('');
//截取摘要的后8位字符
var r1 = priKey.match(/.*(.{8})/)[1] ;
//截取摘要的前9位字符
var r2 = priKey.substring(0,9);
/*
*如果摘要的后8位字符是随机码的开头,摘要的前9位字符是随机码的结尾,则证明数据未遭到篡改
*原因是:后台为了加大随机码校验的难度,将传送的19位随机码经过处理,移除掉2位,
*再把剩下的17位拆分成两半部分分别追加到后台生成的摘要首尾
*/
if(random_num.indexOf(r1) == 0 && random_num.indexOf(r2) == 10 ){
priKey=priKey.substring(9,priKey.length - 8);
random_num= '';
}else{
alert("数据异常,请重新登录!");
return null;
}
}
//将原始数据使用base64加密
var base64Data = Base64Copy.encode(orginData);
//将base64加密的原始数据使用md5加密(32位)
var MD5Data = hex_md5(base64Data);
//截取将base64加密的原始数据的前半部分
base64Data = base64Data.substring(0,base64Data.length/2);
//截取将md5加密的数据的后半部分
MD5Data = MD5Data.substring(MD5Data.length/2,MD5Data.length);
//前台生成摘要
var remark = $.trim(base64Data)+$.trim(MD5Data);
//将前台生成的摘要与后台生成的摘要作比较,如果不一样,则遭到篡改
if(priKey.indexOf(remark) == -1){//此处可自由修改判断条件
alert("数据异常,请重新登录!");
loginOut();
return null;
}
return orginData;
},
cache:false
});
//产生随机数函数
function createRandom(){
var str="zxc1vbn2mlkjhg3fdsa4qwert5yuiop8QWERTY6UIOPAS9DFGHJK7LZXCVBNM0";
var rnd="";
for(var i=0;i<19;i++)
rnd+=str.charAt(Math.floor(Math.random()*10));
var s1 = rnd.substring(0,6);
var s2 = rnd.substring(6,10);
var s3 = rnd.substring(10,17);
var s4 = rnd.substring(17);
return s1 + ','+s2+','+s3+','+s4;
}
//产生随机数函数
function createHeaderRandom(){
var str="zxc1vbn2mlkjhg3fdsa4qwert5yuiop8QWERTY6UIOPAS9DFGHJK7LZXCVBNM0";
var rnd="";
for(var i=0;i<19;i++)
rnd+=str.charAt(Math.floor(Math.random()*10));
var s1 = rnd.substring(0,6);
var s2 = rnd.substring(6,10);
var s3 = rnd.substring(10,17);
var s4 = rnd.substring(17);
random_num = s3 + s2 + s1 + s4;
return {
"x-agent-with":s1,
"hhkj-Language":s2,
"fildpath":s3,
"ranne-x":s4
};
}
package com.system.filter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import com.system.EncryptDataUtils;
/**
* 返回值输出过滤器,这里用来加密返回值
*
* @Title: ResponseFilter
* @Description:
* @author kokJuis
* @date 上午9:52:42
*/
public class ResponseFilter implements Filter
{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException
{
HttpServletRequest httprequest = (HttpServletRequest) request;
HttpServletResponse httpresponse=(HttpServletResponse)response;
String url = httprequest.getRequestURL().toString();
if(check(url)){
filterChain.doFilter(request, response);
return;
}
ResponseWrapper wrapperResponse = new ResponseWrapper((HttpServletResponse)response);//转换成代理类
// 这里只拦截返回,直接让请求过去,如果在请求前有处理,可以在这里处理
filterChain.doFilter(request, wrapperResponse);
if("application/json;charset=UTF-8".equals(wrapperResponse.getContentType()) ||
"application/json;charset=utf-8".equals(wrapperResponse.getContentType())){
String headerName1 = httprequest.getHeader("x-agent-with");
String headerName2 = httprequest.getHeader("hhkj-Language");
String headerName3 = httprequest.getHeader("fildpath");
String headerName4 = httprequest.getHeader("ranne-x");
String headerName = headerName3 + headerName2 + headerName1 + headerName4;
//针对某些特殊的链接 前台无法追加header的 可追加到链接之后 此处根据各自情况而定
if(headerName == null || "".equals(headerName) || "null".equals(headerName) || headerName.length() < 19){
String headerName11 = httprequest.getHeader("x-agent-with");
String headerName21 = httprequest.getHeader("hhkj-Language");
String headerName31 = httprequest.getHeader("fildpath");
String headerName41 = httprequest.getHeader("ranne-x");
headerName = headerName31 + headerName21 + headerName11 + headerName41;
}
if(headerName == null || "".equals(headerName) || "null".equals(headerName) || headerName.length() < 19){
headerName = "";
}
byte[] content = wrapperResponse.getContent();//获取返回值
String str = new String(content,"utf-8");
String res = "";
if(url.indexOf("message/page/0") != -1
|| url.indexOf("message/page/1") != -1
|| url.indexOf("common/todotasks") != -1
|| url.indexOf("common/getMessage") != -1){//此处需要根据各自业务逻辑进行不加密处理防范 TODO
res = str;
}else{
//随机码倒叙
res = EncryptDataUtils.Encrypt(str,new StringBuffer(headerName).reverse().toString());
}
//注意:防止自动截取字符串 全量输出因为 有些返回值过长会导致输出不全
httpresponse.setContentLength(res.getBytes("utf-8").length);
httpresponse.getWriter().write(res);
httpresponse.getWriter().close();
httpresponse.getWriter().flush();
return;
}else{
byte[] content = wrapperResponse.getContent();
response.getWriter().write(new String(content,"utf-8"));
return;
}
}
private static final String NO_CHECK = "whiteUrls";
private static final String REDIRECT_PATH = "redirectPath";
private List<String> noCheckList = new ArrayList<String>();
@Override
public void init(FilterConfig arg0)
throws ServletException
{
String noChecks = arg0.getInitParameter(NO_CHECK);
if(StringUtils.isNotBlank(noChecks)){
if(StringUtils.indexOf(noChecks,",")!=-1){
for(String no : noChecks.split(",")){
noCheckList.add(StringUtils.trimToEmpty(no));
}
}else{
noCheckList.add(noChecks);
}
}
}
private boolean check(String path) {
if (noCheckList == null || noCheckList.size() <= 0)
return false;
for (String s : noCheckList) {
if (path.indexOf(s) > -1) {
return true;
}
}
return false;
}
@Override
public void destroy()
{
}
}
package com.system.utils;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.system.utils.Base64;
import com.system.utils.MD5Utils;
public class EncryptDataUtils {
/**
* 加密规则:
* 1- 将原始数据先转换成string类型 ,标记为o;
* 2- 将转换成string类型的原始数据先使用base64加密,此处是为了防止中文乱码。标记结果a;
* 3- 将第2步的结果使用md5加密,取加密结果的后半部分备用,标记结果b;
* 4- 将a截取前半部分备用,标记为c;
* 5- 随机码的后半部分 标记为x
* 6- 随机码的前半部分 标记为y
* 7- 加密标记 e
* 8- 将c与b拼接成字符串,作为最终的摘要信息
* 9- 最后的结果数据格式是: x+c+e+b+y|o
* @param data
* @param random 随机码
* @return
*/
public static String Encrypt(Object data,String random){
String str = "";
String MD5Str = "";//定义MD5存放的数据
String Base64Str = "";//定义base64存放的数据
String isEncrpty = "0e5a0c2cca384a1b";//标记: 验证是否加密的标记 如果有这个标记 则返回值已经加密 否则 没有加密 bjhhwykjgfyxgs的MD5加密的16位
if(data instanceof JSONObject){
JSONObject ja = (JSONObject) data;
str = JSONObject.toJSONString(ja);
}else if(data instanceof JSONArray){
JSONArray ja = (JSONArray) data;
str = JSONArray.toJSONString(ja);
}else if(data instanceof String){
str = data+"";
}
if("".equals(str)){
return null;
}
//定义报文加密格式 1- 原始报文使用Base64加密
Base64Str = Base64.encode(str);
//1、获取MD5加密的原报文
MD5Str = MD5Utils.md5(Base64Str);
//2、获取MD5加密的原报文的后半部分
MD5Str = MD5Str.substring(MD5Str.length()/2,MD5Str.length());
//3、获取base64加密报文之后的前半部分
Base64Str = Base64Str.substring(0,Base64Str.length()/2);
//定义摘要算法 将base64加密报文之后的前一半和MD5加密之后原报文的后一半拼接起来使用MD5生成摘要 将加密标记放置在中间位置 用来前端识别
//随机码的后半部分 + base64加密报文之后的前一半部分 + 加密标记 + MD5加密之后原报文的后一半部分 + 随机码的前半部分
String remark = random.substring(random.length()/2) + Base64Str+isEncrpty+MD5Str + random.substring(0, random.length()/2);
//截取屌密文部分的首尾字符
String mi = remark.trim().substring(1,remark.length()-1) + "|"+ str;
String returns = mi;
return returns;
}
public static void main(String[] args) {
}
}
package com.system.filter;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* 返回值输出代理类
*
* @Title: ResponseWrapper
* @Description:
* @author Administrator
* @date 2018-07-02
*/
public class ResponseWrapper extends HttpServletResponseWrapper
{
private ByteArrayOutputStream buffer;
private ServletOutputStream out;
public ResponseWrapper(HttpServletResponse httpServletResponse)
{
super(httpServletResponse);
buffer = new ByteArrayOutputStream();
out = new WrapperOutputStream(buffer);
}
@Override
public ServletOutputStream getOutputStream()
throws IOException
{
return out;
}
@Override
public void flushBuffer()
throws IOException
{
if (out != null)
{
out.flush();
}
}
public byte[] getContent()
throws IOException
{
flushBuffer();
return buffer.toByteArray();
}
class WrapperOutputStream extends ServletOutputStream
{
private ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos)
{
this.bos = bos;
}
@Override
public void write(int b)
throws IOException
{
bos.write(b);
}
@Override
public boolean isReady()
{
// TODO Auto-generated method stub
return false;
}
@Override
public void setWriteListener(WriteListener arg0)
{
// TODO Auto-generated method stub
}
}
}
相关base64加密和md5加密的js文件和java文件下载链接: