Skip to content

Commit

Permalink
Elasticsearch scan
Browse files Browse the repository at this point in the history
  • Loading branch information
v-byte-cpu committed Apr 6, 2021
1 parent 8a4652c commit ef816ab
Show file tree
Hide file tree
Showing 7 changed files with 450 additions and 6 deletions.
6 changes: 5 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ issues:
exclude-rules:
- linters:
- gosec
text: "G404"
text: "G404" # math/rand; used to generate pseudo-random source ports
- path: pkg/scan/elastic
linters:
- gosec
text: "G402" # TLS insecure; used in Elasticsearch scan to ignore TLS cert
- linters:
- funlen
path: _test\.go
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The goal of this project is to create the fastest network scanner with clean and
* **Custom TCP scans with any TCP flags**: Send whatever exotic packets you want and get a result with all the TCP flags set in the reply packet
* **UDP scan**: Scan UDP ports and get full ICMP replies to detect open ports or firewall rules
* **SOCKS5 scan**: Detect live SOCKS5 proxies by scanning ip range or list of ip/port pairs from a file
* **Elasticsearch scan**: Detect open Elasticsearch nodes by pulling out cluster information and all index names
* **JSON output support**: sx is designed specifically for convenient automatic processing of results

## Build from source
Expand Down Expand Up @@ -349,7 +350,46 @@ sample input file:
You can also specify a range of ports to scan:

```
socks -p 1080-4567 -f ips_file.jsonl
./sx socks -p 1080-4567 -f ips_file.jsonl
```

In this case only ip addresses will be taken from the file and the **port** field is no longer necessary.

### Elasticsearch scan

Elasticsearch scan retrieves the cluster information and a list of all indexes along with aliases.

For example, an IP range scan:

```
./sx elastic -p 9200 10.0.0.1/16
```

By default the scan uses the http protocol, to use the https protocol specify the `--proto` option:

```
./sx elastic --proto https -p 9200 10.0.0.1/16
```

scan ip/port pairs from a file with JSON output:

```
./sx elastic --json -f ip_ports_file.jsonl 2> /dev/null | tee results.jsonl
```

Each line of the input file is a json string, which must contain the **ip** and **port** fields.

sample input file:

```
{"ip":"10.0.1.1","port":9200}
{"ip":"10.0.2.2","port":9201}
```

You can also specify a range of ports to scan:

```
./sx elastic -p 9200-9267 -f ips_file.jsonl
```

In this case only ip addresses will be taken from the file and the **port** field is no longer necessary.
Expand Down
76 changes: 76 additions & 0 deletions command/elastic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package command

import (
"context"
"errors"
"os"
"os/signal"
"strings"
"time"

"github.com/spf13/cobra"
"github.com/v-byte-cpu/sx/command/log"
"github.com/v-byte-cpu/sx/pkg/ip"
"github.com/v-byte-cpu/sx/pkg/scan"
"github.com/v-byte-cpu/sx/pkg/scan/elastic"
)

var cliHTTPProtoFlag string

func init() {
elasticCmd.Flags().StringVarP(&cliPortsFlag, "ports", "p", "", "set ports to scan")
elasticCmd.Flags().StringVarP(&cliIPPortFileFlag, "file", "f", "", "set JSONL file with ip/port pairs to scan")
elasticCmd.Flags().StringVar(&cliHTTPProtoFlag, "proto", "", "set protocol to use, http is used by default; only http or https are valid")
rootCmd.AddCommand(elasticCmd)
}

var elasticCmd = &cobra.Command{
Use: "elastic [flags] [subnet]",
Example: strings.Join([]string{
"elastic -p 9200 192.168.0.1/24", "elastic -p 9200-9300 10.0.0.1",
"elastic -f ip_ports_file.jsonl", "elastic -p 9200-9300 -f ips_file.jsonl"}, "\n"),
Short: "Perform Elasticsearch scan",
PreRunE: func(cmd *cobra.Command, args []string) (err error) {
if len(cliHTTPProtoFlag) == 0 {
cliHTTPProtoFlag = "http"
}
if cliHTTPProtoFlag != "http" && cliHTTPProtoFlag != "https" {
return errors.New("invalid HTTP proto flag: http or https required")
}
if len(args) == 0 && len(cliIPPortFileFlag) == 0 {
return errors.New("requires one ip subnet argument or file with ip/port pairs")
}
if len(args) == 0 {
return
}
cliDstSubnet, err = ip.ParseIPNet(args[0])
return
},
RunE: func(cmd *cobra.Command, args []string) (err error) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

var logger log.Logger
if logger, err = getLogger("elastic", os.Stdout); err != nil {
return
}

engine := newElasticScanEngine(ctx)
return startScanEngine(ctx, engine,
newEngineConfig(
withLogger(logger),
withScanRange(&scan.Range{
DstSubnet: cliDstSubnet,
Ports: cliPortRanges,
}),
))
},
}

