一行代码引来的安全漏洞,就让我们丢失了整个服务器的控制权
來源 |?程序員石頭
責編|?Carol
封圖 |?CSDN 付費下載自視覺中國
之前在某廠的某次項目開發(fā)中,項目組同學設計和實現(xiàn)了一個“引以為傲”,額,有點夸張,不過自認為還說得過去的 feature,結果臨上線前被啪啪打臉,因為實現(xiàn)過程中因為一行代碼(沒有標題黨,真的是一行代碼)帶來的安全漏洞讓我們丟失了整個服務器控制權(測試環(huán)境)。多虧了上線之前有公司安全團隊的人會對代碼進行掃描,才讓這個漏洞被扼殺在搖籃里。
下面我們就一起來看看這個事故,啊,不對,是故事。
背景說明
我們的項目是一個面向全球用戶的 Web 項目,用 SpringBoot 開發(fā)。在項目開發(fā)過程中,離不開各種異常信息的處理,比如表單提交參數(shù)不符合預期,業(yè)務邏輯的處理時離不開各種異常信息(例如網(wǎng)絡抖動等)的處理。于是利用 SpringBoot 各種現(xiàn)成的組件支持,設計了一個統(tǒng)一的異常信息處理組件,統(tǒng)一管理各種業(yè)務流程中可能出現(xiàn)的錯誤碼和錯誤信息,通過國際化的資源配置文件進行統(tǒng)一輸出給用戶。
1、統(tǒng)一錯誤信息配置管理
我們的用戶遍布全球,為了給各個國家用戶比較好的體驗會進行不同的翻譯。具體而言,實現(xiàn)的效果如下,為了方便理解,以“找回登錄密碼”這樣一個業(yè)務場景來進行闡述說明。
假設找回密碼時,需要用戶輸入手機或者郵箱驗證碼,假設這個時候用戶輸入的驗證碼通過后臺數(shù)據(jù)庫(可能是Redis)對比發(fā)現(xiàn)已經(jīng)過期。在業(yè)務代碼中,只需要簡單的throw new ErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED)?即可。具體而言,針對不同國家地區(qū)不同的語言看到的效果不一樣:
中文用戶看到的提示就是“您輸入的驗證碼已過期,請重新獲取”;
歐美用戶看到的效果是“The verification code you input is expired, ...”;
德國用戶看到的是:“Der von Ihnen eingegebene Verifizierungscode ist abgelaufen, bitte wiederholen” 。(我瞎找的翻譯,不一定準)
……
2、統(tǒng)一錯誤信息配置管理代碼實現(xiàn)
關鍵信息其實就在于一個 GlobalExceptionHandler,對所有 Controller 入口進行 AOP 攔截,根據(jù)不同的錯誤信息,獲取相應資源文件配置的 key,并從語言資源文件中讀取不同國家的錯誤翻譯信息。
@ControllerAdvice public?class?GlobalExceptionHandler?{@ExceptionHandler(BadRequestException.class)@ResponseBodypublic?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)@ResponseBodypublic?ResponseEntity?handle(HttpServletRequest?request,?ErrorCodeException?e){String?i18message?=?getI18nMessage(e.getKey(),?request);return?ResponseEntity.status(HttpStatus.OK).body(Response.error(e.getCode(),?i18message));} }不同語言的資源文件示例
詳細代碼實現(xiàn)可以參考本人之前寫的這篇文章一文教你實現(xiàn) SpringBoot 中的自定義 Validator 和錯誤信息國際化配置,上面有附完整的代碼實現(xiàn)。
3、基于注解的表單校驗(含自定義注解)
還有一種常見的業(yè)務場景就是后端接口需要對用戶提交的表單進行校驗。以“注冊用戶”這樣的場景舉例說明, 注冊用戶時,往往會提交昵稱,性別,郵箱等信息進行注冊,簡單起見,就以這 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?就是一個自定義的注解。
4、基于含自定義注解的表單校驗關鍵代碼
自定義注解的實現(xiàn)主要的其實就是一個自定義注解的定義以及一個校驗邏輯。例如定義一個自定義注解?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();} }校驗邏輯的實現(xiàn)?CustomValidator:
public?class?CustomValidator?implements?ConstraintValidator<CustomParam,?String>?{@Overridepublic?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;}}@Overridepublic?void?initialize(CustomParam?constraintAnnotation)?{}private?static?void?error(ConstraintValidatorContext?context,?String?message)?{context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate(message).addConstraintViolation();} }上面例子只為了闡述說明問題,其中校驗邏輯沒有實際意義,這樣,如果輸入?yún)?shù)不滿足條件,就會明確提示用戶輸入的哪個參數(shù)不滿足條件。例如輸入?yún)?shù)xx,則會直接提示:Invalid params: xx。
(點擊查看大圖)
這個跟第一部分的處理方式類似,因為現(xiàn)有的 validator 組件實現(xiàn)中,如果違反相應的約束也是一種拋異常的方式實現(xiàn)的,因此只需要在上述的GlobalExceptionHandler中添加相應的異常信息即可,這里就不詳述了。這不是本文的重點,這里就不詳細闡述了。
場景重現(xiàn)
一切都顯得很完美,直到上線前代碼提交至安全團隊掃描,就被“啪啪打臉”,掃描報告反饋了一個嚴重的安全漏洞。而這個安全漏洞,屬于很高危的遠程代碼執(zhí)行漏洞。
用前文提到的自定義 Validator,輸入的參數(shù)用:“1+1=${1+1}”,看看效果:
(點擊查看大圖)
太 TM 神奇了,居然幫我運算出來了,返回"message": "Invalid params: 1+1=2"。
問題就出現(xiàn)在實現(xiàn)自定義注解進行校驗的這行代碼(如下圖所示):
其實,最開始的時候,這里直接返回了“Invalid params”,當初為了更好的用戶體驗,要明確告訴用戶哪個參數(shù)沒有通過校驗,因此在輸出的提示上加上了用戶輸入的字段,也就是上面的"Invalid?params: " + s,沒想到,這闖了大禍了(回過頭來想,感覺這里沒必要這么詳細啊,因為前端已經(jīng)有相應的校驗了,正常情況下回攔住,針對不守規(guī)矩的用非常規(guī)手段來的接口請求,直接返回校驗不通過就行了,畢竟不是對外提供的 OpenAPI 服務)。
仔細看,這個方法實際上是ConstraintValidatorContext這個接口中聲明的,看方法名字其實能知道輸入?yún)?shù)是一個字符串模板,內部會進行解析替換的(這其實也符合“見名知意”的良好編程習慣)。(教訓:大家應該把握好自己寫的每一行代碼背后實際在做什么。)
/*?......*?@param?messageTemplate?new?un-interpolated?constraint?message*?@return?returns?a?constraint?violation?builder*/ ConstraintViolationBuilder?buildConstraintViolationWithTemplate(String?messageTemplate);這個 case,源碼調試進去之后,就能跟蹤到執(zhí)行翻譯階段,在如下方法中:org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage。
(點擊查看大圖)
再往后,就是表達式求值了。
以為這樣就完了嗎?
剛開始感覺,能幫忙算簡單的運算規(guī)則也就完了吧,你還能把我怎么樣?其實這個相當于暴露了一個入口,支持用戶輸入任意 EL 表達式進行執(zhí)行。網(wǎng)上通過關鍵字 “SpEL表達式注入漏洞” 找找,就能發(fā)現(xiàn)事情并沒有想象中那么簡單。
我們構造恰當?shù)?EL 表達式(注意各種轉義,下文的輸入?yún)?shù)相對比較明顯在做什么了,實際上還有更多黑科技,比如各種二進制轉義編碼啊等等),就能直接執(zhí)行輸入代碼,例如:可以直接執(zhí)行命令,“l(fā)s -al”, 返回了一個 UNIXProcess 實例,命令已經(jīng)被執(zhí)行過了。
(點擊查看大圖)
比如,我們執(zhí)行個打開計算器的命令,搞個計算器玩玩~
(圖片放大看得更清楚)
我錄制了一個動圖,來個演示可能更生動一些。
這還得了嗎?這相當于直接在公網(wǎng)上提供了一個 WebShell 的功能呀,你看,想運行啥命令就能運行啥命令,例如 ping 本人博客地址(ping www.tanglei.name),下面動圖(gif 圖上傳總是失敗,試試微信公眾號嵌入視頻功能)演示一下整個過程(從運行 ping 到 kill ping)。
這樣豈不是直接創(chuàng)建一個用戶,然后遠程登錄就可以了。后果非常嚴重啊,別人想干嘛就干嘛了。
漏洞根因
我們跟蹤下對應的代碼,看看內部實現(xiàn),就會“恍然大悟”了。
(點擊查看大圖)
(點擊查看大圖)
經(jīng)驗教訓
幸虧這個漏洞被扼殺在搖籃里,否則后果還真的挺嚴重的。通過這個案例,我們有啥經(jīng)驗和教訓呢?那就是作為程序員,我們要對每一行代碼都保持“敬畏”之心。也許就是因為你的不經(jīng)意的一行代碼就帶來了嚴重的安全漏洞,要是不小心被壞人利用,輕則……重則……(自己想象吧)
此外,我們也應該看到,程序員需要對常見的安全漏洞(例如XSS/CSRF/SQL注入等等)有所了解,并且要有足夠的安全意識(其實有時候研究一些安全問題還挺好玩的,例如:
用戶權限分離:運行程序的用戶不應該用 root,例如新建一個“web”或者“www”之類的用戶,并設置該用戶的權限,比如不能有可執(zhí)行 xx 的權限之類的。本文 case,如果權限進行了分離(遵循最小權限原則),應該也不會這么嚴重。(本文就剛好是因為是測試環(huán)境,所以沒有強制實施)
任何時候都不要相信用戶的輸入,必須對用戶輸入的進行校驗和過濾,又特別是針對公網(wǎng)上的應用。
敏感信息加密保存。退一萬步講,假設攻擊者攻入了你的服務器,如果這個時候,你的數(shù)據(jù)庫賬戶信息等配置都直接明文保存在服務器中。那數(shù)據(jù)庫也被脫走了。
如果可能的話,需要對開發(fā)者的代碼進行漏洞掃描。一些常見的安全漏洞現(xiàn)在應該是有現(xiàn)成的工具支持的。另外,讓專業(yè)的人做專業(yè)的事情,例如要有安全團隊,可能你會說你們公司沒有不也活的好好的,哈哈,只不過可能還沒有被壞人盯上而已,壞人也會考慮到他們的成本和預期收益的,當然這就更加對我們開發(fā)者提高了要求。一些敏感權限盡量控制在少部分人手中,配合相應的流程來支撐(不得不說,大公司繁瑣的流程還是有一定道理的)。
如果你對本文有不同意見或者更好的建議,歡迎留言參與討論。
推薦閱讀
云計算,巨頭們的背水一戰(zhàn)
整理了一份Docker系統(tǒng)知識,從安裝到熟練操作看這篇就夠了|?原力計劃
借助大數(shù)據(jù)進行社交媒體營銷,企業(yè)們得這么玩!
追憶童年,教你用Python畫出兒時卡通人物
AI 終極問題:我們的大腦是一臺超級計算機嗎?
公鏈的歷史交叉口:PoS還能走多遠?
真香,朕在看了!
總結
以上是生活随笔為你收集整理的一行代码引来的安全漏洞,就让我们丢失了整个服务器的控制权的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 玛莎拉蒂“跨界”腾讯车联打造车载互联系统
- 下一篇: 拯救运维工程师,数据链 DNA 来袭!