当前位置: 首页 > 科技观察

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

时间:2023-03-13 20:57:11 科技观察

一行代码造成的安全漏洞让我们失去了对整个服务器扩展的控制,但是本以为还过得去的功能,还没上线就被打脸了,因为一行代码造成的安全漏洞(没有头条党,真的是一行代码)在实现过程中让我们失去了整个服务器的控制权(测试环境)。感谢公司安全团队人员上线前扫码,将此漏洞扼杀在摇篮中。一起来看看这个事故吧,啊,不对,是个故事。背景描述我们的项目是一个面向全球用户的web项目,使用SpringBoot开发。在项目开发过程中,离不开各种异常信息的处理,比如表单提交参数不符合预期,业务逻辑的处理也离不开各种异常信息的处理(比如网络抖动,ETC。)。因此利用SpringBoot各种现成的组件支持,设计了一个统一的异常信息处理组件,统一管理各种业务流程中可能出现的错误代码和错误信息,并通过国际化的资源配置文件统一输出给用户。统一错误信息配置管理。我们的用户遍布世界各地。为了给不同国家的用户更好的体验,我们会进行不同的翻译。具体实现的效果如下。为便于理解,以“找回登录密码”等业务场景进行说明。假设用户在找回密码时,需要输入手机或邮箱验证码。假设此时用户输入的验证码通过与后台数据库(可能是Redis)比对发现已经过期。在业务代码中,只需抛出newErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED)即可。具体来说,不同国家和地区不同语言的结果是不同的:中国用户看到提示“您输入的验证码已过期,请重新获取”;欧美用户看到效果“您输入的验证码已过期,...”;德国用户看到:“DervonIhneneingegebeneVerifizierungscodeistabgelaufen,bittewiederholen”。(我找的翻译不一定准确)...统一错误信息配置管理代码的key信息其实是一个GlobalExceptionHandler,它对所有Controller表项进行AOP拦截,根据获取对应资源文件配置的key不同的错误信息。并从语言资源文件中读取不同国家的错误翻译信息。@ControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(BadRequestException.class)@ResponseBodypublicResponseEntityhandle(HttpServletRequestrequest,BadRequestExceptione){Stringi18message=getI18nMessage(e.getKey(),request);returnResponseEntity.status(HttpStatus.BAD_REQUESTC.error(respongetC.error)),i18message));}@ExceptionHandler(ErrorCodeException.class)@ResponseBodypublicResponseEntityhandle(HttpServletRequestrequest,ErrorCodeExceptione){Stringi18message=getI18nMessage(e.getKey(),request);returnResponse.Stusporp.status(Htt)e.getCode(),i18message)));}}不同语言的资源文件例子基于注解的表单验证(包括自定义注解)另一个常见的业务场景是后台接口需要对用户提交的表单进行验证。以“注册用户”的场景为例。注册用户时,往往会提交昵称、性别、邮箱等信息进行注册。为简单起见,以这三个属性为例。定义的表单如下:publicclassUserRegForm{privateStringnickname;privateStringgender;privateStringemail;}对于表单的约束,我们有:昵称字段:“昵称”为必填项,长度必须为6到20位数字;性别字段:“性别”可选,如果填写必须为“男/女/其他/”之一。除了男人和女人,你是什么意思?是的。毕竟全球用户,去看看必死无疑,还有很多。Email:“email”,必填,必须符合email格式。对于上面的约束,我们只需要在对应的字段中添加如下注解即可。publicclassUserRegForm{@Length(min=6,max=20,message="validate.userRegForm.nickname")privateStringnickname;@Gender(message="validate.userRegForm.gender")privateStringgender;@NotNull@Email(message="validate.userRegForm.email")privateStringemail;}然后在各个语言资源文件中配置相应的错误信息提示。其中,@Gender为自定义注解。基于带有自定义注解的表单验证关键代码,自定义注解的实现主要是自定义注解的定义和验证逻辑。例如定义一个自定义注解CustomParam:@Documented@Constraint(validatedBy=CustomValidator.class)@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)public@interfaceCustomParam{Stringmessage()default"name.tanglei.www.validator.CustomArray.defaultMessage";Cl??ass[]groups()default{};Class[]payload()default{};@Documented@Retention(RetentionPolicy.RUNTIME)@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE})@interfaceList{CustomParam[]value();}}校讯报道的现实||s.isEmpty()){returntrue;}if(s.equals("tanglei")){returntrue;}else{error(constraintValidatorContext,"Invalidparams:"+s);returnfalse;}}@Overridepublicvoidinitialize(CustomParamconstraintAnnotation){}privatestaticvoiderror(ConstraintValidatorContextcontext,Stringmessage){context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate(message).addConstraintViolation();}}上面的例子只是为了说明问题,验证逻辑没有实际意义。这样,如果输入的参数不符合条件,就会明确提示用户是哪个参数输入不符合条件。比如输入参数xx,会直接提示:Invalidparams:xx。这个和第一部分的处理方式类似,因为在现有的validator组件实现中,如果违反了对应的约束,也是以抛出异常的方式实现的,所以只需要添加对应的异常信息即可关于上面提到的GlobalExceptionHandler,这里就不赘述了。这不是本文的重点,这里不再赘述。场景再现,一切看似完美,直到代码上线前提交给安全团队扫描,被“打脸”,扫描报告报告存在严重安全漏洞。并且这个安全漏洞是一个非常高危的远程代码执行漏洞。使用上面提到的自定义Validator,使用入参:"1+1=${1+1}",看看效果:太TM神奇了,居然帮我算出来了,返回"message":"Invalid参数:1+1=2”。问题出在实现自定义注解验证的那一行代码中(如下图):其实最开始这里直接返回了“Invalidparams”。为了更好的用户体验,应该明确告诉用户哪些参数没有通过验证,所以在输出提示中增加了用户输入字段,也就是上面的"Invalidparams:"+s。没想到,这就酿成大祸了(回想起来,感觉这里没必要讲的那么详细,因为前端已经有了相应的验证,正常情况下会被屏蔽。对于不遵守规则的接口请求并且采用非常规手段,直接返回验证失败即可,毕竟不是对外提供OpenAPI服务)。仔细看,这个方法其实是在ConstraintValidatorContext接口中声明的。看方法名其实可以知道入参是一个字符串模板,内部会解析替换。).(教训:每个人都应该掌握你写的每一行代码背后到底发生了什么。)这种情况下,源码调试后,可以追溯到执行翻译阶段,在下面的方法中:org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage。之后就是表达式求值。[图片上传失败...(image-9239c-1591863495667)]以为就这样了?一开始觉得如果能帮上简单的计算规则就完事了。你还能对我做什么?其实这相当于暴露了一个入口,支持用户输入任意的EL表达式来执行。在网上搜索关键词“SpEL表达式注入漏洞”,发现事情并没有想象的那么简单。我们构造合适的EL表达式(注意各种转义,下面输入的参数比较明显是干什么的,其实还有更多的黑科技,比如各种二进制转义编码等),我们可以直接执行输入的代码,例如:可以直接执行命令,“ls-al”,返回一个UNIXProcess实例,命令已经执行。比如我们执行一条打开计算器的命令,玩一玩计算器~我录了个动图,演示一下可能更形象。这值得吗?这相当于提供了一个webshel??l功能。你可以运行任何你想要的命令,比如ping我的博客地址(pingwww.tanglei.name)。下面的动画演示了整个过程(从运行ping到killping)。不是可以直接创建用户,然后远程登录吗。后果很严重,其他人可以为所欲为。我们跟踪对应的代码,看看内部实现,就会“恍然大悟”。经验教训幸好这个漏洞被扼杀在摇篮里,否则后果真的很严重。通过这个案例,我们有哪些经验和教训?即作为程序员,我们要对每一行代码都保持“敬畏”。也许是因为你不经意的一行代码带来了严重的安全漏洞。如果不小心被坏人利用了,轻则...对常见的安全漏洞(如XSS/CSRF/SQL注入等)有了解,有足够的安全意识(其实有的时候是相当的)研究一些安全问题很有趣)。例如:用户权限分离:运行程序的用户不要使用root,例如新建一个用户如“web”或“www”,并设置用户的权限,如无权执行xx。以本文为例,如果权限是分开的(遵循最小权限原则),应该不会那么严重。(本文只是因为是测试环境,所以不强制执行)任何时候都不要相信用户的输入,一定要对用户的输入进行验证和过滤,尤其是公网应用。敏感信息被加密并保存。退后一步,假设攻击者侵入了您的服务器。如果此时,你的数据库账号信息等配置直接明文保存在服务器中。该数据库也被拖走了。如果可能,需要对开发人员的代码进行漏洞扫描。现成的工具现在应该支持一些常见的安全漏洞。另外,让专业的人做专业的事,比如有一个安全团队,你可能会说你的公司没有它也做得很好,哈哈,但不一定会被坏人盯上,坏人也会考虑他们当然,这对我们的开发人员提出了更高的要求。一些敏感权限尽可能控制在少数人手中,并有相应的流程支持(不得不说,大公司繁琐的流程还是有一定道理的)。毕竟我不是网络安全的专业研究人员,上面说的未必正确。如果您有不同的意见或更好的建议,欢迎留言参与讨论。

猜你喜欢