Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/utils/telnetmini/telnet.go
2843 views
1
// telnetmini.go
2
// Minimal Telnet helper for authentication + simple I/O over an existing or new connection.
3
4
package telnetmini
5
6
import (
7
"bufio"
8
"context"
9
"errors"
10
"fmt"
11
"io"
12
"net"
13
"strings"
14
"time"
15
)
16
17
// Telnet protocol constants
18
const (
19
IAC = 255 // Interpret As Command
20
WILL = 251 // Will
21
WONT = 252 // Won't
22
DO = 253 // Do
23
DONT = 254 // Don't
24
SB = 250 // Subnegotiation Begin
25
SE = 240 // Subnegotiation End
26
ENCRYPT = 38 // Encryption option (0x26)
27
)
28
29
// EncryptionInfo contains information about telnet encryption support
30
type EncryptionInfo struct {
31
SupportsEncryption bool
32
Banner string
33
Options map[int][]int
34
}
35
36
// Client wraps a Telnet connection with tiny helpers.
37
type Client struct {
38
Conn net.Conn
39
rd *bufio.Reader
40
wr *bufio.Writer
41
LoginPrompts []string // matched case-insensitively
42
UserPrompts []string // alternative to LoginPrompts; if empty, LoginPrompts used for username step
43
PasswordPrompts []string
44
FailBanners []string // e.g., "login incorrect", "authentication failed"
45
ShellPrompts []string // e.g., "$ ", "# ", "> "
46
ReadCapBytes int // safety cap while scanning (default 64 KiB)
47
}
48
49
// Defaults sets reasonable prompt patterns if none provided.
50
func (c *Client) Defaults() {
51
if c.ReadCapBytes == 0 {
52
c.ReadCapBytes = 64 * 1024
53
}
54
if len(c.LoginPrompts) == 0 {
55
c.LoginPrompts = []string{"login:", "username:"}
56
}
57
if len(c.PasswordPrompts) == 0 {
58
c.PasswordPrompts = []string{"password:"}
59
}
60
if len(c.FailBanners) == 0 {
61
c.FailBanners = []string{"login incorrect", "authentication failed", "login failed"}
62
}
63
if len(c.ShellPrompts) == 0 {
64
c.ShellPrompts = []string{"$ ", "# ", "> "}
65
}
66
if len(c.UserPrompts) == 0 {
67
c.UserPrompts = c.LoginPrompts
68
}
69
}
70
71
// New wraps an existing net.Conn.
72
func New(conn net.Conn) *Client {
73
c := &Client{
74
Conn: conn,
75
rd: bufio.NewReader(conn),
76
wr: bufio.NewWriter(conn),
77
}
78
c.Defaults()
79
return c
80
}
81
82
// Close closes the underlying connection.
83
func (c *Client) Close() error {
84
return c.Conn.Close()
85
}
86
87
// DetectEncryption detects if a telnet server supports encryption.
88
// Based on Nmap's telnet-encryption.nse script functionality.
89
// WARNING: The connection becomes unusable after calling this function
90
// due to the encryption negotiation packets sent.
91
func DetectEncryption(conn net.Conn, timeout time.Duration) (*EncryptionInfo, error) {
92
if timeout == 0 {
93
timeout = 7 * time.Second
94
}
95
96
// Set connection timeout
97
_ = conn.SetDeadline(time.Now().Add(timeout))
98
99
// Send encryption negotiation packet (based on Nmap script)
100
// FF FD 26 FF FB 26 = IAC DO ENCRYPT IAC WILL ENCRYPT
101
encryptionPacket := []byte{IAC, DO, ENCRYPT, IAC, WILL, ENCRYPT}
102
_, err := conn.Write(encryptionPacket)
103
if err != nil {
104
return nil, fmt.Errorf("failed to send encryption packet: %w", err)
105
}
106
107
// Process server responses
108
options := make(map[int][]int)
109
supportsEncryption := false
110
banner := ""
111
112
// Read responses until we get encryption info or timeout
113
for {
114
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
115
buffer := make([]byte, 1024)
116
n, err := conn.Read(buffer)
117
if err != nil {
118
// Timeout or connection closed, break
119
break
120
}
121
122
if n > 0 {
123
data := buffer[:n]
124
// Check if this contains banner text (non-IAC bytes)
125
for _, b := range data {
126
if b != IAC {
127
banner += string(b)
128
}
129
}
130
131
// Process telnet options
132
encrypted, opts := processTelnetOptions(data)
133
if encrypted {
134
supportsEncryption = true
135
}
136
137
// Merge options
138
for opt, cmds := range opts {
139
if options[opt] == nil {
140
options[opt] = make([]int, 0)
141
}
142
options[opt] = append(options[opt], cmds...)
143
}
144
145
// Check if we have encryption info
146
if cmds, exists := options[ENCRYPT]; exists {
147
for _, cmd := range cmds {
148
if cmd == WILL || cmd == DO {
149
supportsEncryption = true
150
break
151
}
152
}
153
}
154
}
155
}
156
157
return &EncryptionInfo{
158
SupportsEncryption: supportsEncryption,
159
Banner: banner,
160
Options: options,
161
}, nil
162
}
163
164
// processTelnetOptions processes telnet protocol options and returns encryption support status
165
func processTelnetOptions(data []byte) (bool, map[int][]int) {
166
options := make(map[int][]int)
167
supportsEncryption := false
168
169
for i := 0; i < len(data); i++ {
170
if data[i] == IAC && i+2 < len(data) {
171
cmd := data[i+1]
172
option := data[i+2]
173
174
// Initialize option slice if not exists
175
optInt := int(option)
176
if options[optInt] == nil {
177
options[optInt] = make([]int, 0)
178
}
179
options[optInt] = append(options[optInt], int(cmd))
180
181
// Check for encryption support
182
if option == ENCRYPT && (cmd == WILL || cmd == DO) {
183
supportsEncryption = true
184
}
185
186
// Handle subnegotiation
187
if cmd == SB {
188
// Skip until SE
189
for j := i + 3; j < len(data); j++ {
190
if data[j] == IAC && j+1 < len(data) && data[j+1] == SE {
191
i = j + 1
192
break
193
}
194
}
195
} else {
196
i += 2 // Skip command and option
197
}
198
}
199
}
200
201
return supportsEncryption, options
202
}
203
204
// Auth performs a minimal Telnet username/password interaction.
205
// It waits for a username/login prompt, sends username, waits for a password prompt,
206
// sends password, and then looks for fail banners or shell prompts.
207
// A timeout should be enforced via ctx.
208
func (c *Client) Auth(ctx context.Context, username, password string) error {
209
// Wait for username/login prompt
210
if _, _, err := c.readUntil(ctx, c.UserPrompts...); err != nil {
211
return fmt.Errorf("waiting for login/username prompt: %w", err)
212
}
213
if err := c.writeLine(ctx, username); err != nil {
214
return fmt.Errorf("sending username: %w", err)
215
}
216
217
// Wait for password prompt
218
if _, _, err := c.readUntil(ctx, c.PasswordPrompts...); err != nil {
219
return fmt.Errorf("waiting for password prompt: %w", err)
220
}
221
if err := c.writeLine(ctx, password); err != nil {
222
return fmt.Errorf("sending password: %w", err)
223
}
224
225
// Post-auth: look quickly for explicit failure, else accept shell prompt / silence.
226
match, got, err := c.readUntil(ctx,
227
append(append([]string{}, c.FailBanners...), c.ShellPrompts...)...,
228
)
229
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
230
return fmt.Errorf("post-auth read: %s (got: %s)", preview(got, 200), err)
231
}
232
low := strings.ToLower(match)
233
for _, fb := range c.FailBanners {
234
if low == strings.ToLower(fb) {
235
return errors.New("authentication failed")
236
}
237
}
238
// success (matched a shell prompt or timed out without explicit failure)
239
return nil
240
}
241
242
// Exec sends a command followed by CRLF and returns text captured until one of
243
// the provided prompts appears (typically your shell prompt). Provide a deadline via ctx.
244
func (c *Client) Exec(ctx context.Context, command string, until ...string) (string, error) {
245
if err := c.writeLine(ctx, command); err != nil {
246
return "", err
247
}
248
_, out, err := c.readUntil(ctx, until...)
249
return out, err
250
}
251
252
// --- internals ---
253
254
// writeLine writes s + CRLF and flushes.
255
func (c *Client) writeLine(ctx context.Context, s string) error {
256
c.setDeadlineFromCtx(ctx, true)
257
if _, err := io.WriteString(c.wr, s+"\r\n"); err != nil {
258
return err
259
}
260
return c.wr.Flush()
261
}
262
263
// readUntil scans bytes, handles minimal Telnet IAC negotiation, and returns when any needle appears.
264
func (c *Client) readUntil(ctx context.Context, needles ...string) (matched string, bufStr string, err error) {
265
if len(needles) == 0 {
266
return "", "", errors.New("readUntil: no needles provided")
267
}
268
c.setDeadlineFromCtx(ctx, false)
269
270
lowNeedles := make([]string, len(needles))
271
for i, n := range needles {
272
lowNeedles[i] = strings.ToLower(n)
273
}
274
275
var b strings.Builder
276
tmp := make([]byte, 1)
277
278
// Maximum iteration counter to prevent infinite loops
279
maxIterations := 20
280
iterationCount := 0
281
282
for {
283
iterationCount++
284
// if we have iterated more than maxIterations, return
285
if iterationCount > maxIterations {
286
return "", b.String(), nil
287
}
288
// honor context deadline on every read
289
c.setDeadlineFromCtx(ctx, false)
290
_, err := c.rd.Read(tmp)
291
if err != nil {
292
if ne, ok := err.(net.Error); ok && ne.Timeout() {
293
return "", b.String(), context.DeadlineExceeded
294
}
295
return "", b.String(), err
296
}
297
298
// Telnet IAC (Interpret As Command)
299
if tmp[0] == 255 { // IAC
300
cmd, err := c.rd.ReadByte()
301
if err != nil {
302
return "", b.String(), err
303
}
304
switch cmd {
305
case 251, 252, 253, 254: // WILL, WONT, DO, DONT
306
opt, err := c.rd.ReadByte()
307
if err != nil {
308
return "", b.String(), err
309
}
310
// Politely refuse everything: DONT to WILL; WONT to DO.
311
var reply []byte
312
if cmd == 251 { // WILL
313
reply = []byte{255, 254, opt} // DONT
314
}
315
if cmd == 253 { // DO
316
reply = []byte{255, 252, opt} // WONT
317
}
318
if len(reply) > 0 {
319
c.setDeadlineFromCtx(ctx, true)
320
_, _ = c.wr.Write(reply)
321
_ = c.wr.Flush()
322
}
323
case 250: // SB (subnegotiation): skip until SE
324
for {
325
bb, err := c.rd.ReadByte()
326
if err != nil {
327
return "", b.String(), err
328
}
329
if bb == 255 {
330
if se, err := c.rd.ReadByte(); err == nil && se == 240 { // SE
331
break
332
}
333
}
334
}
335
default:
336
// NOP for other commands (IAC NOP, GA, etc.)
337
}
338
continue
339
}
340
341
// regular data byte
342
b.WriteByte(tmp[0])
343
lower := strings.ToLower(b.String())
344
for i, n := range lowNeedles {
345
if strings.Contains(lower, n) {
346
return needles[i], b.String(), nil
347
}
348
}
349
if b.Len() > c.ReadCapBytes {
350
return "", b.String(), errors.New("prompt not found (read cap reached)")
351
}
352
}
353
}
354
355
func (c *Client) setDeadlineFromCtx(ctx context.Context, write bool) {
356
if ctx == nil {
357
return
358
}
359
if dl, ok := ctx.Deadline(); ok {
360
_ = c.Conn.SetReadDeadline(dl)
361
if write {
362
_ = c.Conn.SetWriteDeadline(dl)
363
}
364
}
365
}
366
367
func preview(s string, n int) string {
368
if len(s) <= n {
369
return s
370
}
371
return s[:n] + "..."
372
}
373
374