about summary refs log tree commit diff
diff options
context:
space:
mode:
authorhanemile <mail@emile.space>2020-07-02 21:19:53 +0200
committerhanemile <mail@emile.space>2020-07-02 21:19:53 +0200
commit9095838d3c02c864c7a9642f492aa8387b58744e (patch)
tree7274bb76ad7ef3d3a7bd981dc251ed11485a0532
parent4828eb16f3f4cfc24e38fb3e2b26b0596ca0b255 (diff)
Initial commit
-rw-r--r--README.md37
-rw-r--r--go.mod8
-rw-r--r--go.sum11
-rw-r--r--main.go188
-rw-r--r--structs.go52
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"`
+}
+