├── hooks └── .gitkeep ├── static └── img │ ├── 2.jpg │ ├── img.png │ ├── img_1.png │ ├── img_2.png │ ├── img_20.png │ ├── img_21.png │ ├── img_22.png │ ├── img_23.png │ ├── img_24.png │ ├── img_25.png │ ├── img_3.png │ ├── img_4.png │ ├── img_5.png │ ├── img_6.png │ ├── img-0401.png │ ├── img_logo.png │ ├── architecture.jpg │ ├── img_030101.png │ ├── img_030102.png │ ├── img_030103.png │ ├── img_030301.png │ ├── img_030801.png │ ├── img_030901.png │ └── img_070201.jpg ├── main.go ├── pkg ├── util │ ├── encode.go │ ├── randomIdGenerator.go │ └── randomIdGenerator_test.go └── logger │ ├── formatter.go │ ├── logger.go │ └── rotate.go ├── internal ├── core │ ├── ahttp │ │ ├── hook │ │ │ ├── requestHook.go │ │ │ └── hook.go │ │ ├── ahttp_test.go │ │ ├── ahttpModify_test.go │ │ ├── dumpHttp.go │ │ ├── ahttp.go │ │ └── ahttpModify.go │ ├── module │ │ ├── DoS │ │ │ ├── rateLimitDetector.go │ │ │ ├── DoSDetector.go │ │ │ └── resourceSizeDetector.go │ │ ├── authorize │ │ │ ├── authGroup.go │ │ │ ├── authoriedDetector_test.go │ │ │ └── authoriedDetector.go │ │ ├── detect.go │ │ ├── SSRF │ │ │ └── SSRFDetector.go │ │ ├── OpenRedirect │ │ │ └── OpenRedirectDetector.go │ │ └── CSRF │ │ │ └── CSRFDetector.go │ ├── origin │ │ ├── realTimeOrigin │ │ │ ├── realTimeOrigin_test.go │ │ │ └── realTimeOrigin.go │ │ ├── fileInputOrigin │ │ │ ├── burpFile_test.go │ │ │ ├── burpFile.go │ │ │ └── fileInputOrigin.go │ │ └── origin.go │ ├── filter │ │ ├── filter.go │ │ ├── duplicateFilter.go │ │ ├── httpFilter.go │ │ └── staticResourceFilter.go │ ├── notify │ │ ├── notify.go │ │ ├── dingding.go │ │ └── lark.go │ ├── data │ │ ├── buildResult.go │ │ └── meta.go │ ├── database │ │ ├── mysql_test.go │ │ ├── db.go │ │ └── mysql.go │ ├── async │ │ ├── asyncCheckEngineX_test.go │ │ └── asyncCheckEngineX.go │ ├── handler.go │ └── aio │ │ └── repeatReadCloser.go ├── web │ ├── backend │ │ ├── web_test.go │ │ └── web.go │ └── frontend │ │ └── www │ │ └── index.html └── runner │ ├── banner.go │ ├── option.go │ └── runner.go ├── .gitignore ├── apikiller.sql ├── dbDeploy.sh ├── ca.crt ├── go.mod ├── config ├── config.release.yaml └── config.dev.yaml ├── README.md ├── LICENSE └── go.sum /hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/2.jpg -------------------------------------------------------------------------------- /static/img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img.png -------------------------------------------------------------------------------- /static/img/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_1.png -------------------------------------------------------------------------------- /static/img/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_2.png -------------------------------------------------------------------------------- /static/img/img_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_20.png -------------------------------------------------------------------------------- /static/img/img_21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_21.png -------------------------------------------------------------------------------- /static/img/img_22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_22.png -------------------------------------------------------------------------------- /static/img/img_23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_23.png -------------------------------------------------------------------------------- /static/img/img_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_24.png -------------------------------------------------------------------------------- /static/img/img_25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_25.png -------------------------------------------------------------------------------- /static/img/img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_3.png -------------------------------------------------------------------------------- /static/img/img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_4.png -------------------------------------------------------------------------------- /static/img/img_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_5.png -------------------------------------------------------------------------------- /static/img/img_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_6.png -------------------------------------------------------------------------------- /static/img/img-0401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img-0401.png -------------------------------------------------------------------------------- /static/img/img_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_logo.png -------------------------------------------------------------------------------- /static/img/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/architecture.jpg -------------------------------------------------------------------------------- /static/img/img_030101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030101.png -------------------------------------------------------------------------------- /static/img/img_030102.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030102.png -------------------------------------------------------------------------------- /static/img/img_030103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030103.png -------------------------------------------------------------------------------- /static/img/img_030301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030301.png -------------------------------------------------------------------------------- /static/img/img_030801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030801.png -------------------------------------------------------------------------------- /static/img/img_030901.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_030901.png -------------------------------------------------------------------------------- /static/img/img_070201.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aur0ra-m/APIKiller/HEAD/static/img/img_070201.jpg -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "APIKiller/internal/runner" 4 | 5 | func main() { 6 | // create project runtime 7 | runner.NewRunner() 8 | } 9 | -------------------------------------------------------------------------------- /pkg/util/encode.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "encoding/base64" 4 | 5 | func B64Encode(targetStr string) string { 6 | return base64.StdEncoding.EncodeToString([]byte(targetStr)) 7 | } 8 | -------------------------------------------------------------------------------- /internal/core/ahttp/hook/requestHook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import "net/http" 4 | 5 | type RequestHook interface { 6 | HookBefore(*http.Request) // hook before initiating http request 7 | HookAfter(*http.Request) // hook after finishing http request 8 | } 9 | -------------------------------------------------------------------------------- /internal/core/ahttp/hook/hook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | var Hooks []RequestHook 4 | 5 | // 6 | // RegisterHooks 7 | // @Description: append http request hook to modify request data 8 | // @param requestHook 9 | // 10 | func RegisterHooks(requestHook RequestHook) { 11 | Hooks = append(Hooks, requestHook) 12 | } 13 | -------------------------------------------------------------------------------- /internal/web/backend/web_test.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import "testing" 4 | 5 | func TestServer(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | }{ 9 | // TODO: Add test cases. 10 | {}, 11 | } 12 | for _, tt := range tests { 13 | t.Run(tt.name, func(t *testing.T) { 14 | NewAPIServer() 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/util/randomIdGenerator.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func GenerateRandomId() string { 10 | year := time.Now().Format("2006") 11 | month := time.Now().Format("01") 12 | day := time.Now().Format("02") 13 | 14 | return fmt.Sprintf("%v%v%v%v", year, month, day, rand.Int()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/core/module/DoS/rateLimitDetector.go: -------------------------------------------------------------------------------- 1 | package DoS 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | ) 6 | 7 | type rateLimitDetector struct { 8 | } 9 | 10 | func (r *rateLimitDetector) Detect(item *data.DataItem) { 11 | // 12 | } 13 | 14 | func newRateLimitDetector() *rateLimitDetector { 15 | return &rateLimitDetector{} 16 | } 17 | -------------------------------------------------------------------------------- /internal/core/origin/realTimeOrigin/realTimeOrigin_test.go: -------------------------------------------------------------------------------- 1 | package realTimeOrigin 2 | 3 | import "testing" 4 | 5 | func TestProxyN(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | }{ 9 | // TODO: Add test cases. 10 | {}, 11 | } 12 | for _, tt := range tests { 13 | t.Run(tt.name, func(t *testing.T) { 14 | //ProxyN() 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | .dev 8 | out 9 | gen 10 | log/* 11 | 12 | test.go 13 | test_test.go 14 | /APIKiller.exe 15 | # 基于自搭建的TCP高效反连平台 16 | /internal/core/async/asyncCheckEngine.go 17 | /internal/core/async/asyncCheckEngine_test.go 18 | /internal/core/async/client.go 19 | -------------------------------------------------------------------------------- /pkg/util/randomIdGenerator_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestGenerateRandomId(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | want string 9 | }{ 10 | // TODO: Add test cases. 11 | {}, 12 | } 13 | for _, tt := range tests { 14 | t.Run(tt.name, func(t *testing.T) { 15 | if got := GenerateRandomId(); got != tt.want { 16 | t.Errorf("GenerateRandomId() = %v, want %v", got, tt.want) 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/core/module/authorize/authGroup.go: -------------------------------------------------------------------------------- 1 | package authorize 2 | 3 | type authGroup struct { 4 | Domain []string `mapstructure:"domain"` 5 | ReplaceGroups []replaceGroup `mapstructure:"replaceGroup"` 6 | } 7 | 8 | type replaceGroup struct { 9 | Position int `mapstructure:"position"` 10 | Key string `mapstructure:"key"` 11 | Value string `mapstructure:"value"` 12 | } 13 | 14 | // position codes 15 | const ( 16 | Replace_Position_Code_Header = 0 17 | Replace_Position_Code_Query = 1 18 | Replace_Position_Code_Body = 2 19 | ) 20 | -------------------------------------------------------------------------------- /internal/core/origin/fileInputOrigin/burpFile_test.go: -------------------------------------------------------------------------------- 1 | package fileInputOrigin 2 | 3 | import "testing" 4 | 5 | func TestFileInputOrigin_parseData(t *testing.T) { 6 | type fields struct { 7 | path string 8 | } 9 | tests := []struct { 10 | name string 11 | fields fields 12 | }{ 13 | // TODO: Add test cases. 14 | { 15 | name: "", 16 | fields: fields{ 17 | path: "C:\\Users\\Lenovo\\Desktop\\src.txt", 18 | }, 19 | }, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | //o := &FileInputOrigin{ 24 | // path: tt.fields.path, 25 | //} 26 | //o.parseData() 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/core/module/authorize/authoriedDetector_test.go: -------------------------------------------------------------------------------- 1 | package authorize 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "testing" 7 | ) 8 | 9 | func Test(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | }{ 13 | // TODO: Add test cases. 14 | {}, 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | viper.SetConfigFile("D:\\Projects\\GO\\APIKiller\\config\\config.dev.yaml") 19 | viper.ReadInConfig() 20 | 21 | var authGroups = []authGroup{} 22 | viper.UnmarshalKey("app.module.authorizedDetector.authGroup", &authGroups) 23 | 24 | fmt.Println(authGroups) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/runner/banner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import "fmt" 4 | 5 | var banner = fmt.Sprintf(` 6 | █████╗ ██████╗ ██╗██╗ ██╗██╗██╗ ██╗ ███████╗██████╗ 7 | ██╔══██╗██╔══██╗██║██║ ██╔╝██║██║ ██║ ██╔════╝██╔══██╗ 8 | ███████║██████╔╝██║█████╔╝ ██║██║ ██║ █████╗ ██████╔╝ 9 | ██╔══██║██╔═══╝ ██║██╔═██╗ ██║██║ ██║ ██╔══╝ ██╔══██╗ 10 | ██║ ██║██║ ██║██║ ██╗██║███████╗███████╗███████╗██║ ██║ 11 | ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ 12 | Version: %s`+"\n", VERSION) 13 | 14 | // 15 | // showBanner 16 | // @Description: show banner on the terminal 17 | // 18 | func showBanner() { 19 | fmt.Println(banner) 20 | } 21 | -------------------------------------------------------------------------------- /internal/core/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | const ( 8 | FilterPass = true 9 | FilterBlocked = false 10 | ) 11 | 12 | var ( 13 | filters []Filter 14 | ) 15 | 16 | type Filter interface { 17 | // 18 | // Filter 19 | // @Description: filter out *http.Request that do not meet the conditions 20 | // @param *http.Request 21 | // @return bool 22 | // 23 | Filter(*http.Request) bool 24 | } 25 | 26 | func RegisterFilter(filter Filter) { 27 | if filter == nil { 28 | return 29 | } 30 | 31 | filters = append(filters, filter) 32 | } 33 | 34 | func GetFilters() []Filter { 35 | if filters != nil { 36 | return filters 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/core/module/detect.go: -------------------------------------------------------------------------------- 1 | package module 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | ) 6 | 7 | const ( 8 | AsyncDetectVulnTypeSeperator = "^" 9 | ) 10 | 11 | var ( 12 | modules []Detecter 13 | ) 14 | 15 | type Detecter interface { 16 | // 17 | // Detect 18 | // @Description: detect the target api and return the result 19 | // @param item 20 | // @return result 21 | // 22 | Detect(item *data.DataItem) (result *data.DataItem) 23 | } 24 | 25 | func RegisterModule(d Detecter) { 26 | if d == nil { 27 | return 28 | } 29 | 30 | modules = append(modules, d) 31 | } 32 | 33 | func GetModules() []Detecter { 34 | if modules != nil { 35 | return modules 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/core/ahttp/ahttp_test.go: -------------------------------------------------------------------------------- 1 | package ahttp 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestCopyRequest(t *testing.T) { 9 | type args struct { 10 | src *http.Request 11 | } 12 | request, _ := http.NewRequest("GET", "http://127.0.0.1/list", nil) 13 | tests := []struct { 14 | name string 15 | args args 16 | wantDst *http.Request 17 | }{ 18 | // TODO: Add test cases. 19 | { 20 | name: "13213", 21 | args: args{request}, 22 | wantDst: nil, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | 28 | //request.Body = aio.TransformReadCloser(request.Body) 29 | 30 | client := http.Client{} 31 | client.Do(request) 32 | client.Do(request) 33 | RequestClone(request) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/core/ahttp/ahttpModify_test.go: -------------------------------------------------------------------------------- 1 | package ahttp 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestModifyPostFormParameter(t *testing.T) { 10 | type args struct { 11 | req *http.Request 12 | paramName string 13 | newValue string 14 | } 15 | request, _ := http.NewRequest("POST", "https://localhost", bytes.NewBuffer([]byte("test=123"))) 16 | tests := []struct { 17 | name string 18 | args args 19 | }{ 20 | // TODO: Add test cases. 21 | { 22 | name: "test", 23 | args: args{ 24 | req: request, 25 | paramName: "test", 26 | newValue: "hacker", 27 | }, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | modifyPostFormParam(tt.args.req, tt.args.paramName, tt.args.newValue) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/runner/option.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import "flag" 4 | 5 | type CommandOptions struct { 6 | ConfigPath string 7 | Web bool 8 | Thread int 9 | FileInput string 10 | } 11 | 12 | // 13 | // ParseCommandOptions 14 | // @Description: parse command options through flag package 15 | // @return *CommandOptions 16 | // 17 | func ParseCommandOptions() *CommandOptions { 18 | c := &CommandOptions{} 19 | // bind data 20 | flag.StringVar(&c.ConfigPath, "conf", "", "project config path") 21 | flag.BoolVar(&c.Web, "web", false, "web operations platform option") 22 | flag.IntVar(&c.Thread, "thread", 100, "go routine concurrency control") 23 | flag.StringVar(&c.FileInput, "f", "", "load requests from target brup file") 24 | 25 | // parse cmd line 26 | flag.Parse() 27 | //fmt.Println(c) 28 | return c 29 | } 30 | -------------------------------------------------------------------------------- /internal/core/origin/origin.go: -------------------------------------------------------------------------------- 1 | package origin 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type TransferItem struct { 8 | Req *http.Request 9 | Resp *http.Response 10 | } 11 | 12 | var transferItemQueue = make(chan *TransferItem) 13 | 14 | type Origin interface { 15 | // 16 | // LoadOriginRequest 17 | // @Description: load request and transport via channel transferItemQueue 18 | // 19 | LoadOriginRequest() 20 | } 21 | 22 | // 23 | // TransportOriginRequest 24 | // @Description: transport requests from origin via transferItemQueue 25 | // @param item 26 | // 27 | func TransportOriginRequest(item *TransferItem) { 28 | transferItemQueue <- item 29 | } 30 | 31 | // 32 | // GetOriginRequest 33 | // @Description: get requests from transferItemQueue 34 | // 35 | func GetOriginRequest() *TransferItem { 36 | return <-transferItemQueue 37 | } 38 | -------------------------------------------------------------------------------- /internal/core/filter/duplicateFilter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "APIKiller/pkg/logger" 5 | "fmt" 6 | "golang.org/x/exp/slices" 7 | "net/http" 8 | ) 9 | 10 | type DuplicateFilter struct { 11 | history []string // []string{"GET domain /admin/index",} 12 | } 13 | 14 | func (f *DuplicateFilter) Filter(req *http.Request) bool { 15 | logger.Debugln("[Filter] duplicate") 16 | 17 | // format 18 | curr := fmt.Sprintf("%s %s %s", req.Method, req.Host, req.URL.Path) 19 | 20 | // duplication 21 | if slices.Contains(f.history, curr) { 22 | logger.Infoln("duplicate data") 23 | return FilterBlocked 24 | } 25 | 26 | // append to history 27 | f.history = append(f.history, curr) 28 | 29 | return FilterPass 30 | } 31 | 32 | func NewDuplicateFilter() *DuplicateFilter { 33 | logger.Infoln("[Load Filter] duplicate filter") 34 | return &DuplicateFilter{} 35 | } 36 | -------------------------------------------------------------------------------- /internal/core/filter/httpFilter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "APIKiller/pkg/logger" 5 | "github.com/spf13/viper" 6 | "net/http" 7 | "regexp" 8 | ) 9 | 10 | type HttpFilter struct { 11 | hostsExp []string 12 | } 13 | 14 | func (f *HttpFilter) Filter(req *http.Request) bool { 15 | logger.Debugln("[Filter] ahttp filter") 16 | 17 | // match through RegExp 18 | if len(f.hostsExp) != 0 { 19 | reqHost := req.Host 20 | flag := FilterBlocked 21 | for _, hostExp := range f.hostsExp { 22 | if matched, _ := regexp.Match(hostExp, []byte(reqHost)); matched { 23 | flag = FilterPass 24 | break 25 | } 26 | } 27 | return flag 28 | } 29 | 30 | return FilterPass // default 31 | } 32 | 33 | func NewHttpFilter() Filter { 34 | logger.Infoln("[Load Filter] http filter") 35 | 36 | return &HttpFilter{ 37 | hostsExp: viper.GetStringSlice("app.filter.httpFilter.host"), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/core/filter/staticResourceFilter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "APIKiller/pkg/logger" 5 | "github.com/spf13/viper" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | type StaticResourceFilter struct { 11 | forbidenExts []string 12 | } 13 | 14 | func (f *StaticResourceFilter) Filter(req *http.Request) bool { 15 | logger.Debugln("[Filter] static file filter") 16 | 17 | // get request path extension 18 | lastIndex := strings.LastIndex(req.URL.Path, ".") 19 | if lastIndex == -1 { 20 | return FilterPass 21 | } 22 | ext := req.URL.Path[lastIndex+1:] 23 | 24 | // filter 25 | for _, forbidenExt := range f.forbidenExts { 26 | if forbidenExt == ext { 27 | return FilterBlocked 28 | } 29 | } 30 | 31 | return FilterPass 32 | } 33 | 34 | func NewStaticFileFilter() Filter { 35 | logger.Infoln("[Load Filter] static file filter") 36 | 37 | return &StaticResourceFilter{ 38 | forbidenExts: viper.GetStringSlice("app.filter.staticFileFilter.ext"), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/core/notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | ) 6 | 7 | type Notify interface { 8 | // 9 | // Notify 10 | // @Description: 11 | // @param item 12 | // 13 | Notify(item *data.DataItem) 14 | } 15 | 16 | var ( 17 | notificationQueue chan *data.DataItem 18 | notifier Notify 19 | ) 20 | 21 | // 22 | // CreateNotification 23 | // @Description: create new notification and throw it into queue 24 | // @param notification 25 | // 26 | func CreateNotification(notification *data.DataItem) { 27 | if notificationQueue != nil { 28 | notificationQueue <- notification 29 | } 30 | } 31 | 32 | func BindNotifier(n Notify) { 33 | notifier = n 34 | 35 | // init notificationQueue 36 | notificationQueue = make(chan *data.DataItem, 1024) 37 | 38 | // notification queue handle 39 | go func() { 40 | var item *data.DataItem 41 | for { 42 | item = <-notificationQueue 43 | notifier.Notify(item) 44 | } 45 | }() 46 | } 47 | -------------------------------------------------------------------------------- /internal/core/module/DoS/DoSDetector.go: -------------------------------------------------------------------------------- 1 | package DoS 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "APIKiller/internal/core/module" 6 | "APIKiller/pkg/logger" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | type DosDetector struct { 11 | typeFlag string 12 | d1 *rateLimitDetector 13 | d2 *resourceSizeDetector 14 | } 15 | 16 | func (d DosDetector) Detect(item *data.DataItem) (result *data.DataItem) { 17 | logger.Debugln("[Detect] DoS detect") 18 | 19 | // rate limit 20 | //d.d1.Detect( item) 21 | 22 | // the size of resource lack of control 23 | return d.d2.Detect(item) 24 | } 25 | 26 | func NewDoSDetector() module.Detecter { 27 | if viper.GetInt("app.module.DoSDetector.option") == 0 { 28 | return nil 29 | } 30 | 31 | logger.Infoln("[Load Module] DoS detect module") 32 | 33 | return &DosDetector{ 34 | typeFlag: viper.GetString("app.module.DoSDetector.typeFlag"), 35 | d1: newRateLimitDetector(), 36 | d2: newResourceSizeDetector(), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/core/data/buildResult.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "APIKiller/pkg/util" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // 11 | // BuildResult 12 | // @Description: build result through create a new *data.DateItem 13 | // @param dataItem 14 | // @param vulnType 15 | // @param vulnReq 16 | // @param vulnResp 17 | // @return *data.DataItem 18 | // 19 | func BuildResult(dataItem *DataItem, vulnType string, vulnReq *http.Request, vulnResp *http.Response) *DataItem { 20 | return &DataItem{ 21 | Id: util.GenerateRandomId(), 22 | Domain: dataItem.Domain, 23 | Url: dataItem.Url, 24 | Method: dataItem.Method, 25 | Https: dataItem.Https, 26 | SourceRequest: dataItem.SourceRequest, 27 | SourceResponse: dataItem.SourceResponse, 28 | VulnType: vulnType, 29 | VulnRequest: vulnReq, 30 | VulnResponse: vulnResp, 31 | ReportTime: fmt.Sprintf("%v", time.Now().Unix()), 32 | CheckState: false, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/core/database/mysql_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm" 6 | "testing" 7 | ) 8 | 9 | func TestMysql_Init(t *testing.T) { 10 | 11 | } 12 | 13 | func TestMysql_ListAllInfo(t *testing.T) { 14 | fmt.Println("Test") 15 | m := new(Mysql) 16 | m.connect("192.168.52.153", "3306", "apikiller", "root", "123456") 17 | m.addHttpItem("123123") 18 | } 19 | 20 | func TestMysql_addHttpItem(t *testing.T) { 21 | type fields struct { 22 | db *gorm.DB 23 | MaxCount int 24 | } 25 | type args struct { 26 | item string 27 | } 28 | tests := []struct { 29 | name string 30 | fields fields 31 | args args 32 | want string 33 | }{ 34 | // TODO: Add test cases. 35 | { 36 | name: "", 37 | fields: fields{}, 38 | args: args{}, 39 | want: "", 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | m := &Mysql{ 45 | db: tt.fields.db, 46 | MaxCount: tt.fields.MaxCount, 47 | } 48 | if got := m.addHttpItem(tt.args.item); got != tt.want { 49 | t.Errorf("addHttpItem() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/core/notify/dingding.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "bytes" 6 | "fmt" 7 | "github.com/spf13/viper" 8 | "net/http" 9 | ) 10 | 11 | type Dingding struct { 12 | webhookUrl string 13 | } 14 | 15 | func (d *Dingding) Notify(item *data.DataItem) { 16 | //logger.Infoln("notify dingding robot") 17 | 18 | var jsonData []byte 19 | 20 | // Message format setting 21 | MessageFormat := fmt.Sprintf("%s-%s exists %s", item.Domain, item.Url, item.VulnType) 22 | 23 | jsonData = []byte(fmt.Sprintf(`{ 24 | "at": { 25 | "isAtAll": true 26 | }, 27 | "text": { 28 | "content":"%s" 29 | }, 30 | "msgtype":"text" 31 | }`, MessageFormat)) 32 | 33 | request, _ := http.NewRequest("POST", d.webhookUrl, bytes.NewBuffer(jsonData)) 34 | request.Header.Set("Content-Type", "application/json; charset=UTF-8") 35 | 36 | client := http.Client{} 37 | response, _ := client.Do(request) 38 | 39 | defer response.Body.Close() 40 | } 41 | 42 | func NewDingdingNotifer() *Dingding { 43 | // get config 44 | webhookUrl := viper.GetString("app.notifier.Dingding.webhookUrl") 45 | // create 46 | notifer := &Dingding{webhookUrl: webhookUrl} 47 | 48 | return notifer 49 | } 50 | -------------------------------------------------------------------------------- /apikiller.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE `apikiller` character set utf8; 2 | 3 | use apikiller; 4 | 5 | CREATE TABLE `data_item_strs` ( 6 | `id` varchar(50) NOT NULL, 7 | `domain` varchar(100) DEFAULT NULL, 8 | `Url` varchar(500) DEFAULT NULL, 9 | `method` varchar(10) DEFAULT NULL, 10 | `https` tinyint(1) DEFAULT NULL, 11 | `source_request` varchar(50) DEFAULT NULL, 12 | `source_response` varchar(50) DEFAULT NULL, 13 | `vuln_type` varchar(100) DEFAULT NULL, 14 | `vuln_request` varchar(500) DEFAULT NULL, 15 | `vuln_response` varchar(500) DEFAULT NULL, 16 | `check_state` tinyint(1) DEFAULT NULL, 17 | `report_time` varchar(20) DEFAULT NULL 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; 19 | 20 | 21 | 22 | CREATE TABLE `http_items` ( 23 | `id` int(11) NOT NULL AUTO_INCREMENT, 24 | `item` varchar(15000) CHARACTER SET utf8 DEFAULT NULL, 25 | PRIMARY KEY (`id`) 26 | ) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=latin1; 27 | 28 | 29 | -- v0.0.4 -------------------------------------------------------------------------------- /internal/core/module/SSRF/SSRFDetector.go: -------------------------------------------------------------------------------- 1 | package SSRF 2 | 3 | import ( 4 | ahttp2 "APIKiller/internal/core/ahttp" 5 | "APIKiller/internal/core/data" 6 | "APIKiller/internal/core/module" 7 | "APIKiller/pkg/logger" 8 | util2 "APIKiller/pkg/util" 9 | "fmt" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | type SSRFDetector struct { 14 | ReverseConnectionPlatform string // end with “/” 15 | } 16 | 17 | func NewSSRFDetector() module.Detecter { 18 | if viper.GetInt("app.module.SSRFDetector.option") == 0 { 19 | return nil 20 | } 21 | 22 | logger.Infoln("[Load Module] SSRF detect module") 23 | 24 | return &SSRFDetector{ 25 | ReverseConnectionPlatform: "http://zpysri.ceye.io/", 26 | } 27 | } 28 | 29 | func (d *SSRFDetector) Detect(item *data.DataItem) (result *data.DataItem) { 30 | logger.Debugln("[Detect] SSRF detect") 31 | 32 | //srcResp := item.SourceResponse 33 | srcReq := item.SourceRequest 34 | 35 | token := util2.GenerateRandomId() 36 | 37 | newReq := ahttp2.ModifyQueryParamByRegExp(srcReq, `https?://[^\s&]+`, d.ReverseConnectionPlatform+fmt.Sprintf("%s%s%s", "SSRF", module.AsyncDetectVulnTypeSeperator, token)) 38 | if newReq == nil { 39 | logger.Debugln("parameter not found") 40 | return 41 | } 42 | 43 | // do newReq 44 | newResp := ahttp2.DoRequest(newReq) 45 | 46 | // asynchronous result 47 | return data.BuildResult(item, "SSRF"+module.AsyncDetectVulnTypeSeperator+token, newReq, newResp) 48 | } 49 | -------------------------------------------------------------------------------- /internal/core/origin/fileInputOrigin/burpFile.go: -------------------------------------------------------------------------------- 1 | package fileInputOrigin 2 | 3 | import ( 4 | "APIKiller/internal/core/origin" 5 | "APIKiller/pkg/logger" 6 | "encoding/base64" 7 | "github.com/beevik/etree" 8 | ) 9 | 10 | // parseData 11 | // 12 | // @Description: parse data from burpsuite file 13 | // @receiver o 14 | func (o *FileInputOrigin) parseDataFromBurpFile() { 15 | 16 | doc := etree.NewDocument() 17 | 18 | if err := doc.ReadFromFile(o.path); err != nil { 19 | panic(err) 20 | } 21 | 22 | root := doc.SelectElement("items") 23 | for _, item := range root.SelectElements("item") { 24 | url := item.SelectElement("url") 25 | //fmt.Println(url.Text()) 26 | rawUrl := url.Text() 27 | 28 | request := item.SelectElement("request") 29 | rawRequestBytes, err2 := base64.StdEncoding.DecodeString(request.Text()) 30 | if err2 != nil { 31 | logger.Errorln("base64 decode error", err2) 32 | panic(err2) 33 | } 34 | 35 | response := item.SelectElement("response") 36 | rawResponseBytes, err3 := base64.StdEncoding.DecodeString(response.Text()) 37 | if err2 != nil { 38 | logger.Errorln("base64 decode error", err3) 39 | panic(err3) 40 | } 41 | 42 | req, resp := RecoverHttpRequest(string(rawRequestBytes), rawUrl, string(rawResponseBytes)) 43 | 44 | //transport via channel 45 | origin.TransportOriginRequest(&origin.TransferItem{ 46 | Req: req, 47 | Resp: resp, 48 | }) 49 | } 50 | 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /internal/core/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | ) 6 | 7 | type Database interface { 8 | ListAllInfo() []data.DataItemStr 9 | AddInfo(item *data.DataItem) 10 | Exist(domain, url, method string) bool 11 | UpdateVulnType(vulnType string) 12 | } 13 | 14 | var ( 15 | saveTaskQueue chan *data.DataItem 16 | updateTaskQueue chan string 17 | db Database 18 | ) 19 | 20 | // 21 | // CreateSaveTask 22 | // @Description: create save result task 23 | // @param item 24 | // 25 | func CreateSaveTask(item *data.DataItem) { 26 | saveTaskQueue <- item 27 | } 28 | 29 | func CreateUpdateTask(vulnType string) { 30 | updateTaskQueue <- vulnType 31 | } 32 | 33 | // 34 | // BindDatabase 35 | // @Description: bind global database with provided db object 36 | // @param database 37 | // 38 | func BindDatabase(database Database) { 39 | db = database 40 | 41 | // create result save task system 42 | saveTaskQueue = make(chan *data.DataItem, 1024) 43 | // result-save queue 44 | go func() { 45 | var item *data.DataItem 46 | for { 47 | item = <-saveTaskQueue 48 | db.AddInfo(item) 49 | } 50 | }() 51 | 52 | // create update task system 53 | updateTaskQueue = make(chan string, 1024) 54 | // update vulnType queue 55 | go func() { 56 | var vulnType string 57 | for { 58 | vulnType = <-updateTaskQueue 59 | // update vuln type in db 60 | db.UpdateVulnType(vulnType) 61 | } 62 | }() 63 | } 64 | -------------------------------------------------------------------------------- /internal/core/data/meta.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | //_ "gorm.aio/gorm" 5 | "net/http" 6 | ) 7 | 8 | type DataItem struct { 9 | Id string 10 | Domain string 11 | Url string 12 | Method string 13 | Https bool //http/https flag 14 | SourceRequest *http.Request 15 | SourceResponse *http.Response 16 | VulnType string 17 | VulnRequest *http.Request 18 | VulnResponse *http.Response 19 | ReportTime string 20 | CheckState bool 21 | } 22 | 23 | type DataItemStr struct { 24 | Id string `json:"Id" form:"Id" ` 25 | Domain string `json:"Domain" form:"Domain" ` 26 | Url string `json:"Url" form:"Url" ` 27 | Method string `json:"Method" form:"Method"` 28 | Https bool `json:"Https" form:"Https" ` 29 | SourceRequest string `json:"SourceRequest" form:"SourceRequest" ` 30 | SourceResponse string `json:"SourceResponse" form:"SourceResponse" ` 31 | VulnType string `json:"VulnType" form:"VulnType" ` 32 | VulnRequest string `json:"VulnRequest" form:"VulnRequest" ` 33 | VulnResponse string `json:"VulnResponse" form:"VulnResponse" ` 34 | ReportTime string `json:"ReportTime" form:"ReportTime" ` 35 | CheckState bool `json:"CheckState" form:"CheckState" ` 36 | } 37 | 38 | type HttpItem struct { 39 | // 40 | Id int64 `json:"id" form:"id" gorm:"primaryKey" ` 41 | // string format of http 42 | Item string `json:"item" form:"item" ` 43 | } 44 | -------------------------------------------------------------------------------- /internal/core/async/asyncCheckEngineX_test.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAsyncCheckEngine_Start(t *testing.T) { 8 | type fields struct { 9 | httpAPI string 10 | } 11 | tests := []struct { 12 | name string 13 | fields fields 14 | }{ 15 | { 16 | name: "test", 17 | fields: fields{httpAPI: "http://api.ceye.io/v1/records?token=0920449a5ed8b9db7a287a66a6632498&type=http"}, 18 | }, // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | e := &AsyncCheckEngine{ 23 | httpAPI: tt.fields.httpAPI, 24 | } 25 | e.Start() 26 | }) 27 | } 28 | } 29 | 30 | func TestAsyncCheckEngine_heartbeat(t *testing.T) { 31 | type fields struct { 32 | httpAPI string 33 | lastRecordId string 34 | } 35 | tests := []struct { 36 | name string 37 | fields fields 38 | want bool 39 | }{ 40 | { 41 | name: "", 42 | fields: fields{ 43 | httpAPI: "http://api.ceye.io/v1/records?token=0920449a5ed8b9db7a287a66a6632498&type=http", 44 | lastRecordId: "xxxxx", 45 | }, 46 | want: false, 47 | }, // TODO: Add test cases. 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | e := &AsyncCheckEngine{ 52 | httpAPI: tt.fields.httpAPI, 53 | lastRecordId: tt.fields.lastRecordId, 54 | } 55 | if got := e.heartbeat(); got != tt.want { 56 | t.Errorf("heartbeat() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/core/module/DoS/resourceSizeDetector.go: -------------------------------------------------------------------------------- 1 | package DoS 2 | 3 | import ( 4 | ahttp2 "APIKiller/internal/core/ahttp" 5 | "APIKiller/internal/core/data" 6 | "github.com/spf13/viper" 7 | "net/http" 8 | "strconv" 9 | ) 10 | 11 | type resourceSizeDetector struct { 12 | sizeParams []string 13 | } 14 | 15 | func (d *resourceSizeDetector) Detect(item *data.DataItem) (result *data.DataItem) { 16 | srcReq := item.SourceRequest 17 | srcResp := item.SourceResponse 18 | 19 | for _, param := range d.sizeParams { 20 | 21 | // replace value of params in new newReq 22 | newReq := ahttp2.ModifyParam(srcReq, param, "10000") 23 | if newReq == nil { 24 | continue 25 | } 26 | 27 | // do newReq 28 | newResp := ahttp2.DoRequest(newReq) 29 | 30 | if newResp == nil { 31 | return 32 | } 33 | 34 | // judge 35 | if d.judge(srcResp, newResp) { 36 | return data.BuildResult(item, "DoS-ResourceSizeNotStrict", newReq, newResp) 37 | } 38 | 39 | return nil 40 | } 41 | return nil 42 | 43 | } 44 | 45 | func (d *resourceSizeDetector) judge(srcResp, newResp *http.Response) bool { 46 | srcCL, _ := strconv.Atoi(srcResp.Header.Get("Content-Length")) 47 | newCL, _ := strconv.Atoi(newResp.Header.Get("Content-Length")) 48 | if newCL/10 > srcCL { // successfully 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | func newResourceSizeDetector() *resourceSizeDetector { 55 | return &resourceSizeDetector{ 56 | sizeParams: viper.GetStringSlice("app.module.DoSDetector.sizeParam"), 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/core/handler.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "APIKiller/internal/core/database" 6 | "APIKiller/internal/core/module" 7 | "APIKiller/internal/core/notify" 8 | "APIKiller/internal/core/origin" 9 | "APIKiller/pkg/logger" 10 | "fmt" 11 | "strings" 12 | ) 13 | 14 | var notifier notify.Notify 15 | 16 | func NewHandler(httpItem *origin.TransferItem) { 17 | r := httpItem.Req 18 | 19 | // assembly DataItem 20 | item := &data.DataItem{ 21 | Id: "", 22 | Domain: r.Host, 23 | Url: r.URL.Path, 24 | Https: r.URL.Scheme == "https", 25 | Method: r.Method, 26 | SourceRequest: r, 27 | SourceResponse: httpItem.Resp, 28 | VulnType: "", 29 | VulnRequest: nil, 30 | VulnResponse: nil, 31 | ReportTime: "", 32 | CheckState: false, 33 | } 34 | 35 | // enum all modules and detect 36 | modules := module.GetModules() 37 | for i, _ := range modules { 38 | go func(x int) { 39 | resultDataItem := modules[x].Detect(item) 40 | 41 | // exist vulnerable 42 | if resultDataItem != nil { 43 | if strings.Index(resultDataItem.VulnType, module.AsyncDetectVulnTypeSeperator) <= 0 { 44 | logger.Infoln(fmt.Sprintf("[Found Vulnerability] %s%s-->%s", resultDataItem.Domain, resultDataItem.Url, resultDataItem.VulnType)) 45 | // create notification 46 | notify.CreateNotification(resultDataItem) 47 | } 48 | // save result 49 | database.CreateSaveTask(resultDataItem) 50 | } 51 | }(i) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /dbDeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define variables 4 | MYSQL_ROOT_PASSWORD="123456" 5 | MYSQL_USER="apikiller" 6 | MYSQL_PASSWORD="password" 7 | MYSQL_DATABASE="apikiller" 8 | 9 | # ANSI color codes 10 | bold=$(tput bold) 11 | normal=$(tput sgr0) 12 | green=$(tput setaf 2) 13 | 14 | # Pull mysql:5.7 image 15 | docker pull mysql 16 | 17 | # Start mysql container 18 | docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} -p 3306:3306 -d mysql 19 | 20 | # Wait for mysql service to start 21 | until docker exec -it mysql-server mysqladmin ping --silent &> /dev/null; do 22 | echo "waiting for mysql to start ..." 23 | sleep 10 24 | done 25 | 26 | 27 | # Create new user and import data 28 | docker cp ./apikiller.sql mysql-server:/tmp/apikiller.sql 29 | docker exec -i mysql-server mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e "CREATE USER '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; GRANT ALL PRIVILEGES ON ${MYSQL_DATABASE}.* TO '${MYSQL_USER}'@'%'; FLUSH PRIVILEGES; source /tmp/apikiller.sql;" 30 | 31 | echo "MySQL setup complete!" 32 | 33 | # Print username and password in green color 34 | echo "\r\n${bold}${green}\r\n======================================================================" 35 | echo "Login MySQL server IP address (default returns info of the first network interface): `hostname -I | awk '{print $1}'`" 36 | echo "Login MySQL server username: ${MYSQL_USER}" 37 | echo "Login MySQL server password: ${MYSQL_PASSWORD}" 38 | echo "Project database: ${MYSQL_DATABASE}${normal}" 39 | echo "======================================================================" -------------------------------------------------------------------------------- /internal/core/aio/repeatReadCloser.go: -------------------------------------------------------------------------------- 1 | package aio 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | "unsafe" 8 | ) 9 | 10 | type RepeatReadCloser struct { 11 | Reader *bytes.Buffer 12 | } 13 | 14 | // 15 | // Read reset point after each read 16 | // @Description: 17 | // @receiver p 18 | // @param val 19 | // @return n 20 | // @return err 21 | // 22 | func (p *RepeatReadCloser) Read(val []byte) (n int, err error) { 23 | if p.Reader.Len() == 0 { 24 | // reset offset 25 | p.resetBufferOffset() 26 | 27 | return 0, io.EOF 28 | } 29 | 30 | n, err = p.Reader.Read(val) 31 | 32 | return 33 | } 34 | 35 | // 36 | // resetBufferOffset 37 | // @Description: reset offset and lastRead of buffer 38 | // @receiver p 39 | // 40 | func (p *RepeatReadCloser) resetBufferOffset() { 41 | r := reflect.ValueOf(p.Reader) 42 | buffer := r.Elem() 43 | 44 | // set buffer.off = 0 45 | offValue := buffer.FieldByName("off") 46 | offValue = reflect.NewAt(offValue.Type(), unsafe.Pointer(offValue.UnsafeAddr())).Elem() 47 | offValue.SetInt(0) 48 | 49 | // sync set buffer.lastRead = opInvalid 50 | lastReadValue := buffer.FieldByName("lastRead") 51 | lastReadValue = reflect.NewAt(lastReadValue.Type(), unsafe.Pointer(lastReadValue.UnsafeAddr())).Elem() 52 | lastReadValue.SetInt(0) 53 | } 54 | 55 | func (p *RepeatReadCloser) Close() error { 56 | 57 | return nil 58 | } 59 | 60 | // 61 | // TransformReadCloser 62 | // @Description: quickly transform aio.Reader into RepeatReadCloser 63 | // @param r 64 | // 65 | func TransformReadCloser(r io.Reader) *RepeatReadCloser { 66 | buf := new(bytes.Buffer) 67 | buf.ReadFrom(r) 68 | 69 | return &RepeatReadCloser{Reader: buf} 70 | } 71 | -------------------------------------------------------------------------------- /pkg/logger/formatter.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultLogFormat = "[%lvl%]: %time% - %msg%" 11 | defaultTimestampFormat = time.RFC3339 12 | ) 13 | 14 | // Formatter implements logrus.Formatter interface. 15 | type Formatter struct { 16 | // Timestamp format 17 | TimestampFormat string 18 | // Available standard keys: time, msg, lvl 19 | // Also can include custom fields but limited to strings. 20 | // All of fields need to be wrapped inside %% i.e %time% %msg% 21 | LogFormat string 22 | 23 | // Disables the truncation of the level text to 4 characters. 24 | DisableLevelTruncation bool 25 | } 26 | 27 | // 28 | // Format 29 | // @Description: building log message 30 | // @receiver f 31 | // @param entry 32 | // @return []byte 33 | // @return error 34 | // 35 | func (f *Formatter) Format(entry *logrus.Entry) ([]byte, error) { 36 | output := f.LogFormat 37 | if output == "" { 38 | output = defaultLogFormat 39 | } 40 | 41 | timestampFormat := f.TimestampFormat 42 | if timestampFormat == "" { 43 | timestampFormat = defaultTimestampFormat 44 | } 45 | 46 | output = strings.Replace(output, "%time%", entry.Time.Format(timestampFormat), 1) 47 | output = strings.Replace(output, "%msg%", entry.Message, 1) 48 | level := strings.ToUpper(entry.Level.String()) 49 | if !f.DisableLevelTruncation { 50 | level = level[:4] 51 | } 52 | output = strings.Replace(output, "%lvl%", level, 1) 53 | 54 | for k, v := range entry.Data { 55 | if s, ok := v.(string); ok { 56 | output = strings.Replace(output, "%"+k+"%", s, 1) 57 | } 58 | } 59 | output += "\n" 60 | 61 | return []byte(output), nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/core/ahttp/dumpHttp.go: -------------------------------------------------------------------------------- 1 | package ahttp 2 | 3 | import ( 4 | "APIKiller/pkg/logger" 5 | "net/http" 6 | "net/http/httputil" 7 | ) 8 | 9 | // 10 | // DumpRequest 11 | // @Description: convert *http.Request to string 12 | // @param request 13 | // @return string 14 | // 15 | func DumpRequest(request *http.Request) string { 16 | if request == nil { 17 | logger.Debugln("dump response error: request is nil") 18 | return "" 19 | } 20 | 21 | dumpRequest, _ := httputil.DumpRequest(request, request.Body != nil) 22 | 23 | return string(dumpRequest) 24 | } 25 | 26 | // 27 | // DumpResponse 28 | // @Description: convert *http.Response to string 29 | // @param response 30 | // @return string 31 | // 32 | func DumpResponse(response *http.Response) string { 33 | if response == nil { 34 | logger.Debugln("dump response error: response is nil") 35 | return "" 36 | } 37 | 38 | dumpRequest, _ := httputil.DumpResponse(response, response.Body != nil) 39 | 40 | return string(dumpRequest) 41 | } 42 | 43 | // 44 | // DumpRequests 45 | // @Description: convert []*http.Request to []string 46 | // @param requests 47 | // @return []string 48 | // 49 | func DumpRequests(requests []*http.Request) []string { 50 | var result []string 51 | for _, request := range requests { 52 | dumpRequest := DumpRequest(request) 53 | result = append(result, dumpRequest) 54 | } 55 | 56 | return result 57 | } 58 | 59 | // 60 | // DumpResponses 61 | // @Description: convert []*http.Response to []string 62 | // @param requests 63 | // @return []string 64 | // 65 | func DumpResponses(responses []*http.Response) []string { 66 | var result []string 67 | for _, response := range responses { 68 | dumpResponse := DumpResponse(response) 69 | result = append(result, dumpResponse) 70 | } 71 | 72 | return result 73 | } 74 | -------------------------------------------------------------------------------- /internal/core/origin/fileInputOrigin/fileInputOrigin.go: -------------------------------------------------------------------------------- 1 | package fileInputOrigin 2 | 3 | import ( 4 | "APIKiller/pkg/logger" 5 | "bufio" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | type FileInputOrigin struct { 14 | path string 15 | } 16 | 17 | func (o *FileInputOrigin) LoadOriginRequest() { 18 | logger.Infoln("[Load Request] load request from file input origin") 19 | 20 | if stat, _ := os.Stat(o.path); stat.IsDir() { 21 | // load origin from target directory 22 | 23 | } else { 24 | // load origin from target file[eg. burp file] 25 | o.parseDataFromBurpFile() 26 | } 27 | 28 | } 29 | 30 | // RecoverHttpRequest 31 | // 32 | // @Description: create one new http.Request with rawRequest and rawURL 33 | // @param rawRequest 34 | // @param rawURL 35 | // @return *http.Request 36 | func RecoverHttpRequest(rawRequest, rawURL, rawResponse string) (*http.Request, *http.Response) { 37 | b := bufio.NewReader(strings.NewReader(rawRequest)) 38 | 39 | req, err := http.ReadRequest(b) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | // We can't have this set. And it only contains "/pkg/net/http/" anyway 45 | req.RequestURI = "" 46 | 47 | // Since the req.URL will not have all the information set, 48 | // such as protocol scheme and host, we create a new URL 49 | u, err := url.Parse(rawURL) 50 | if err != nil { 51 | panic(err) 52 | } 53 | req.URL = u 54 | 55 | b2 := bufio.NewReader(strings.NewReader(rawResponse)) 56 | 57 | resp, _ := http.ReadResponse(b2, req) 58 | 59 | return req, resp 60 | } 61 | 62 | func NewFileInputOrigin(path string) *FileInputOrigin { 63 | logger.Infoln("[Origin] file input origin") 64 | 65 | // determine whether the path is a file or a directory 66 | _, err := os.Stat(path) 67 | if os.IsNotExist(err) { 68 | logger.Errorln(fmt.Sprintf("%s does not exist", path)) 69 | panic(fmt.Sprintf("%s does not exist", path)) 70 | } 71 | 72 | // instantiate FileInputOrigin 73 | return &FileInputOrigin{path: path} 74 | } 75 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | var logger = logrus.New() 11 | 12 | func Initial(level logrus.Level, logPath string) { 13 | formatter := &Formatter{ 14 | LogFormat: "%time% [%lvl%] %msg%", 15 | TimestampFormat: "2006-01-02 15:04:05", 16 | } 17 | 18 | // Output to stdout instead of the default stderr 19 | // Can be any io.Writer, see below for File example 20 | logger.SetFormatter(formatter) 21 | logger.SetOutput(os.Stdout) 22 | logger.SetLevel(level) 23 | 24 | // Output to file 25 | logFilePath := filepath.Join(logPath, "./log/new.log") 26 | rotateFileHook, err := NewRotateFileHook(RotateFileConfig{ 27 | Filename: logFilePath, 28 | MaxSize: 50, 29 | MaxBackups: 1024, 30 | MaxAge: 30, 31 | LocalTime: true, 32 | Level: level, 33 | Formatter: formatter, 34 | }) 35 | if err != nil { 36 | fmt.Printf("Create log rotate hooks error: %s\n", err) 37 | return 38 | } 39 | logger.AddHook(rotateFileHook) 40 | } 41 | 42 | func Debugln(args ...interface{}) { 43 | logger.Debug(args...) 44 | } 45 | 46 | func Debugf(format string, args ...interface{}) { 47 | logger.Debugf(format, args...) 48 | } 49 | 50 | func Infoln(args ...interface{}) { 51 | logger.Info(args...) 52 | } 53 | 54 | func Infof(format string, args ...interface{}) { 55 | logger.Infof(format, args...) 56 | } 57 | 58 | func Warnln(args ...interface{}) { 59 | logger.Warn(args...) 60 | } 61 | 62 | func Warnf(format string, args ...interface{}) { 63 | logger.Warnf(format, args...) 64 | } 65 | 66 | func Errorln(args ...interface{}) { 67 | logger.Error(args...) 68 | } 69 | 70 | func Errorf(format string, args ...interface{}) { 71 | logger.Errorf(format, args...) 72 | } 73 | 74 | func Panic(args ...interface{}) { 75 | logrus.Panic(args...) 76 | } 77 | 78 | func Fatal(args ...interface{}) { 79 | logrus.Fatal(args...) 80 | } 81 | 82 | func Fatalf(format string, args ...interface{}) { 83 | logrus.Fatalf(format, args...) 84 | } 85 | -------------------------------------------------------------------------------- /ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD 3 | VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM 4 | B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0 5 | aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0 6 | MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE 7 | CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV 8 | BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI 9 | hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP 10 | ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9 11 | 3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP 12 | sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9 13 | V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh 14 | hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr 15 | lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq 16 | j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo 17 | WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD 18 | fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj 19 | YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh 20 | WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj 21 | UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4 22 | uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB 23 | CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F 24 | AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0 25 | C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3 26 | Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin 27 | o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye 28 | i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr 29 | bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY 30 | VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft 31 | 8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86 32 | NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV 33 | BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA== 34 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /internal/core/module/OpenRedirect/OpenRedirectDetector.go: -------------------------------------------------------------------------------- 1 | package OpenRedirect 2 | 3 | import ( 4 | ahttp2 "APIKiller/internal/core/ahttp" 5 | "APIKiller/internal/core/data" 6 | "APIKiller/internal/core/module" 7 | "APIKiller/pkg/logger" 8 | "github.com/spf13/viper" 9 | "io/ioutil" 10 | "net/http" 11 | "strings" 12 | ) 13 | 14 | type OpenRedirectDetector struct { 15 | rawQueryParams []string 16 | failFlag []string 17 | evilLink string 18 | } 19 | 20 | func (d *OpenRedirectDetector) Detect(item *data.DataItem) (result *data.DataItem) { 21 | logger.Debugln("[Detect] Open-Redirect detect") 22 | 23 | srcResp := item.SourceResponse 24 | srcReq := item.SourceRequest 25 | 26 | // filter by features of redirect 27 | if srcResp.Header.Get("Location") == "" { 28 | return 29 | } 30 | 31 | newReq := ahttp2.ModifyQueryParamByRegExp(srcReq, `https?://[^\s&]+`, d.evilLink) 32 | if newReq == nil { 33 | logger.Debugln("parameter not found") 34 | return 35 | } 36 | 37 | // do newReq 38 | newResp := ahttp2.DoRequest(newReq) 39 | 40 | // judge 41 | if d.judge(srcResp, newResp) { 42 | return data.BuildResult(item, "Open-Redirect", newReq, newResp) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func (d *OpenRedirectDetector) judge(srcResp, newResp *http.Response) bool { 49 | if newResp.StatusCode == srcResp.StatusCode && strings.Index(newResp.Header.Get("Location"), d.evilLink) != -1 { 50 | return true 51 | } 52 | 53 | // black list 54 | if newResp.Body != nil { 55 | bytes, _ := ioutil.ReadAll(newResp.Body) 56 | for _, flag := range d.failFlag { 57 | if strings.Contains(string(bytes), flag) { 58 | return false 59 | } 60 | } 61 | } 62 | 63 | return false 64 | } 65 | 66 | func NewOpenRedirectDetector() module.Detecter { 67 | if viper.GetInt("app.module.openRedirectDetector.option") == 0 { 68 | return nil 69 | } 70 | 71 | logger.Infoln("[Load Module] Open-Redirect detect module") 72 | 73 | d := &OpenRedirectDetector{ 74 | rawQueryParams: viper.GetStringSlice("app.module.openRedirectDetector.rawQueryParams"), 75 | evilLink: "https://www.baidu.com", 76 | failFlag: viper.GetStringSlice("app.module.openRedirectDetector.failFlag"), 77 | } 78 | 79 | return d 80 | } 81 | -------------------------------------------------------------------------------- /pkg/logger/rotate.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | "os" 8 | "time" 9 | ) 10 | 11 | type RotateFileConfig struct { 12 | Filename string 13 | MaxSize int 14 | MaxBackups int 15 | MaxAge int 16 | Level logrus.Level 17 | LocalTime bool 18 | Formatter logrus.Formatter 19 | } 20 | 21 | type RotateFileHook struct { 22 | Config RotateFileConfig 23 | nextRotateTime time.Time 24 | logWriter *lumberjack.Logger 25 | } 26 | 27 | func NewRotateFileHook(config RotateFileConfig) (logrus.Hook, error) { 28 | hook := RotateFileHook{ 29 | Config: config, 30 | } 31 | 32 | // load rotate log system 33 | err := hook.rotateLogFile() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return &hook, nil 39 | } 40 | 41 | func (hook *RotateFileHook) Levels() []logrus.Level { 42 | return logrus.AllLevels[:hook.Config.Level+1] 43 | } 44 | 45 | func (hook *RotateFileHook) Fire(entry *logrus.Entry) (err error) { 46 | b, err := hook.Config.Formatter.Format(entry) 47 | if err != nil { 48 | return err 49 | } 50 | _, err = hook.logWriter.Write(b) 51 | return 52 | } 53 | 54 | func (hook *RotateFileHook) rotateLogFile() error { 55 | now := time.Now() 56 | if now.After(hook.nextRotateTime) { 57 | // close current log file 58 | if hook.logWriter != nil { 59 | err := hook.logWriter.Close() 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | 65 | // calculate next rotate time 66 | hook.nextRotateTime = now.Truncate(24 * time.Hour).Add(24 * time.Hour) 67 | 68 | // rename log filename according to date 69 | date := fmt.Sprintf("%d-%d-%d", now.Year(), now.Month(), now.Day()) 70 | 71 | _, err1 := os.Stat(hook.Config.Filename) 72 | if err1 == nil { 73 | err := os.Rename(hook.Config.Filename, "./log/"+date+".log") 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | 79 | // create new log file 80 | hook.logWriter = &lumberjack.Logger{ 81 | Filename: hook.Config.Filename, 82 | MaxSize: hook.Config.MaxSize, 83 | MaxBackups: hook.Config.MaxBackups, 84 | MaxAge: hook.Config.MaxAge, 85 | LocalTime: hook.Config.LocalTime, 86 | } 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module APIKiller 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/antlabs/strsim v0.0.3 7 | github.com/beevik/etree v1.1.0 8 | github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 9 | github.com/gin-gonic/gin v1.8.2 10 | github.com/sirupsen/logrus v1.9.0 11 | github.com/spf13/viper v1.15.0 12 | golang.org/x/exp v0.0.0-20230223210539-50820d90acfd 13 | golang.org/x/net v0.5.0 14 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 15 | gorm.io/driver/mysql v1.4.5 16 | gorm.io/gorm v1.24.3 17 | ) 18 | 19 | require ( 20 | github.com/fsnotify/fsnotify v1.6.0 // indirect 21 | github.com/gin-contrib/sse v0.1.0 // indirect 22 | github.com/go-playground/locales v0.14.1 // indirect 23 | github.com/go-playground/universal-translator v0.18.0 // indirect 24 | github.com/go-playground/validator/v10 v10.11.1 // indirect 25 | github.com/go-sql-driver/mysql v1.7.0 // indirect 26 | github.com/goccy/go-json v0.10.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/jinzhu/inflection v1.0.0 // indirect 29 | github.com/jinzhu/now v1.1.5 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/leodido/go-urn v1.2.1 // indirect 32 | github.com/magiconair/properties v1.8.7 // indirect 33 | github.com/mattn/go-isatty v0.0.17 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 38 | github.com/spf13/afero v1.9.3 // indirect 39 | github.com/spf13/cast v1.5.0 // indirect 40 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 41 | github.com/spf13/pflag v1.0.5 // indirect 42 | github.com/subosito/gotenv v1.4.2 // indirect 43 | github.com/tidwall/gjson v1.14.4 // indirect 44 | github.com/tidwall/match v1.1.1 // indirect 45 | github.com/tidwall/pretty v1.2.1 // indirect 46 | github.com/ugorji/go/codec v1.2.8 // indirect 47 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect 48 | golang.org/x/sys v0.4.0 // indirect 49 | golang.org/x/text v0.6.0 // indirect 50 | google.golang.org/protobuf v1.28.1 // indirect 51 | gopkg.in/ini.v1 v1.67.0 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /internal/core/notify/lark.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "APIKiller/pkg/logger" 6 | "bytes" 7 | "crypto/hmac" 8 | "crypto/sha256" 9 | "encoding/base64" 10 | "fmt" 11 | "github.com/spf13/viper" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | type Lark struct { 17 | webhookUrl string 18 | secret string 19 | signature string 20 | timestamp int64 21 | } 22 | 23 | func (l *Lark) genSign() { 24 | //get timestamp 25 | l.timestamp = time.Now().Unix() 26 | 27 | //timestamp + key 做sha256, 再进行base64 encode 28 | stringToSign := fmt.Sprintf("%v", l.timestamp) + "\n" + l.secret 29 | 30 | var data []byte 31 | h := hmac.New(sha256.New, []byte(stringToSign)) 32 | _, err := h.Write(data) 33 | if err != nil { 34 | logger.Errorln("lark generate signature error") 35 | panic("Lark generate signature error") 36 | } 37 | 38 | l.signature = base64.StdEncoding.EncodeToString(h.Sum(nil)) 39 | } 40 | 41 | func (l *Lark) init() { 42 | //generate signature 43 | if l.secret != "" { 44 | l.genSign() 45 | } 46 | 47 | } 48 | 49 | // NewLarkNotifier 50 | // 51 | // @Description: create a lark object 52 | // @param webhook lark webhook url 53 | // @param signature lark webhook authorize parameter(optional) 54 | // @return *Lark 55 | func NewLarkNotifier() *Lark { 56 | // get config 57 | webhookUrl := viper.GetString("app.notifier.Lark.webhookUrl") 58 | secret := viper.GetString("app.notifier.Lark.secret") 59 | 60 | // create 61 | lark := &Lark{ 62 | webhookUrl: webhookUrl, 63 | signature: secret, 64 | } 65 | 66 | // init object 67 | lark.init() 68 | 69 | return lark 70 | } 71 | 72 | func (l *Lark) Notify(item *data.DataItem) { 73 | //logger.Infoln("notify lark robot") 74 | 75 | var jsonData []byte 76 | 77 | // Message format setting 78 | MessageFormat := fmt.Sprintf("Domain:%s-Url:%s --> %s", item.Domain, item.Url, item.VulnType) 79 | 80 | if l.secret != "" { 81 | jsonData = []byte(fmt.Sprintf(` 82 | { 83 | "timestamp": "%v", 84 | "sign": "%v", 85 | "msg_type": "text", 86 | "content": { 87 | "text": "%v" 88 | } 89 | }`, l.timestamp, l.signature, MessageFormat)) 90 | } else { 91 | jsonData = []byte(fmt.Sprintf(`{"msg_type":"text","content":{"text":"%v"}}`, MessageFormat)) 92 | } 93 | 94 | request, _ := http.NewRequest("POST", l.webhookUrl, bytes.NewBuffer(jsonData)) 95 | request.Header.Set("Content-Type", "application/json; charset=UTF-8") 96 | 97 | client := http.Client{} 98 | response, _ := client.Do(request) 99 | 100 | defer response.Body.Close() 101 | } 102 | -------------------------------------------------------------------------------- /internal/core/async/asyncCheckEngineX.go: -------------------------------------------------------------------------------- 1 | package async 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "APIKiller/internal/core/database" 6 | "APIKiller/internal/core/notify" 7 | "APIKiller/pkg/logger" 8 | "github.com/tidwall/gjson" 9 | "strings" 10 | "time" 11 | 12 | "fmt" 13 | "io/ioutil" 14 | "net/http" 15 | ) 16 | 17 | type AsyncCheckEngine struct { 18 | httpAPI string 19 | lastRecordId string 20 | } 21 | 22 | func NewAsyncCheckEngine() *AsyncCheckEngine { 23 | return &AsyncCheckEngine{ 24 | httpAPI: "http://api.ceye.io/v1/records?token=0920449a5ed8b9db7a287a66a6632498&type=http", 25 | lastRecordId: "0", 26 | } 27 | } 28 | 29 | // 30 | // Start 31 | // @Description: start to check 32 | // @receiver e 33 | // 34 | func (e *AsyncCheckEngine) Start() { 35 | // heart beat detection 36 | if !e.heartbeat() { 37 | logger.Errorln("cannot access target website successfully: http://api.ceye.io") 38 | return 39 | } 40 | 41 | // build a request 42 | request, _ := http.NewRequest("GET", e.httpAPI, nil) 43 | client := http.Client{} 44 | // polling API interface 45 | for { 46 | // make a http request 47 | response, _ := client.Do(request) 48 | 49 | // get data from json body 50 | if response.Body != nil { 51 | all, err := ioutil.ReadAll(response.Body) 52 | if err != nil { 53 | logger.Errorln(err) 54 | } 55 | results := gjson.Get(string(all), "data").Array() 56 | 57 | for _, result := range results { 58 | if result.Get("id").String() <= e.lastRecordId { 59 | continue 60 | } 61 | 62 | name := result.Get("name") 63 | token := strings.Replace(name.String(), "http://zpysri.ceye.io/", "", 1) 64 | 65 | go e.check(token) 66 | } 67 | 68 | if len(results) > 0 { 69 | e.lastRecordId = results[0].Get("id").String() 70 | } 71 | 72 | } 73 | 74 | // sleep 75 | time.Sleep(5 * 1000 * time.Millisecond) 76 | } 77 | } 78 | 79 | // 80 | // heartbeat 81 | // @Description: check the health of http://api.ceye.io/ three times 82 | // @receiver e 83 | // @return bool 84 | // 85 | func (e *AsyncCheckEngine) heartbeat() bool { 86 | request, _ := http.NewRequest("GET", e.httpAPI, nil) 87 | client := http.Client{} 88 | 89 | for i := 0; i < 3; i++ { 90 | response, err := client.Do(request) 91 | if err != nil || response.StatusCode >= 500 { 92 | logger.Debugln(err) 93 | } else { 94 | return true 95 | } 96 | } 97 | return false 98 | } 99 | 100 | // 101 | // check 102 | // @Description: notify async check token and update the corresponding vulnerability record 103 | // @receiver e 104 | // @param token 105 | // 106 | func (e *AsyncCheckEngine) check(token string) { 107 | logger.Infoln(fmt.Sprintf("[async check] token: %s", token)) 108 | 109 | // notify 110 | notify.CreateNotification(&data.DataItem{ 111 | Id: "", 112 | Domain: "异步检测", 113 | Url: "", 114 | Method: "", 115 | Https: false, 116 | SourceRequest: nil, 117 | SourceResponse: nil, 118 | VulnType: token, 119 | VulnRequest: nil, 120 | VulnResponse: nil, 121 | ReportTime: "", 122 | CheckState: false, 123 | }) 124 | 125 | // update database 126 | database.CreateUpdateTask(token) 127 | } 128 | -------------------------------------------------------------------------------- /internal/core/ahttp/ahttp.go: -------------------------------------------------------------------------------- 1 | package ahttp 2 | 3 | import ( 4 | "APIKiller/internal/core/ahttp/hook" 5 | "APIKiller/internal/core/aio" 6 | "APIKiller/pkg/logger" 7 | "bufio" 8 | "crypto/tls" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | ) 14 | 15 | type requestCacheBlock struct { 16 | key *http.Request //method-domain-url 17 | value string 18 | } 19 | 20 | var ( 21 | cache = make([]*requestCacheBlock, 128) 22 | updatePoint = 0 23 | ) 24 | 25 | // 26 | // DoRequest 27 | // @Description: make a http request without auto 30x redirect 28 | // @param r 29 | // @return *http.Response 30 | // 31 | func DoRequest(r *http.Request) *http.Response { 32 | var Client http.Client 33 | 34 | logger.Debugln("Do request: ", r.URL) 35 | 36 | // https request 37 | if r.URL.Scheme == "https" { 38 | // ignore certificate verification 39 | tr := &http.Transport{ 40 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 41 | } 42 | // https client 43 | Client = http.Client{ 44 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 45 | return http.ErrUseLastResponse 46 | }, 47 | Transport: tr, 48 | } 49 | } else { 50 | // http client 51 | Client = http.Client{ 52 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 53 | return http.ErrUseLastResponse 54 | }, 55 | } 56 | } 57 | 58 | // hook before initiating http request 59 | for _, requestHook := range hook.Hooks { 60 | requestHook.HookBefore(r) 61 | } 62 | 63 | response, err := Client.Do(r) 64 | if err != nil { 65 | logger.Errorln(err) 66 | return nil 67 | } 68 | 69 | // hook after finishing http request 70 | for _, requestHook := range hook.Hooks { 71 | requestHook.HookAfter(r) 72 | } 73 | 74 | // transform aio.Reader 75 | if response.Body != nil { 76 | response.Body = aio.TransformReadCloser(response.Body) 77 | } 78 | 79 | return response 80 | } 81 | 82 | // 83 | // RequestClone 84 | // @Description: clone request with source request 85 | // @param src 86 | // @return *http.Request 87 | // 88 | func RequestClone(src *http.Request) *http.Request { 89 | // dump request 90 | reqStr := "" 91 | for _, c := range cache { 92 | if c == nil { 93 | break 94 | } 95 | if c.key == src { 96 | reqStr = c.value 97 | break 98 | } 99 | } 100 | if reqStr == "" { 101 | reqStr = DumpRequest(src) 102 | 103 | if cache[updatePoint] == nil { 104 | cache[updatePoint] = &requestCacheBlock{} 105 | } 106 | cache[updatePoint].key = src 107 | cache[updatePoint].value = reqStr 108 | updatePoint = (updatePoint + 1) % 128 109 | } 110 | 111 | // http.ReadRequest 112 | request, err := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr))) 113 | if err != nil { 114 | logger.Errorln("read request error: ", err) 115 | } 116 | 117 | // we can't have this set. And it only contains "/pkg/net/http/" anyway 118 | request.RequestURI = "" 119 | 120 | // set url 121 | u, err := url.Parse(src.URL.String()) 122 | if err != nil { 123 | logger.Errorln("parse url error: ", err) 124 | } 125 | request.URL = u 126 | 127 | // transform body 128 | if request.Body != nil { 129 | request.Body = aio.TransformReadCloser(request.Body) 130 | 131 | // update content-length 132 | all, _ := ioutil.ReadAll(request.Body) 133 | request.ContentLength = int64(len(all)) 134 | } 135 | 136 | return request 137 | } 138 | 139 | func ResponseClone(src *http.Response, req *http.Request) (dst *http.Response) { 140 | 141 | // dump response 142 | respStr := DumpResponse(src) 143 | 144 | // http.ReadResponse 145 | response, err := http.ReadResponse(bufio.NewReader(strings.NewReader(respStr)), req) 146 | if err != nil { 147 | logger.Errorln("read response error: ", err) 148 | } 149 | 150 | // transform body 151 | response.Body = aio.TransformReadCloser(response.Body) 152 | return response 153 | } 154 | 155 | // 156 | // ExistsParam 157 | // @Description: 158 | // @param req 159 | // @param paramName 160 | // @return bool 161 | // 162 | func ExistsParam(req *http.Request, paramName string) bool { 163 | return false 164 | } 165 | -------------------------------------------------------------------------------- /internal/core/module/CSRF/CSRFDetector.go: -------------------------------------------------------------------------------- 1 | package CSRF 2 | 3 | import ( 4 | http2 "APIKiller/internal/core/ahttp" 5 | "APIKiller/internal/core/data" 6 | "APIKiller/internal/core/module" 7 | "APIKiller/pkg/logger" 8 | "github.com/antlabs/strsim" 9 | "github.com/spf13/viper" 10 | "io/ioutil" 11 | "net/http" 12 | "regexp" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | type CSRFDetector struct { 18 | csrfTokenPattern string 19 | csrfInvalidPattern []string 20 | samesitePolicy map[string]string 21 | mu sync.Mutex 22 | } 23 | 24 | func (d *CSRFDetector) Detect(item *data.DataItem) (result *data.DataItem) { 25 | logger.Debugln("[Detect] CSRF detect") 26 | 27 | srcResp := item.SourceResponse 28 | srcReq := item.SourceRequest 29 | 30 | // same-site check with lock 31 | d.mu.Lock() 32 | if d.samesitePolicy[srcReq.Host] == "" { 33 | d.getSameSitePolicy(item) 34 | } 35 | d.mu.Unlock() 36 | 37 | policy := d.samesitePolicy[srcReq.Host] 38 | if policy == "Strict" { 39 | return 40 | } else if policy == "Lax" && item.Method != "GET" { 41 | return 42 | } else { 43 | // no same-site policy or the policy is fail 44 | } 45 | 46 | // cors--Access-Control-Allow-Origin 47 | value := srcResp.Header.Get("Access-Control-Allow-Origin") 48 | if value != "" && value != "*" { 49 | return 50 | } 51 | 52 | // copy newReq 53 | newReq := http2.RequestClone(srcReq) 54 | 55 | // delete referer and origin 56 | if newReq.Header.Get("Referer") != "" { 57 | newReq.Header.Del("Referer") 58 | } 59 | 60 | if newReq.Header.Get("Origin") != "" { 61 | newReq.Header.Del("Origin") 62 | } 63 | 64 | // find token position and detect before delete csrf token 65 | // 1. row query 66 | 67 | if newReq.URL.RawQuery != "" { 68 | editedRawQuery := []string{} 69 | 70 | for _, kv := range strings.Split(newReq.URL.RawQuery, "&") { 71 | splits := strings.Split(kv, "=") 72 | key := splits[0] 73 | //value := splits[1] 74 | 75 | match, _ := regexp.Match(d.csrfTokenPattern, []byte(key)) 76 | if match { 77 | continue 78 | } 79 | 80 | // add to editedRawQuery 81 | editedRawQuery = append(editedRawQuery, kv) 82 | } 83 | newReq.URL.RawQuery = strings.Join(editedRawQuery, "&") 84 | } 85 | 86 | // 2. post body(application/x-www-form-urlencoded,multipart/form-data ) 87 | for k, _ := range newReq.PostForm { 88 | match, _ := regexp.Match(d.csrfTokenPattern, []byte(k)) 89 | if match { 90 | newReq.PostForm.Del(k) 91 | } 92 | } 93 | 94 | // make newReq 95 | newResp := http2.DoRequest(newReq) 96 | if newResp == nil { 97 | return 98 | } 99 | 100 | // judge and save result 101 | if d.judge(srcResp, newResp) { 102 | return data.BuildResult(item, "CSRF", newReq, newResp) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // getSameSitePolicy 109 | // 110 | // @Description: get same-site policy from response received from the request deleted cookie 111 | // @receiver d 112 | // @param 113 | // @param item 114 | func (d *CSRFDetector) getSameSitePolicy(item *data.DataItem) { 115 | // copy request 116 | request := http2.RequestClone(item.SourceRequest) 117 | // delete cookie and get set-cookie header from response 118 | request.Header.Del("Cookie") 119 | response := http2.DoRequest(request) 120 | setCookie := response.Header.Get("Set-Cookie") 121 | 122 | var policy string 123 | // parse policy from Set-Cookie header 124 | if strings.Contains(setCookie, "SameSite=Lax") { 125 | policy = "Lax" 126 | } else if strings.Contains(setCookie, "SameSite=Strict") { 127 | policy = "Strict" 128 | } else { // if there is not same-site policy or the policy is None 129 | policy = "None" 130 | } 131 | 132 | // save policy to samesitePolicy 133 | key := request.Host 134 | d.samesitePolicy[key] = policy 135 | 136 | //logger.Infoln(fmt.Sprintf("Host: %s, Same-Site policy: %s", key, policy)) 137 | } 138 | 139 | // judge 140 | // 141 | // @Description: 142 | // @receiver d 143 | // @return bool true -- exists vulnerable 144 | func (d *CSRFDetector) judge(srcResponse, response *http.Response) bool { 145 | bytes2, _ := ioutil.ReadAll(response.Body) 146 | 147 | // black keyword match 148 | for _, s := range d.csrfInvalidPattern { 149 | if strings.Contains(string(bytes2), s) { 150 | return false 151 | } 152 | } 153 | 154 | // body similarity compare 155 | bytes, _ := ioutil.ReadAll(srcResponse.Body) 156 | 157 | sim := strsim.Compare(string(bytes), string(bytes2)) 158 | if sim > 0.9 { 159 | return true 160 | } 161 | 162 | return false 163 | } 164 | 165 | func NewCSRFDetector() module.Detecter { 166 | if viper.GetInt("app.module.CSRFDetector.option") == 0 { 167 | return nil 168 | } 169 | 170 | logger.Infoln("[Load Module] csrf detector module") 171 | 172 | // instantiate CSRFDetector 173 | detector := &CSRFDetector{ 174 | csrfTokenPattern: viper.GetString("app.module.CSRFDetector.csrfTokenPattern"), 175 | csrfInvalidPattern: viper.GetStringSlice("app.module.CSRFDetector.csrfInvalidPattern"), 176 | samesitePolicy: make(map[string]string, 100), 177 | } 178 | 179 | return detector 180 | } 181 | -------------------------------------------------------------------------------- /internal/core/database/mysql.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "APIKiller/internal/core/ahttp" 5 | "APIKiller/internal/core/data" 6 | "APIKiller/internal/core/module" 7 | log "APIKiller/pkg/logger" 8 | "encoding/base64" 9 | "fmt" 10 | "github.com/spf13/viper" 11 | "gorm.io/driver/mysql" 12 | "gorm.io/gorm" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | type Mysql struct { 18 | db *gorm.DB 19 | MaxCount int //the max num of per-query 20 | } 21 | 22 | func (m *Mysql) UpdateVulnType(token string) { 23 | m.db.Model(&data.DataItemStr{}).Where("vuln_type = ?", token).Update("vuln_type", strings.Split(token, module.AsyncDetectVulnTypeSeperator)[0]) 24 | } 25 | 26 | // ListAllInfo fetch all results and return 27 | func (m *Mysql) ListAllInfo() []data.DataItemStr { 28 | items := make([]data.DataItemStr, m.MaxCount) //需要动态设置,能先查有多少条记录,再创建? 29 | 30 | m.db.Where("vuln_type not like ?", "%"+module.AsyncDetectVulnTypeSeperator+"%").Order("domain").Order("url").Find(&items) 31 | 32 | // recover http item string from id 33 | for i, item := range items { 34 | // item.SourceRequest 35 | items[i].SourceRequest = m.getHttpItembyId(item.SourceRequest) 36 | 37 | //item.SourceResponse 38 | items[i].SourceResponse = m.getHttpItembyId(item.SourceResponse) 39 | 40 | //item.VulnRequest 41 | items[i].VulnRequest = m.getHttpItembyId(item.VulnRequest) 42 | 43 | //item.VulnResponse 44 | items[i].VulnResponse = m.getHttpItembyId(item.VulnResponse) 45 | } 46 | 47 | return items 48 | } 49 | 50 | func (m *Mysql) Exist(domain, url, method string) bool { 51 | var count int64 52 | v := &data.DataItemStr{} 53 | 54 | m.db.Model(&v).Where("url = ?", url).Where("domain = ?", domain).Where("method = ?", method).Count(&count) 55 | 56 | if count > 0 { 57 | return true 58 | } 59 | 60 | return false 61 | } 62 | 63 | // addInfo append new result 64 | func (m *Mysql) AddInfo(item *data.DataItem) { 65 | // transfer DataItem to DataItemStr 66 | itemStr := data.DataItemStr{ 67 | Id: item.Id, 68 | Domain: item.Domain, 69 | Url: item.Url, 70 | Https: item.Https, 71 | Method: item.Method, 72 | SourceRequest: m.addHttpItem(ahttp.DumpRequest(item.SourceRequest)), 73 | SourceResponse: m.addHttpItem(ahttp.DumpResponse(item.SourceResponse)), 74 | VulnType: item.VulnType, 75 | VulnRequest: m.addHttpItem(ahttp.DumpRequest(item.VulnRequest)), 76 | VulnResponse: m.addHttpItem(ahttp.DumpResponse(item.VulnResponse)), 77 | ReportTime: item.ReportTime, 78 | CheckState: item.CheckState, 79 | } 80 | 81 | // store DataItemStr 82 | m.db.Create(&itemStr) 83 | } 84 | 85 | // addHttpItem 86 | // 87 | // @Description: store request or response in form of string and return id 88 | // @receiver m 89 | // @param item 90 | // @return string 91 | func (m *Mysql) addHttpItem(itemStr string) string { 92 | // substr if itemStr is too long 93 | if len(itemStr) > 10000 { 94 | itemStr = itemStr[:10000] 95 | } 96 | 97 | // base64 encode 98 | b64 := base64.StdEncoding.EncodeToString([]byte(itemStr)) 99 | 100 | httpItem := &data.HttpItem{ 101 | Item: b64, 102 | } 103 | 104 | m.db.Create(&httpItem) 105 | 106 | return fmt.Sprintf("%v", httpItem.Id) 107 | } 108 | 109 | func (m *Mysql) getHttpItembyId(Id string) string { 110 | // convert string to id 111 | id, _ := strconv.Atoi(Id) 112 | 113 | item := &data.HttpItem{} 114 | 115 | m.db.Find(item).Where("id = ?", id) 116 | 117 | // decode base64 118 | decodeString, _ := base64.StdEncoding.DecodeString(item.Item) 119 | 120 | return string(decodeString) 121 | } 122 | 123 | // addHttpItems 124 | // 125 | // @Description: store requests or responses in form of string and return ids seperated by comma 126 | // @receiver m 127 | // @param item 128 | // @return string 129 | func (m *Mysql) addHttpItems(items []string) string { 130 | if len(items) == 0 { 131 | return "" 132 | } 133 | 134 | var Ids []string 135 | 136 | for _, item := range items { 137 | Id := m.addHttpItem(item) 138 | Ids = append(Ids, Id) 139 | } 140 | 141 | return strings.Join(Ids, ",") 142 | } 143 | 144 | // test data: connect("192.168.52.153", "3306","apikiller", "root","123456") 145 | func (m *Mysql) connect(host, port, dbname, username, password string) { 146 | //dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 147 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", username, password, host, port, dbname) 148 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 149 | 150 | if err != nil { 151 | log.Errorln("Connect database error", err) 152 | panic(err) 153 | } 154 | 155 | m.db = db 156 | } 157 | 158 | // init 159 | // 160 | // @Description: 161 | // @receiver m 162 | func (m *Mysql) init() { 163 | // disable logging 164 | m.db.Logger.LogMode(1) 165 | } 166 | 167 | func NewMysqlClient() *Mysql { 168 | mysqlcli := &Mysql{} 169 | 170 | //parse config 171 | host := viper.GetString("app.db.mysql.host") 172 | port := viper.GetString("app.db.mysql.port") 173 | dbname := viper.GetString("app.db.mysql.dbname") 174 | username := viper.GetString("app.db.mysql.username") 175 | password := viper.GetString("app.db.mysql.password") 176 | 177 | //connect db and return DB object 178 | mysqlcli.connect(host, port, dbname, username, password) 179 | 180 | // init mysql 181 | mysqlcli.init() 182 | 183 | return mysqlcli 184 | } 185 | -------------------------------------------------------------------------------- /config/config.release.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # APIKiller Project Release Configuration 3 | # 4 | app: # 系统全局配置文件 5 | db: # 数据库配置(当前只支持mysql) 6 | mysql: # mysql 7 | host: 10.1.1.10 8 | port: '3306' 9 | dbname: apikiller 10 | username: root 11 | password: '123456' 12 | origin: # 数据源配置 13 | realTime: # 实时监听数据源配置 14 | address: 127.0.0.1 15 | port: '8080' # 监听端口 16 | module: # 核心模块配置 17 | authorizedDetector: # 未授权&越权检测模块配置 18 | option: 1 # 模块开关 19 | authGroup: # position type code 0-header,1-query param, 2-body param 20 | - domain: 21 | - "127.0.0.1" 22 | - "10.10.10.10" 23 | - "127.0.0.1:8000" 24 | replaceGroup: 25 | - position: 0 26 | key: "Cookie" 27 | value: "Aur0ra" 28 | - position: 1 29 | key: "key" 30 | value: "Aur0ra" 31 | - position: 2 32 | key: "postKey" 33 | value: "Aur0ra" 34 | - domain: 35 | - "127.0.0.1" 36 | - 37 | - 38 | replaceItem: 39 | - position: 0 40 | key: "Cookie" 41 | value: "TEST" 42 | - position: 0 43 | key: "Cookie" 44 | value: "TEST" 45 | ipHeader: # 后端常见请求ip后门(通过特定的header来判断是否是本地请求,从而进行豁免) 46 | - Access-Control-Allow-Origin 47 | - Base-Url 48 | - CF-Connecting_IP 49 | - CF-Connecting-IP 50 | - Client-IP 51 | - Cluster-Client-IP 52 | - Destination 53 | - Forwarded-For-Ip 54 | - Forwarded-For 55 | - Forwarded-Host 56 | - Forwarded 57 | - Host 58 | - Http-Url 59 | - Origin 60 | - Profile 61 | - Proxy-Host 62 | - Proxy-Url 63 | - Proxy 64 | - Real-Ip 65 | - Redirect 66 | - Referer 67 | - Referrer 68 | - Request-Uri 69 | - True-Client-IP 70 | - Uri 71 | - Url 72 | - X-Arbitrary 73 | - X-Client-IP 74 | - X-Custom-IP-Authorization 75 | - X-Forward-For 76 | - X-Forward 77 | - X-Forwarded-By 78 | - X-Forwarded-For-Original 79 | - X-Forwarded-For 80 | - X-Forwarded-Host 81 | - X-Forwarded-Proto 82 | - X-Forwarded-Server 83 | - X-Forwarded 84 | - X-Forwarder-For 85 | - X-Host 86 | - X-HTTP-DestinationURL 87 | - X-HTTP-Host-Override 88 | - X-Original-Remote-Addr 89 | - X-Original-URL 90 | - X-Originally-Forwarded-For 91 | - X-Originating-IP 92 | - X-Proxy-Url 93 | - X-ProxyUser-Ip 94 | - X-Real-Ip 95 | - X-Real-IP 96 | - X-Referrer 97 | - X-Remote-Addr 98 | - X-Remote-IP 99 | - X-Rewrite-URL 100 | - X-True-IP 101 | - X-WAP-Profile 102 | ip: 127.0.0.1 # 特定豁免ip 103 | apiVersion: # api版本格式,例如有 /apiv1/或者/api/v1/等,如下是/api1/的示例 104 | format: "api\\d" 105 | prefix: "api" 106 | pathFuzz: # 路径fuzz列表 107 | midPadding: 108 | - "" 109 | - "." 110 | - "..;" 111 | - ".;" 112 | endPadding: 113 | - "?" 114 | - "??" 115 | - "." 116 | - ".." 117 | - "./" 118 | - "%20" 119 | - "%09" 120 | - "%0a" 121 | - "#" 122 | judgement: # 判断引擎配置 123 | blackStatusCodes: # 鉴权失败响应码 124 | - 403 125 | - 401 126 | blackKeywords: # 鉴权失败响应关键字 127 | - forbidden 128 | - error 129 | CSRFDetector: # csrf检测模块 130 | option: 1 # 模块开关 131 | csrfTokenPattern: csrf # token对应的参数名或者请求头 132 | csrfInvalidPattern: # csrf鉴权失败返回的标识 133 | - invalid 134 | - csrf 135 | openRedirectDetector: # openRedirect检测 136 | option: 1 137 | rawQueryParams: 138 | - url 139 | - redirect 140 | - uri 141 | - redirection 142 | - next 143 | - returnto 144 | - return_to 145 | - origin 146 | - callback 147 | - authorize_callback 148 | - target 149 | - link 150 | failFlag: 151 | - error 152 | - fail 153 | DoSDetector: # dos检测模块 154 | option: 1 155 | sizeParam: # 资源查询大小控制参数 156 | - size 157 | - Size 158 | rateLimit: 159 | failFlag: # 存在频控的标识 160 | - exceed 161 | - captcha 162 | - too many 163 | - rate limit 164 | - Maximum login 165 | SSRFDetector: 166 | option: 1 167 | filter: # 过滤器配置 168 | httpFilter: # http过滤器配置:目前只支持根据指定的host,对其进行检测,如果未设置,则默认对所有流量进行检测 169 | host: 170 | - "127.0.0.1:8000" 171 | staticFileFilter: # 静态文件过滤器:对获取静态资源的流量不做处理 172 | ext: 173 | - js 174 | - gif 175 | - jpg 176 | - png 177 | - css 178 | - jpeg 179 | - xml 180 | - img 181 | - svg 182 | - ico 183 | notifier: # 通知方式配置 184 | Lark: # Lark飞书(支持token检验) 185 | webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/a814553d-3fc0-4c5b-8e98-694830cc3121' 186 | secret: '' 187 | Dingding: # 钉钉 188 | webhookUrl: '' 189 | other: # 其他配置 190 | reverseTarget: 127.0.0.1 # 反连平台目标 191 | web: # web运营平台配置 192 | ipaddr: 127.0.0.1 193 | port: '80' 194 | -------------------------------------------------------------------------------- /config/config.dev.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # APIKiller Project Development Configuration 3 | # 4 | app: # 系统全局配置文件 5 | db: # 数据库配置(当前只支持mysql) 6 | mysql: # mysql 7 | host: 10.1.1.10 8 | port: '3306' 9 | dbname: apikiller 10 | username: root 11 | password: '123456' 12 | origin: # 数据源配置 13 | realTime: # 实时监听数据源配置 14 | address: 127.0.0.1 15 | port: '8080' # 监听端口 16 | module: # 核心模块配置 17 | authorizedDetector: # 未授权&越权检测模块配置 18 | option: 1 # 模块开关 19 | authGroup: # position type code 0-header,1-query param, 2-body param 20 | - domain: 21 | - "127.0.0.1" 22 | - "10.10.10.10" 23 | - "127.0.0.1:8000" 24 | replaceGroup : 25 | - position: 0 26 | key: "Cookie" 27 | value: "Aur0ra" 28 | - position: 1 29 | key: "key" 30 | value: "Aur0ra" 31 | - position: 2 32 | key: "postKey" 33 | value: "Aur0ra" 34 | - domain: 35 | - "127.0.0.1" 36 | - 37 | - 38 | replaceItem: 39 | - position: 0 40 | key: "Cookie" 41 | value: "TEST" 42 | - position: 0 43 | key: "Cookie" 44 | value: "TEST" 45 | 46 | ipHeader: # 后端常见请求ip后门(通过特定的header来判断是否是本地请求,从而进行豁免) 47 | - Access-Control-Allow-Origin 48 | - Base-Url 49 | - CF-Connecting_IP 50 | - CF-Connecting-IP 51 | - Client-IP 52 | - Cluster-Client-IP 53 | - Destination 54 | - Forwarded-For-Ip 55 | - Forwarded-For 56 | - Forwarded-Host 57 | - Forwarded 58 | - Host 59 | - Http-Url 60 | - Origin 61 | - Profile 62 | - Proxy-Host 63 | - Proxy-Url 64 | - Proxy 65 | - Real-Ip 66 | - Redirect 67 | - Referer 68 | - Referrer 69 | - Request-Uri 70 | - True-Client-IP 71 | - Uri 72 | - Url 73 | - X-Arbitrary 74 | - X-Client-IP 75 | - X-Custom-IP-Authorization 76 | - X-Forward-For 77 | - X-Forward 78 | - X-Forwarded-By 79 | - X-Forwarded-For-Original 80 | - X-Forwarded-For 81 | - X-Forwarded-Host 82 | - X-Forwarded-Proto 83 | - X-Forwarded-Server 84 | - X-Forwarded 85 | - X-Forwarder-For 86 | - X-Host 87 | - X-HTTP-DestinationURL 88 | - X-HTTP-Host-Override 89 | - X-Original-Remote-Addr 90 | - X-Original-URL 91 | - X-Originally-Forwarded-For 92 | - X-Originating-IP 93 | - X-Proxy-Url 94 | - X-ProxyUser-Ip 95 | - X-Real-Ip 96 | - X-Real-IP 97 | - X-Referrer 98 | - X-Remote-Addr 99 | - X-Remote-IP 100 | - X-Rewrite-URL 101 | - X-True-IP 102 | - X-WAP-Profile 103 | ip: 127.0.0.1 # 特定豁免ip 104 | apiVersion: # api版本格式,例如有 /apiv1/或者/api/v1/等,如下是/api1/的示例 105 | format: "api\\d" 106 | prefix: "api" 107 | pathFuzz: # 路径fuzz列表 108 | midPadding: 109 | - "" 110 | - "." 111 | - "..;" 112 | - ".;" 113 | endPadding: 114 | - "?" 115 | - "??" 116 | - "." 117 | - ".." 118 | - "./" 119 | - "%20" 120 | - "%09" 121 | - "%0a" 122 | - "#" 123 | judgement: # 判断引擎配置 124 | blackStatusCodes: # 鉴权失败响应码 125 | - 403 126 | - 401 127 | blackKeywords: # 鉴权失败响应关键字 128 | - forbidden 129 | - error 130 | CSRFDetector: # csrf检测模块 131 | option: 0 # 模块开关 132 | csrfTokenPattern: csrf # token对应的参数名或者请求头 133 | csrfInvalidPattern: # csrf鉴权失败返回的标识 134 | - invalid 135 | - csrf 136 | openRedirectDetector: # openRedirect检测 137 | option: 0 138 | rawQueryParams: 139 | - url 140 | - redirect 141 | - uri 142 | - redirection 143 | - next 144 | - returnto 145 | - return_to 146 | - origin 147 | - callback 148 | - authorize_callback 149 | - target 150 | - link 151 | failFlag: 152 | - error 153 | - fail 154 | DoSDetector: # dos检测模块 155 | option: 0 156 | sizeParam: # 资源查询大小控制参数 157 | - size 158 | - Size 159 | rateLimit: 160 | failFlag: # 存在频控的标识 161 | - exceed 162 | - captcha 163 | - too many 164 | - rate limit 165 | - Maximum login 166 | SSRFDetector: 167 | option: 0 168 | filter: # 过滤器配置 169 | httpFilter: # http过滤器配置:目前只支持根据指定的host,对其进行检测,如果未设置,则默认对所有流量进行检测 170 | host: 171 | - "127.0.0.1:8000" 172 | staticFileFilter: # 静态文件过滤器:对获取静态资源的流量不做处理 173 | ext: 174 | - js 175 | - gif 176 | - jpg 177 | - png 178 | - css 179 | - jpeg 180 | - xml 181 | - img 182 | - svg 183 | - ico 184 | notifier: # 通知方式配置 185 | Lark: # Lark飞书(支持token检验) 186 | webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/a814553d-3fc0-4c5b-8e98-694830cc3121' 187 | secret: '' 188 | Dingding: # 钉钉 189 | webhookUrl: '' 190 | other: # 其他配置 191 | reverseTarget: 127.0.0.1 # 反连平台目标 192 | web: # web运营平台配置 193 | ipaddr: 127.0.0.1 194 | port: '80' 195 | -------------------------------------------------------------------------------- /internal/web/backend/web.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "APIKiller/internal/core/data" 5 | "APIKiller/internal/core/module" 6 | "APIKiller/pkg/logger" 7 | "encoding/base64" 8 | "fmt" 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/viper" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/gorm" 14 | "io/ioutil" 15 | "net/http" 16 | "os" 17 | "os/exec" 18 | "runtime" 19 | "strconv" 20 | "syscall" 21 | ) 22 | 23 | type APIServer struct { 24 | Page int `form:"page"` 25 | Size int `form:"size"` 26 | Ids []int `form:"ids"` 27 | db *gorm.DB 28 | } 29 | 30 | // 31 | // init 32 | // @Description: initial APIServer and start gin server 33 | // @receiver s 34 | // @param ipaddr 35 | // @param port 36 | // 37 | func (s *APIServer) init(ipaddr, port string) { 38 | // load database 39 | s.loadDatabase() 40 | 41 | server := gin.Default() 42 | 43 | // append route 44 | s.route(server) 45 | 46 | // start server 47 | server.Run(fmt.Sprintf("%s:%s", ipaddr, port)) 48 | } 49 | 50 | // 51 | // route 52 | // @Description: bind route to gin server 53 | // @receiver s 54 | // @param server 55 | // 56 | func (s *APIServer) route(server *gin.Engine) { 57 | 58 | // api path 59 | APIGroup := server.Group("/api") 60 | APIGroup.GET("/test", s.test) 61 | APIGroup.GET("/list", s.list) 62 | APIGroup.GET("/check", s.updateCheckState) 63 | 64 | // bind static directory path 65 | server.Static("/index", "./internal/web/frontend/www") 66 | } 67 | 68 | func (s *APIServer) loadDatabase() { 69 | //get config 70 | host := viper.GetString("app.db.mysql.host") 71 | port := viper.GetString("app.db.mysql.port") 72 | dbname := viper.GetString("app.db.mysql.dbname") 73 | username := viper.GetString("app.db.mysql.username") 74 | password := viper.GetString("app.db.mysql.password") 75 | 76 | //dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 77 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, dbname) 78 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 79 | 80 | // disable logging 81 | db.Logger.LogMode(1) 82 | 83 | if err != nil { 84 | log.Errorln("Connect database error", err) 85 | panic(err) 86 | } 87 | 88 | s.db = db 89 | } 90 | 91 | func (s *APIServer) test(c *gin.Context) { 92 | 93 | c.JSON(http.StatusOK, "Test api") 94 | } 95 | 96 | func (s *APIServer) getHttpItembyId(Id string) string { 97 | // convert string to id 98 | id, _ := strconv.Atoi(Id) 99 | 100 | item := &data.HttpItem{ 101 | Id: int64(id), 102 | } 103 | 104 | s.db.Find(item) 105 | 106 | // decode base64 107 | //decodeString, _ := base64.StdEncoding.DecodeString(item.Item) 108 | 109 | return item.Item 110 | } 111 | 112 | // 113 | // updateCheckState 114 | // @Description: update vulnerability record 115 | // @receiver s 116 | // @param c 117 | // 118 | func (s *APIServer) updateCheckState(c *gin.Context) { 119 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // ignore CORS 120 | 121 | logger.Debugln(c.Query("Id")) 122 | 123 | tx := s.db.Model(&data.DataItemStr{}).Where("Id=?", c.Query("Id")).Update("check_state", true) 124 | if tx.Error != nil { 125 | logger.Errorln(tx.Error.Error()) 126 | } 127 | c.JSON(http.StatusOK, "success!") 128 | } 129 | 130 | // 131 | // list 132 | // @Description: list vulnerability records 133 | // @receiver s 134 | // @param c 135 | // 136 | func (s *APIServer) list(c *gin.Context) { 137 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // ignore CORS 138 | 139 | _ = c.Bind(&s) 140 | 141 | items := make([]data.DataItemStr, 1024) 142 | 143 | s.db.Where("vuln_type not like ?", "%"+module.AsyncDetectVulnTypeSeperator+"%").Order("domain").Order("url").Find(&items).Limit(128) 144 | 145 | // recover http item string from id 146 | for i, item := range items { 147 | // item.SourceRequest 148 | decodeString, _ := base64.StdEncoding.DecodeString(s.getHttpItembyId(item.SourceRequest)) 149 | items[i].SourceRequest = string(decodeString) 150 | 151 | //item.SourceResponse 152 | decodeString2, _ := base64.StdEncoding.DecodeString(s.getHttpItembyId(item.SourceResponse)) 153 | items[i].SourceResponse = string(decodeString2) 154 | 155 | //item.VulnRequest 156 | decodeString3, _ := base64.StdEncoding.DecodeString(s.getHttpItembyId(item.VulnRequest)) 157 | items[i].VulnRequest = string(decodeString3) 158 | 159 | //item.VulnResponse 160 | decodeString4, _ := base64.StdEncoding.DecodeString(s.getHttpItembyId(item.VulnResponse)) 161 | items[i].VulnResponse = string(decodeString4) 162 | } 163 | 164 | data := make(map[string]interface{}) 165 | 166 | data["list"] = items 167 | //data["total"] = total 168 | c.JSON(http.StatusOK, items) 169 | } 170 | 171 | // 172 | // autoWakeup 173 | // @Description: Automatically wake up the browser when running locally 174 | // @receiver s 175 | // 176 | func (s *APIServer) autoWakeup(ipaddr, port string) { 177 | var err error 178 | 179 | // secure handle 180 | _, err2 := strconv.Atoi(port) 181 | if err2 != nil { 182 | logger.Debugln("the format of port is invalid") 183 | logger.Errorln(err2) 184 | } 185 | 186 | targetUrl := "http://127.0.0.1:" + port + "/index/index.html" 187 | 188 | switch runtime.GOOS { 189 | case "linux": 190 | err = exec.Command("xdg-open", targetUrl).Start() 191 | break 192 | case "windows": 193 | cmd := exec.Command("cmd", "/c", "start", targetUrl) 194 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 195 | err = cmd.Start() 196 | break 197 | case "darwin": 198 | err = exec.Command("open", targetUrl).Start() 199 | break 200 | default: 201 | err = os.ErrInvalid 202 | } 203 | 204 | if err != nil { 205 | fmt.Println(err) 206 | logger.Errorln(err) 207 | } 208 | } 209 | 210 | func NewAPIServer() { 211 | server := APIServer{} 212 | 213 | // disable logging 214 | gin.DefaultWriter = ioutil.Discard 215 | 216 | ipaddr := viper.GetString("app.web.ipaddr") 217 | port := viper.GetString("app.web.port") 218 | 219 | // wakeup browser 220 | if ipaddr == "127.0.0.1" { 221 | server.autoWakeup(ipaddr, port) 222 | } 223 | 224 | server.init(ipaddr, port) 225 | } 226 | -------------------------------------------------------------------------------- /internal/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "APIKiller/internal/core" 5 | hook2 "APIKiller/internal/core/ahttp/hook" 6 | "APIKiller/internal/core/aio" 7 | "APIKiller/internal/core/async" 8 | database2 "APIKiller/internal/core/database" 9 | filter2 "APIKiller/internal/core/filter" 10 | "APIKiller/internal/core/module" 11 | "APIKiller/internal/core/module/CSRF" 12 | "APIKiller/internal/core/module/DoS" 13 | "APIKiller/internal/core/module/OpenRedirect" 14 | "APIKiller/internal/core/module/SSRF" 15 | "APIKiller/internal/core/module/authorize" 16 | notify2 "APIKiller/internal/core/notify" 17 | "APIKiller/internal/core/origin" 18 | "APIKiller/internal/core/origin/fileInputOrigin" 19 | "APIKiller/internal/core/origin/realTimeOrigin" 20 | "APIKiller/internal/web/backend" 21 | "APIKiller/pkg/logger" 22 | "fmt" 23 | "github.com/sirupsen/logrus" 24 | "github.com/spf13/viper" 25 | "os" 26 | "plugin" 27 | "runtime" 28 | "strings" 29 | ) 30 | 31 | const ( 32 | VERSION = "1.2.1" 33 | LoggerLevel = logrus.InfoLevel 34 | ) 35 | 36 | type Runner struct { 37 | } 38 | 39 | // 40 | // Start 41 | // @Description: start the core application 42 | // @receiver r 43 | // 44 | func (r *Runner) Start(cmdOptions *CommandOptions) { 45 | 46 | // load database\modules\filters\notifier and so on 47 | loadLogger() 48 | loadConfig(cmdOptions.ConfigPath) 49 | loadDatabase() 50 | loadModules() 51 | loadAsyncCheckEngine() 52 | loadFilter() 53 | loadNotifer() 54 | loadHooks() 55 | 56 | // load ops platform 57 | if cmdOptions.Web { 58 | loadOPSPlatform() 59 | } 60 | 61 | // load request data from different origins 62 | loadRequestDatafromOrigin(cmdOptions.FileInput) 63 | 64 | // load data from channel and start to handle 65 | startHandle(cmdOptions.Thread) 66 | 67 | // 68 | } 69 | 70 | // 71 | // Stop 72 | // @Description: stop the core application 73 | // @receiver r 74 | // 75 | func (r *Runner) Stop() { 76 | 77 | } 78 | 79 | // 80 | // NewRunner 81 | // @Description: 82 | // 83 | func NewRunner() { 84 | R := &Runner{} 85 | 86 | // show banner 87 | showBanner() 88 | 89 | // parse command option 90 | cmdOptions := ParseCommandOptions() 91 | 92 | // start runner 93 | R.Start(cmdOptions) 94 | 95 | // and so on 96 | } 97 | 98 | func startHandle(MaxThreadNum int) { 99 | logger.Infoln("start handle") 100 | 101 | // goroutine control 102 | limit := make(chan int, MaxThreadNum) 103 | 104 | for { 105 | transferItem := origin.GetOriginRequest() 106 | 107 | // filter requests 108 | flag := true // true -pass false -block 109 | for _, f := range filter2.GetFilters() { 110 | if f.Filter(transferItem.Req) == filter2.FilterBlocked { 111 | flag = false 112 | 113 | logger.Infoln(fmt.Sprintf("filter %v, %v", transferItem.Req.Host, transferItem.Req.URL.Path)) 114 | break 115 | } 116 | } 117 | if !flag { 118 | continue 119 | } 120 | 121 | // transform io.Reader 122 | transferItem.Req.Body = aio.TransformReadCloser(transferItem.Req.Body) 123 | transferItem.Resp.Body = aio.TransformReadCloser(transferItem.Resp.Body) 124 | 125 | go func() { 126 | limit <- 1 127 | 128 | core.NewHandler(transferItem) 129 | 130 | <-limit 131 | }() 132 | } 133 | } 134 | 135 | func loadRequestDatafromOrigin(filePath string) { 136 | // load request from different origins 137 | go func() { 138 | if filePath != "" { 139 | inputOrigin := fileInputOrigin.NewFileInputOrigin(filePath) 140 | inputOrigin.LoadOriginRequest() 141 | } else { 142 | inputOrigin := realTimeOrigin.NewRealTimeOrigin() 143 | inputOrigin.LoadOriginRequest() 144 | } 145 | }() 146 | } 147 | 148 | func loadOPSPlatform() { 149 | logger.Infoln("loading OPS platform") 150 | 151 | go backend.NewAPIServer() 152 | } 153 | 154 | func loadLogger() { 155 | logger.Initial(LoggerLevel, ".") 156 | } 157 | 158 | func loadNotifer() { 159 | logger.Infoln("loading notifier") 160 | 161 | if viper.GetString("app.notifier.Lark.webhookUrl") != "" { 162 | notify2.BindNotifier(notify2.NewLarkNotifier()) 163 | } else if viper.GetString("app.notifier.Dingding.webhookUrl") != "" { 164 | notify2.BindNotifier(notify2.NewDingdingNotifer()) 165 | } else { 166 | } 167 | } 168 | 169 | func loadDatabase() { 170 | logger.Infoln("loading database") 171 | 172 | // bind global database 173 | database2.BindDatabase(database2.NewMysqlClient()) 174 | } 175 | 176 | func loadModules() { 177 | logger.Infoln("loading modules") 178 | 179 | module.RegisterModule(authorize.NewAuthorizedDetector()) 180 | module.RegisterModule(CSRF.NewCSRFDetector()) 181 | module.RegisterModule(OpenRedirect.NewOpenRedirectDetector()) 182 | module.RegisterModule(DoS.NewDoSDetector()) 183 | module.RegisterModule(SSRF.NewSSRFDetector()) 184 | 185 | } 186 | 187 | func loadFilter() { 188 | logger.Infoln("loading filters") 189 | 190 | filter2.RegisterFilter(filter2.NewHttpFilter()) 191 | filter2.RegisterFilter(filter2.NewStaticFileFilter()) 192 | filter2.RegisterFilter(filter2.NewDuplicateFilter()) 193 | 194 | } 195 | 196 | func loadConfig(configPath string) { 197 | logger.Infoln("loading config") 198 | 199 | // use the specified configuration file when configPath option is not blank 200 | if configPath == "" { 201 | // using file .env 202 | _, err := os.Stat("./.env") 203 | if err == nil { 204 | configPath = "./config/config.dev.yaml" 205 | } else { 206 | configPath = "./config/config.release.yaml" 207 | } 208 | } 209 | 210 | logger.Debugln(fmt.Sprintf("current config: %s", configPath)) 211 | 212 | viper.SetConfigFile(configPath) 213 | 214 | err := viper.ReadInConfig() 215 | if err != nil { 216 | panic(err) 217 | } 218 | } 219 | 220 | func loadHooks() { 221 | // except windows os 222 | if runtime.GOOS == "windows" { 223 | logger.Infoln("not support windows operation system") 224 | } 225 | 226 | logger.Infoln("loading hooks") 227 | 228 | // ./hooks directory does not exist 229 | _, err2 := os.Stat("./hooks") 230 | if os.IsNotExist(err2) { 231 | logger.Errorln("target directory does not exist") 232 | 233 | // make directory 234 | err := os.Mkdir("./hooks", os.ModePerm) 235 | if err != nil { 236 | panic(err) 237 | } 238 | } 239 | 240 | // list directory 241 | entries, err := os.ReadDir("./hooks") 242 | if err != nil { 243 | logger.Errorln(fmt.Sprintf("loading hooks error: %v", err)) 244 | panic(entries) 245 | } 246 | 247 | for _, entry := range entries { 248 | soName := entry.Name() 249 | 250 | // filter directory and none so file 251 | if entry.IsDir() == true || strings.Index(soName, ".so") == -1 { 252 | continue 253 | } 254 | 255 | // load plugins and register them via RegisterHooks 256 | logger.Infoln(fmt.Sprintf("[Load Hook] load hook %s", strings.Replace(soName, ".so", "", 1))) 257 | open, err := plugin.Open("./hooks/" + soName) 258 | if err != nil { 259 | logger.Errorln(fmt.Sprintf("load hook %s error: %v", soName, err)) 260 | panic(err) 261 | } 262 | 263 | Hook, err := open.Lookup("Hook") 264 | if err != nil { 265 | logger.Errorln(fmt.Sprintf("load hook %s error: %v", soName, err)) 266 | panic(err) 267 | } 268 | 269 | var Hookk hook2.RequestHook 270 | Hookk, ok := Hook.(hook2.RequestHook) 271 | if !ok { 272 | logger.Errorln(fmt.Sprintf("load hook %s error: unexpected type from module symbol", soName)) 273 | panic(err) 274 | } 275 | 276 | hook2.RegisterHooks(Hookk) 277 | } 278 | } 279 | 280 | func loadAsyncCheckEngine() { 281 | logger.Infoln("loading asynchronous check engine") 282 | 283 | // start asynchronous check engine 284 | go async.NewAsyncCheckEngine().Start() 285 | } 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

4 | 简介 • 5 | 架构 • 6 | Feature • 7 | 食用宝典 • 8 | 二次开发文档 • 9 | 更新 • 10 | 项目社区 • 11 | 致谢 12 |
24 |
25 |
26 | ## Feature
27 | - 支持HTTP/HTTPS流量检测
28 | - 多来源检测
29 | - 支持流量监听
30 | - 支持历史流量回扫\[目前只支持burpsuite存储流量\]
31 | - 支持测试流量区分、流量清洗
32 | - 允许通过hook,对所有测试请求进行添加标识header等方式,区分测试流量或者将测试流量导入到pre、boe等非生产环境中
33 | - 多功能扫描模块
34 | - 越权检测模块,高效精准,支持多情景检测
35 | - 40x bypass 模块
36 | - CSRF检测模块
37 | - open-redirect 检测模块
38 | - DoS检测模块【谨慎配置,避免出现大量脏数据】
39 | - 【欢迎大家积极提PR】
40 | - 多功能Filter处理,默认自带多个filter
41 | - 针对性扫描,例如只对 baidu.com域名进行扫描
42 | - 去重扫描,提高效率
43 | - 自动过滤静态文件(js,gif,jpg,png,css,jpeg,xml,img,svg...)
44 | - API 运维
45 | - 提供简易的API Security运维平台
46 | - 多方式漏洞发现提醒
47 | - Lark飞书
48 | - 钉钉
49 | - ...
50 | - 对抗常见风控手段
51 | - 频控
52 | - **【重磅】以上都可以快速进行二次开发**
53 |
54 | ## 食用宝典
55 | > 详细请查看:https://github.com/Aur0ra-m/APIKiller/wiki
56 | 1. 安装好数据库环境(我个人采用的是docker)
57 | 1. **一键部署**
58 | 1. 将项目clone到服务器后,直接运行 ```sudo bash dbDeploy.sh```
59 |
60 | 
61 | 2. 根据返回的数据,在config.yaml中完成相关配置
62 |
63 | 2. **手动部署**
64 | 1. docker pull 数据库镜像
65 | ```shell
66 | sudo docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql
67 | ```
68 | 2. 导入apikiller.sql文件
69 | ```shell
70 | sudo docker cp /tmp/apikiller.sql mysql-server:/tmp/apikiller.sql
71 | ```
72 | 3. 登入mysql
73 | ```shell
74 | docker exec -it mysql-server mysql -uroot -p123456
75 | source /tmp/apikiller.sql
76 | ```
77 | 4. 【重点】在 config.yaml 中进行相关配置
78 |
79 |
80 |
81 | 2. 安装根目录下的https证书[windows环境]
82 | 1. 找到根目录下的ca.crt证书
83 |
84 |
85 |
86 | 2. 点击安装即可,将其添加到系统根信任证书颁发位置
87 |
88 | 3. 配置漏洞发现通知Webhook
89 | 1. 根据[飞书指导](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN),开启一个bot,并复制相关的webhook【支持secret鉴权操作】
90 |
91 | 2. 在根路径下的config.json中进行配置(如果有secret,就进行配置)
92 |
93 |
94 |
95 |
96 |
97 | 3. 配置成功后,当发现漏洞时,会立即推送漏洞信息
98 |
99 |
100 |
101 |
102 |
103 | 3. 一键启动【配置文件位于./config/目录下(默认是config.release.yaml),或自己指定】
104 |
105 |
106 |
107 |
108 |
109 | 4. **ding~,发现新漏洞,快去看鸭**
110 |
111 | 5. 漏洞运营,及时对漏洞进行研判和修复
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | > **基本配置:数据库模块、过滤器模块、通知模块**
120 | >
121 | > **除基本配置外,还必须进行如下的模块配置。(其中的option必须配置为1,才代表启动该模块)**
122 | ### API越权检测
123 | > 这里基于[VAPI越权靶场](https://www.freebuf.com/vuls/332312.html) 进行实战模拟
124 | > 配好环境后,先根据项目鉴权机制,提供另一个不同权限的账号,配置好config.yaml
125 | 1. 根据企业开发规范,配置好越权模块的相关配置
126 |
127 |
128 | 2. 启动项目,访问接口
131 |
132 | 3. **成功检测出越权和csrf**
133 |
134 |
135 |
136 | ### 403 bypass模块
137 | > 当前可以进行大小写、path fuzz、api版本降级等方式,来进行探测
138 |
139 | 
141 |
142 |
143 | ### CSRF检测
144 | > 基于pikachu靶场,进行漏洞检测
145 |
146 |
147 | 处理csrf模块的配置
149 |
150 |
151 |
152 |
153 |
154 |
155 | ## HTTP HOOK机制
156 | > 为避免扫描时造成过无效流量,可以通过提供的HTTP HOOK机制,对请求流量自定义修改,例如添加header,来区分测试流量和实际流量
157 |
158 | 【注意】当前由于golang plugin机制特性,暂不支持windows下的流量修改
159 |
160 | 1. HTTP HOOK 样例
161 | ```go
162 | package main
163 |
164 | import (
165 | "fmt"
166 | "net/http"
167 | )
168 |
169 | type RequestHook interface {
170 | HookBefore(*http.Request) // hook before initiating http newReq
171 | HookAfter(*http.Request) // hook after finishing http newReq
172 | }
173 |
174 | type AddHeaderHook struct {
175 | }
176 |
177 | func (a AddHeaderHook) HookBefore(newReq *http.Request) {
178 | fmt.Println("HOOK Before: hhhhhhh")
179 | // ....
180 | }
181 |
182 | func (a AddHeaderHook) HookAfter(newReq *http.Request) {
183 |
184 | }
185 |
186 | // Hook this is exported, and this name must be set Hook
187 | var Hook AddHeaderHook
188 | ```
189 |
190 | 【严格按照上面的代码规范,其中最后一行代码,命名必须设置为Hook】
191 |
192 | 2. 生成对应的so链接库
193 | ```shell
194 | go build -buildmode=plugin APIKillerHookSample.go
195 | ```
196 |
197 | ```shell
198 | $ ls
199 | APIKillerHookSample.go APIKillerHookSample.so go.mod
200 | ```
201 | 3. 将生成的so放置到项目的hooks目录下
202 | ```shell
203 | $ ls ./hooks
204 | APIKillerHookSample.so
205 | ```
206 | 4. 启动项目即可完成流量更改
207 |
208 |
209 | ## 二次开发文档
210 | https://github.com/Aur0ra-m/APIKiller/wiki
211 |
212 | ## 更新记录
213 | ### v0.0.2
214 | - 【功能】修正对https请求的处理
215 | - 【功能】优化csrf检测模块
216 | - 【功能】添加对钉钉通知的支持
217 | - 【优化】对整体架构进行优化,提高效率(通知模块优化、数据库存储模块优化)
218 | - 【优化】重改数据库设计,同时数据库存储时进行base64转码操作
219 |
220 |
221 |
222 | ### v0.0.3
223 | - 【功能】新增40xbypass模块,支持常见架构层绕过和接口层绕过
224 | - 【优化】优化权限检测模块,向甲方实际情况靠齐
225 | - 【优化】调整配置解析,从json迁移至yaml,同时优化全局解析过程,提高检测效率
226 | - 【优化】调整filter顺序,同时对duplicationFilter查询过程由数据库查询到成员变量查询
227 | - 【bugFix】修复线程安全导致的数据重复等问题
228 | - 【bugFix】调整全局的chance-recovery 机制为clone机制
229 |
230 | ### v0.0.4
231 | - 【功能】添加HTTP HOOK功能,可满足区分测试产生的http脏数据、流量清洗功能。
266 |
267 | ## 致谢
268 | 【**最后感谢项目中所使用到的各种开源组件的作者**】
269 |
--------------------------------------------------------------------------------
/internal/web/frontend/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | | {{ config.name }} | 76 |
|---|
|
81 |
82 |
83 | {{ paramItem[config.name] }}
84 |
85 |
86 | {{ getFormattedTime(paramItem[config.name]) }}
87 |
88 |
89 |
90 |
96 |
97 |
98 |
106 |
107 |
108 |
109 |
132 |
133 |
110 |
131 |
111 |
121 | 112 | 117 | {{ config.name }} 118 | 119 |120 |
125 |
130 |
126 |
127 | {{ paramItem[config.name] }}
128 |
129 | |
134 |