安全测试的老哥说我的系统不抗揍

Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。

我们部门每次大版本发布,都需要走一道公司的安全测试。

这不最近公司的安全测试标准提高了,我所负责的用户服务被一口气提了10个安全问题。

好家伙,3.25没跑了。

不过用户中心是核心的底层业务服务,它的数据安全性与系统稳定性都是极其重要的,发现了Bug,我们只能逐个去修复了。

本文将针对其中比较典型三个问题做分析与解决方案阐述。

一、IP伪造

日常业务开发的过程中,我们可能会需要获取请求接口的用户IP信息。

为了防止黑客通过爆破的方式登陆系统,我将记录每一次用户登陆的IP,在一定时间范围内连续输入错误的用户名或者密码,将锁定IP。此IP在锁定时间内无法再请求登陆接口。

修复前获取IP逻辑

 static String getIpAddr(HttpServletRequest request) {
       if (request == null) {
           return "unknown";
       }
       String ip = request.getHeader("x-forwarded-for");
       if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("Proxy-Client-IP");
       }
       if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("X-Forwarded-For");
       }
       if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("WL-Proxy-Client-IP");
       }
       if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("X-Real-IP");
       }
  
       return "0:0:0:0:0:0:0:1".equals(ip) ? LOCAL_IP : ip;
 } 

从业务功能使用的角度上来看,这段代码没有任何问题,我们能够从HttpServletRequest中获取到报文中的IP数据。

但是发现没有,我们获取的IP数据都是从请求头中获取的,而请求头的所有报文信息都是可以通过报文进行伪造的。只要攻击的黑客弄一个IP池,不断的变化,我们的防爆破机制就失效了。

解决思路

说实话,我当时为了完成这个IP获取需求,上面的代码也是直接百度了一份,发现能用也就用了。

我并不知道Header中获取到的IP值的意思是什么(文中不阐述比如:Proxy-lient-IP这些请求头的含义)

不过好在安全测试给出了修复建议:

IP数据获取需要从remoteAddr中获取。

remote_addr 是服务端根据请求TCP包的ip指定的。假设从client到server中间没有任何代理,那么web服务器(Nginx,Apache等)就会把client的IP设为remote_addr;如果存在代理转发HTTP请求,web服务器会把最后一次代理服务器的IP设置为remote_addr。

因为我们的服务都是统一走的nginx代理,所以可以在nginx中取到remote_addr,然后设置一个独立的业务请求头传递给用户中心。

1.增加nginx配置

2.编码实现

 /**
  * 获取真实ip,防止ip伪造
  *
  * @param request
  * @return
  */
 private static String getIpAddrFromRemoteAddr(HttpServletRequest request){
   String ip = request.getHeader("X-Real-IP");
   if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
       return ip;
   }
   ip = request.getHeader("X-Forwarded-For");
   if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
   // 多次反向代理后会有多个IP值,第一个为真实IP。
       int index = ip.indexOf(',');
       if (index != -1) {
           return ip.substring(0, index);
       } else {
           return ip;
       }
   } else {
       return request.getRemoteAddr();
   }
 } 

二、登陆未使用验证码

基本上所有的登陆都会通过使用验证码的方式去防刷登陆接口。

我们产品最开始不想要验证码逻辑,为了防止暴力破解密码。我们使用了同一IP不能连续失败的逻辑防止盗刷,但是新规范下,安全测试还是不认。

没办法,他们掌握着我们的产品上架的生杀大权,我只能去加上验证码的功能。

验证码的方案无非两种:前端生成验证码还是后端生成验证码。

由于我们的前端大佬比较懒,只能我们后端生成验证码了。

验证码的生成工具我选择了Hutool,开箱即用。

先来看一下Hutool生成验证码的使用方式

 //定义图形验证码的长、宽、验证码字符数、干扰元素个数
 CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
 ​
 //获取验证码的base64
 String captchaImage = circleCaptcha.getImageBase64Data();
 ​
 //获取验证码
 String code = circleCaptcha.getCode(); 

生成的验证码例如

简易版验证码前后端的校验逻辑:

1.获取验证码接口

前端请求后端生成验证码接口,后端生成验证码,将base64做为key,验证码code作为value保存至redis,然后返回base64给前端

2.登陆

前端将用户输入的code与base64传到后端,校验base64在redis的值

三、DDos攻击

验证码逻辑做完之后发现还是存在了一个攻击点。

后端在生成验证码的时候是需要把base64作为redis的key存储到redis中的。

高频请求验证码接口的情况下,大量的base64的key导致redis的响应变慢,甚至撑爆redis。

这就是DDos攻击

一般来说是指攻击者利用“肉鸡”对目标网站在较短的时间内发起大量请求,大规模消耗目标网站的主机资源,让它无法正常服务。在线游戏、互联网金融等领域是 DDoS 攻击的高发行业。

我们公司是安全公司,有专门的安全产品可以处理这种场景。

那如果不购买对应的安全产品,我们如何在应用层面防止DDos攻击呢?

DDos攻击就是高频的恶意请求,也就是高并发,高并发防刷你能想到什么?

可不就是限流吗?

3.1.网关限流

如果你使用的是gateway网关作为业务请求的入口,你可以直接设置一个单位时间内同一ip请求同一个url的限流器。

