一行代码引来的安全漏洞就让咱们丢失了整个服务器的控制权

以前在某厂的某次项目开发中,项目组同窗设计和实现了一个“引觉得傲”,额,有点扩张,不过自认为还说得过去的 feature,结果临上线前被啪啪打脸,由于实现过程当中由于一行代码(没有标题党,真的是一行代码)带来的安全漏洞让咱们丢失了整个服务器控制权(测试环境)。多亏了上线以前有公司安全团队的人会对代码进行扫描,才让这个漏洞被扼杀在摇篮里。html

下面咱们就一块儿来看看这个事故,啊,不对,是故事。前端

背景说明

咱们的项目是一个面向全球用户的 Web 项目,用 SpringBoot 开发。在项目开发过程当中,离不开各类异常信息的处理,好比表单提交参数不符合预期,业务逻辑的处理时离不开各类异常信息(例如网络抖动等)的处理。因而利用 SpringBoot 各类现成的组件支持,设计了一个统一的异常信息处理组件,统一管理各类业务流程中可能出现的错误码和错误信息,经过国际化的资源配置文件进行统一输出给用户。程序员

统一错误信息配置管理

咱们的用户遍及全球,为了给各个国家用户比较好的体验会进行不一样的翻译。具体而言,实现的效果以下,为了方便理解,以“找回登陆密码”这样一个业务场景来进行阐述说明。web

假设找回密码时,须要用户输入手机或者邮箱验证码,假设这个时候用户输入的验证码经过后台数据库(多是Redis)对比发现已通过期。在业务代码中,只须要简单的 throw new ErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED) 便可。具体而言,针对不一样国家地区不一样的语言看到的效果不同:算法

  • 中文用户看到的提示就是“您输入的验证码已过时,请从新获取”;
  • 欧美用户看到的效果是“The verification code you input is expired, ...”;
  • 德国用户看到的是:“Der von Ihnen eingegebene Verifizierungscode ist abgelaufen, bitte wiederholen” 。(我瞎找的翻译,不必定准)
  • ……

统一错误信息配置管理代码实现

关键信息其实就在于一个 GlobalExceptionHandler,对全部Controller 入口进行 AOP 拦截,根据不一样的错误信息,获取相应资源文件配置的 key,并从语言资源文件中读取不一样国家的错误翻译信息。spring

@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) @ResponseBody public ResponseEntity handle(HttpServletRequest request, BadRequestException e){ String i18message = getI18nMessage(e.getKey(), request); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message)); } @ExceptionHandler(ErrorCodeException.class) @ResponseBody public ResponseEntity handle(HttpServletRequest request, ErrorCodeException e){ String i18message = getI18nMessage(e.getKey(), request); return ResponseEntity.status(HttpStatus.OK).body(Response.error(e.getCode(), i18message)); } } 

不一样语言的资源文件示例不一样语言的资源文件示例shell

private String getI18nMessage(String key, HttpServletRequest request) { try { return messageSource.getMessage(key, null, LanguaggeUtils.currentLocale(request)); } catch (Exception e) { // log return key; } } 

详细代码实现能够参考本人以前写的这篇文章一文教你实现 SpringBoot 中的自定义 Validator 和错误信息国际化配置,上面有附完整的代码实现。数据库

基于注解的表单校验(含自定义注解)

还有一种常见的业务场景就是后端接口须要对用户提交的表单进行校验。以“注册用户”这样的场景举例说明, 注册用户时,每每会提交昵称,性别,邮箱等信息进行注册,简单起见,就以这 3 个属性为例。编程

定义的表单以下:后端

public class UserRegForm { private String nickname; private String gender; private String email; } 

对于表单的约束,咱们有:

  • 昵称字段:“nickname” 必填,长度必须是 6 到 20 位;
  • 性别字段:“gender” 可选,若是填了,就必须是“Male/Female/Other/”中的一种。说啥,除了男女还有其余?对,是的。毕竟全球用户嘛,你去看看非死不可,还有更多。
  • 邮箱: “email”,必填,必须知足邮箱格式。

对于以上约束,咱们只须要在对应的字段上添加以下注解便可。

public class UserRegForm { @Length(min = 6, max = 20, message = "validate.userRegForm.nickname") private String nickname; @Gender(message="validate.userRegForm.gender") private String gender; @NotNull @Email(message="validate.userRegForm.email") private String email; } 

而后在各个语言资源文件中配置好相应的错误信息提示便可。其中, @Gender 就是一个自定义的注解。

基于含自定义注解的表单校验关键代码

自定义注解的实现主要的其实就是一个自定义注解的定义以及一个校验逻辑。 例如定义一个自定义注解 CustomParam

@Documented @Constraint(validatedBy = CustomValidator.class) @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface CustomParam { String message() default "name.tanglei.www.validator.CustomArray.defaultMessage"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default { }; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) @interface List { CustomParam[] value(); } } 

校验逻辑的实现 CustomValidator

public class CustomValidator implements ConstraintValidator<CustomParam, String> { @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { if (null == s || s.isEmpty()) { return true; } if (s.equals("tanglei")) { return true; } else { error(constraintValidatorContext, "Invalid params: " + s); return false; } } @Override public void initialize(CustomParam constraintAnnotation) { } private static void error(ConstraintValidatorContext context, String message) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(message).addConstraintViolation(); } } 

上面例子只为了阐述说明问题,其中校验逻辑没有实际意义,这样,若是输入参数不知足条件,就会明确提示用户输入的哪一个参数不知足条件。例如输入参数 xx,则会直接提示:Invalid params: xx

