diff --git a/modules/zerogod/zerogod_acceptor.go b/modules/zerogod/zerogod_acceptor.go index 81988f02..76fe2897 100644 --- a/modules/zerogod/zerogod_acceptor.go +++ b/modules/zerogod/zerogod_acceptor.go @@ -42,6 +42,7 @@ type Acceptor struct { } type HandlerContext struct { + service string mod *ZeroGod client net.Conn srvHost string @@ -100,8 +101,9 @@ func (a *Acceptor) Start() (err error) { a.mod.Error("%v", err) } } else { - a.mod.Info("accepted connection for service %s (port %d): %v", tui.Green(a.service), a.port, conn.RemoteAddr()) + a.mod.Debug("accepted connection for service %s (port %d): %v", tui.Green(a.service), a.port, conn.RemoteAddr()) go a.handler.Handle(&HandlerContext{ + service: a.service, mod: a.mod, client: conn, srvHost: a.srvHost, diff --git a/modules/zerogod/zerogod_advertise.go b/modules/zerogod/zerogod_advertise.go index 2aa8ae27..9c5eee3f 100644 --- a/modules/zerogod/zerogod_advertise.go +++ b/modules/zerogod/zerogod_advertise.go @@ -101,7 +101,9 @@ func (mod *ZeroGod) startAdvertiser(fileName string) error { return fmt.Errorf("could not deserialize %s: %v", fileName, err) } - mod.Info("loaded %d services from %s", len(services), fileName) + numServices := len(services) + + mod.Info("loaded %d services from %s", numServices, fileName) advertiser := &Advertiser{ Filename: fileName, @@ -110,36 +112,29 @@ func (mod *ZeroGod) startAdvertiser(fileName string) error { } // paralleize initialization - svcChan := make(chan error) + svcChan := make(chan error, numServices) for _, svc := range advertiser.Services { go func(svc *ServiceData) { // deregister the service from the network first if err := svc.Unregister(mod); err != nil { svcChan <- fmt.Errorf("could not unregister service %s: %v", svc.FullName(), err) - return - } - - // give some time to the network to adjust - time.Sleep(time.Duration(1) * time.Second) - - // register it - if err := svc.Register(mod); err != nil { - svcChan <- err } else { - svcChan <- nil + // give some time to the network to adjust + time.Sleep(time.Duration(1) * time.Second) + // register it + if err := svc.Register(mod, hostName); err != nil { + svcChan <- err + } else { + svcChan <- nil + } } }(svc) } - got := 0 - for err := range svcChan { - if err != nil { + for i := 0; i < numServices; i++ { + if err := <-svcChan; err != nil { return err } - got++ - if got == len(advertiser.Services) { - break - } } // now create the tcp acceptors for entries without an explicit responder address diff --git a/modules/zerogod/zerogod_discovery.go b/modules/zerogod/zerogod_discovery.go index 8a88b45e..2322dfc8 100644 --- a/modules/zerogod/zerogod_discovery.go +++ b/modules/zerogod/zerogod_discovery.go @@ -100,6 +100,11 @@ func NewZeroGod(s *session.Session) *ZeroGod { tls.CertConfigToModule("zerogod.advertise", &mod.SessionModule, tls.DefaultLegitConfig) + mod.AddParam(session.NewStringParameter("zerogod.ipp.save_path", + "~/.bettercap/zerogod/documents/", + "", + "If an IPP acceptor is started, this setting defines where to save documents received for printing.")) + return mod } diff --git a/modules/zerogod/zerogod_generic_handler.go b/modules/zerogod/zerogod_generic_handler.go index f3b57a1a..02bcff0c 100644 --- a/modules/zerogod/zerogod_generic_handler.go +++ b/modules/zerogod/zerogod_generic_handler.go @@ -2,6 +2,8 @@ package zerogod import ( "fmt" + + "github.com/evilsocket/islazy/tui" ) func Dump(by []byte) string { @@ -53,6 +55,8 @@ func viewString(b []byte) string { func handleGenericTCP(ctx *HandlerContext) { defer ctx.client.Close() + ctx.mod.Debug("accepted generic tcp connection for service %s (port %d): %v", tui.Green(ctx.service), ctx.srvPort, ctx.client.RemoteAddr()) + buf := make([]byte, 1024) for { if read, err := ctx.client.Read(buf); err != nil { diff --git a/modules/zerogod/zerogod_ipp_handler.go b/modules/zerogod/zerogod_ipp_handler.go index e39a3911..a0e1145d 100644 --- a/modules/zerogod/zerogod_ipp_handler.go +++ b/modules/zerogod/zerogod_ipp_handler.go @@ -3,17 +3,28 @@ package zerogod import ( "bufio" "bytes" + "encoding/json" "fmt" "io" + "io/ioutil" "net/http" + "os" + "path" + "slices" + "strconv" + "strings" "time" + "github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/ops" + "github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/tui" "github.com/phin1x/go-ipp" ) +const IPP_CHUNK_MAX_LINE_SIZE = 1024 + var IPP_REQUEST_NAMES = map[int16]string{ // https://tools.ietf.org/html/rfc2911#section-4.4.15 0x0002: "Print-Job", @@ -57,6 +68,31 @@ var IPP_USER_ATTRIBUTES = map[string]string{ "ppd-name": "everywhere", } +type ClientData struct { + IP string `json:"ip"` + UA string `json:"user_agent"` +} + +type JobData struct { + Name string `json:"name"` + UUID string `json:"uuid"` + User string `json:"username"` +} + +type DocumentData struct { + Name string `json:"name"` + Format string `json:"format"` + Data []byte `json:"data"` +} + +type PrintData struct { + CreatedAt time.Time `json:"created_at"` + Service string `json:"service"` + Client ClientData `json:"client"` + Job JobData `json:"job"` + Document DocumentData `json:"document"` +} + func init() { ipp.AttributeTagMapping["printer-uri-supported"] = ipp.TagUri ipp.AttributeTagMapping["uri-authentication-supported"] = ipp.TagKeyword @@ -83,11 +119,115 @@ func init() { ipp.AttributeTagMapping["printer-privacy-policy-uri"] = ipp.TagUri ipp.AttributeTagMapping["printer-location"] = ipp.TagText ipp.AttributeTagMapping["ppd-name"] = ipp.TagName + ipp.AttributeTagMapping["job-state-reasons"] = ipp.TagKeyword + ipp.AttributeTagMapping["job-state"] = ipp.TagEnum + ipp.AttributeTagMapping["job-uri"] = ipp.TagUri + ipp.AttributeTagMapping["job-id"] = ipp.TagInteger + ipp.AttributeTagMapping["job-printer-uri"] = ipp.TagUri + ipp.AttributeTagMapping["job-name"] = ipp.TagName + ipp.AttributeTagMapping["job-originating-user-name"] = ipp.TagName + ipp.AttributeTagMapping["time-at-creation"] = ipp.TagInteger + ipp.AttributeTagMapping["time-at-completed"] = ipp.TagInteger + ipp.AttributeTagMapping["job-printer-up-time"] = ipp.TagInteger +} + +func ippReadChunkSizeHex(ctx *HandlerContext) string { + var buf []byte + + for b := make([]byte, 1); ; { + if n, err := ctx.client.Read(b); err != nil { + ctx.mod.Error("could not read chunked byte: %v", err) + } else if n == 0 { + break + } else if b[0] == '\n' { + break + } else { + // ctx.mod.Info("buf += 0x%x (%c)", b[0], b[0]) + buf = append(buf, b[0]) + } + + if len(buf) >= IPP_CHUNK_MAX_LINE_SIZE { + ctx.mod.Warning("buffer size exceeded %d bytes when reading chunk size", IPP_CHUNK_MAX_LINE_SIZE) + break + } + } + + return str.Trim(string(buf)) +} + +func ippReadChunkSize(ctx *HandlerContext) (uint64, error) { + if chunkSizeHex := ippReadChunkSizeHex(ctx); chunkSizeHex != "" { + ctx.mod.Debug("got chunk size: 0x%s", chunkSizeHex) + return strconv.ParseUint(chunkSizeHex, 16, 64) + } + return 0, nil +} + +func ippReadChunkedBody(ctx *HandlerContext) ([]byte, error) { + var chunkedBody []byte + // read chunked loop + for { + // read the next chunk size + if chunkSize, err := ippReadChunkSize(ctx); err != nil { + return nil, fmt.Errorf("error reading next chunk size: %v", err) + } else if chunkSize == 0 { + break + } else { + chunk := make([]byte, chunkSize) + if n, err := ctx.client.Read(chunk); err != nil { + return nil, fmt.Errorf("error while reading chunk of %d bytes: %v", chunkSize, err) + } else if n != int(chunkSize) { + return nil, fmt.Errorf("expected chunk of size %d, got %d bytes", chunkSize, n) + } else { + chunkedBody = append(chunkedBody, chunk...) + } + } + } + + return chunkedBody, nil +} + +func ippReadRequestBody(ctx *HandlerContext, http_req *http.Request) (io.ReadCloser, error) { + ipp_body := http_req.Body + + // check for an Expect 100-continue + if http_req.Header.Get("Expect") == "100-continue" { + buf := make([]byte, 4096) + + // inform the client we're ready to read the request body + ctx.client.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n")) + + if slices.Contains(http_req.TransferEncoding, "chunked") { + ctx.mod.Debug("detected chunked encoding") + if body, err := ippReadChunkedBody(ctx); err != nil { + return nil, err + } else { + ipp_body = io.NopCloser(bytes.NewReader(body)) + } + } else { + // read the body in a single step + read, err := ctx.client.Read(buf) + if err != nil { + if err == io.EOF { + return nil, nil + } + return nil, fmt.Errorf("error while reading ipp body from %v: %v", ctx.client.RemoteAddr(), err) + } else if read == 0 { + return nil, fmt.Errorf("error while reading ipp body from %v: no data", ctx.client.RemoteAddr()) + } + + ipp_body = io.NopCloser(bytes.NewReader(buf[0:read])) + } + } + + return ipp_body, nil } func ippClientHandler(ctx *HandlerContext) { defer ctx.client.Close() + clientIP := strings.SplitN(ctx.client.RemoteAddr().String(), ":", 2)[0] + buf := make([]byte, 4096) // read raw request @@ -96,53 +236,38 @@ func ippClientHandler(ctx *HandlerContext) { if err == io.EOF { return } - ctx.mod.Error("error while reading from %v: %v", ctx.client.RemoteAddr(), err) + ctx.mod.Warning("error while reading from %v: %v", clientIP, err) return } else if read == 0 { - ctx.mod.Error("error while reading from %v: no data", ctx.client.RemoteAddr()) + ctx.mod.Warning("error while reading from %v: no data", clientIP) return } raw_req := buf[0:read] - ctx.mod.Debug("read %d bytes from %v:\n%s\n", read, ctx.client.RemoteAddr(), Dump(raw_req)) + ctx.mod.Debug("read %d bytes from %v:\n%s\n", read, clientIP, Dump(raw_req)) // parse as http reader := bufio.NewReader(bytes.NewReader(raw_req)) http_req, err := http.ReadRequest(reader) if err != nil { - ctx.mod.Error("error while parsing http request from %v: %v", ctx.client.RemoteAddr(), err) + ctx.mod.Error("error while parsing http request from %v: %v", clientIP, err) return } - ctx.mod.Info("%v -> %s", ctx.client.RemoteAddr(), tui.Green(http_req.UserAgent())) + clientUA := http_req.UserAgent() + ctx.mod.Debug("%v -> %s", clientIP, tui.Green(clientUA)) - ipp_body := http_req.Body - - // check for an Expect 100-continue - if http_req.Header.Get("Expect") == "100-continue" { - // inform the client we're ready to read the request body - ctx.client.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n")) - // read the body - read, err := ctx.client.Read(buf) - if err != nil { - if err == io.EOF { - return - } - ctx.mod.Error("error while reading ipp body from %v: %v", ctx.client.RemoteAddr(), err) - return - } else if read == 0 { - ctx.mod.Error("error while reading ipp body from %v: no data", ctx.client.RemoteAddr()) - return - } - - ipp_body = io.NopCloser(bytes.NewReader(buf[0:read])) + ipp_body, err := ippReadRequestBody(ctx, http_req) + if err != nil { + ctx.mod.Error("%v", err) + return } // parse as IPP ipp_req, err := ipp.NewRequestDecoder(ipp_body).Decode(nil) if err != nil { - ctx.mod.Error("error while parsing ip request from %v: %v", ctx.client.RemoteAddr(), err) + ctx.mod.Error("error while parsing ipp request from %v: %v -> %++v", clientIP, err, *http_req) return } @@ -151,12 +276,29 @@ func ippClientHandler(ctx *HandlerContext) { ipp_op_name = name } - ctx.mod.Info("%v op=%s attributes=%v", ctx.client.RemoteAddr(), tui.Bold(ipp_op_name), ipp_req.OperationAttributes) + ctx.mod.Info("%s <- %s (%s) %s", + tui.Yellow(ctx.service), + clientIP, + tui.Green(clientUA), + tui.Bold(ipp_op_name)) + ctx.mod.Debug(" %++v", *ipp_req) switch ipp_req.Operation { // Get-Printer-Attributes case 0x000B: ippOnGetPrinterAttributes(ctx, ipp_req) + // Validate-Job + case 0x0004: + ippOnValidateJob(ctx, ipp_req) + // Get-Jobs + case 0x000A: + ippOnGetJobs(ctx, ipp_req) + // Print-Job + case 0x0002: + ippOnPrintJob(ctx, http_req, ipp_req) + // Get-Job-Attributes + case 0x0009: + ippOnGetJobAttributes(ctx, ipp_req) default: ippOnUnhandledRequest(ctx, ipp_req, ipp_op_name) @@ -196,13 +338,275 @@ func ippSendResponse(ctx *HandlerContext, response *ipp.Response) { } func ippOnUnhandledRequest(ctx *HandlerContext, ipp_req *ipp.Request, ipp_op_name string) { - ctx.mod.Warning("unhandled request from %v: operation=%s", ctx.client.RemoteAddr(), ipp_op_name) + ctx.mod.Warning("unhandled request from %v: operation=%s - %++v", ctx.client.RemoteAddr(), ipp_op_name, *ipp_req) ippSendResponse(ctx, ipp.NewResponse( ipp.StatusErrorOperationNotSupported, ipp_req.RequestId)) } +func ippOnValidateJob(ctx *HandlerContext, ipp_req *ipp.Request) { + jobName := "" + jobUUID := "" + jobUser := "" + + if value, found := ipp_req.OperationAttributes["job-name"]; found { + jobName = value.(string) + } + + if value, found := ipp_req.OperationAttributes["requesting-user-name"]; found { + jobUser = value.(string) + } + + if value, found := ipp_req.JobAttributes["job-uuid"]; found { + jobUUID = value.(string) + } + + ctx.mod.Debug("validating job_name=%s job_uuid=%s job_user=%s", tui.Yellow(jobName), tui.Dim(jobUUID), tui.Green(jobUser)) + + ipp_resp := ipp.NewResponse(ipp.StatusOk, ipp_req.RequestId) + + // https://tools.ietf.org/html/rfc2911 section 3.1.4.2 Response Operation Attributes + ipp_resp.OperationAttributes["attributes-charset"] = []ipp.Attribute{ + { + Value: "utf-8", + Tag: ipp.TagCharset, + }, + } + ipp_resp.OperationAttributes["attributes-natural-language"] = []ipp.Attribute{ + { + Value: "en", + Tag: ipp.TagLanguage, + }, + } + + ippSendResponse(ctx, ipp_resp) +} + +func ippOnGetJobAttributes(ctx *HandlerContext, ipp_req *ipp.Request) { + ipp_resp := ipp.NewResponse(ipp.StatusOk, ipp_req.RequestId) + + // https://tools.ietf.org/html/rfc2911 section 3.1.4.2 Response Operation Attributes + ipp_resp.OperationAttributes["attributes-charset"] = []ipp.Attribute{ + { + Value: "utf-8", + Tag: ipp.TagCharset, + }, + } + ipp_resp.OperationAttributes["attributes-natural-language"] = []ipp.Attribute{ + { + Value: "en", + Tag: ipp.TagLanguage, + }, + } + + jobID := 666 + + ipp_resp.OperationAttributes["job-uri"] = []ipp.Attribute{ + { + Value: fmt.Sprintf("%s://%s:%d/jobs/%d", ops.Ternary(ctx.srvTLS, "ipps", "ipp"), ctx.srvHost, ctx.srvPort, jobID), + Tag: ipp.TagUri, + }, + } + ipp_resp.OperationAttributes["job-id"] = []ipp.Attribute{ + { + Value: jobID, + Tag: ipp.TagInteger, + }, + } + ipp_resp.OperationAttributes["job-state"] = []ipp.Attribute{ + { + Value: 9, // 9=completed https://tools.ietf.org/html/rfc2911#section-4.3.7 + Tag: ipp.TagEnum, + }, + } + ipp_resp.OperationAttributes["job-state-reasons"] = []ipp.Attribute{ + { + Value: []string{ + "job-completed-successfully", + }, + Tag: ipp.TagKeyword, + }, + } + ipp_resp.OperationAttributes["job-printer-uri"] = []ipp.Attribute{ + { + Value: fmt.Sprintf("%s://%s:%d/printer", ops.Ternary(ctx.srvTLS, "ipps", "ipp"), ctx.srvHost, ctx.srvPort), + Tag: ipp.TagUri, + }, + } + ipp_resp.OperationAttributes["job-name"] = []ipp.Attribute{ + { + Value: "Print job 666", + Tag: ipp.TagName, + }, + } + ipp_resp.OperationAttributes["job-originating-user-name"] = []ipp.Attribute{ + { + Value: "bettercap", // TODO: check if this must match the actual job user from a print operation + Tag: ipp.TagName, + }, + } + ipp_resp.OperationAttributes["time-at-creation"] = []ipp.Attribute{ + { + Value: 0, + Tag: ipp.TagInteger, + }, + } + ipp_resp.OperationAttributes["time-at-completed"] = []ipp.Attribute{ + { + Value: 0, + Tag: ipp.TagInteger, + }, + } + ipp_resp.OperationAttributes["job-printer-up-time"] = []ipp.Attribute{ + { + Value: time.Now().Unix(), + Tag: ipp.TagInteger, + }, + } + + ippSendResponse(ctx, ipp_resp) +} + +func ippOnGetJobs(ctx *HandlerContext, ipp_req *ipp.Request) { + jobUser := "" + if value, found := ipp_req.OperationAttributes["requesting-user-name"]; found { + jobUser = value.(string) + } + + ctx.mod.Debug("responding with empty jobs list to requesting_user=%s", tui.Green(jobUser)) + + // respond with an empty list of jobs, which probably breaks the rfc + // if the client asked for completed jobs https://tools.ietf.org/html/rfc2911#section-3.2.6.2 + ipp_resp := ipp.NewResponse(ipp.StatusOk, ipp_req.RequestId) + + // https://tools.ietf.org/html/rfc2911 section 3.1.4.2 Response Operation Attributes + ipp_resp.OperationAttributes["attributes-charset"] = []ipp.Attribute{ + { + Value: "utf-8", + Tag: ipp.TagCharset, + }, + } + ipp_resp.OperationAttributes["attributes-natural-language"] = []ipp.Attribute{ + { + Value: "en", + Tag: ipp.TagLanguage, + }, + } + + ippSendResponse(ctx, ipp_resp) +} + +func ippOnPrintJob(ctx *HandlerContext, http_req *http.Request, ipp_req *ipp.Request) { + var err error + + createdAt := time.Now() + + data := PrintData{ + CreatedAt: createdAt, + Service: ctx.service, + Client: ClientData{ + UA: http_req.UserAgent(), + IP: strings.SplitN(ctx.client.RemoteAddr().String(), ":", 2)[0], + }, + Job: JobData{}, + Document: DocumentData{}, + } + + if value, found := ipp_req.OperationAttributes["job-name"]; found { + data.Job.Name = value.(string) + } + if value, found := ipp_req.OperationAttributes["requesting-user-name"]; found { + data.Job.User = value.(string) + } + if value, found := ipp_req.JobAttributes["job-uuid"]; found { + data.Job.UUID = value.(string) + } + if value, found := ipp_req.JobAttributes["document-name-supplied"]; found { + data.Document.Name = value.(string) + } + if value, found := ipp_req.OperationAttributes["document-format"]; found { + data.Document.Format = value.(string) + } + + // TODO: check if not chunked + data.Document.Data, err = ippReadChunkedBody(ctx) + if err != nil { + ctx.mod.Error("could not read document body: %v", err) + } + + var docPath string + if err, docPath = ctx.mod.StringParam("zerogod.ipp.save_path"); err != nil { + ctx.mod.Error("can't read parameter zerogod.ipp.save_path: %v", err) + } else if docPath, err = fs.Expand(docPath); err != nil { + ctx.mod.Error("can't expand %s: %v", docPath, err) + } else { + // make sure the path exists + if err := os.MkdirAll(docPath, 0755); err != nil { + ctx.mod.Error("could not create directory %s: %v", docPath, err) + } + + docName := path.Join(docPath, fmt.Sprintf("%d.json", createdAt.UnixMicro())) + ctx.mod.Debug("saving to %s: %++v", docName, data) + jsonData, err := json.Marshal(data) + if err != nil { + ctx.mod.Error("could not marshal data to json: %v", err) + } else if err := ioutil.WriteFile(docName, jsonData, 0644); err != nil { + ctx.mod.Error("could not write data to %s: %v", docName, err) + } else { + ctx.mod.Info(" document saved to %s", tui.Yellow(docName)) + } + } + + ipp_resp := ipp.NewResponse(ipp.StatusOk, ipp_req.RequestId) + + // https://tools.ietf.org/html/rfc2911 section 3.1.4.2 Response Operation Attributes + ipp_resp.OperationAttributes["attributes-charset"] = []ipp.Attribute{ + { + Value: "utf-8", + Tag: ipp.TagCharset, + }, + } + ipp_resp.OperationAttributes["attributes-natural-language"] = []ipp.Attribute{ + { + Value: "en", + Tag: ipp.TagLanguage, + }, + } + + jobID := 666 + + ipp_resp.OperationAttributes["job-uri"] = []ipp.Attribute{ + { + Value: fmt.Sprintf("%s://%s:%d/jobs/%d", ops.Ternary(ctx.srvTLS, "ipps", "ipp"), ctx.srvHost, ctx.srvPort, jobID), + Tag: ipp.TagUri, + }, + } + ipp_resp.OperationAttributes["job-id"] = []ipp.Attribute{ + { + Value: jobID, + Tag: ipp.TagInteger, + }, + } + ipp_resp.OperationAttributes["job-state"] = []ipp.Attribute{ + { + Value: 3, // 3=pending https://tools.ietf.org/html/rfc2911#section-4.3.7 + Tag: ipp.TagEnum, + }, + } + ipp_resp.OperationAttributes["job-state-reasons"] = []ipp.Attribute{ + { + Value: []string{ + "job-incoming", + "job-data-insufficient", + }, + Tag: ipp.TagKeyword, + }, + } + + ippSendResponse(ctx, ipp_resp) +} + func ippOnGetPrinterAttributes(ctx *HandlerContext, ipp_req *ipp.Request) { ipp_resp := ipp.NewResponse(ipp.StatusOk, ipp_req.RequestId) diff --git a/modules/zerogod/zerogod_service.go b/modules/zerogod/zerogod_service.go index 1ef651b3..f84c0a50 100644 --- a/modules/zerogod/zerogod_service.go +++ b/modules/zerogod/zerogod_service.go @@ -28,7 +28,7 @@ func (svc ServiceData) FullName() string { strings.Trim(svc.Domain, ".")) } -func (svc *ServiceData) Register(mod *ZeroGod) (err error) { +func (svc *ServiceData) Register(mod *ZeroGod, localHostName string) (err error) { // now create it again to actually advertise if svc.Responder == "" { // use our own IP @@ -42,9 +42,11 @@ func (svc *ServiceData) Register(mod *ZeroGod) (err error) { return fmt.Errorf("could not create service %s: %v", svc.FullName(), err) } - mod.Info("advertising %s with responder=%s port=%d", + mod.Info("advertising %s with hostname=%s ipv4=%s ipv6=%s port=%d", tui.Yellow(svc.FullName()), - tui.Red(svc.Responder), + tui.Red(localHostName), + tui.Red(mod.Session.Interface.IpAddress), + tui.Red(mod.Session.Interface.Ip6Address), svc.Port) } else { responderHostName := ""