├── .gitignore ├── LICENSE ├── README.md ├── doc ├── body读取.md └── untar.md ├── go.mod ├── go.sum ├── main.go └── pkg ├── conf └── conf.go ├── core └── server.go ├── filter └── auth.go ├── handler ├── ping.go ├── research │ ├── README.md │ ├── RealIp.go │ ├── cleanPath.go │ ├── error.go │ ├── goroutine.go │ ├── overflow_test.go │ ├── read_body.go │ ├── slice_test.go │ ├── unzipBody.go │ └── utf8_test.go ├── safe │ ├── exec.go │ ├── file.go │ ├── proxy.go │ └── upload_file.go ├── unsafe │ ├── exec.go │ ├── range_dos.go │ ├── read_file.go │ ├── sqli.go │ ├── ssrf.go │ ├── ssti.go │ └── untar.go └── user.go └── router.go /.gitignore: -------------------------------------------------------------------------------- 1 | vuln-go-app 2 | stdout.log 3 | 4 | .idea 5 | test 6 | ./upload 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017-present leveryd, https://github.com/leveryd/go-sec-code 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go漏洞靶场 2 | "漏洞类型"包括: 3 | * 任意文件读取 4 | * 任意文件写入 5 | * SSRF 6 | * 命令执行注入 7 | * SQL注入 8 | * 模板注入 9 | * 并发攻击 10 | 11 | # 目录说明 12 | * research: 比较少见的漏洞和研究性质的安全问题 13 | * unsafe: 常见的安全问题 14 | * safe: 对比"unsafe"安全的编码方式 15 | 16 | # 资源 17 | * [腾讯的Go安全编码指南](https://github.com/Tencent/secguide/blob/main/Go安全指南.md) 18 | * [审计规则和工具](https://gist.github.com/leveryd/51b1ec0130d4b4e9df76d9413ae41239) 19 | * [怎么做go安全研究](https://gist.github.com/leveryd/8581605b0f3532f8284bcfc4128f708c) 20 | 21 | # 报告过的漏洞 22 | * [CVE-2022-24863 swagger组件DoS](https://github.com/swaggo/http-swagger/security/advisories/GHSA-xg75-q3q5-cqmv) 23 | * [CVE-2022-25757 json实现差异](https://www.openwall.com/lists/oss-security/2022/03/28/2) 24 | * [CNVD-2022-51761 api/rpc框架gozero的dos问题](https://www.cnvd.org.cn/flaw/show/CNVD-2022-51761) 25 | 26 | # 其他 27 | // 不保证api接口稳定性,请自行测试 28 | 29 | 部分代码使用了 [copilot](https://github.com/github/copilot-docs) 自动生成。 30 | 31 | -------------------------------------------------------------------------------- /doc/body读取.md: -------------------------------------------------------------------------------- 1 | * flask读取body 2 | 3 | ``` 4 | from flask import Flask 5 | from flask import request 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route("/", methods=['POST']) 10 | def hello(): 11 | print(request.form) 12 | return "

hello world!

