官术网_书友最值得收藏!

4.3.2 漏洞分析

我們首先對漏洞涉及的處理流程進行分析描述,在明確了相關流程后,再對漏洞點進行剖析。

漏洞位于staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go中。upgradeaware.go主要用來處理API Server的代理邏輯。其中ServeHTTP函數用來具體處理一個代理請求:


//staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.go
//ServeHTTP handles the proxy request
func (h *UpgradeAwareHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if h.tryUpgrade(w, req) {
        return
    }
    if h.UpgradeRequired {
        h.Responder.Error(w, req, errors.NewBadRequest("Upgrade request required"))
        return
    }
    //省略
}

它在最開始調用了tryUpgrade函數,嘗試進行協議升級。漏洞正存在于該函數的處理邏輯之中,我們仔細看一下。

首先,該函數要判斷原始請求是否為協議升級請求(請求頭中是否包含Connection和Upgrade項):


if !httpstream.IsUpgradeRequest(req) {
    glog.V(6).Infof("Request was not an upgrade")
    return false
}

接著,它建立了到后端服務的連接:


if h.InterceptRedirects {
    glog.V(6).Infof("Connecting to backend proxy (intercepting redirects)
        %s\n  Headers: %v", &location, clone.Header)
    backendConn, rawResponse, err = utilnet.ConnectWithRedirects(req.Method,
        &location, clone.Header, req.Body, utilnet.DialerFunc(h.DialForUpgrade))
} else {
    glog.V(6).Infof("Connecting to backend proxy (direct dial) %s\n  Headers:
        %v", &location, clone.Header)
    clone.URL = &location
    backendConn, err = h.DialForUpgrade(clone)
}
if err != nil {
    glog.V(6).Infof("Proxy connection error: %v", err)
    h.Responder.Error(w, req, err)
    return true
}
defer backendConn.Close()

然后,tryUpgrade函數進行了HTTP Hijack操作,簡單來說,就是不再將HTTP連接處理委托給Go語言內置的處理流程,程序自身在TCP連接基礎上進行HTTP交互,這是從HTTP升級到WebSocket的關鍵步驟之一:


//Once the connection is hijacked, the ErrorResponder will no longer work, so
//hijacking should be the last step in the upgrade.
requestHijacker, ok := w.(http.Hijacker)
if !ok {
    glog.V(6).Infof("Unable to hijack response writer: %T", w)
    h.Responder.Error(w, req, fmt.Errorf("request connection cannot be
        hijacked: %T", w))
    return true
}
requestHijackedConn, _, err := requestHijacker.Hijack()
if err != nil {
    glog.V(6).Infof("Unable to hijack response: %v", err)
    h.Responder.Error(w, req, fmt.Errorf("error hijacking connection: %v", err))
    return true
}
defer requestHijackedConn.Close()

緊接著,tryUpgrade將后端針對上一次請求的響應返回給客戶端:


//Forward raw response bytes back to client.
if len(rawResponse) > 0 {
    glog.V(6).Infof("Writing %d bytes to hijacked connection", len(rawResponse))
    if _, err = requestHijackedConn.Write(rawResponse); err != nil {
        utilruntime.HandleError(fmt.Errorf("Error proxying response from backend
            to client: %v", err))
    }
}

函數的最后,客戶端到后端服務的代理通道被建立起來:


//Proxy the connection.
wg := &sync.WaitGroup{}
wg.Add(2)

go func() {
    var writer io.WriteCloser
    //省略
}()

go func() {
    var reader io.ReadCloser
    //省略
}()

wg.Wait()
return true

這是API Server視角下建立代理的流程。那么,在這個過程中,后端服務又是如何參與的呢?

我們以Kubelet為例,當用戶對某個Pod執行exec操作時,該請求經過上面API Server的代理,發給Kubelet。Kubelet在初始化時會啟動一個自己的API Server(為便于區分,后文所有單獨出現的API Server均指的是Kubernetes API Server,用Kubelet API Server指代Kubelet內部的API Server),其代碼實現在pkg/kubelet/server/server.go中。從該文件中我們可以看到,Kubelet啟動時會注冊一系列API,/exec就在其中(由InstallDebuggingHandlers函數注冊),注冊的對應處理函數為:


//getExec handles requests to run a command inside a container.
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
    params := getExecRequestParams(request)
    //創建一個Options實例
    streamOpts, err := remotecommandserver.NewOptions(request.Request)
    if err != nil {
        utilruntime.HandleError(err)
        response.WriteError(http.StatusBadRequest, err)
        return
    }
    pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
    if !ok {
        response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
        return
    }
    //將客戶端與Pod對接,客戶端直接與Pod交互,執行命令,獲取結果
    podFullName := kubecontainer.GetPodFullName(pod)
    url, err := s.host.GetExec(podFullName, params.podUID, params.containerName,
        params.cmd, *streamOpts)
    if err != nil {
        streaming.WriteError(err, response.ResponseWriter)
        return
    }
    if s.redirectContainerStreaming {
        http.Redirect(response.ResponseWriter, request.Request, url.String(),
            http.StatusFound)
        return
    }
    proxyStream(response.ResponseWriter, request.Request, url)
}

也就是說,如果一切順利的話,當客戶端發起一個對Pod執行exec操作的請求時,經過API Server的代理、Kubelet的轉發,最終客戶端與Pod間建立起了連接。

那么,問題可能出現在什么地方呢?我們分情況討論一下:

1)如果請求本身不具有相應Pod的操作權限,它在API Server環節就會被攔截下來,不會到達Kubelet,這個處理沒有問題。

2)如果請求本身具有相應Pod的操作權限,且請求符合API要求(URL正確、參數齊全等),API Server建立起代理,Kubelet將流量轉發到Pod上,一條客戶端到指定Pod的命令執行連接被建立,這也沒有問題,因為客戶端本身具有相應Pod的操作權限。

3)如果請求本身具有相應Pod的操作權限,但是發出的請求并不符合API要求(如參數指定錯誤等),API Server同樣會建立起代理,將請求轉發給Kubelet,這種情況下會發生什么呢?

回顧上面給出的在Kubelet的/exec處理函數getExec中,一個Options實例被創建:


streamOpts, err := remotecommandserver.NewOptions(request.Request)

跟進看一下remotecommandserver.NewOptions函數:


//NewOptions creates a new Options from the Request.
func NewOptions(req *http.Request) (*Options, error) {
    tty := req.FormValue(api.ExecTTYParam) == "1"
    stdin := req.FormValue(api.ExecStdinParam) == "1"
    stdout := req.FormValue(api.ExecStdoutParam) == "1"
    stderr := req.FormValue(api.ExecStderrParam) == "1"
    if tty && stderr {
        glog.V(4).Infof("Access to exec with tty and stderr is not supported,
            bypassing stderr")
        stderr = false
    }
    if !stdin && !stdout && !stderr {
        return nil, fmt.Errorf("you must specify at least 1 of stdin, stdout, stderr")
    }
    return &Options{
        Stdin:  stdin,
        Stdout: stdout,
        Stderr: stderr,
        TTY:    tty,
    }, nil
}

可以看到,如果請求中stdin、stdout和stderr三個參數都沒有給出,Options實例將創建失敗,getExec函數將直接返回給客戶端一個http.StatusBadRequest信息:


if err != nil {
    utilruntime.HandleError(err)
    response.WriteError(http.StatusBadRequest, err)
    return
}

回到我們上面說的第三種情況。結合API Server tryUpgrade代碼可以發現,API Server并沒有對這種錯誤情況進行處理,依然通過兩個Goroutine為客戶端到Kubelet建立了WebSocket連接!問題在于,這個連接并沒有對接到某個Pod上(因為前面getExec失敗返回了),也沒有被銷毀,客戶端可以繼續通過這個連接向Kubelet下發指令。由于經過了API Server的代理,因此指令是以API Server的權限向Kubelet下發的。也就是說,客戶端自此能夠自由向該Kubelet下發指令而不受限制,從而實現了權限提升,這就是CVE-2018-1002105漏洞的成因。

主站蜘蛛池模板: 金秀| 肃南| 革吉县| 临漳县| 增城市| 吉隆县| 芮城县| 凤翔县| 克什克腾旗| 门头沟区| 集贤县| 曲周县| 德安县| 监利县| 高台县| 万全县| 阿克| 安顺市| 姚安县| 宜君县| 彝良县| 吴旗县| 镇平县| 监利县| 栖霞市| 南投市| 威信县| 姜堰市| 收藏| 黎平县| 新安县| 霞浦县| 德令哈市| 儋州市| 普兰店市| 六盘水市| 营口市| 永仁县| 斗六市| 靖州| 云南省|