From 2a33435cea7d48cd3b900951f71ac3d4194c7ce3 Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 24 Feb 2022 14:40:27 +0000 Subject: [PATCH 1/4] feat: implement basic features --- .gitignore | 1 + Makefile | 70 ++++++++++++++++++++++ README.md | 18 ++++++ go.mod | 5 ++ go.sum | 2 + main.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++ populate.sql | 26 +++++++++ 7 files changed, 283 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 populate.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7fdc32 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +sqli-playground diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e262408 --- /dev/null +++ b/Makefile @@ -0,0 +1,70 @@ +.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 + + +.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 + + +.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..9f1e4f1 --- /dev/null +++ b/main.go @@ -0,0 +1,161 @@ +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) + http.HandleFunc("/headers", headersHandler) + + 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 headersHandler(w http.ResponseWriter, req *http.Request) { + for name, headers := range req.Header { + for _, h := range headers { + fmt.Fprintf(w, "%v: %v\n", name, h) + } + } +} + +func getenv(name string) string { + val := os.Getenv(name) + abortif(val == "", "env %s does not exists or is empty", name) + if val == "" { + os.Exit(1) + } + 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) + + defer 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 From 31a68fbd1b69d0b6710a775e1d866f867eaf64de Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 24 Feb 2022 14:42:26 +0000 Subject: [PATCH 2/4] fix: binary name --- .gitignore | 2 +- Makefile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f7fdc32..e972362 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -sqli-playground +sqli-play diff --git a/Makefile b/Makefile index e262408..ddce3f9 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ dbtest: ## test db connectivity .PHONY: build build: ## build sqli - go build + go build -o ./sqli-play .PHONY: up @@ -58,7 +58,7 @@ run: build ## run DBUSER=$(DOCKER_DBUSER) \ DBPASS=$(DOCKER_DBPASS) \ DBADDR=$(DOCKER_DBADDR) \ - ./sqli + ./sqli-play .PHONY: help From ccbb5934bd0a1ec1dba9b94432f82d294272c3d4 Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 24 Feb 2022 14:44:14 +0000 Subject: [PATCH 3/4] fix makefile style --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index ddce3f9..c5fc872 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ dbtest: ## test db connectivity -u$(DOCKER_DBUSER) -p$(DOCKER_DBPASS) -D $(DOCKER_DBNAME) \ -e "show status;" >/dev/null + .PHONY: build build: ## build sqli go build -o ./sqli-play @@ -52,6 +53,7 @@ build: ## build sqli .PHONY: up up: db run ## build, setup db and start sqli service. + .PHONY: run run: build ## run @DBNAME=$(DOCKER_DBNAME) \ From b092302bffcbbf8ddb4ca331db271d78442e844c Mon Sep 17 00:00:00 2001 From: Tiago Natel Date: Thu, 24 Feb 2022 14:48:45 +0000 Subject: [PATCH 4/4] remove headersHandler --- main.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/main.go b/main.go index 9f1e4f1..3bbb43d 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,6 @@ func main() { checkdb() http.HandleFunc("/news", newsHandler) - http.HandleFunc("/headers", headersHandler) err := http.ListenAndServe(":8080", nil) abortif(err != nil, "failed to start http server: %v", err) @@ -46,7 +45,6 @@ func newsHandler(w http.ResponseWriter, r *http.Request) { defer db.Close() var news []newsDetail - var query = "SELECT title,body from news" filters, ok := r.URL.Query()["filter"] @@ -119,20 +117,9 @@ func httpError(w http.ResponseWriter, format string, args ...interface{}) { log.Printf("error: "+format, args...) } -func headersHandler(w http.ResponseWriter, req *http.Request) { - for name, headers := range req.Header { - for _, h := range headers { - fmt.Fprintf(w, "%v: %v\n", name, h) - } - } -} - func getenv(name string) string { val := os.Getenv(name) abortif(val == "", "env %s does not exists or is empty", name) - if val == "" { - os.Exit(1) - } return val } @@ -155,7 +142,7 @@ func checkdb() { db, err := sql.Open("mysql", dbconn()) abortif(err != nil, "failed to open db connection: %v", err) - defer db.Close() + db.Close() } var sprintf = fmt.Sprintf