Skip to content

Commit 80444e5

Browse files
committed
improve telnet logging
1 parent 8112624 commit 80444e5

3 files changed

Lines changed: 244 additions & 41 deletions

File tree

‎docs/logging.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Example shape:
9696
| `opcua` (TCP) | Array of per-direction frames: `direction`, `command`, `path`, `message_type`, `service`, `endpoint_url`, `security_policy`, `application_uri`, `application_name`, `username`, `payload` | `command` is `service` (or `message_type` if empty). `path` copies `endpoint_url`. Username identity is recorded; password bytes are not copied into `decoded`. |
9797
| `mongodb` (TCP) | Array of per-direction frames: `direction`, `header`, `opcode_str`, `command`, `status`, `payload` | `command` is the BSON command name. Writes set `status` `ok`. |
9898
| `mcp` (TCP) | Array of per-direction frames: `direction`, `command`, `path`, `status`, `session_id`, `payload` | `command` is the JSON-RPC method or HTTP verb. Writes set HTTP `status`. Shares the idle session table with HTTP. |
99-
| `telnet` (TCP) | Array of per-direction frames: `direction`, `command`, `message` | Login reads set `command` `username`/`password`; shell reads use the first token. IAC negotiation is a separate `read` frame with no command. |
99+
| `telnet` (TCP) | Array of per-direction frames: `direction`, `command`, `path`, `message`, `payload_hash` | Login reads set `command` `username`/`password`; shell reads use the first token. `wget`/`curl` lines set `path` to the http(s) URL and `payload_hash` when the sample fetch succeeds. IAC negotiation is a separate `read` frame with no command. Process log: Info `telnet login` with `src_ip`/`src_port`/`dest_port`/`username`/`password`; shell lines at Debug. |
100100
| `smtp` (TCP) | Array of per-direction frames: `direction`, `command`, `status`, `payload` | Reads set `command` to the SMTP verb. Writes set `status` to the 3-digit reply code. |
101101
| `ftp` (TCP) | Array of per-direction frames: `direction`, `command`, `path`, `status`, `payload`, `payload_hash` | `command` is the FTP verb. STOR/RETR set `path`. Writes set `status` to the numeric reply code. |
102102
| `rfb` (TCP) | Array of per-direction frames: `direction`, `command`, `payload` | `command` is `ProtocolVersion`, `Security`, `ServerInit`, or `ClientInit`. |

‎protocols/tcp/telnet.go‎

Lines changed: 133 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"net"
1212
"net/http"
1313
"regexp"
14+
"strconv"
1415
"strings"
16+
"sync"
1517
"time"
1618

