diff --git a/README.md b/README.md index c949de3..49d11a5 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,21 @@ ### Start +1. Configure *config.yml* for your competition. A detailed description of the quick configuration is [here](./docs/config.md) + +2. Start the farm ```bash make up ``` Credentials for basic auth and the token for sending flags via start_exploit.py will be printed to stdout. +3. After the game ends, turn off the farm and clean the database and queue +```bash +make down +make clean-all +``` + ## Features - Uploading exploits in ui @@ -48,6 +57,7 @@ Credentials for basic auth and the token for sending flags via start_exploit.py - Bash script - Binary - View logs of running exploits and sending flags on ui +- Configuring vulnboxes ip addresses using [various methods](./docs/config.md) ## Components diff --git a/config.yml b/config.yml index d9fdd8b..92f46c3 100644 --- a/config.yml +++ b/config.yml @@ -3,6 +3,19 @@ exploit_runner: - 10.10.1.2 - 10.10.2.2 - 10.10.3.2 + team_ip_cidrs: + - "10.228.79.0/24" + - "10.229.79.0/24" + team_ip_ranges: + - "10.200.78.2-10.200.78.200" + - "10.200.78.221-10.200.78.233" + team_ip_from_N: # N - range(n_start, n_end); X = N / block + offset_x, Y = N % block + offset_y + n_start: 0 + n_end: 2000 + offset_x: 32 + offset_y: 0 + block: 200 + ip_template: "10.{X}.{Y}.2" flag_format: '[A-Z0-9]{31}=' run_duration: 10s exploit_directory: ./exploits diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..e13685b --- /dev/null +++ b/docs/config.md @@ -0,0 +1,37 @@ +# Config + +## Start configuration + +The initial farm configuration is located in **config.yml** and must be configured before starting the farm. + +You have to change: + +### Section exploit_runner + +1. Vulnboxes ip addresses (target ip addresses for exploits). +This can be done in one of four ways (you can remove unnecessary fields from config.yml): + - **team_ips** - list of IP addresses of team vulnboxes + - **team_ip_cidrs** - cidrs of team vulnboxes + - **team_ip_ranges** - range of team vulnboxes + - **team_ip_from_N** - range of team vulnboxes calculating by formula for template like "10.{X}.{Y}.2" + - N - range(**n_start**, **n_end**) + - X = N / **block** + **offset_x** + - Y = N % **block** + **offset_y** +2. **flag_format** - format of flag in CTF competition. This regular expression will extract flags from the exploit text + +### Section flag_sender + +1. **plugin** - module for sending flags in jury system +2. **jury_flag_url_or_host** - url or host of jury system where the flags will be sent +- For HTTP plugins should be url like: http://example.com/flags or http://1.2.3.4:5555/flags +- For TCP plugins should be hostname + port like: example.com:5555 or 1.2.3.4:5555 +3. **token** - auth token for jury system (if needed) +4. **flag_ttl** - time to live of flags + +## Real-time configuration + +All settings of this configuration can be changed in real-time in ui. The exception is parameter **plugin** in section **flag_sender**. To change it, you need to restart the farm. + +It is also not recommended to add ip addresses via the ui, as there is no option for bulk addition. + +![ui config img](img/ui_config.png) diff --git a/docs/logo.png b/docs/img/logo.png similarity index 100% rename from docs/logo.png rename to docs/img/logo.png diff --git a/docs/img/ui_config.png b/docs/img/ui_config.png new file mode 100644 index 0000000..4721868 Binary files /dev/null and b/docs/img/ui_config.png differ diff --git a/workers/config_loader/go.mod b/workers/config_loader/go.mod index 768bb91..1aee8cf 100644 --- a/workers/config_loader/go.mod +++ b/workers/config_loader/go.mod @@ -7,18 +7,23 @@ require ( github.com/jackc/pgx/v5 v5.7.5 github.com/jacute/prettylogger v0.0.7 github.com/kelseyhightower/envconfig v1.4.0 + github.com/stretchr/testify v1.10.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.17.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/workers/config_loader/go.sum b/workers/config_loader/go.sum index 3f5ab41..6754665 100644 --- a/workers/config_loader/go.sum +++ b/workers/config_loader/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,6 +18,10 @@ github.com/jacute/prettylogger v0.0.7 h1:inKCDEJ42j31hNVB6wAYZWOrc7E4QJ//x2hcR0L github.com/jacute/prettylogger v0.0.7/go.mod h1:3lynOiaGfyYdX6g8mz6cEg9CyLBZSTnPWwXdeQlao2w= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -24,6 +29,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -40,6 +47,8 @@ golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/workers/config_loader/internal/config/config.go b/workers/config_loader/internal/config/config.go index dbe6296..f9a6ee4 100644 --- a/workers/config_loader/internal/config/config.go +++ b/workers/config_loader/internal/config/config.go @@ -16,6 +16,9 @@ type Config struct { type ExploitRunnerConfig struct { TeamIPs []string `yaml:"team_ips"` + TeamIPRange []string `yaml:"team_ip_ranges"` + TeamIPCidr []string `yaml:"team_ip_cidrs"` + TeamIPFromN *TeamIPFromN `yaml:"team_ip_from_N"` FlagFormat string `yaml:"flag_format"` RunDuration time.Duration `yaml:"run_duration"` MaxConcurrentExploits int `yaml:"max_concurrent_exploits"` @@ -43,6 +46,15 @@ type DBConfig struct { DBName string `envconfig:"PG_DB_NAME"` } +type TeamIPFromN struct { + NStart int `yaml:"n_start"` + NEnd int `yaml:"n_end"` + OffsetX int `yaml:"offset_x"` + OffsetY int `yaml:"offset_y"` + Block int `yaml:"block"` + IPTemplate string `yaml:"ip_template"` +} + const defaultConfigFilepath = "./config.yml" func MustParseConfig() *Config { diff --git a/workers/config_loader/internal/service/config.go b/workers/config_loader/internal/service/config.go index 719aefc..889d076 100644 --- a/workers/config_loader/internal/service/config.go +++ b/workers/config_loader/internal/service/config.go @@ -4,6 +4,7 @@ import ( "config_loader/internal/config" "config_loader/internal/models" "config_loader/internal/postgres" + "config_loader/internal/utils" "config_loader/pkg/common_config" "context" "errors" @@ -18,19 +19,53 @@ func (s *Service) LoadConfigIntoDB(ctx context.Context, cfg *config.Config) erro log := s.log.With(slog.String("op", op)) var existTeams []string - for _, ip := range cfg.ExploitRunner.TeamIPs { - _, err := s.db.AddTeam(ctx, &models.Team{ - IP: ip, - }) + + // add single ips + err := s.addIps(ctx, cfg.ExploitRunner.TeamIPs, existTeams) + if err != nil { + log.Warn("error adding ips", slog.Any("ips", cfg.ExploitRunner.TeamIPs), prettylogger.Err(err)) + } + + // add ip ranges + for _, ipRange := range cfg.ExploitRunner.TeamIPRange { + ips, err := utils.ExpandRange(ipRange) if err != nil { - if errors.Is(err, postgres.ErrTeamAlreadyExists) { - existTeams = append(existTeams, ip) - continue - } - log.Warn("team cannot be added", prettylogger.Err(err)) - return err + log.Warn("error adding ip range", slog.String("ip_range", ipRange), prettylogger.Err(err)) + continue + } + err = s.addIps(ctx, ips, existTeams) + if err != nil { + log.Warn("error adding ips", slog.Any("ips", ips), prettylogger.Err(err)) + } + } + + // add ip cidrs + for _, ipCIDR := range cfg.ExploitRunner.TeamIPCidr { + ips, err := utils.ExpandCIDR(ipCIDR) + if err != nil { + log.Warn("error adding ip cidr", slog.String("ip_cidr", ipCIDR), prettylogger.Err(err)) + continue + } + err = s.addIps(ctx, ips, existTeams) + if err != nil { + log.Warn("error adding ips", slog.Any("ips", ips), prettylogger.Err(err)) } } + + // add ip from N + ips := utils.ExpandIpFromN( + cfg.ExploitRunner.TeamIPFromN.NStart, + cfg.ExploitRunner.TeamIPFromN.NEnd, + cfg.ExploitRunner.TeamIPFromN.OffsetX, + cfg.ExploitRunner.TeamIPFromN.OffsetY, + cfg.ExploitRunner.TeamIPFromN.Block, + cfg.ExploitRunner.TeamIPFromN.IPTemplate, + ) + err = s.addIps(ctx, ips, existTeams) + if err != nil { + log.Warn("error adding ips", slog.Any("ips", ips), prettylogger.Err(err)) + } + if len(existTeams) > 0 { log.Info("some teams already exist", slog.Any("teams", existTeams)) } @@ -70,3 +105,24 @@ func (s *Service) LoadConfigIntoDB(ctx context.Context, cfg *config.Config) erro return nil } + +func (s *Service) addIps(ctx context.Context, ips []string, existTeams []string) error { + const op = "service.jacfarm.addIps" + log := s.log.With(slog.String("op", op)) + + for _, ip := range ips { + _, err := s.db.AddTeam(ctx, &models.Team{ + IP: ip, + }) + if err != nil { + if errors.Is(err, postgres.ErrTeamAlreadyExists) { + existTeams = append(existTeams, ip) + continue + } + log.Warn("team cannot be added", prettylogger.Err(err)) + return err + } + } + + return nil +} diff --git a/workers/config_loader/internal/utils/utils.go b/workers/config_loader/internal/utils/utils.go new file mode 100644 index 0000000..da31678 --- /dev/null +++ b/workers/config_loader/internal/utils/utils.go @@ -0,0 +1,88 @@ +package utils + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +// ExpandCIDR returns all IP addresses in a CIDR range without network and broadcast addresses +// Example: ExpandCIDR("38.0.101.0/30") returns []string{"38.0.101.1", "38.0.101.2"} +func ExpandCIDR(cidr string) ([]string, error) { + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("invalid cidr") + } + + var ips []string + for ip := nextIP(ipnet.IP.Mask(ipnet.Mask)); ipnet.Contains(ip); ip = nextIP(ip) { + ips = append(ips, ip.String()) + } + return ips[:len(ips)-1], nil +} + +func nextIP(ip net.IP) net.IP { + ip = append(net.IP(nil), ip...) + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] != 0 { + break + } + } + return ip +} + +// ExpandRange returns all IP addresses in range +// Example: ExpandRange("38.0.100.255-38.101.1") returns []string{"38.0.100.255", "38.0.101.0", "38.101.1"} +func ExpandRange(iprange string) ([]string, error) { + parts := strings.SplitN(iprange, "-", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid ip range") + } + + startIP, endIP := parts[0], parts[1] + s := net.ParseIP(startIP).To4() + if s == nil { + return nil, fmt.Errorf("invalid start range IP") + } + e := net.ParseIP(endIP).To4() + if e == nil { + return nil, fmt.Errorf("invalid end range IP") + } + + var ips []string + for ip := append(net.IP(nil), s...); !ipAfter(ip, e); ip = nextIP(ip) { + ips = append(ips, ip.String()) + } + return ips, nil +} + +func ipAfter(a, b net.IP) bool { + for i := 0; i < 4; i++ { + if a[i] > b[i] { + return true + } + if a[i] < b[i] { + return false + } + } + return false +} + +// ExpandIpFromN returns all IP addresses by template with variables {X}, {Y} +// Example: ExpandIpFromN(0, 6, 32, 1, 3, "10.{X}.{Y}.5") returns []string{"10.32.1.5", "10.32.2.5", "10.33.1.5", "10.33.2.5", "10.34.1.5"} +func ExpandIpFromN(nStart, nEnd, offsetX, offsetY, block int, ipTmpl string) []string { + ips := make([]string, 0, nEnd-nStart) + for n := nStart; n < nEnd; n++ { + ip := string([]byte(ipTmpl)) + ip = strings.Replace( + ip, "{X}", strconv.Itoa(n/block+offsetX), 1, + ) + ip = strings.Replace( + ip, "{Y}", strconv.Itoa(n%block+offsetY), 1, + ) + ips = append(ips, ip) + } + return ips +} diff --git a/workers/config_loader/internal/utils/utils_test.go b/workers/config_loader/internal/utils/utils_test.go new file mode 100644 index 0000000..e1f54af --- /dev/null +++ b/workers/config_loader/internal/utils/utils_test.go @@ -0,0 +1,137 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExpandCIDR(t *testing.T) { + testcases := []struct { + name string + cidr string + addresses []string + }{ + { + name: "ok 10.10.0.0/30", + cidr: "10.10.0.0/30", + addresses: []string{ + "10.10.0.1", + "10.10.0.2", + }, + }, + { + name: "ok 192.168.55.0/29", + cidr: "192.168.55.0/29", + addresses: []string{ + "192.168.55.1", + "192.168.55.2", + "192.168.55.3", + "192.168.55.4", + "192.168.55.5", + "192.168.55.6", + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + addresses, err := ExpandCIDR(tc.cidr) + require.NoError(t, err) + require.Equal(t, tc.addresses, addresses) + }) + } +} + +func TestExpandRange(t *testing.T) { + testcases := []struct { + name string + iprange string + addresses []string + }{ + { + name: "ok 10.10.0.254-10.10.1.1", + iprange: "10.10.0.254-10.10.1.1", + addresses: []string{ + "10.10.0.254", + "10.10.0.255", + "10.10.1.0", + "10.10.1.1", + }, + }, + { + name: "ok 10.10.5.0-10.10.5.2", + iprange: "10.10.5.0-10.10.5.2", + addresses: []string{ + "10.10.5.0", + "10.10.5.1", + "10.10.5.2", + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + addresses, err := ExpandRange(tc.iprange) + require.NoError(t, err) + require.Equal(t, tc.addresses, addresses) + }) + } +} + +func TestExpandIpFromN(t *testing.T) { + testcases := []struct { + name string + nStart, nEnd int + offsetX, offsetY int + block int + ipTmpl string + addresses []string + }{ + { + name: "ok1", + nStart: 0, + nEnd: 6, + offsetX: 32, + offsetY: 1, + block: 2, + ipTmpl: "10.{X}.{Y}.1", + addresses: []string{ + "10.32.1.1", + "10.32.2.1", + "10.33.1.1", + "10.33.2.1", + "10.34.1.1", + "10.34.2.1", + }, + }, + { + name: "ok2", + nStart: 0, + nEnd: 6, + offsetX: 25, + offsetY: 0, + block: 5, + ipTmpl: "10.{X}.{Y}.1", + addresses: []string{ + "10.25.0.1", + "10.25.1.1", + "10.25.2.1", + "10.25.3.1", + "10.25.4.1", + "10.26.0.1", + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + addresses := ExpandIpFromN( + tc.nStart, tc.nEnd, + tc.offsetX, tc.offsetY, + tc.block, tc.ipTmpl, + ) + require.Equal(t, tc.addresses, addresses) + }) + } +}