func newElasticScanEngine(ctx context.Context) scan.EngineResulter {
// TODO custom dataTimeout
scanner := elastic.NewScanner(cliHTTPProtoFlag, elastic.WithDataTimeout(5*time.Second))
results := scan.NewResultChan(ctx, 1000)
// TODO custom workerCount
return scan.NewScanEngine(newIPPortGenerator(), scanner, results, scan.WithScanWorkerCount(50))
}
126 changes: 126 additions & 0 deletions pkg/scan/elastic/elastic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//go:generate easyjson -output_filename result_easyjson.go elastic.go

package elastic

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/v-byte-cpu/sx/pkg/scan"
)

const (
ScanType = "elastic"

defaultDataTimeout = 5 * time.Second
)

//easyjson:json
type ScanResult struct {
ScanType string `json:"scan"`
Proto string `json:"proto"`
Host string `json:"host"`
Info map[string]interface{} `json:"info"`
Indexes map[string]interface{} `json:"indexes"`
}

func (r *ScanResult) String() string {
return fmt.Sprintf("%s://%s %s %d", r.Proto, r.Host, r.Info["cluster_name"], len(r.Indexes))
}

func (r *ScanResult) ID() string {
return r.Host
}

type Scanner struct {
elastic *elasticClient
proto string
}

// Assert that elastic.Scanner conforms to the scan.Scanner interface
var _ scan.Scanner = (*Scanner)(nil)

type ScannerOption func(*Scanner)

func WithDataTimeout(timeout time.Duration) ScannerOption {
return func(s *Scanner) {
s.elastic.dataTimeout = timeout
}
}

func NewScanner(proto string, opts ...ScannerOption) *Scanner {
tr := &http.Transport{
MaxConnsPerHost: 1,
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
ec := &elasticClient{
client: &http.Client{
Transport: tr,
},
proto: proto,
dataTimeout: defaultDataTimeout,
}
s := &Scanner{ec, proto}
for _, o := range opts {
o(s)
}
return s
}

func (s *Scanner) Scan(ctx context.Context, r *scan.Request) (result scan.Result, err error) {
// TODO DNS names
host := fmt.Sprintf("%s:%d", r.DstIP.String(), r.DstPort)
// retrieve main info
var info map[string]interface{}
if info, err = s.elastic.GetInfo(ctx, host); err != nil {
return
}
// retrieve all indexes with aliases ignoring error
indexes, _ := s.elastic.GetIndexes(ctx, host)
result = &ScanResult{
ScanType: ScanType,
Proto: s.proto,
Host: host,
Info: info,
Indexes: indexes,
}
return
}

type elasticClient struct {
client *http.Client
proto string
dataTimeout time.Duration
}

func (c *elasticClient) GetInfo(ctx context.Context, host string) (info map[string]interface{}, err error) {
return c.Get(ctx, fmt.Sprintf("%s://%s/", c.proto, host))
}

func (c *elasticClient) GetIndexes(ctx context.Context, host string) (info map[string]interface{}, err error) {
return c.Get(ctx, fmt.Sprintf("%s://%s/_aliases", c.proto, host))
}

func (c *elasticClient) Get(ctx context.Context, url string) (data map[string]interface{}, err error) {
ctx, cancel := context.WithTimeout(ctx, c.dataTimeout)
defer cancel()
var req *http.Request
if req, err = http.NewRequestWithContext(ctx, "GET", url, nil); err != nil {
return
}
var resp *http.Response
if resp, err = c.client.Do(req); err != nil {
return
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&data)
return
}
Loading

0 comments on commit ef816ab

Please sign in to comment.