1719
"github.com/mushorg/glutton/connection"
@@ -23,6 +25,9 @@ import (
2325
// busyboxBanner is the BusyBox ash greeting Mirai checks for after applet probes.
2426
const busyboxBanner = "BusyBox v1.16.1 (2014-03-04 16:00:18 CST) built-in shell (ash)\r\nEnter 'help' for a list of built-in commands.\r\n"
2527

28+
// maxTelnetSample caps bytes fetched from wget/curl URLs.
29+
const maxTelnetSample = 10 << 20
30+
2631
// Mirai botnet - https://github.com/CymmetriaResearch/MTPot/blob/master/mirai_conf.json
2732
// Hajime botnet - https://security.rapiditynetworks.com/publications/2016-10-16/hajime.pdf
2833
var miraiCom = map[string][]string{
@@ -64,27 +69,31 @@ var miraiCom = map[string][]string{
6469
}
6570

6671
type parsedTelnet struct {
67-
Direction string `json:"direction,omitempty"`
68-
Command string `json:"command,omitempty"`
69-
Message string `json:"message,omitempty"`
72+
Direction string `json:"direction,omitempty"`
73+
Command string `json:"command,omitempty"`
74+
Path string `json:"path,omitempty"` // wget/curl URL when present
75+
Message string `json:"message,omitempty"`
76+
PayloadHash string `json:"payload_hash,omitempty"`
7077
}
7178

7279
type telnetServer struct {
73-
events []parsedTelnet
74-
conn net.Conn
75-
reader *bufio.Reader
76-
client *http.Client
77-
step string
80+
events []parsedTelnet
81+
conn net.Conn
82+
reader *bufio.Reader
83+
client *http.Client
84+
step string
85+
sampleWG sync.WaitGroup
86+
sampleMu sync.Mutex
87+
samples map[int]string // event index -> sample hash
7888
}
7989

8090
func newTelnetServer(conn net.Conn) *telnetServer {
8191
return &telnetServer{
82-
events: []parsedTelnet{},
83-
conn: conn,
84-
reader: bufio.NewReader(conn),
85-
client: &http.Client{
86-
Timeout: 5 * time.Second,
87-
},
92+
events: []parsedTelnet{},
93+
conn: conn,
94+
reader: bufio.NewReader(conn),
95+
client: &http.Client{Timeout: 5 * time.Second},
96+
samples: map[int]string{},
8897
}
8998
}
9099

@@ -100,6 +109,10 @@ func telnetShellCommand(msg string) string {
100109
return line
101110
}
102111

112+
func telnetCredential(msg string) string {
113+
return strings.TrimRight(msg, "\r\n\x00")
114+
}
115+
103116
func telnetWriteCommand(msg string) string {
104117
switch msg {
105118
case "Username: ":
@@ -111,6 +124,30 @@ func telnetWriteCommand(msg string) string {
111124
}
112125
}
113126

127+
// telnetDownloadURL extracts an http(s) URL from a wget/curl shell line.
128+
func telnetDownloadURL(cmd string) (string, bool) {
129+
line := strings.TrimRight(cmd, "\r\n\x00")
130+
lower := strings.ToLower(line)
131+
if !strings.Contains(lower, "wget") && !strings.Contains(lower, "curl") {
132+
return "", false
133+
}
134+
idx := strings.Index(lower, "http://")
135+
if idx < 0 {
136+
idx = strings.Index(lower, "https://")
137+
}
138+
if idx < 0 {
139+
return "", false
140+
}
141+
url := line[idx:]
142+
if end := strings.IndexAny(url, " \t\r\n;|&\"'"); end >= 0 {
143+
url = url[:end]
144+
}
145+
if url == "" {
146+
return "", false
147+
}
148+
return url, true
149+
}
150+
114151
// write writes a telnet message to the connection
115152
func (s *telnetServer) write(msg string) error {
116153
if _, err := s.conn.Write([]byte(msg)); err != nil {
@@ -192,35 +229,31 @@ func (s *telnetServer) read() (string, error) {
192229
return msg, err
193230
}
194231

195-
func (s *telnetServer) getSample(cmd string, logger interfaces.Logger) error {
196-
url := cmd[strings.Index(cmd, "http"):]
197-
url = strings.Split(url, " ")[0]
198-
url = strings.TrimSpace(url)
232+
func (s *telnetServer) fetchSample(url string, logger interfaces.Logger) (string, error) {
199233
logger.Debug("Fetching sample", slog.String("url", url), slog.String("handler", "telnet"))
200234
resp, err := s.client.Get(url)
201235
if err != nil {
202-
return err
203-
}
204-
if resp.StatusCode != 200 {
205-
return errors.New("failed to fetch sample: " + resp.Status)
236+
return "", err
206237
}
207238
defer resp.Body.Close()
208-
if resp.ContentLength <= 0 {
209-
return errors.New("content length is 0")
239+
if resp.StatusCode != 200 {
240+
return "", errors.New("failed to fetch sample: " + resp.Status)
210241
}
211242

212-
data, err := io.ReadAll(resp.Body)
243+
data, err := io.ReadAll(io.LimitReader(resp.Body, maxTelnetSample))
213244
if err != nil {
214-
return err
245+
return "", err
215246
}
216-
217247
if len(data) == 0 {
218-
return errors.New("empty response body")
248+
return "", errors.New("empty response body")
219249
}
220250

221251
sha256Hash, err := helpers.Store(data, "samples")
222252
if err != nil {
223-
return err
253+
return "", err
254+
}
255+
if sha256Hash == "" {
256+
sha256Hash = helpers.SHA256Hex(data)
224257
}
225258

226259
logger.Info(
@@ -229,7 +262,43 @@ func (s *telnetServer) getSample(cmd string, logger interfaces.Logger) error {
229262
slog.String("sample_hash", sha256Hash),
230263
slog.String("source", url),
231264
)
232-
return nil
265+
return sha256Hash, nil
266+
}
267+
268+
func (s *telnetServer) startSampleFetch(eventIdx int, url string, logger interfaces.Logger) {
269+
s.sampleWG.Add(1)
270+
go func() {
271+
defer s.sampleWG.Done()
272+
hash, err := s.fetchSample(url, logger)
273+
if err != nil {
274+
logger.Error("Failed to get sample", slog.String("handler", "telnet"), slog.String("source", url), producer.ErrAttr(err))
275+
return
276+
}
277+
s.sampleMu.Lock()
278+
s.samples[eventIdx] = hash
279+
s.sampleMu.Unlock()
280+
}()
281+
}
282+
283+
func (s *telnetServer) applySampleHashes() {
284+
s.sampleWG.Wait()
285+
s.sampleMu.Lock()
286+
defer s.sampleMu.Unlock()
287+
for idx, hash := range s.samples {
288+
if idx >= 0 && idx < len(s.events) {
289+
s.events[idx].PayloadHash = hash
290+
}
291+
}
292+
}
293+
294+
// lastReadEventIndex returns the index of the most recent non-empty read frame.
295+
func (s *telnetServer) lastReadEventIndex() int {
296+
for i := len(s.events) - 1; i >= 0; i-- {
297+
if s.events[i].Direction == "read" && s.events[i].Message != "" {
298+
return i
299+
}
300+
}
301+
return -1
233302
}
234303

235304
// HandleTelnet handles telnet communication on a connection
@@ -240,6 +309,7 @@ func HandleTelnet(ctx context.Context, conn net.Conn, md connection.Metadata, lo
240309
func handleTelnet(ctx context.Context, s *telnetServer, md connection.Metadata, logger interfaces.Logger, h interfaces.Honeypot) error {
241310
endReason := connection.EndHandlerClose
242311
defer func() {
312+
s.applySampleHashes()
243313
md.EndReason = endReason
244314
if err := h.ProduceTCP("telnet", s.conn, md, []byte(helpers.FirstOrEmpty(s.events).Message), s.events); err != nil {
245315
logger.Error("Failed to produce message", slog.String("protocol", "telnet"), producer.ErrAttr(err))
@@ -249,6 +319,9 @@ func handleTelnet(ctx context.Context, s *telnetServer, md connection.Metadata,
249319
}
250320
}()
251321

322+
host, srcPort, _ := net.SplitHostPort(s.conn.RemoteAddr().String())
323+
destPort := strconv.Itoa(int(md.TargetPort))
324+
252325
if err := h.UpdateConnectionTimeout(ctx, s.conn); err != nil {
253326
logger.Debug("Failed to set connection timeout", slog.String("protocol", "telnet"), producer.ErrAttr(err))
254327
endReason = connection.EndTimeout
@@ -269,21 +342,34 @@ func handleTelnet(ctx context.Context, s *telnetServer, md connection.Metadata,
269342
return err
270343
}
271344
s.step = "username"
272-
if _, err := s.read(); err != nil {
345+
userMsg, err := s.read()
346+
if err != nil {
273347
logger.Debug("Failed to read from connection", slog.String("protocol", "telnet"), producer.ErrAttr(err))
274348
endReason = connection.EndReasonFromRead(err)
275349
return nil
276350
}
351+
username := telnetCredential(userMsg)
277352
if err := s.write("Password: "); err != nil {
278353
endReason = connection.EndWriteError
279354
return err
280355
}
281356
s.step = "password"
282-
if _, err := s.read(); err != nil {
357+
passMsg, err := s.read()
358+
if err != nil {
283359
logger.Debug("Failed to read from connection", slog.String("protocol", "telnet"), producer.ErrAttr(err))
284360
endReason = connection.EndReasonFromRead(err)
285361
return nil
286362
}
363+
password := telnetCredential(passMsg)
364+
logger.Info(
365+
"telnet login",
366+
slog.String("handler", "telnet"),
367+
slog.String("src_ip", host),
368+
slog.String("src_port", srcPort),
369+
slog.String("dest_port", destPort),
370+
slog.String("username", username),
371+
slog.String("password", password),
372+
)
287373
if err := s.write("welcome\r\n> "); err != nil {
288374
endReason = connection.EndWriteError
289375
return err
@@ -302,16 +388,23 @@ func handleTelnet(ctx context.Context, s *telnetServer, md connection.Metadata,
302388
endReason = connection.EndReasonFromRead(err)
303389
return nil
304390
}
391+
logger.Debug(
392+
"telnet command",
393+
slog.String("handler", "telnet"),
394+
slog.String("src_ip", host),
395+
slog.String("src_port", srcPort),
396+
slog.String("dest_port", destPort),
397+
slog.String("command", telnetShellCommand(msg)),
398+
slog.String("message", telnetCredential(msg)),
399+
)
400+
if url, ok := telnetDownloadURL(msg); ok {
401+
if idx := s.lastReadEventIndex(); idx >= 0 {
402+
s.events[idx].Path = url
403+
s.startSampleFetch(idx, url, logger)
404+
}
405+
}
305406
skipPrompt := false
306407
for _, cmd := range strings.Split(msg, ";") {
307-
if strings.Contains(strings.Trim(cmd, " "), "wget http") {
308-
go func() {
309-
err := s.getSample(strings.Trim(cmd, " "), logger)
310-
if err != nil {
311-
logger.Error("Failed to get sample", slog.String("handler", "telnet"), producer.ErrAttr(err))
312-
}
313-
}()
314-
}
315408
if strings.TrimRight(cmd, "") == " rm /dev/.t" {
316409
continue
317410
}

0 commit comments

Comments
 (0)