这个跟第一部分的处理方式相似,由于现有的 validator 组件实现中,若是违反相应的约束也是一种抛异常的方式实现的,所以只须要在上述的 GlobalExceptionHandler中添加相应的异常信息便可,这里就不详述了。 这不是本文的重点,这里就不详细阐述了。 详细代码实现能够参考本人以前写的这篇文章一文教你实现 SpringBoot 中的自定义 Validator 和错误信息国际化配置,上面有附完整的代码实现。

场景重现

一切都显得很完美,直到上线前代码提交至安全团队扫描,就被“啪啪打脸”,扫描报告反馈了一个严重的安全漏洞。而这个安全漏洞,属于很高危的远程代码执行漏洞。

用前文提到的自定义 Validator,输入的参数用: “1+1=${1+1}”,看看效果:

太 TM 神奇了,竟然帮我运算出来了,返回 "message": "Invalid params: 1+1=2"

问题就出如今实现自定义注解进行校验的这行代码(以下图所示):

其实,最开始的时候,这里直接返回了“Invalid params”,当初为了更好的用户体验,要明确告诉用户哪一个参数没有经过校验,所以在输出的提示上加上了用户输入的字段,也就是上面的"Invalid params: " + s,没想到,这闯了大祸了(回过头来想,感受这里不必这么详细啊,由于前端已经有相应的校验了,正常状况下回拦住,针对不守规矩的用很是规手段来的接口请求,直接返回校验不经过就好了,毕竟不是对外提供的 OpenAPI 服务)。

仔细看,这个方法其实是 ConstraintValidatorContext这个接口中声明的,看方法名字其实能知道输入参数是一个字符串模板,内部会进行解析替换的(这其实也符合“见名知意”的良好编程习惯)。(教训:你们应该把握好本身写的每一行代码背后实际在作什么。)

/* ...... * @param messageTemplate new un-interpolated constraint message * @return returns a constraint violation builder */ ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate); 

这个 case,源码调试进去以后,就能跟踪到执行翻译阶段,在以下方法中: org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage

再日后,就是表达式求值了。

觉得就这样就完了吗?

刚开始感受,能帮忙算简单的运算规则也就完了吧,你还能把我怎么样?其实这个至关于暴露了一个入口,支持用户输入任意 EL 表达式进行执行。网上经过关键字 “SpEL表达式注入漏洞” 找找,就能发现事情并无想象中那么简单。

咱们构造恰当的 EL 表达式(注意各类转义,下文的输入参数相对比较明显在作什么了,实际上还有更多黑科技,好比各类二进制转义编码啊等等),就能直接执行输入代码,例如:能够直接执行命令,“ls -al”, 返回了一个 UNIXProcess 实例,命令已经被执行过了。

好比,咱们执行个打开计算器的命令,搞个计算器玩玩~

我录制了一个动图,来个演示可能更生动一些。

这还得了吗?这至关于提供了一个 webshell 的功能呀,你看想运行啥命令就能运行啥命令,例如 ping 本人博客地址(ping www.tanglei.name),下面动图演示一下整个过程(从运行 ping 到 kill ping)。

我录制了一个视频,点击这里能够访问。

岂不是直接建立一个用户,而后远程登陆就能够了。后果很严重啊,别人想干吗就干吗了。

咱们跟踪下对应的代码,看看内部实现,就会“恍然大悟”了。

经验教训

幸好这个漏洞被扼杀在摇篮里,不然后果还真的挺严重的。经过这个案例,咱们有啥经验和教训呢?那就是做为程序员,咱们要对每一行代码都保持“敬畏”之心。也许就是由于你的不经意的一行代码就带来了严重的安全漏洞,要是不当心被坏人利用,轻则……重则……(本身想象吧)

此外,咱们也应该看到,程序员须要对常见的安全漏洞(例如XSS/CSRF/SQL注入等等)有所了解,而且要有足够的安全意识(其实有时候研究一些安全问题还挺好玩的,好比这篇《RSA算法及一种"旁门左道"的攻击方式》就比较有趣)。例如:

  • 用户权限分离:运行程序的用户不该该用 root,例如新建一个“web”或者“www”之类的用户,并设置该用户的权限,好比不能有可执行 xx 的权限之类的。本文 case,若是权限进行了分离(遵循最小权限原则),应该也不会这么严重。(本文就恰好是由于是测试环境,因此没有强制实施)
  • 任什么时候候都不要相信用户的输入,必须对用户输入的进行校验和过滤,又特别是针对公网上的应用。
  • 敏感信息加密保存。退一万步讲,假设攻击者攻入了你的服务器,若是这个时候,你的数据库帐户信息等配置都直接明文保存在服务器中。那数据库也被脱走了。

若是可能的话,须要对开发者的代码进行漏洞扫描。一些常见的安全漏洞如今应该是有现成的工具支持的。另外,让专业的人作专业的事情,例如要有安全团队,可能你会说大家公司没有不也活的好好的,哈哈,只不过可能尚未被坏人盯上而已,坏人也会考虑到他们的成本和预期收益的,固然这就更加对咱们开发者提升了要求。一些敏感权限尽可能控制在少部分人手中,配合相应的流程来支撑(不得不说,大公司繁琐的流程仍是有必定道理的)。

毕竟我不是专业研究Web安全的,以上说得可能也不必定对,若是你有不一样意见或者更好的建议欢迎留言参与讨论。

这篇文章从写代码作实验,到录屏作视频动图等等耗时还蛮久的(好几个周末的时间呢),原创真心不易,但愿你能帮我个小忙呗,若是本文内容你以为有所启发,有所收获,请帮忙点个“在看”呗,或者转发分享让更多的小伙伴看到。

精彩推荐

文章首发于本人微信公众号(ID:tangleithu),请感兴趣的同窗关注个人微信公众号,及时获取技术干货。

相关文章
相关标签/搜索