1.限流器

 @Configuration
 public class LimitConfig {
 ​
   @Bean
   @Primary
   KeyResolver hostResolver() {
       return exchange ->{
           ServerHttpRequest serverHttpRequest = Objects.requireNonNull(exchange.getRequest());
           return Mono.just(serverHttpRequest.getLocalAddress().getAddress().getHostAddress()+":"+serverHttpRequest.getURI().getPath());
       };
   }
 ​
 } 

2.增加限流过滤工厂类

 @Component
 @ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
 public class BaiyanRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<BaiyanRateLimiterGatewayFilterFactory.Config> {
 ​
   private final RateLimiter defaultRateLimiter;
 ​
   private final KeyResolver defaultKeyResolver;
 ​
   public BaiyanRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
                                                 KeyResolver defaultKeyResolver) {
       super(Config.class);
       this.defaultRateLimiter = defaultRateLimiter;
       this.defaultKeyResolver = defaultKeyResolver;
   }
 ​
   public KeyResolver getDefaultKeyResolver() {
       return defaultKeyResolver;
   }
 ​
   public RateLimiter getDefaultRateLimiter() {
       return defaultRateLimiter;
   }
 ​
   @SuppressWarnings("unchecked")
   @Override
   public GatewayFilter apply(BaiyanRateLimiterGatewayFilterFactory.Config config) {
       return new InnerFilter(config,this);
   }
 ​
   /**
      * 内部配置加载类
      */
   public static class Config {
 ​
       private KeyResolver keyResolver;
 ​
       private RateLimiter rateLimiter;
 ​
       private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;
 ​
       public KeyResolver getKeyResolver() {
           return keyResolver;
       }
 ​
       public BaiyanRateLimiterGatewayFilterFactory.Config setKeyResolver(KeyResolver keyResolver) {
           this.keyResolver = keyResolver;
           return this;
       }
 ​
       public RateLimiter getRateLimiter() {
           return rateLimiter;
       }
 ​
       public BaiyanRateLimiterGatewayFilterFactory.Config setRateLimiter(RateLimiter rateLimiter) {
           this.rateLimiter = rateLimiter;
           return this;
       }
 ​
       public HttpStatus getStatusCode() {
           return statusCode;
       }
 ​
       public BaiyanRateLimiterGatewayFilterFactory.Config setStatusCode(HttpStatus statusCode) {
           this.statusCode = statusCode;
           return this;
       }
 ​
   }
 ​
   /**
      * 内部类,用于指定限流过滤器的级别
      */
   private class InnerFilter implements GatewayFilter, Ordered {
 ​
       private Config config;
 ​
       private BaiyanRateLimiterGatewayFilterFactory factory;
 ​
       InnerFilter(BaiyanRateLimiterGatewayFilterFactory.Config config,BaiyanRateLimiterGatewayFilterFactory factory) {
           this.config = config;
           this.factory = factory;
       }
 ​
       @Override
       @SuppressWarnings("unchecked")
       public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
           KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
           RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
 ​
           Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
 ​
           return resolver.resolve(exchange).flatMap(key ->
                   limiter.isAllowed(route.getId(), key).flatMap(response -> {
 ​
                       for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
                           exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                       }
 ​
                       if (response.isAllowed()) {
                           return chain.filter(exchange);
                       }
                       ServerHttpResponse rs = exchange.getResponse();
                       byte[] datas = GsonUtil.gsonToString(Result.error(429,"too many request","访问过快",null))
                               .getBytes(StandardCharsets.UTF_8);
                       DataBuffer buffer = rs.bufferFactory().wrap(datas);
                       rs.setStatusCode(HttpStatus.UNAUTHORIZED);
                       rs.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                       return rs.writeWith(Mono.just(buffer));
                   }));
       }
 ​
       @Override
       public int getOrder() {
           return GatewayFilterOrderConstant.RATE_LIMITER_FILTER;
       }
   }
 ​
 } 

3.增加配置

 spring:
 cloud:
   gateway:
     # 网关路由策略
     routes:
       - id: auth
         uri: lb://auth
         predicates:
           - Path=/api/**
         filters:
           #限流配置
           - name: BaiyanRateLimiter
             args:
             # 每秒补充10个
               redis-rate-limiter.replenishRate: 10 
               # 突发20个
               redis-rate-limiter.burstCapacity: 20
               # 每次请求消耗1个
               redis-rate-limiter.requestedTokens: 1 
               key-resolver: "#{@hostResolver}" 

3.2.应用限流

没有使用网关的系统,我们可以单独使用AOP,过滤器,或者拦截器的方式进行的单应用服务限流。

思路其实与网关限流很类似。

成熟的限流方案有滑动窗口、令牌桶或者漏桶,不做展开讲解。

四、总结

本文针对我在工作中碰到的三个安全测试问题做了详细的问题描述,并针对问题进行分析逐步得到解决方案。

现将问题与解决方案总结如下

五、联系我

文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~

微信:baiyan_lou

公众号:柏炎大叔### 如何入门网络安全

建议

多看书

阅读永远是最有效的方法,尽管书籍并不一定是最好的入门方式,但书籍的理解需要一定的基础;但是就目前来看,书籍是比较靠谱的入门资料。

现在Web安全书籍比较多,因此大家在学习的过程中可以少走了不少的弯路。如果以上推荐书籍阅读有困难,那就找自己能看得进的 Web 安全的书

当然纸上谈兵终觉浅,最好还是实践一下。

对于那些没有学习方向和资料的同学,可以看下我整理的资源,这份资料经历过社会的实践,可以说是当下全网较全的网络安全知识体系:
①网络安全学习路线
②20份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥网络安全必备书籍
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值