" 13 | ``` 14 | 15 | * gin读取body 16 | ``` 17 | func ReadBody(c *gin.Context) { 18 | //c.Request.Body 19 | } 20 | ``` 21 | 22 | 23 | * 区别和联系 24 | 25 | flask、net/http 在读body时,都有可能因为没读到Content-Length大小的字节而阻塞。 26 | 27 | c.Request.Body 实现了io.Reader接口,read方法返回`(0 <= n <= len(p))`个字节 28 | 29 | ``` 30 | type Reader interface { 31 | Read(p []byte) (n int, err error) 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /doc/untar.md: -------------------------------------------------------------------------------- 1 | # 正常使用时 2 | 3 | ``` 4 | ➜ vuln-go-app tar -czvf normal.tar.gz main.go 5 | a main.go 6 | ``` 7 | 8 | ``` 9 | ➜ vuln-go-app curl -F'file=@normal.tar.gz' 127.0.0.1:8089/unsafe/decompress_tar 10 | ``` 11 | 12 | # 恶意攻击 13 | 制作恶意tar包 14 | ``` 15 | ➜ vuln-go-app touch ../backdoor 16 | ➜ vuln-go-app tar -P -f evil.tar.gz -zcv ../backdoor 17 | a ../backdoor 18 | ➜ vuln-go-app rm ../backdoor 19 | ``` 20 | 21 | 上传压缩包后,服务端会解压压缩包,并且会把backdoor解压到上级目录。 22 | ``` 23 | curl -F'file=@evil.tar.gz' 127.0.0.1:8089/unsafe/decompress_tar 24 | ``` 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module vuln-go-app 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gin-contrib/static v0.0.1 7 | github.com/gin-gonic/gin v1.7.7 8 | github.com/go-sql-driver/mysql v1.6.0 9 | github.com/google/uuid v1.3.0 10 | ) 11 | 12 | require ( 13 | github.com/gin-contrib/sse v0.1.0 // indirect 14 | github.com/go-playground/locales v0.13.0 // indirect 15 | github.com/go-playground/universal-translator v0.17.0 // indirect 16 | github.com/go-playground/validator/v10 v10.4.1 // indirect 17 | github.com/golang/protobuf v1.3.3 // indirect 18 | github.com/json-iterator/go v1.1.9 // indirect 19 | github.com/leodido/go-urn v1.2.0 // indirect 20 | github.com/mattn/go-isatty v0.0.12 // indirect 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 22 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 23 | github.com/ugorji/go/codec v1.1.7 // indirect 24 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect 25 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect 26 | gopkg.in/yaml.v2 v2.2.8 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 6 | github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= 7 | github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 10 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 11 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 12 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 13 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 14 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 15 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 16 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 17 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 18 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 19 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 20 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 21 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 22 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 23 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 24 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 25 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 26 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 28 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 29 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 30 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 31 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 32 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 33 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 34 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 35 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 36 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 42 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 43 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 44 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 45 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 46 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 49 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 52 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 54 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 56 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 57 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 62 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 63 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "vuln-go-app/pkg/core" 10 | ) 11 | 12 | func main() { 13 | s := core.NewServer() 14 | 15 | go func() { 16 | s.InitConfig() 17 | s.InitRouter() 18 | s.Start() 19 | }() 20 | 21 | // 当前的 Goroutine 等待信号量 22 | quit := make(chan os.Signal) 23 | // 监控信号:SIGINT, SIGTERM, SIGQUIT 24 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 25 | // 这里会阻塞当前 Goroutine 等待信号 26 | <-quit 27 | 28 | log.Println("Shutdown Server ...") 29 | // 调用Server.Shutdown graceful结束 30 | if err := s.Server.Shutdown(context.Background()); err != nil { 31 | log.Fatal("Server Shutdown:", err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var ( 10 | ServerHost = "0.0.0.0" 11 | ServerPort = 8089 12 | 13 | // DataSourceName [username[:password]@][protocol[(address)]]/dbname[?param1=value1¶mN=valueN] 14 | DataSourceName = "" 15 | 16 | // Database 17 | username = "root" 18 | password = "123456" 19 | protocol = "tcp" 20 | address = "127.0.0.1" 21 | dbname = "test" 22 | ) 23 | 24 | // 环境变量优于配置文件 25 | func set(variable *string, envName string) { 26 | if value := os.Getenv(envName); value != "" { 27 | *variable = value 28 | } 29 | } 30 | 31 | func buildDataSourceName() { 32 | set(&username, "username") 33 | set(&password, "password") 34 | set(&protocol, "protocol") 35 | set(&address, "address") 36 | set(&dbname, "dbname") 37 | 38 | if os.Getenv("DataSourceName") != "" { 39 | DataSourceName = os.Getenv("DataSourceName") 40 | } else { 41 | DataSourceName = username + ":" + password + "@" + protocol + "(" + address + ")/" + dbname + "?charset=utf8&multiStatements=true" // 允许执行多条语句 https://github.com/go-sql-driver/mysql#multistatements 42 | } 43 | } 44 | 45 | func initDB() { 46 | buildDataSourceName() 47 | 48 | db, err := sql.Open("mysql", DataSourceName) 49 | defer func(db *sql.DB) { 50 | err := db.Close() 51 | if err != nil { 52 | log.Println(err) 53 | } 54 | }(db) 55 | 56 | if err != nil { 57 | log.Println(err) 58 | return 59 | } 60 | 61 | // 创建数据库 62 | { 63 | _, err := db.Exec("CREATE DATABASE IF NOT EXISTS " + dbname) 64 | if err != nil { 65 | log.Println(err) 66 | return 67 | } 68 | } 69 | 70 | // 创建表 71 | { 72 | createTableSql := ` 73 | use ` + dbname + `; 74 | CREATE TABLE IF NOT EXISTS user ( 75 | id INT UNSIGNED AUTO_INCREMENT, 76 | username VARCHAR(255) NOT NULL, 77 | password VARCHAR(255) NOT NULL, 78 | PRIMARY KEY (id) 79 | )ENGINE=InnoDB DEFAULT CHARSET=utf8; 80 | ` 81 | _, err := db.Exec(createTableSql) 82 | if err != nil { 83 | log.Println(err) 84 | return 85 | } 86 | } 87 | 88 | // 初始化表数据 89 | { 90 | initTableSql := ` 91 | use ` + dbname + `; 92 | INSERT INTO user VALUES (1, 'admin', 'admin123'); 93 | INSERT INTO user VALUES (2, 'test', 'test123'); 94 | ` 95 | _, err := db.Exec(initTableSql) 96 | if err != nil { 97 | return 98 | } 99 | } 100 | } 101 | 102 | func init() { 103 | go initDB() 104 | } 105 | -------------------------------------------------------------------------------- /pkg/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net" 6 | "net/http" 7 | "strconv" 8 | "vuln-go-app/pkg" 9 | "vuln-go-app/pkg/conf" 10 | ) 11 | 12 | type Server struct { 13 | e *gin.Engine 14 | Server *http.Server 15 | } 16 | 17 | func NewServer() *Server { 18 | //gin.SetMode(gin.ReleaseMode) 19 | gin.SetMode(gin.DebugMode) 20 | 21 | s := &Server{} 22 | s.e = gin.New() 23 | //s.e.RemoveExtraSlash = true // 影响路由匹配 24 | 25 | return s 26 | } 27 | 28 | func (s *Server) InitConfig() { 29 | //viper 30 | } 31 | 32 | func (s *Server) InitRouter() { 33 | pkg.InitRouter(s.e) 34 | } 35 | 36 | func (s *Server) Start() { 37 | 38 | addr := net.JoinHostPort(conf.ServerHost, strconv.Itoa(conf.ServerPort)) 39 | server := http.Server{ 40 | Addr: addr, 41 | Handler: s.e, 42 | } 43 | s.Server = &server 44 | 45 | // KILL信号会让ListenAndServe返回err 46 | //err := server.ListenAndServe() 47 | //if err != nil { 48 | // log.Fatal(err) 49 | //} 50 | server.ListenAndServe() 51 | } 52 | -------------------------------------------------------------------------------- /pkg/filter/auth.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func Auth(c *gin.Context) { 6 | c.Writer.Header().Set("auth", "success") 7 | c.Next() 8 | } 9 | -------------------------------------------------------------------------------- /pkg/handler/ping.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "time" 6 | ) 7 | 8 | func Ping(c *gin.Context) { 9 | _, err := c.Writer.Write([]byte("pong")) 10 | if err != nil { 11 | return 12 | } 13 | } 14 | 15 | func SimulateLongTask(_ *gin.Context) { 16 | time.Sleep(10 * time.Second) 17 | println("long task done") 18 | } 19 | -------------------------------------------------------------------------------- /pkg/handler/research/README.md: -------------------------------------------------------------------------------- 1 | 包括: 2 | * gin框架的误用 3 | -------------------------------------------------------------------------------- /pkg/handler/research/RealIp.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func RealIP(c *gin.Context) { 6 | //gin.SetMode(gin.ReleaseMode) 7 | c.Writer.Write([]byte(c.ClientIP())) 8 | } 9 | 10 | // https://github.com/gin-gonic/gin#dont-trust-all-proxies 11 | -------------------------------------------------------------------------------- /pkg/handler/research/cleanPath.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func MistakeCleanPath(c *gin.Context) { 12 | 13 | prefix := os.TempDir() 14 | log.Println(prefix + "/example.txt") 15 | ioutil.WriteFile(prefix+"/example.txt", []byte("Hello World"), 0644) 16 | 17 | path := c.Param("dir") + c.Param("filename") 18 | absPath := filepath.Join(prefix, filepath.Clean(path)) 19 | 20 | { 21 | file, err := ioutil.ReadFile(absPath) 22 | if err != nil { 23 | return 24 | } 25 | c.Writer.Write(file) 26 | } 27 | } 28 | 29 | // https://mp.weixin.qq.com/s/dqJ3F_fStlj78S0qhQ3Ggw 30 | 31 | // /research/mistake/./../../../../../../../etc/hosts 32 | // /research/mistake/./example 33 | -------------------------------------------------------------------------------- /pkg/handler/research/error.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | func Panic(_ *gin.Context) { 8 | panic("test") 9 | } 10 | 11 | func DeepRecursive(c *gin.Context) { 12 | DeepRecursive(c) 13 | } 14 | 15 | // /research/fatal_error 16 | // /research/panic 17 | 18 | /* 19 | 20 | net/http库有recover,所以项目中panic不会导致程序挂掉,但是"fatal error"会导致程序直接挂掉 21 | 22 | json解析等功能会用到递归算法 23 | 24 | */ 25 | 26 | // [背事故?分享 6 种常见的 Go 致命错误场景](https://segmentfault.com/a/1190000041173313) 27 | -------------------------------------------------------------------------------- /pkg/handler/research/goroutine.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | var goodUser = false 10 | 11 | func ConcurrentSecurity(c *gin.Context) { 12 | 13 | goodUser = false 14 | 15 | if c.Query("tell") != "" { 16 | // 模拟耗时io操作,可能导致协程调度 17 | //time.Sleep(time.Millisecond * 50) 18 | http.Get("http://www.baidu.com") // tell baidu i am good user 19 | } 20 | 21 | log.Println("goodUser:", goodUser) 22 | if goodUser == true { 23 | c.JSON(200, gin.H{ 24 | "message": "good user", 25 | }) 26 | return 27 | } 28 | c.JSON(200, gin.H{ 29 | "message": "bad user", 30 | }) 31 | } 32 | 33 | func init() { 34 | go func() { 35 | log.Print("start check good user") 36 | for { 37 | //"good user?" 38 | goodUser = true 39 | 40 | // 模拟耗时io操作,可能导致协程调度 41 | // time.Sleep(time.Millisecond * 50) 42 | http.Get("http://www.baidu.com") // tell baidu i am good user 43 | 44 | //emm, bad user 45 | goodUser = false 46 | } 47 | }() 48 | } 49 | 50 | // 访问/research/goodman, 会一直提示bad user 51 | // 访问/research/goodman?tell=1, 有可能会提示good user 52 | -------------------------------------------------------------------------------- /pkg/handler/research/overflow_test.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestUnsignedOverflow(t *testing.T) { 10 | var a uint32 = 2147483648 11 | var b uint32 = 2147483648 12 | var sum = a + b 13 | fmt.Println(reflect.TypeOf(sum)) 14 | fmt.Printf("Sum is : %d", sum) 15 | //uint32 16 | //Sum is : 0 17 | } 18 | 19 | func TestSignedOverflow(t *testing.T) { 20 | var a int8 = 127 21 | var b int8 = 1 22 | var sum = a + b 23 | fmt.Println(reflect.TypeOf(sum)) 24 | fmt.Printf("Sum is : %d", sum) 25 | //int8 26 | //-128 27 | } 28 | 29 | // https://bycsec.top/2021/02/07/golang%E7%9A%84%E4%B8%80%E4%BA%9B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/ 30 | -------------------------------------------------------------------------------- /pkg/handler/research/read_body.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var fileLock sync.Mutex 14 | 15 | var tmpFilepath = fmt.Sprintf("%s/%s", os.TempDir(), "go-sec-code-tmp") 16 | 17 | func ReadBody(c *gin.Context) { 18 | 19 | fileLock.Lock() 20 | defer fileLock.Unlock() 21 | 22 | file, err := os.OpenFile(tmpFilepath, os.O_RDWR|os.O_CREATE, 0640) 23 | if err != nil { 24 | return 25 | } 26 | defer os.Remove(tmpFilepath) 27 | 28 | hw := io.MultiWriter(file) 29 | io.Copy(hw, c.Request.Body) 30 | } 31 | 32 | func PrintFlag(c *gin.Context) { 33 | file, err := ioutil.ReadFile(tmpFilepath) 34 | if err != nil { 35 | return 36 | } 37 | if len(file) != 10 { 38 | return 39 | } 40 | 41 | time.Sleep(3 * time.Second) // check again after 3 second 42 | 43 | file, err = ioutil.ReadFile(tmpFilepath) 44 | if err != nil { 45 | return 46 | } 47 | if len(file) != 10 { 48 | return 49 | } 50 | 51 | c.JSON(200, "${ReadBody_flag}") 52 | } 53 | 54 | // https://www.leavesongs.com/PENETRATION/gitea-remote-command-execution.html 55 | // doc/body读取.md 56 | 57 | /* 58 | curl 127.0.0.1:8089/research/http/read_body -d '0123456789' -H 'Content-Length:20' 59 | curl 127.0.0.1:8089/research/http/read_body_flag 60 | */ 61 | -------------------------------------------------------------------------------- /pkg/handler/research/slice_test.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSlice(t *testing.T) { 9 | a := make([]uint64, 0) 10 | a = append(a, 1) 11 | a = append(a, 2) 12 | a = append(a, 3) 13 | // a = append(a, 4) 14 | 15 | b := append(a, 5) 16 | c := append(a, 6) 17 | 18 | fmt.Println(a) 19 | fmt.Println(b) 20 | fmt.Println(c) 21 | 22 | //result: 23 | //[1 2 3] 24 | //[1 2 3 6] 25 | //[1 2 3 6] 26 | } 27 | 28 | // https://bycsec.top/2021/02/07/golang%E7%9A%84%E4%B8%80%E4%BA%9B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/ 29 | -------------------------------------------------------------------------------- /pkg/handler/research/unzipBody.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "compress/gzip" 5 | "github.com/gin-gonic/gin" 6 | "io/ioutil" 7 | "strings" 8 | ) 9 | 10 | func GunzipHandler(c *gin.Context) { 11 | if strings.Contains(c.GetHeader("Content-Encoding"), "gzip") { 12 | reader, err := gzip.NewReader(c.Request.Body) 13 | if err != nil { 14 | return 15 | } 16 | defer reader.Close() 17 | 18 | { 19 | all, err := ioutil.ReadAll(reader) 20 | if err != nil { 21 | c.String(200, "read all error: %s", err.Error()) 22 | } 23 | c.JSON(200, gin.H{ 24 | "len": len(all), 25 | }) 26 | } 27 | } 28 | } 29 | 30 | /* 31 | dd if=/dev/zero of=/tmp/bigfile bs=512 count=200000 32 | cat bigfile | gzip -c > /tmp/test.zip 33 | */ 34 | 35 | /* 36 | import requests 37 | with open("/tmp/test.zip", "rb") as f: 38 | requests.post("http://127.0.0.1:8089/research/http/unzip", headers={"Content-Encoding": "gzip"}, data=f.read()) 39 | */ 40 | 41 | // https://stackoverflow.com/questions/56629115/how-to-protect-service-from-gzip-bomb 42 | -------------------------------------------------------------------------------- /pkg/handler/research/utf8_test.go: -------------------------------------------------------------------------------- 1 | package research 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "testing" 9 | "unicode/utf8" 10 | ) 11 | 12 | // 两字节的UTF-8编码,特殊字符\在末尾 13 | func TestSpecialUTF82(t *testing.T) { 14 | testChar := '\'' 15 | for i := 0; i < 256; i++ { 16 | input := []byte{byte(i), byte(testChar)} 17 | output := escape(string(input)) 18 | 19 | count := 0 20 | for _, ch := range output { 21 | if ch == '\\' { 22 | count++ 23 | } 24 | } 25 | if count == 0 { 26 | t.Errorf("%v -> %v", input, output) 27 | } 28 | } 29 | } 30 | 31 | // 三字节的UTF-8编码,特殊字符\在末尾 32 | func TestSpecialUTF8(t *testing.T) { 33 | testChar := '\'' 34 | for i := 0; i < 256; i++ { 35 | for j := 0; j < 256; j++ { 36 | input := []byte{byte(i), byte(j), byte(testChar)} 37 | output := escape(string(input)) 38 | 39 | count := 0 40 | for _, ch := range output { 41 | if ch == '\\' { 42 | count++ 43 | } 44 | } 45 | if count == 0 { 46 | t.Errorf("%v -> %v", input, output) 47 | } 48 | } 49 | } 50 | } 51 | 52 | // 四字节的UTF-8编码,特殊字符\在末尾 53 | func TestSpecialUTF8mb4(t *testing.T) { 54 | testChar := '\'' 55 | for i := 0; i < 256; i++ { 56 | for j := 0; j < 256; j++ { 57 | for n := 0; j < 256; j++ { 58 | input := []byte{byte(i), byte(j), byte(n), byte(testChar)} 59 | output := escape(string(input)) 60 | 61 | count := 0 62 | for _, ch := range output { 63 | if ch == '\\' { 64 | count++ 65 | } 66 | } 67 | if count == 0 { 68 | t.Errorf("%v -> %v", input, output) 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | func build2(x, y byte, char byte) string { 76 | i := []byte{x, y, char} // 用户输入是byte数组 77 | 78 | var buf bytes.Buffer 79 | buf.WriteString(string(i)) // byte数组转string 80 | return buf.String() 81 | } 82 | 83 | func build1(x byte, char byte) string { 84 | i := []byte{x, char} // 用户输入是byte数组 85 | 86 | var buf bytes.Buffer 87 | buf.WriteString(string(i)) // byte数组转string 88 | return buf.String() 89 | } 90 | 91 | func escape(input string) string { 92 | var b strings.Builder 93 | 94 | for _, ch := range input { 95 | switch ch { 96 | case '\x00': 97 | b.WriteString(`\x00`) 98 | case '\r': 99 | b.WriteString(`\r`) 100 | case '\n': 101 | b.WriteString(`\n`) 102 | case '\\': 103 | b.WriteString(`\\`) 104 | case '\'': 105 | b.WriteString(`\'`) 106 | case '"': 107 | b.WriteString(`\"`) 108 | case '\x1a': 109 | b.WriteString(`\x1a`) 110 | default: 111 | b.WriteRune(ch) 112 | } 113 | } 114 | 115 | return b.String() 116 | } 117 | 118 | func TestRune(t *testing.T) { 119 | for i := 0; i < 256; i++ { 120 | for j := 0; j < 256; j++ { 121 | x := i*256*256 + j*256 + '\\' 122 | r := rune(x) 123 | if utf8.ValidRune(r) { 124 | log.Println(i, j, x, r) 125 | } 126 | } 127 | } 128 | } 129 | 130 | func TestInvalidUtf8(t *testing.T) { 131 | x := []byte{'\xce', '\x28'} 132 | r := string(x) 133 | log.Println(len(r)) 134 | for _, ch := range r { 135 | if utf8.ValidRune(ch) { 136 | t.Errorf("%v is valid rune", ch) 137 | } 138 | } 139 | } 140 | 141 | func TestInvalidUtf8Another(t *testing.T) { 142 | x := []byte{'\xce', '\x28'} 143 | nihongo := string(x) 144 | 145 | for i, w := 0, 0; i < len(nihongo); i += w { 146 | runeValue, width := utf8.DecodeRuneInString(nihongo[i:]) 147 | fmt.Printf("%#U starts at byte position %d\n", runeValue, i) 148 | w = width 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pkg/handler/safe/exec.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "os/exec" 6 | "regexp" 7 | ) 8 | 9 | func DigHost(c *gin.Context) { 10 | host, exists := c.GetQuery("host") 11 | if exists == false { 12 | c.JSON(200, "no host arg") 13 | return 14 | } 15 | 16 | // safe: whitelist 17 | { 18 | compile, err := regexp.Compile(`^([\w\d.\-_]+)$`) 19 | if err != nil { 20 | return 21 | } 22 | if compile.Match([]byte(host)) == false { 23 | c.JSON(200, "invalid host arg") 24 | return 25 | } 26 | } 27 | 28 | commandResults, err := exec.Command("dig", host).Output() 29 | 30 | if err != nil { 31 | c.JSON(200, "fail to execute") 32 | return 33 | } 34 | c.Writer.Write(commandResults) 35 | } 36 | 37 | // /safe/dig?host=www.baidu.com 38 | 39 | // https://github.com/leveryd/go-sec-code/issues/2 40 | // 即使没有使用bash -c,也需要注意命令可能有参数能够被用来实施攻击 41 | 42 | // 或许需要补充其他类型的安全写法,除了黑名单 43 | -------------------------------------------------------------------------------- /pkg/handler/safe/file.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // FileRead safe/fileread?fpath=../../../../../../etc/hosts 10 | func FileRead(c *gin.Context) { 11 | filePath := c.Query("fpath") 12 | filePath = filepath.Clean("/" + filePath) // safe 13 | filePath = filepath.Join("./", filePath) 14 | 15 | openFile, err := os.Open(filePath) 16 | if err != nil { 17 | c.JSON(200, "read file fail") 18 | return 19 | } 20 | b := make([]byte, 100) 21 | openFile.Read(b) 22 | c.Writer.Write(b) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/handler/safe/proxy.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "strings" 13 | ) 14 | 15 | var noAvailableIP = errors.New("no available ip") 16 | var noSupportedProtocol = errors.New("no supported protocol") 17 | 18 | func isInternalIp(ip net.IP) bool { 19 | // 可能的更全的黑名单见 https://github.com/leveryd/go-sec-code/issues/3 20 | if ip.IsLoopback() || // 127.0.0.0/8 21 | ip.IsLinkLocalMulticast() || // 224.0.0.0/24 22 | ip.IsLinkLocalUnicast() { // 169.254.0.0/16 23 | return true 24 | } 25 | 26 | // 10.0.0.0/8 27 | // 172.16.0.0/12 28 | // 192.168.0.0/16 29 | if ip.IsPrivate() { 30 | return true 31 | } 32 | 33 | if ip4 := ip.To4(); ip4 != nil { 34 | return ip4[0] == 0 // 0.0.0.0/8 35 | } 36 | return false 37 | } 38 | 39 | func httpGet(xUrl string) ([]byte, error) { 40 | 41 | // 对于http请求,需要防止dns重绑定、防止30x跳转、检查是否请求内网资源 42 | // 自定义DialContext,使用"检查后的ip"建立tcp链接,防止dns重绑定 43 | // 自定义CheckRedirect,不允许30x跳转 (这里可能和实际业务需求不符合) 44 | // 需要保证请求头的Host与请求的url一致,而不是ip,否则可能会影响业务 45 | // https请求校验证书(这里可能和实际业务需求不符合) 46 | if strings.HasPrefix(xUrl, "http://") || strings.HasPrefix(xUrl, "https://") { 47 | customTransport := http.DefaultTransport.(*http.Transport).Clone() 48 | customTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 49 | host, port, err := net.SplitHostPort(addr) 50 | // 解析host和 端口 51 | if err != nil { 52 | return nil, err 53 | } 54 | // dns解析域名 55 | ips, err := net.LookupIP(host) 56 | if err != nil { 57 | return nil, err 58 | } 59 | // 对所有的ip进行串行发起请求,选一个可用的 60 | for _, ip := range ips { 61 | fmt.Printf("%v -> %v is localip?: %v\n", addr, ip.String(), isInternalIp(ip)) 62 | if isInternalIp(ip) { 63 | continue 64 | } 65 | // 拼接地址 66 | addr := net.JoinHostPort(ip.String(), port) 67 | con, err := net.Dial(network, addr) 68 | if err == nil { 69 | return con, nil 70 | } 71 | } 72 | 73 | return nil, noAvailableIP 74 | } 75 | client := http.Client{ 76 | Transport: customTransport, 77 | CheckRedirect: func(req *http.Request, via []*http.Request) error { // 防止30x跳转 78 | return http.ErrUseLastResponse 79 | }, 80 | } 81 | 82 | request, err := http.NewRequest("GET", xUrl, nil) // 添加host头 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | response, err := client.Do(request) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | defer func(Body io.ReadCloser) { 93 | err := Body.Close() 94 | if err != nil { 95 | 96 | } 97 | }(response.Body) 98 | 99 | body, err := ioutil.ReadAll(response.Body) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return body, nil 104 | } 105 | return nil, noSupportedProtocol 106 | } 107 | 108 | func GoodHTTPGet(c *gin.Context) { 109 | xUrl := c.Query("url") 110 | get, err := httpGet(xUrl) 111 | if err != nil { 112 | c.Writer.Write([]byte(err.Error())) 113 | } 114 | c.Writer.Write(get) 115 | } 116 | 117 | // [Go中的SSRF攻防战](https://segmentfault.com/a/1190000039009572) 118 | // [golang中设置Host Header的小Tips](https://www.cnblogs.com/jinsdu/p/5161962.html) 119 | 120 | // 需要兼顾业务功能和安全 121 | -------------------------------------------------------------------------------- /pkg/handler/safe/upload_file.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "os" 6 | ) 7 | 8 | func GoodUploadFile(c *gin.Context) { 9 | file, err := c.FormFile("filename") 10 | if err != nil { 11 | return 12 | } 13 | if err != nil { 14 | c.String(400, "Bad request") 15 | return 16 | } 17 | 18 | // safe check 19 | //if strings.Contains(file.Filename, "..") { 20 | // c.String(400, "Bad request") 21 | // return 22 | //} 23 | 24 | os.Mkdir("./upload", os.ModePerm) 25 | filepath := "./upload/" + file.Filename 26 | 27 | { 28 | err := c.SaveUploadedFile(file, filepath) 29 | if err != nil { 30 | c.String(200, "Upload failed") 31 | return 32 | } 33 | } 34 | 35 | defer os.Remove(filepath) 36 | 37 | c.String(200, "Upload success") 38 | } 39 | 40 | /* 41 | FormFile函数会保证文件名不包含.. ,所以不需要判断文件名中是否有 .. 42 | */ 43 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/exec.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "log" 7 | "os/exec" 8 | ) 9 | 10 | func DigHost(c *gin.Context) { 11 | host, exists := c.GetQuery("host") 12 | if exists == false { 13 | c.JSON(200, "no host arg") 14 | return 15 | } 16 | 17 | //cmd := fmt.Sprintf("/bin/bash -c 'dig %s'", host) 18 | cmd := fmt.Sprintf("dig %s", host) 19 | log.Print(cmd) 20 | 21 | commandResults, err := exec.Command("/bin/bash", "-c", cmd).Output() 22 | if err != nil { 23 | c.JSON(200, "fail to execute") 24 | return 25 | } 26 | c.Writer.Write(commandResults) 27 | } 28 | 29 | // /unsafe/dig?host=www.baidu.com%26%26id 30 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/range_dos.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/read_file.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func BadFileRead1(c *gin.Context) { 11 | 12 | filePath := c.Query("fpath") 13 | filePath = filepath.Clean(filePath) 14 | filePath = filepath.Join("./", filePath) 15 | 16 | openFile, err := os.Open(filePath) 17 | if err != nil { 18 | c.JSON(200, "read file fail") 19 | return 20 | } 21 | b := make([]byte, 100) 22 | 23 | { 24 | _, err := openFile.Read(b) 25 | if err != nil { 26 | return 27 | } 28 | } 29 | 30 | { 31 | _, err := c.Writer.Write(b) 32 | if err != nil { 33 | return 34 | } 35 | } 36 | } 37 | 38 | func BadFileRead2(c *gin.Context) { 39 | filePath := c.Query("fpath") 40 | filePath = filepath.Clean(filePath) 41 | filePath = filepath.Join("./", filePath) 42 | 43 | data, err := ioutil.ReadFile(filePath) 44 | if err != nil { 45 | c.JSON(200, "read file fail") 46 | return 47 | } 48 | 49 | { 50 | _, err := c.Writer.Write(data) 51 | if err != nil { 52 | return 53 | } 54 | } 55 | } 56 | 57 | // /unsafe/read_file2?fpath=../../../../../../etc/hosts 58 | // /unsafe/read_file1?fpath=../../../../../../etc/hosts 59 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/sqli.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/gin-gonic/gin" 6 | _ "github.com/go-sql-driver/mysql" 7 | "log" 8 | "vuln-go-app/pkg/conf" 9 | ) 10 | 11 | func BadQueryUser(c *gin.Context) { 12 | db, err := sql.Open("mysql", conf.DataSourceName) 13 | 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer db.Close() 18 | 19 | { 20 | querySQL := "select username from test.user where id = " + c.Query("id") 21 | 22 | rows, err := db.QueryContext(c, querySQL) 23 | if err != nil { 24 | // c.String(200, "Error: %s", err) // 避免报错注入 25 | log.Print(err) 26 | c.String(200, "execute sql error") 27 | return 28 | } 29 | defer func(rows *sql.Rows) { 30 | err := rows.Close() 31 | if err != nil { 32 | panic(err) 33 | } 34 | }(rows) 35 | 36 | var name string 37 | for rows.Next() { 38 | err := rows.Scan(&name) 39 | if err != nil { 40 | panic(err) 41 | } 42 | break 43 | } 44 | c.JSON(200, gin.H{ 45 | "username": name, 46 | }) 47 | } 48 | } 49 | 50 | // /unsafe/query_user?id=%31%3b%73%65%6c%65%63%74%28%73%6c%65%65%70%28%33%29%29%3b ---> "1;select(3);" 51 | // /unsafe/query_user?id=1 52 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/ssrf.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "io/ioutil" 7 | "net/http" 8 | ) 9 | 10 | func httpGet(url string) []byte { 11 | resp, err := http.Get(url) 12 | if err != nil { 13 | fmt.Println(err) 14 | } 15 | 16 | defer resp.Body.Close() 17 | body, err := ioutil.ReadAll(resp.Body) 18 | if err != nil { 19 | fmt.Println(err) 20 | return nil 21 | } 22 | 23 | return body 24 | } 25 | 26 | func SSRF(c *gin.Context) { 27 | url := c.Query("url") 28 | c.Writer.Write(httpGet(url)) 29 | } 30 | 31 | // /unsafe/ssrf?url=http://www.baidu.com 32 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/ssti.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "html/template" 7 | ) 8 | 9 | type User struct { 10 | ID int 11 | Email string 12 | Password string 13 | } 14 | 15 | // BadTemplate1 unsafe/ssti?q={{.Password}} 16 | func BadTemplate1(c *gin.Context) { 17 | var tmpl = fmt.Sprintf(` 18 | 19 | 20 | SSTI 21 | 22 |

SSTI Research

23 |

No search results for %s

24 |

Hi {{ .Email }}

25 | `, c.Query("q")) 26 | 27 | t, err := template.New("page").Parse(tmpl) 28 | 29 | if err != nil { 30 | fmt.Println(err) 31 | } 32 | 33 | u := User{ 34 | ID: 1, 35 | Email: "admin@qq.com", 36 | Password: "123456", 37 | } 38 | 39 | { 40 | err := t.Execute(c.Writer, &u) 41 | if err != nil { 42 | return 43 | } 44 | } 45 | } 46 | 47 | func BadTemplate2(c *gin.Context) { 48 | var tmpl = fmt.Sprintf(` 49 | 50 | your ip: {{.ClientIP}}
51 | search result for "%s": 52 | `, c.Query("q")) 53 | 54 | t, err := template.New("page").Parse(tmpl) 55 | 56 | if err != nil { 57 | fmt.Println(err) 58 | } 59 | 60 | t.Execute(c.Writer, c) 61 | } 62 | 63 | /* 64 | ssti场景1: 65 | 66 | 泄漏密码 67 | /unsafe/ssti?q={{.Password}} 68 | 69 | 反射xss 70 | /unsafe/ssti1?q=%7b%7bdefine%20%22T1%22%7d%7d%3Cscript%3Ealert%281%29%3C%2fscript%3E%7b%7bend%7d%7d%20%7b%7btemplate%20%22T1%22%7d%7d 71 | 72 | */ 73 | 74 | /* 75 | ssti场景2:写任意文件 76 | 77 | POST /unsafe/ssti2?q=%7b%7b%2e%53%61%76%65%55%70%6c%6f%61%64%65%64%46%69%6c%65%20%28%2e%46%6f%72%6d%46%69%6c%65%20%22%66%69%6c%65%22%29%20%22%2f%74%6d%70%2f%71%77%65%72%31%31%31%22%7d%7d HTTP/1.1 78 | Host: 127.0.0.1:8089 79 | User-Agent: curl/7.64.1 80 | Content-Length: 636 81 | Content-Type: multipart/form-data; boundary=------------------------8d9d289eba116e71 82 | Connection: close 83 | 84 | --------------------------8d9d289eba116e71 85 | Content-Disposition: form-data; name="file"; filename="hosts" 86 | Content-Type: application/octet-stream 87 | 88 | file content 89 | 90 | --------------------------8d9d289eba116e71-- 91 | 92 | */ 93 | 94 | // https://www.imwxz.com/crack_ctf/223.html 95 | // https://blog.takemyhand.xyz/2020/05/ssti-breaking-gos-template-engine-to.html 96 | -------------------------------------------------------------------------------- /pkg/handler/unsafe/untar.go: -------------------------------------------------------------------------------- 1 | package unsafe 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "github.com/gin-gonic/gin" 8 | "io" 9 | "os" 10 | "path" 11 | "strings" 12 | ) 13 | 14 | func DeCompressTar(srcFilePath, destDirPath string) error { 15 | // Create destination directory 16 | os.Mkdir(destDirPath, os.ModePerm) 17 | fr, err := os.Open(srcFilePath) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | defer func(fr *os.File) { 23 | err := fr.Close() 24 | if err != nil { 25 | panic(err) 26 | } 27 | }(fr) 28 | 29 | // Gzip reader 30 | gr, err := gzip.NewReader(fr) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | defer func(gr *gzip.Reader) { 36 | err := gr.Close() 37 | if err != nil { 38 | 39 | } 40 | }(gr) 41 | 42 | // Tar reader 43 | tr := tar.NewReader(gr) 44 | for { 45 | hdr, err := tr.Next() 46 | if err == io.EOF { 47 | // End of tar archive 48 | break 49 | } 50 | if err != nil { 51 | return err 52 | } 53 | fmt.Println("UnTarGzing file..." + hdr.Name) 54 | // Check if it is diretory or file 55 | if hdr.Typeflag != tar.TypeDir { 56 | // Get files from archive 57 | // Create diretory before create file 58 | err := os.MkdirAll(destDirPath+"/"+path.Dir(hdr.Name), os.ModePerm) 59 | if err != nil { 60 | return err 61 | } 62 | // Write data to file 63 | fmt.Println(hdr.Name) 64 | fw, _ := os.Create(destDirPath + "/" + hdr.Name) 65 | if err != nil { 66 | return err 67 | } 68 | _, err = io.Copy(fw, tr) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func BadTarDecompress(c *gin.Context) { 78 | // Get file from request 79 | file, err := c.FormFile("file") 80 | if err != nil { 81 | c.JSON(400, gin.H{ 82 | "message": "Unable to get file from request", 83 | }) 84 | return 85 | } 86 | // Get filename from request 87 | filename := file.Filename 88 | 89 | if strings.Contains(filename, "..") { 90 | c.JSON(400, gin.H{ 91 | "message": "Invalid filename", 92 | }) 93 | return 94 | } 95 | // Save file to server 96 | err = c.SaveUploadedFile(file, "./tmp/"+filename) 97 | if err != nil { 98 | c.JSON(200, gin.H{ 99 | "message": "Unable to save file to server", 100 | }) 101 | } 102 | // UnTarGz file 103 | err = DeCompressTar("./tmp/"+filename, "./tmp/") 104 | if err != nil { 105 | c.JSON(400, gin.H{ 106 | "message": "Unable to unTarGz file", 107 | }) 108 | return 109 | } 110 | 111 | // Delete file 112 | err = os.Remove("./tmp/" + filename) 113 | if err != nil { 114 | c.JSON(200, gin.H{ 115 | "message": "Unable to delete file", 116 | }) 117 | } 118 | c.JSON(200, gin.H{ 119 | "message": "UnTarGz file successfully", 120 | }) 121 | } 122 | 123 | // doc/untar.md 124 | -------------------------------------------------------------------------------- /pkg/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type User struct { 8 | username string 9 | password string 10 | } 11 | 12 | func Login(c *gin.Context) { 13 | //body, err := c.Request.GetBody() 14 | //c.Request.Body 15 | //if err != nil { 16 | // return 17 | //} 18 | 19 | //content := make([]byte, c.Request.ContentLength) 20 | //_, err := c.Request.Body.Read(content) 21 | //if err != nil { 22 | // return 23 | //} 24 | 25 | //u := &User{} 26 | //json.Unmarshal(content, u) 27 | // 28 | //if u.username == "admin" && u.password == "admin" { 29 | // c.Writer.Header().Set("username", "admin") 30 | //} 31 | username := c.PostForm("username") 32 | password := c.PostForm("password") 33 | if username == "admin" && password == "admin" { 34 | c.Writer.Header().Set("username", "admin") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/router.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "github.com/gin-contrib/static" 5 | "github.com/gin-gonic/gin" 6 | "vuln-go-app/pkg/filter" 7 | "vuln-go-app/pkg/handler" 8 | "vuln-go-app/pkg/handler/research" 9 | "vuln-go-app/pkg/handler/safe" 10 | "vuln-go-app/pkg/handler/unsafe" 11 | ) 12 | 13 | func InitRouter(e *gin.Engine) { 14 | e.Use(filter.Auth) // attention: middleware and "handler func" order 15 | 16 | e.GET("/ping", handler.Ping) 17 | e.POST("/user/login", handler.Login) 18 | e.GET("/tests/longtask", handler.SimulateLongTask) 19 | 20 | // vulnerable 21 | e.GET("/unsafe/dig", unsafe.DigHost) 22 | e.GET("/unsafe/ssrf", unsafe.SSRF) 23 | e.GET("/unsafe/read_file1", unsafe.BadFileRead1) 24 | e.GET("/unsafe/read_file2", unsafe.BadFileRead2) 25 | e.POST("/unsafe/decompress_tar", unsafe.BadTarDecompress) 26 | e.GET("/unsafe/ssti1", unsafe.BadTemplate1) 27 | e.POST("/unsafe/ssti2", unsafe.BadTemplate2) 28 | e.GET("/unsafe/query_user", unsafe.BadQueryUser) 29 | 30 | // safe 31 | e.GET("/safe/fileread", safe.FileRead) 32 | e.GET("/safe/dig", safe.DigHost) 33 | e.POST("/safe/upload", safe.GoodUploadFile) 34 | e.GET("/safe/proxy", safe.GoodHTTPGet) 35 | 36 | // research 37 | e.GET("/research/realip", research.RealIP) 38 | e.GET("/research/panic", research.Panic) 39 | e.GET("/research/fatal_error", research.DeepRecursive) 40 | e.GET("/research/goodman", research.ConcurrentSecurity) 41 | e.GET("/research/mistake/:dir/*filename", research.MistakeCleanPath) 42 | e.POST("/research/http/read_body", research.ReadBody) 43 | e.GET("/research/http/read_body_flag", research.PrintFlag) 44 | e.POST("/research/http/unzip", research.GunzipHandler) 45 | 46 | //e.Static("/files/", "/etc/") 47 | e.Use(static.Serve("/files/", static.LocalFile("/etc", false))) 48 | } 49 | --------------------------------------------------------------------------------