-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
auth.go
284 lines (245 loc) · 9.31 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package x11
import (
"context"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os/exec"
"strings"
"time"
"github.com/gravitational/trace"
)
const (
// mitMagicCookieProto is the default xauth protocol used for X11 forwarding.
mitMagicCookieProto = "MIT-MAGIC-COOKIE-1"
// mitMagicCookieSize is the number of bytes in an mit magic cookie.
mitMagicCookieSize = 16
)
// XAuthEntry is an entry in an XAuthority database which can be used to authenticate
// and authorize requests from an XServer to the associated X display.
type XAuthEntry struct {
// Display is an X display in the format - [hostname]:[display_number].[screen_number]
Display Display `json:"display"`
// Proto is an XAuthority protocol, generally "MIT-MAGIC-COOKIE-1"
Proto string `json:"proto"`
// Cookie is a hex encoded XAuthority cookie
Cookie string `json:"cookie"`
}
// NewFakeXAuthEntry creates a fake xauth entry with a randomly generated MIT-MAGIC-COOKIE-1.
func NewFakeXAuthEntry(display Display) (*XAuthEntry, error) {
cookie, err := newCookie(mitMagicCookieSize)
if err != nil {
return nil, trace.Wrap(err)
}
return &XAuthEntry{
Display: display,
Proto: mitMagicCookieProto,
Cookie: cookie,
}, nil
}
// SpoofXAuthEntry creates a new xauth entry with a random cookie with the
// same length as the original entry's cookie. This is used to create a
// believable spoof of the client's xauth data to send to the server.
func (e *XAuthEntry) SpoofXAuthEntry() (*XAuthEntry, error) {
spoofedCookie, err := newCookie(hex.DecodedLen(len(e.Cookie)))
if err != nil {
return nil, trace.Wrap(err)
}
return &XAuthEntry{
Display: e.Display,
Proto: e.Proto,
Cookie: spoofedCookie,
}, nil
}
// newCookie makes a random hex-encoded cookie with the given byte length.
func newCookie(byteLength int) (string, error) {
cookieBytes := make([]byte, byteLength)
if _, err := rand.Read(cookieBytes); err != nil {
return "", trace.Wrap(err)
}
return hex.EncodeToString(cookieBytes), nil
}
// XAuthCommand is a os/exec.Cmd wrapper for running xauth commands.
type XAuthCommand struct {
*exec.Cmd
}
// NewXAuthCommand reate a new "xauth" command. xauthFile can be
// optionally provided to run the xauth command against a specific xauth file.
func NewXAuthCommand(ctx context.Context, xauthFile string) *XAuthCommand {
var args []string
if xauthFile != "" {
args = []string{"-f", xauthFile}
}
return &XAuthCommand{exec.CommandContext(ctx, "xauth", args...)}
}
// ReadEntry runs "xauth list" to read the first xauth entry for the given display.
func (x *XAuthCommand) ReadEntry(display Display) (*XAuthEntry, error) {
x.Cmd.Args = append(x.Cmd.Args, "list", display.String())
out, err := x.output()
if err != nil {
return nil, trace.Wrap(err)
}
if len(out) == 0 {
return nil, trace.NotFound("no xauth entry found")
}
// Ignore entries beyond the first listed.
entry := strings.Split(string(out), "\n")[0]
splitEntry := strings.Split(entry, " ")
if len(splitEntry) != 3 {
return nil, trace.Errorf("invalid xAuthEntry, expected entry to have three parts")
}
proto, cookie := splitEntry[1], splitEntry[2]
return &XAuthEntry{
Display: display,
Proto: proto,
Cookie: cookie,
}, nil
}
// RemoveEntries runs "xauth remove" to remove any xauth entries for the given display.
func (x *XAuthCommand) RemoveEntries(display Display) error {
x.Cmd.Args = append(x.Cmd.Args, "remove", display.String())
return trace.Wrap(x.run())
}
// AddEntry runs "xauth add" to add the given xauth entry.
func (x *XAuthCommand) AddEntry(entry XAuthEntry) error {
x.Cmd.Args = append(x.Cmd.Args, "add", entry.Display.String(), entry.Proto, entry.Cookie)
return trace.Wrap(x.run())
}
// GenerateUntrustedCookie runs "xauth generate untrusted" to create a new xauth entry with
// an untrusted MIT-MAGIC-COOKIE-1. A timeout can optionally be set for the xauth entry, after
// which the XServer will ignore this cookie.
func (x *XAuthCommand) GenerateUntrustedCookie(display Display, timeout time.Duration) error {
x.Cmd.Args = append(x.Cmd.Args, "generate", display.String(), mitMagicCookieProto, "untrusted")
x.Cmd.Args = append(x.Cmd.Args, "timeout", fmt.Sprint(timeout/time.Second))
return trace.Wrap(x.run())
}
// run the command and return stderr if there is an error.
func (x *XAuthCommand) run() error {
_, err := x.output()
return trace.Wrap(err)
}
// run the command and return stdout or stderr if there is an error.
func (x *XAuthCommand) output() ([]byte, error) {
stdout, err := x.Cmd.StdoutPipe()
if err != nil {
return nil, trace.Wrap(err)
}
stderr, err := x.Cmd.StderrPipe()
if err != nil {
return nil, trace.Wrap(err)
}
if err := x.Cmd.Start(); err != nil {
return nil, trace.Wrap(err)
}
// We add a conservative peak length of 10 KB to prevent potential
// output spam from the client provided `xauth` binary
var peakLength int64 = 10000
out, err := io.ReadAll(io.LimitReader(stdout, peakLength))
if err != nil {
return nil, trace.Wrap(err)
}
errOut, err := io.ReadAll(io.LimitReader(stderr, peakLength))
if err != nil {
return nil, trace.Wrap(err)
}
if err := x.Wait(); err != nil {
return nil, trace.Wrap(err, "command \"%s\" failed with stderr: \"%s\"", strings.Join(x.Cmd.Args, " "), errOut)
}
return out, nil
}
// CheckXAuthPath checks if xauth is runnable in the current environment.
func CheckXAuthPath() error {
_, err := exec.LookPath("xauth")
return trace.Wrap(err)
}
// ReadAndRewriteXAuthPacket reads the initial xauth packet from an XServer request. The xauth packet has 2 parts:
// 1. fixed size buffer (12 bytes) - holds byteOrder bit, and the sizes of the protocol string and auth data
// 2. variable size xauth packet - holds xauth protocol and data used to connect to the remote XServer.
//
// Then it compares the received auth packet with the auth proto and fake cookie
// sent to the server with the original "x11-req". If the data matches, the auth
// packet is returned with the fake cookie replaced by the real cookie to provide
// access to the client's X display.
func ReadAndRewriteXAuthPacket(xreq io.Reader, spoofedXAuthEntry, realXAuthEntry *XAuthEntry) ([]byte, error) {
if spoofedXAuthEntry.Proto != realXAuthEntry.Proto || len(spoofedXAuthEntry.Cookie) != len(realXAuthEntry.Cookie) {
return nil, trace.BadParameter("spoofed and real xauth entries must use the same xauth protocol")
}
// xauth packet starts with a fixed sized buffer of 12 bytes
// which is used to size and decode the remaining bytes
initBuf := make([]byte, xauthPacketInitBufSize)
if _, err := io.ReadFull(xreq, initBuf); err != nil {
return nil, trace.Wrap(err, "X11 channel initial packet buffer missing or too short")
}
protoLen, dataLen, err := readXauthPacketInitBuf(initBuf)
if err != nil {
return nil, trace.Wrap(err)
}
// authPacket size is equal to protoLen (rounded up by 4) + dataLen.
// In openssh, the rounding is performed with: (protoLen + 3) & ~3
authPacketSize := protoLen + (4-protoLen%4)%4 + dataLen
authPacket := make([]byte, authPacketSize)
if _, err := io.ReadFull(xreq, authPacket); err != nil {
return nil, trace.Wrap(err, "X11 channel auth packet missing or too short")
}
proto := authPacket[:protoLen]
authData := authPacket[len(authPacket)-dataLen:]
if string(proto) != spoofedXAuthEntry.Proto || hex.EncodeToString(authData) != spoofedXAuthEntry.Cookie {
return nil, trace.AccessDenied("X11 channel has the wrong authentication data")
}
// Replace auth data with the real auth data
realAuthData, err := hex.DecodeString(realXAuthEntry.Cookie)
if err != nil {
return nil, trace.Wrap(err)
}
copy(authData, realAuthData)
return append(initBuf, authPacket...), trace.Wrap(err)
}
const (
// xauthPacketInitBufSize is the size of the initial
// fixed portion of an xauth packet
xauthPacketInitBufSize = 12
// little endian byte order
littleEndian = 'l'
// big endian byte order
bigEndian = 'B'
)
// readXauthPacketInitBuf reads the initial fixed size portion of
// an xauth packet to get the length of the auth proto and auth data
// portions of the xauth packet.
func readXauthPacketInitBuf(initBuf []byte) (protoLen int, dataLen int, err error) {
// The first byte in the packet determines the
// byte order of the initial buffer's bytes.
var e binary.ByteOrder
switch initBuf[0] {
case bigEndian:
e = binary.BigEndian
case littleEndian:
e = binary.LittleEndian
default:
return 0, 0, trace.BadParameter("X11 channel auth packet has invalid byte order: %v", initBuf[0])
}
// bytes 6-7 and 8-9 are used to determine the length of
// the auth proto and auth data fields respectively.
protoLen = int(e.Uint16(initBuf[6:8]))
dataLen = int(e.Uint16(initBuf[8:10]))
return protoLen, dataLen, nil
}