diff options
-rw-r--r-- | README.md | 37 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 11 | ||||
-rw-r--r-- | main.go | 188 | ||||
-rw-r--r-- | structs.go | 52 |
5 files changed, 295 insertions, 1 deletions
diff --git a/README.md b/README.md index bc0da50..c14ba21 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,37 @@ # ctfdget -Simply fetch all challenges from a CTF hosted using CTFd. + +Ever started playing a ctf hosted using CTFd that felt like the requests were +processed manually by a goat? Not being able to access the challenges, but +being able to access the api? Just wanting to download all challenges at once +for not having to deal with CTFd until you want to submit your flags? + +Here it is: ctfdget, a simple tool for fetching all challenges with their +included files. + +## Building + +``` +go build ./... +``` + +## Usage + +``` +./ctfdget --help [±master ●] +Usage of ./ctfdget: + -out string + The name of the folder to dump the files to (default "challdump") + -session string + The session (the value of the cookie named 'session') (default "9e8831af-ce30-48c3-8663-4b27262f43f1.pjKPVCYufDhuA9GPJAlc_xh45M8") + -url string + The root URL of the CTFd instance (default "https://ctf.example.com") +``` + +## Features + +- Dump all files from all challenges +- A simple directory structure get's created sorting the challenges into the corresponding categories (<ctfname>/<category>/<challengename>/<challengefiles>) + +## Contribution + +Just open issues, pull requests or whatever and we'll work something out diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1850f07 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.darknebu.la/emile/ctfdget + +go 1.13 + +require ( + golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect + gopkg.in/h2non/gentleman.v2 v2.0.4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7908eb2 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/h2non/gentleman.v2 v2.0.4 h1:9R3K6CFYd/RdXDLi0pGXwaPnRx/pn5EZlrN3VkNygWc= +gopkg.in/h2non/gentleman.v2 v2.0.4/go.mod h1:A1c7zwrTgAyyf6AbpvVksYtBayTB4STBUGmdkEtlHeA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..35f32dc --- /dev/null +++ b/main.go @@ -0,0 +1,188 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "strings" + + "gopkg.in/h2non/gentleman.v2" +) + +var session = flag.String("session", "9e8831af-ce30-48c3-8663-4b27262f43f1.pjKPVCYufDhuA9GPJAlc_xh45M8", "The session (the value of the cookie named 'session')") +var rootURL = flag.String("url", "https://ctf.example.com", "The root URL of the CTFd instance") +var outputFolder = flag.String("out", "challdump", "The name of the folder to dump the files to") + +func main() { + flag.Parse() + + // fetch the list of all challenges + challs, err := fetchAllChallenges() + if err != nil { + panic(err) + } + + // iterate over all challenges downloading all files + fmt.Println("Downloading the files included in the challenges") + for _, chall := range challs.Data { + + // define where to store the challenge + filepath := fmt.Sprintf("%s/%s/%s", *outputFolder, chall.Category, chall.Name) + fmt.Printf("→ %s\n", filepath) + err := os.MkdirAll(filepath, os.ModePerm) // create the directory + if err != nil { + fmt.Println(err) + } + + // fetch the challenge information + challenge, err := fetchChallenge(chall.ID) + if err != nil { + fmt.Println(err) + } + + // download all files + for _, file := range challenge.Data.Files { + err := Download(file, filepath) + if err != nil { + fmt.Println(err) + } + } + + // store the description of the challenge in a README.md file + err = saveDescription(challenge, filepath) + if err != nil { + fmt.Println(err) + } + } +} + +// fetchAllChallenges fetches the list of all challs using the ctfs api. +func fetchAllChallenges() (Challenges, error) { + fmt.Println("Fetching all challenges using the ctf api...") + cli := gentleman.New() + cli.URL(*rootURL) + + req := cli.Request() + req.Path("/api/v1/challenges") + + req.SetHeader("Cookie", fmt.Sprintf("session=%s", *session)) + + // Perform the request + res, err := req.Send() + if err != nil { + fmt.Printf("Request error: %s\n", err) + return Challenges{}, err + } + if !res.Ok { + fmt.Printf("Invalid server response: %d\n", res.StatusCode) + return Challenges{}, err + } + + // unmarshal the resulting json into a Challenges struct + var challenges Challenges + if err := json.Unmarshal(res.Bytes(), &challenges); err != nil { + return Challenges{}, err + } + fmt.Println("Done fetching all challenges") + return challenges, nil +} + +// fetchChallenge fetches a single challenge +func fetchChallenge(id int) (Challenge, error) { + cli := gentleman.New() + cli.URL(*rootURL) + + req := cli.Request() + req.Path(fmt.Sprintf("/api/v1/challenges/%d", id)) + + req.SetHeader("Cookie", fmt.Sprintf("session=%s", *session)) + + // Perform the request + res, err := req.Send() + if err != nil { + fmt.Printf("Request error: %s\n", err) + return Challenge{}, err + } + if !res.Ok { + fmt.Printf("Invalid server response: %d\n", res.StatusCode) + return Challenge{}, err + } + + var challenge Challenge + if err := json.Unmarshal(res.Bytes(), &challenge); err != nil { + return Challenge{}, err + } + return challenge, nil +} + +// Download downloads a file from the given URL and stores it at the given +// filepath +func Download(url string, filepath string) error { + + // So what the code below does, is it extracts the filename from the url by + // first splitting the url at slashed and then at questionmarks (could have + // used regex, but this is CTF code ¯\_(ツ)_/¯) + a := strings.Split(url, "/") + b := strings.Split(a[len(a)-1], "?") + filename := b[0] + + prefix := *rootURL + fullurl := fmt.Sprintf("%s%s", prefix, url) + + client := &http.Client{} + req, err := http.NewRequest("GET", fullurl, nil) + req.Header.Add("Cookie", fmt.Sprintf("session=%s", *session)) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Create the file + out, err := os.Create(fmt.Sprintf("%s/%s", filepath, filename)) + if err != nil { + return err + } + defer out.Close() + + // Write the body to file + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + return nil +} + +func saveDescription(challenge Challenge, filepath string) error { + path := fmt.Sprintf("%s/README.md", filepath) + f, err := os.Create(path) + if err != nil { + return err + } + + // fill the readme with some content + f.WriteString(fmt.Sprintf("# %s\n\n", challenge.Data.Name)) + f.WriteString(fmt.Sprintf("Category: %s\n\n", challenge.Data.Category)) + + f.WriteString("Files:\n") + for _, file := range challenge.Data.Files { + + // So what the code below does, is it extracts the filename from the url by + // first splitting the url at slashed and then at questionmarks (could have + // used regex, but this is CTF code ¯\_(ツ)_/¯) + a := strings.Split(file, "/") + b := strings.Split(a[len(a)-1], "?") + filename := b[0] + f.WriteString(fmt.Sprintf("- %s\n", filename)) + } + f.WriteString("\n") + f.WriteString("## Description\n\n") + f.WriteString(fmt.Sprintf("%s\n\n", challenge.Data.Description)) + f.WriteString("## Writeup") + + return nil +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..fe417fd --- /dev/null +++ b/structs.go @@ -0,0 +1,52 @@ +package main + +// Challenges describes the challenges as returned from the /acpi/v1/challenges +// endpoint +type Challenges struct { + Success bool `json:"success"` + Data []struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Value int `json:"value"` + Category string `json:"category"` + Tags []interface{} `json:"tags"` + Template string `json:"template"` + Script string `json:"script"` + } `json:"data"` +} + +// Challenge describes a single challenge as returned from the +// /acpi/v1/challenges/<id> endpoint +type Challenge struct { + Success bool `json:"success"` + Data struct { + ID int `json:"id"` + Name string `json:"name"` + Value int `json:"value"` + Description string `json:"description"` + Category string `json:"category"` + State string `json:"state"` + MaxAttempts int `json:"max_attempts"` + Type string `json:"type"` + TypeData struct { + ID string `json:"id"` + Name string `json:"name"` + Templates struct { + Create string `json:"create"` + Update string `json:"update"` + View string `json:"view"` + } `json:"templates"` + Scripts struct { + Create string `json:"create"` + Update string `json:"update"` + View string `json:"view"` + } `json:"scripts"` + } `json:"type_data"` + Solves int `json:"solves"` + Files []string `json:"files"` + Tags []interface{} `json:"tags"` + Hints []interface{} `json:"hints"` + } `json:"data"` +} + |