diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e972362 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +sqli-play diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c5fc872 --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +.PHONY: default +default: help + +DOCKER_DBIMAGE=mysql:8.0.28 +DOCKER_DBCNT=mysqlinjection +DOCKER_DBNAME=sqli +DOCKER_DBUSER=root +DOCKER_DBPASS=123456 +DOCKER_DBHOST=0.0.0.0 +DOCKER_DBPORT=3306 +DOCKER_DBADDR=$(DOCKER_DBHOST):$(DOCKER_DBPORT) + +ASROOT=sudo +ifeq (, $(shell which $(ASROOT) 2>/dev/null)) +ASROOT=doas +endif + + +.PHONY: db +db: ## Start db + -$(ASROOT) docker rm -vf $(DOCKER_DBCNT) + $(ASROOT) docker run --rm --name $(DOCKER_DBCNT) \ + --net host \ + -e MYSQL_ROOT_PASSWORD=$(DOCKER_DBPASS) \ + -e MYSQL_DATABASE=$(DOCKER_DBNAME) \ + -d $(DOCKER_DBIMAGE) + # check if database is ready + @while !(make dbtest 2>/dev/null 1>/dev/null); do echo -n "."; sleep 1; done + + $(ASROOT) docker run -i --net host --rm $(DOCKER_DBIMAGE) mysql \ + -h $(DOCKER_DBHOST) -u$(DOCKER_DBUSER) -p$(DOCKER_DBPASS) \ + -D $(DOCKER_DBNAME) < populate.sql + + +.PHONY:dbcli +dbcli: ## connects and retrieves a database a shell. + @docker run -it --net host --rm mysql mysql -h $(DOCKER_DBHOST) \ + -u$(DOCKER_DBUSER) -p$(DOCKER_DBPASS) -D $(DOCKER_DBNAME) + + +.PHONY: dbtest +dbtest: ## test db connectivity + @docker run -it --net host --rm mysql mysql -h $(DOCKER_DBHOST) \ + -u$(DOCKER_DBUSER) -p$(DOCKER_DBPASS) -D $(DOCKER_DBNAME) \ + -e "show status;" >/dev/null + + +.PHONY: build +build: ## build sqli + go build -o ./sqli-play + + +.PHONY: up +up: db run ## build, setup db and start sqli service. + + +.PHONY: run +run: build ## run + @DBNAME=$(DOCKER_DBNAME) \ + DBUSER=$(DOCKER_DBUSER) \ + DBPASS=$(DOCKER_DBPASS) \ + DBADDR=$(DOCKER_DBADDR) \ + ./sqli-play + + +.PHONY: help +help: ## Show this help message. + @echo "usage: make [target] ..." + @echo + @echo -e "targets:" + @egrep '.*?:.*?## [^$$]*?$$' ${MAKEFILE_LIST} | \ + sed -r 's/(.*?):\ .*?\#\# (.+?)/\1:\t\2/g' diff --git a/README.md b/README.md index 2935fdd..5d58bf3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ # sqli-playground +A SQL Injection vulnerable service for teaching how to identify and explore the +issue. + +# help + +``` +$ make help +usage: make [target] ... + +targets: +db: Start db +dbcli: connects and retrieves a database a shell. +dbtest: test db connectivity +build: build sqli +up: build, setup db and start sqli service. +run: run +help: Show this help message. +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca081e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/madlambda/sqli-playground + +go 1.17 + +require github.com/go-sql-driver/mysql v1.6.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..20c16d6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3bbb43d --- /dev/null +++ b/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + + _ "github.com/go-sql-driver/mysql" +) + +var ( + dbuser, dbpass, dbaddr, dbname string +) + +func main() { + fmt.Println("sqli example") + + dbuser = getenv("DBUSER") + dbpass = getenv("DBPASS") + dbaddr = getenv("DBADDR") + dbname = getenv("DBNAME") + + checkdb() + + http.HandleFunc("/news", newsHandler) + + err := http.ListenAndServe(":8080", nil) + abortif(err != nil, "failed to start http server: %v", err) +} + +type newsDetail struct { + title string + body string +} + +func newsHandler(w http.ResponseWriter, r *http.Request) { + db, err := sql.Open("mysql", dbconn()) + if err != nil { + httpError(w, "failed to connect to database") + return + } + + defer db.Close() + + var news []newsDetail + var query = "SELECT title,body from news" + + filters, ok := r.URL.Query()["filter"] + if ok && len(filters) > 0 && len(filters[0]) > 1 { + query += " WHERE title LIKE '%" + filters[0] + "%'" + } + + rows, err := db.Query(query) + if err != nil { + httpError(w, "failed to execute query: %s (error: %v)", query, err) + return + } + + for rows.Next() { + var entry newsDetail + err = rows.Scan(&entry.title, &entry.body) + if err == sql.ErrNoRows { + break + } + + if err != nil { + // We return the error message in the HTTP response to easily + // exploit it. Later we can have an option to hide them, so we can + // also teach how to blindly recognize the errors. + httpError(w, "failed to scan resultset: %s (error: %v)", query, err) + return + } + + news = append(news, entry) + } + + renderNews(w, news) +} + +func renderNews(w http.ResponseWriter, news []newsDetail) { + w.WriteHeader(http.StatusOK) + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + + writeBanner(w) + for _, entry := range news { + writeNews(w, entry.title, entry.body) + } + writeFooter(w) +} + +func writeNews(w http.ResponseWriter, title, body string) { + fmt.Fprintf(w, "-> %s\n", title) + fmt.Fprintf(w, " %s\n\n", body) +} + +func writeBanner(w http.ResponseWriter) { + fmt.Fprintf(w, + `+------------------------------------------------------------------------------+ +| madlambda news network | ++------------------------------------------------------------------------------+ +`) +} + +func writeFooter(w http.ResponseWriter) { + fmt.Fprintf(w, + `+-----------------------------------------------------------------------------+ +| Copyright (c) madlambda | ++------------------------------------------------------------------------------+`) +} + +func httpError(w http.ResponseWriter, format string, args ...interface{}) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, format, args...) + + log.Printf("error: "+format, args...) +} + +func getenv(name string) string { + val := os.Getenv(name) + abortif(val == "", "env %s does not exists or is empty", name) + return val +} + +func abortif(cond bool, format string, args ...interface{}) { + if cond { + abort(format, args...) + } +} + +func abort(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} + +func dbconn() string { + return sprintf("%s:%s@tcp(%s)/%s?charset=utf8", dbuser, dbpass, dbaddr, dbname) +} + +func checkdb() { + db, err := sql.Open("mysql", dbconn()) + abortif(err != nil, "failed to open db connection: %v", err) + + db.Close() +} + +var sprintf = fmt.Sprintf diff --git a/populate.sql b/populate.sql new file mode 100644 index 0000000..5d82ba2 --- /dev/null +++ b/populate.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT(6) NOT NULL AUTO_INCREMENT PRIMARY KEY, + user VARCHAR(255) NOT NULL, + pass VARCHAR(255) NOT NULL +); + +CREATE TABLE IF NOT EXISTS news ( + id INT(6) NOT NULL AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + body VARCHAR(1024) NOT NULL +); + +INSERT INTO users (user,pass) VALUES + ("admin", "very-secret-pass"), + ("i4k", "****************"), + ("katz", "i love alan kay"); + +INSERT INTO news (title,body) VALUES + ( + "BITCOIN FALLS BELOW $38,000 AS EVERGROW SET TO BREAK NEW CRYPTO RECORDS", + "Bitcoin price has fallen to below $38,000 for the second time in 2022. Cryptocurrency largest token has struggled since starting the year at $47,000 and despite a rally in early February Bitcoin price is back where it was a month ago. A combination of factors means that investors are increasingly avoiding risk, and in the current climate risk means Bitcoin." + ), + ( + "Russia retreats from crypto ban as it pushes rules for industry", + "Russias Ministry of Finance is planning to regulate cryptocurrencies in the country, despite earlier calls by the central bank for a ban on crypto." + ); \ No newline at end of file