├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── cmd
└── log4shell-tools-server
│ ├── dns.go
│ ├── ldap.go
│ ├── main.go
│ ├── net.go
│ ├── prometheus.go
│ ├── storage
│ ├── db.go
│ ├── memory.go
│ └── storage.go
│ └── templates
│ └── index.html
├── container.nix
├── example
├── pom.xml
└── src
│ └── main
│ └── java
│ └── me
│ └── alexbakker
│ └── cve_2021_44228
│ └── App.java
├── flake.lock
├── flake.nix
├── go.mod
└── go.sum
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on: [pull_request, push]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v4
8 | - uses: DeterminateSystems/nix-installer-action@de22e16c4711fca50c816cc9081563429d1cf563
9 | with:
10 | diagnostic-endpoint:
11 | - name: Build server
12 | run: |
13 | nix build --print-build-logs
14 | # tiny smoke test
15 | ./result/bin/log4shell-tools-server -h
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .tmp
2 | result
3 | example/target/
4 | cmd/log4shell-tools-server/log4shell-tools-server
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Alexander Bakker
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # log4shell.tools [](https://github.com/alexbakker/log4shell-tools/actions/workflows/build.yml)
2 |
3 | __log4shell.tools__ is a tool that allows you to run a test to check whether one
4 | of your applications is affected by the recent vulnerabilities in log4j:
5 | __CVE-2021-44228__ and __CVE-2021-45046__.
6 |
7 | This is the code that runs https://log4shell.alexbakker.me. If you'd like to inspect the
8 | code or run an instance in your own environment, you've come to the right
9 | place.
10 |
11 | ## How does this work?
12 |
13 | The tool generates a unique ID for you to test with. After you click start,
14 | we'll generate a piece of text for you that looks similar to this:
15 | __${jndi:ldap://\*.dns.log4shell.tools:12345/\*}__. Copy it and paste it anywhere
16 | you suspect it might end up getting passed through log4j. For example: search
17 | boxes, form fields or HTTP headers.
18 |
19 | Once an outdated version of log4j sees this string, it will perform a DNS lookup
20 | to get the IP address of __\*.dns.log4shell.tools__. If this happens, it is
21 | considered the first sign of vulnerability to information leakage. Next, it will
22 | attempt and LDAP search request to __log4shell.tools:12345__. The tool responds
23 | with a Java class description, along with a URL for where to obtain it. Log4j
24 | may even attempt to fetch the class file. The tool will return a 404 and
25 | conclude the test.
26 |
27 | ## Screenshot
28 |
29 |
30 |
31 | ## Installation
32 |
33 | The tool was tested with Go 1.16. Make sure it (or a more recent version of Go) is
34 | installed and run the following command:
35 |
36 | ```
37 | go install github.com/alexbakker/log4shell-tools/cmd/log4shell-tools-server
38 | ```
39 |
40 | The binary will be available in ``$GOPATH/bin``
41 |
42 | ### Usage
43 |
44 | Since this tool compiles to a single binary, all you have to do is run it to
45 | start self hosting an instance of log4shell.tools. To make it accessible by
46 | other machines in your network, you'll want to pass a couple of flags to stop
47 | the tool from only listening on the loopback interface. If you're exposing this
48 | to the internet, you'll probably also want to put a reverse proxy in front of
49 | the HTTP server. Ignore the DNS options for now, they're not needed for simple
50 | internal deployments.
51 |
52 | For the full list of available flags, run `log4shell-tools-server -h`:
53 |
54 | ```
55 | Usage of ./log4shell-tools-server:
56 |
57 | This tool only listens on 127.0.0.1 by default. Pass the flags below to customize for your environment.
58 |
59 | -dns-a string
60 | the IPv4 address to respond with to any A record queries for 'dns-zone' (default "127.0.0.1")
61 | -dns-aaaa string
62 | the IPv6 address to respond with to any AAAA record queries for 'dns-zone' (default "::1")
63 | -dns-addr string
64 | listening address for the DNS server (default "127.0.0.1:12346")
65 | -dns-enable
66 | enable the DNS server
67 | -dns-zone string
68 | DNS zone that is forwarded to the tool's DNS server (example: "dns.log4shell.tools")
69 | -http-addr string
70 | listening address for the HTTP server (default "127.0.0.1:8001")
71 | -http-addr-external string
72 | address where the HTTP server can be reached externally (default "127.0.0.1:8001")
73 | -ldap-addr string
74 | listening address for the LDAP server (default "127.0.0.1:12345")
75 | -ldap-addr-external string
76 | address where the LDAP server can be reached externally (default "127.0.0.1:12345")
77 | -ldap-http-proto string
78 | the HTTP protocol to use in the payload URL that the LDAP server responds with (default "http")
79 | -storage string
80 | storage connection URI (either memory:// or a postgres:// URI (default "memory://")
81 | -test-timeout int
82 | test timeout in minutes (default 30)
83 | ```
84 |
85 | #### Storage
86 |
87 | The tool uses its in-memory storage backend by default. If you need test
88 | results to persist across restarts, you may want to use the Postgres backend instead.
89 |
90 | #### DNS
91 |
92 | The DNS server is disabled by default, because its configuration options are
93 | currently very specific to the setup over at https://log4shell.alexbakker.me. Let me
94 | know if you'd like to help make these more generic.
95 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/dns.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/alexbakker/log4shell-tools/cmd/log4shell-tools-server/storage"
9 | "github.com/google/uuid"
10 | "github.com/miekg/dns"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | type DNSServer struct {
15 | s *dns.Server
16 | store storage.Backend
17 | opts DNSServerOpts
18 | }
19 |
20 | type DNSServerOpts struct {
21 | Addr string
22 | Zone string
23 | A string
24 | AAAA string
25 | }
26 |
27 | func NewDNSServer(store storage.Backend, opts DNSServerOpts) *DNSServer {
28 | s := DNSServer{store: store, opts: opts}
29 | mux := dns.NewServeMux()
30 | mux.HandleFunc(fmt.Sprintf("%s.", opts.Zone), s.handleDNSQuery)
31 | server := &dns.Server{Addr: opts.Addr, Net: "udp", Handler: mux}
32 | s.s = server
33 | return &s
34 | }
35 |
36 | func (s *DNSServer) ListenAndServe() error {
37 | return s.s.ListenAndServe()
38 | }
39 |
40 | func (s *DNSServer) handleDNSQuery(w dns.ResponseWriter, r *dns.Msg) {
41 | m := new(dns.Msg)
42 | m.SetReply(r)
43 | m.Compress = false
44 |
45 | if r.Opcode == dns.OpcodeQuery {
46 | if len(m.Question) == 0 {
47 | w.WriteMsg(m)
48 | return
49 | }
50 |
51 | q := m.Question[0]
52 | if q.Qtype != dns.TypeA && q.Qtype != dns.TypeAAAA {
53 | m.SetRcode(r, dns.RcodeSuccess)
54 | w.WriteMsg(m)
55 | return
56 | }
57 |
58 | if strings.HasPrefix(strings.ToLower(q.Name), s.opts.Zone) {
59 | s.writeDNSRes(m, r, q.Name, q.Qtype)
60 | w.WriteMsg(m)
61 | return
62 | }
63 |
64 | ctxLog := log.WithFields(log.Fields{
65 | "server": "dns",
66 | "addr": w.RemoteAddr().String(),
67 | "q": q.Name,
68 | "type": dns.TypeToString[q.Qtype],
69 | })
70 |
71 | parts := strings.Split(q.Name, ".")
72 | id, err := uuid.Parse(parts[0])
73 | if err != nil {
74 | ctxLog.WithError(err).Error("Unable to parse UUID")
75 | m.SetRcode(r, dns.RcodeNameError)
76 | w.WriteMsg(m)
77 | return
78 | }
79 |
80 | ctxLog = ctxLog.WithField("test", id)
81 | ctxLog.Info("Handling DNS query")
82 | counterDNSQueries.Inc()
83 |
84 | test, err := s.store.Test(context.Background(), id)
85 | if err != nil {
86 | ctxLog.WithError(err).Error("Unable to lookup test in storage")
87 | m.SetRcode(r, dns.RcodeNameError)
88 | w.WriteMsg(m)
89 | return
90 | }
91 | if test == nil {
92 | ctxLog.Warn("Test not found")
93 | m.SetRcode(r, dns.RcodeNameError)
94 | w.WriteMsg(m)
95 | return
96 | }
97 | if test.Done(testTimeout) {
98 | ctxLog.Warn("Test already done")
99 | m.SetRcode(r, dns.RcodeNameError)
100 | w.WriteMsg(m)
101 | return
102 | }
103 |
104 | addr, ptr := getAddrPtr(context.Background(), w.RemoteAddr().String())
105 | if err = s.store.InsertTestResult(context.Background(), test, storage.TestResultDnsQuery, addr, ptr); err != nil {
106 | ctxLog.WithError(err).Error("Unable to insert test result")
107 | w.WriteMsg(m)
108 | return
109 | }
110 |
111 | s.writeDNSRes(m, r, q.Name, q.Qtype)
112 | }
113 |
114 | w.WriteMsg(m)
115 | }
116 |
117 | func (s *DNSServer) writeDNSRes(m *dns.Msg, r *dns.Msg, name string, recordType uint16) {
118 | var record string
119 | switch recordType {
120 | case dns.TypeA:
121 | record = s.opts.A
122 | case dns.TypeAAAA:
123 | record = s.opts.AAAA
124 | }
125 |
126 | if record != "" {
127 | rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", name, dns.TypeToString[recordType], record))
128 | if err == nil {
129 | m.Answer = append(m.Answer, rr)
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/ldap.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | ldap "github.com/alexbakker/ldapserver"
8 | "github.com/alexbakker/log4shell-tools/cmd/log4shell-tools-server/storage"
9 | "github.com/google/uuid"
10 | "github.com/lor00x/goldap/message"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | type LDAPServer struct {
15 | s *ldap.Server
16 | store storage.Backend
17 | }
18 |
19 | func NewLDAPServer(store storage.Backend) *LDAPServer {
20 | s := LDAPServer{store: store}
21 |
22 | mux := ldap.NewRouteMux()
23 | mux.Bind(s.handleBind)
24 | mux.Search(s.handleSearch)
25 |
26 | s.s = ldap.NewServer()
27 | s.s.Handle(mux)
28 | return &s
29 | }
30 |
31 | func (s *LDAPServer) ListenAndServe(addr string) error {
32 | return s.s.ListenAndServe(addr)
33 | }
34 |
35 | func (s *LDAPServer) handleSearch(w ldap.ResponseWriter, m *ldap.Message) {
36 | req := m.GetSearchRequest()
37 |
38 | ctxLog := log.WithFields(log.Fields{
39 | "server": "ldap",
40 | "addr": m.Client.Addr(),
41 | "req": "search",
42 | "object": req.BaseObject(),
43 | })
44 | ctxLog.Info("Handling LDAP search request")
45 | counterSearchRequests.Inc()
46 |
47 | id, err := uuid.Parse(string(req.BaseObject()))
48 | if err != nil {
49 | ctxLog.WithError(err).Error("Unable to parse UUID")
50 |
51 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultNoSuchObject)
52 | w.Write(res)
53 | return
54 | }
55 | ctxLog = ctxLog.WithField("test", id)
56 |
57 | test, err := s.store.Test(context.Background(), id)
58 | if err != nil {
59 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultNoSuchObject)
60 | w.Write(res)
61 | return
62 | }
63 | if test == nil || test.Done(testTimeout) {
64 | ctxLog.Warn("Test not found or already done")
65 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultNoSuchObject)
66 | w.Write(res)
67 | return
68 | }
69 |
70 | addr, ptr := getAddrPtr(context.Background(), m.Client.Addr().String())
71 | if err = s.store.InsertTestResult(context.Background(), test, storage.TestResultLdapSearch, addr, ptr); err != nil {
72 | ctxLog.WithError(err).Error("Unable to insert test result")
73 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultOther)
74 | w.Write(res)
75 | return
76 | }
77 |
78 | codeBase := fmt.Sprintf("%s://%s/api/tests/%s/payload/", *flagLDAPHTTPProto, *flagHTTPAddrExternal, id)
79 | e := ldap.NewSearchResultEntry("")
80 | e.AddAttribute("javaClassName", message.AttributeValue(className))
81 | e.AddAttribute("javaCodeBase", message.AttributeValue(codeBase))
82 | e.AddAttribute("objectClass", "javaNamingReference")
83 | e.AddAttribute("javaFactory", message.AttributeValue(className))
84 | w.Write(e)
85 |
86 | res := ldap.NewSearchResultDoneResponse(ldap.LDAPResultSuccess)
87 | w.Write(res)
88 | }
89 |
90 | func (s *LDAPServer) handleBind(w ldap.ResponseWriter, m *ldap.Message) {
91 | ctxLog := log.WithFields(log.Fields{
92 | "server": "ldap",
93 | "addr": m.Client.Addr(),
94 | "req": "bind",
95 | })
96 | ctxLog.Info("Handling LDAP bind request")
97 | counterBindRequests.Inc()
98 |
99 | res := ldap.NewBindResponse(ldap.LDAPResultSuccess)
100 | w.Write(res)
101 | }
102 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | _ "embed"
7 | "flag"
8 | "fmt"
9 | "html/template"
10 | "net/http"
11 | "os"
12 | "sync"
13 | "time"
14 |
15 | ldap "github.com/alexbakker/ldapserver"
16 | "github.com/alexbakker/log4shell-tools/cmd/log4shell-tools-server/storage"
17 | "github.com/google/uuid"
18 | "github.com/julienschmidt/httprouter"
19 | "github.com/prometheus/client_golang/prometheus/promhttp"
20 | log "github.com/sirupsen/logrus"
21 | )
22 |
23 | var (
24 | flagStorage = flag.String("storage", "memory://", "storage connection URI (either memory:// or a postgres:// URI")
25 | flagDNSEnable = flag.Bool("dns-enable", false, "enable the DNS server")
26 | flagDNSAddr = flag.String("dns-addr", "127.0.0.1:12346", "listening address for the DNS server")
27 | flagDNSZone = flag.String("dns-zone", "", "DNS zone that is forwarded to the tool's DNS server (example: \"dns.log4shell.tools\")")
28 | flagDNSA = flag.String("dns-a", "127.0.0.1", "the IPv4 address to respond with to any A record queries for 'dns-zone'")
29 | flagDNSAAAA = flag.String("dns-aaaa", "::1", "the IPv6 address to respond with to any AAAA record queries for 'dns-zone'")
30 | flagLDAPAddr = flag.String("ldap-addr", "127.0.0.1:12345", "listening address for the LDAP server")
31 | flagLDAPAddrExternal = flag.String("ldap-addr-external", "127.0.0.1:12345", "address where the LDAP server can be reached externally")
32 | flagLDAPHTTPProto = flag.String("ldap-http-proto", "http", "the HTTP protocol to use in the payload URL that the LDAP server responds with")
33 | flagHTTPAddr = flag.String("http-addr", "127.0.0.1:8001", "listening address for the HTTP server")
34 | flagHTTPAddrExternal = flag.String("http-addr-external", "127.0.0.1:8001", "address where the HTTP server can be reached externally")
35 | flagTestTimeout = flag.Int("test-timeout", 30, "test timeout in minutes")
36 | flagDiscontinued = flag.Bool("discontinued", false, "whether this service has been discontinued")
37 | testTimeout = time.Duration(*flagTestTimeout)
38 |
39 | className = "Log4Shell"
40 |
41 | //go:embed templates/index.html
42 | tmplIndexText string
43 | tmplIndex *template.Template
44 |
45 | store storage.Backend
46 | statsCache *StatsCache
47 | )
48 |
49 | type IndexModel struct {
50 | New bool
51 | UUID uuid.UUID
52 | Test *storage.Test
53 | Context context.Context
54 | AddrLDAP string
55 | AddrLDAPExternal string
56 | DNSEnabled bool
57 | DNSZone string
58 | ActiveTests int64
59 | Error string
60 | Discontinued bool
61 | }
62 |
63 | type StatsCache struct {
64 | store storage.Backend
65 | l sync.Mutex
66 | activeTests int64
67 | activeTestsFetched time.Time
68 | }
69 |
70 | func init() {
71 | ldap.Logger = ldap.DiscardingLogger
72 |
73 | flag.Usage = func() {
74 | fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n\n", os.Args[0])
75 | fmt.Fprintf(flag.CommandLine.Output(), "This tool only listens on 127.0.0.1 by default. Pass the flags below to customize for your environment.\n\n")
76 | flag.PrintDefaults()
77 | }
78 |
79 | tmplFuncs := template.FuncMap{
80 | "IsTestDone": func(test *storage.Test) bool {
81 | return test.Done(testTimeout)
82 | },
83 | "IsTestTimedOut": func(test *storage.Test) bool {
84 | return test.TimedOut(testTimeout)
85 | },
86 | "GetTestResults": func(ctx context.Context, test *storage.Test) ([]*storage.TestResult, error) {
87 | return store.TestResults(ctx, test)
88 | },
89 | }
90 |
91 | var err error
92 | tmplIndex, err = template.New("index").Funcs(tmplFuncs).Parse(tmplIndexText)
93 | if err != nil {
94 | log.WithError(err).Fatal("Unable to load template")
95 | }
96 | }
97 |
98 | func main() {
99 | flag.Parse()
100 | testTimeout = time.Minute * time.Duration(*flagTestTimeout)
101 |
102 | if !*flagDiscontinued {
103 | log.WithField("uri", *flagStorage).Info("Opening storage backend")
104 | var err error
105 | store, err = storage.NewBackend(*flagStorage)
106 | if err != nil {
107 | log.WithError(err).Fatal("Unable to open storage backend")
108 | }
109 | defer store.Close()
110 | statsCache = &StatsCache{store: store}
111 |
112 | go func() {
113 | for {
114 | <-time.After(1 * time.Minute)
115 |
116 | deleted, err := store.PruneTestResults(context.Background())
117 | if err != nil {
118 | log.WithError(err).Error("Unable to delete old test results")
119 | } else {
120 | log.WithField("count", deleted).Info("Deleted old test results")
121 | }
122 | }
123 | }()
124 |
125 | ldapServer := NewLDAPServer(store)
126 | go func() {
127 | log.WithFields(log.Fields{
128 | "addr": *flagLDAPAddr,
129 | "addr_ext": *flagLDAPAddrExternal,
130 | }).Info("Starting LDAP server")
131 |
132 | if err := ldapServer.ListenAndServe(*flagLDAPAddr); err != nil {
133 | log.WithError(err).Fatal("Unable to run LDAP server")
134 | }
135 | }()
136 |
137 | if *flagDNSEnable {
138 | dnsServer := NewDNSServer(store, DNSServerOpts{
139 | Addr: *flagDNSAddr,
140 | Zone: *flagDNSZone,
141 | A: *flagDNSA,
142 | AAAA: *flagDNSAAAA,
143 | })
144 |
145 | go func() {
146 | log.WithFields(log.Fields{
147 | "addr": *flagDNSAddr,
148 | }).Info("Starting DNS server")
149 |
150 | if err := dnsServer.ListenAndServe(); err != nil {
151 | log.WithError(err).Fatal("Unable to run DNS server")
152 | }
153 | }()
154 | }
155 | } else {
156 | log.Warn("Running in discontinued mode")
157 | }
158 |
159 | router := httprouter.New()
160 | router.GET("/", handleIndex)
161 | if !*flagDiscontinued {
162 | promHandler := promhttp.Handler()
163 | router.GET("/metrics", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
164 | promHandler.ServeHTTP(w, r)
165 | })
166 | router.GET(fmt.Sprintf("/api/tests/:uuid/payload/%s.class", className), handleTestPayloadDownload)
167 | }
168 |
169 | log.WithFields(log.Fields{
170 | "addr": *flagHTTPAddr,
171 | "addr_ext": *flagHTTPAddrExternal,
172 | }).Info("Starting HTTP server")
173 |
174 | if err := http.ListenAndServe(*flagHTTPAddr, router); err != nil {
175 | log.WithError(err).Fatal("Unable to start HTTP server")
176 | }
177 | }
178 |
179 | func handleIndex(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
180 | var activeTests int64
181 | if !*flagDiscontinued {
182 | activeTests = statsCache.getActiveTests(r.Context())
183 | }
184 | model := IndexModel{
185 | Context: r.Context(),
186 | ActiveTests: activeTests,
187 | AddrLDAP: *flagLDAPAddr,
188 | AddrLDAPExternal: *flagLDAPAddrExternal,
189 | DNSEnabled: *flagDNSEnable,
190 | DNSZone: *flagDNSZone,
191 | Discontinued: *flagDiscontinued,
192 | }
193 | ctxLog := log.WithFields(log.Fields{
194 | "server": "http",
195 | "addr": getRemoteAddr(r),
196 | "req": r.URL.Path,
197 | })
198 |
199 | if !*flagDiscontinued {
200 | idString := r.URL.Query().Get("uuid")
201 | if idString != "" {
202 | var err error
203 | if model.UUID, err = uuid.Parse(idString); err != nil {
204 | ctxLog.WithField("id", idString).WithError(err).Error("Unable to parse UUID")
205 | writeHttpError(w, http.StatusBadRequest)
206 | return
207 | }
208 | ctxLog = ctxLog.WithField("test", model.UUID)
209 |
210 | model.Test, err = store.Test(r.Context(), model.UUID)
211 | if err != nil {
212 | ctxLog.WithError(err).Error("Unable to lookup test in storage")
213 | writeHttpError(w, http.StatusInternalServerError)
214 | return
215 | }
216 | if model.Test == nil {
217 | if r.URL.Query().Get("terms") != "y" {
218 | model.Error = "You cannot continue before agreeing to only testing on machines that you have permission to test on."
219 | } else {
220 | ctxLog.Info("Inserting new test")
221 |
222 | if err := store.InsertTest(r.Context(), model.UUID); err != nil {
223 | ctxLog.WithError(err).Error("Unable to insert new test")
224 | writeHttpError(w, http.StatusInternalServerError)
225 | return
226 | }
227 | if model.Test, err = store.Test(r.Context(), model.UUID); err != nil {
228 | ctxLog.WithError(err).Error("Unable to lookup test in storage")
229 | writeHttpError(w, http.StatusInternalServerError)
230 | return
231 | }
232 | if model.Test == nil {
233 | ctxLog.Error("Unable to obtain test right after insert")
234 | writeHttpError(w, http.StatusInternalServerError)
235 | return
236 | }
237 |
238 | counterTestsCreated.Inc()
239 | }
240 | }
241 | } else {
242 | model.New = true
243 | model.UUID = uuid.New()
244 | }
245 | } else {
246 | model.New = true
247 | }
248 |
249 | var buf bytes.Buffer
250 | if err := tmplIndex.Execute(&buf, model); err != nil {
251 | ctxLog.WithError(err).Error("Unable to render template")
252 | writeHttpError(w, http.StatusInternalServerError)
253 | return
254 | }
255 |
256 | w.Header().Add("Content-Type", "text/html")
257 | w.Write(buf.Bytes())
258 | }
259 |
260 | func handleTestPayloadDownload(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
261 | ctxLog := log.WithFields(log.Fields{
262 | "server": "http",
263 | "addr": getRemoteAddr(r),
264 | "req": r.URL.Path,
265 | })
266 |
267 | idString := p.ByName("uuid")
268 | id, err := uuid.Parse(idString)
269 | if err != nil {
270 | ctxLog.WithField("id", idString).WithError(err).Error("Unable to parse UUID")
271 | writeHttpError(w, http.StatusBadRequest)
272 | return
273 | }
274 |
275 | ctxLog = ctxLog.WithField("test", id)
276 | ctxLog.Info("Handling payload download request")
277 | counterPayloadRequests.Inc()
278 |
279 | test, err := store.Test(r.Context(), id)
280 | if err != nil {
281 | ctxLog.WithError(err).Error("Unable to lookup test in storage")
282 | writeHttpError(w, http.StatusInternalServerError)
283 | return
284 | }
285 | if test == nil {
286 | ctxLog.Warn("Test not found")
287 | writeHttpError(w, http.StatusNotFound)
288 | return
289 | }
290 | if test.Done(testTimeout) {
291 | ctxLog.Warn("Test already done")
292 | writeHttpError(w, http.StatusGone)
293 | return
294 | }
295 |
296 | addr, ptr := getAddrPtr(r.Context(), getRemoteAddr(r))
297 | if err = store.InsertTestResult(r.Context(), test, storage.TestResultHttpGet, addr, ptr); err != nil {
298 | ctxLog.WithError(err).Error("Unable to insert test result")
299 | writeHttpError(w, http.StatusInternalServerError)
300 | return
301 | }
302 | if err = store.FinishTest(r.Context(), test); err != nil {
303 | ctxLog.WithError(err).Error("Unable to mark test as finished")
304 | writeHttpError(w, http.StatusInternalServerError)
305 | return
306 | }
307 | counterTestsCompleted.Inc()
308 |
309 | writeHttpError(w, http.StatusNotFound)
310 | }
311 |
312 | func writeHttpError(w http.ResponseWriter, code int) {
313 | http.Error(w, fmt.Sprintf("%d - %s", code, http.StatusText(code)), code)
314 | }
315 |
316 | func (c *StatsCache) getActiveTests(ctx context.Context) int64 {
317 | c.l.Lock()
318 | defer c.l.Unlock()
319 |
320 | if time.Since(c.activeTestsFetched) > 1*time.Minute {
321 | var err error
322 | c.activeTests, err = c.store.ActiveTests(ctx, testTimeout)
323 | if err != nil {
324 | log.WithError(err).Error("Unable to fetch number of active tests")
325 | }
326 | c.activeTestsFetched = time.Now()
327 | }
328 |
329 | return c.activeTests
330 | }
331 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/net.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "net"
6 | "net/http"
7 | "strings"
8 | "time"
9 |
10 | "inet.af/netaddr"
11 | )
12 |
13 | func getRemoteAddr(r *http.Request) string {
14 | for _, header := range []string{"X-Forwarded-For", "X-Real-Ip"} {
15 | ips := strings.Split(r.Header.Get(header), ",")
16 | for i := len(ips) - 1; i >= 0; i-- {
17 | ip := strings.TrimSpace(ips[i])
18 | realIP, err := netaddr.ParseIP(ip)
19 | if err != nil || realIP.IsPrivate() {
20 | continue
21 | }
22 | return ip
23 | }
24 | }
25 | return r.RemoteAddr
26 | }
27 |
28 | func getAddrPtr(ctx context.Context, addr string) (string, *string) {
29 | resCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
30 | defer cancel()
31 |
32 | host, _, err := net.SplitHostPort(addr)
33 | if err != nil {
34 | host = addr
35 | }
36 |
37 | var ptr *string
38 | var resolver net.Resolver
39 | if names, err := resolver.LookupAddr(resCtx, host); err == nil && len(names) != 0 {
40 | ptr = &names[0]
41 | }
42 |
43 | return host, ptr
44 | }
45 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/prometheus.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/prometheus/client_golang/prometheus"
5 | "github.com/prometheus/client_golang/prometheus/promauto"
6 | )
7 |
8 | var (
9 | counterTestsCreated = promauto.NewCounter(prometheus.CounterOpts{
10 | Name: "log4shell_tests_created_total",
11 | Help: "The total number of new tests inserted into the database",
12 | })
13 | counterTestsCompleted = promauto.NewCounter(prometheus.CounterOpts{
14 | Name: "log4shell_tests_completed_total",
15 | Help: "The total number of new tests that were completed (no timeout)",
16 | })
17 | counterBindRequests = promauto.NewCounter(prometheus.CounterOpts{
18 | Name: "log4shell_ldap_bind_requests_total",
19 | Help: "The total number of LDAP bind requests",
20 | })
21 | counterSearchRequests = promauto.NewCounter(prometheus.CounterOpts{
22 | Name: "log4shell_ldap_search_requests_total",
23 | Help: "The total number of LDAP search requests",
24 | })
25 | counterPayloadRequests = promauto.NewCounter(prometheus.CounterOpts{
26 | Name: "log4shell_http_payload_requests_total",
27 | Help: "The total number of RCE payload requests",
28 | })
29 | counterDNSQueries = promauto.NewCounter(prometheus.CounterOpts{
30 | Name: "log4shell_dns_queries_total",
31 | Help: "The total number of DNS queries",
32 | })
33 | )
34 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/storage/db.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 | "github.com/jackc/pgx/v4"
10 | "github.com/jackc/pgx/v4/pgxpool"
11 | )
12 |
13 | const (
14 | schema = `
15 | CREATE TABLE IF NOT EXISTS test (
16 | id UUID NOT NULL,
17 | created timestamp NOT NULL DEFAULT timezone('utc'::text, CURRENT_TIMESTAMP),
18 | finished timestamp,
19 | PRIMARY KEY (id)
20 | );
21 |
22 | CREATE TABLE IF NOT EXISTS test_result (
23 | id BIGSERIAL NOT NULL,
24 | test_id UUID NOT NULL,
25 | created timestamp NOT NULL DEFAULT timezone('utc'::text, CURRENT_TIMESTAMP),
26 | type TEXT NOT NULL,
27 | addr TEXT,
28 | ptr TEXT,
29 | PRIMARY KEY (id),
30 | FOREIGN KEY (test_id) REFERENCES test (id) ON DELETE CASCADE
31 | );
32 |
33 | CREATE INDEX IF NOT EXISTS test_result_test_id_idx ON test_result (test_id);
34 | `
35 | )
36 |
37 | type DB struct {
38 | p *pgxpool.Pool
39 | }
40 |
41 | func NewDB(connStr string) (*DB, error) {
42 | p, err := pgxpool.Connect(context.Background(), connStr)
43 | if err != nil {
44 | return nil, fmt.Errorf("db connect: %s", err)
45 | }
46 |
47 | if _, err = p.Exec(context.Background(), schema); err != nil {
48 | return nil, fmt.Errorf("schema init: %s", err)
49 | }
50 |
51 | return &DB{p: p}, nil
52 | }
53 |
54 | func (db *DB) Close() {
55 | db.p.Close()
56 | }
57 |
58 | func (db *DB) Test(ctx context.Context, id uuid.UUID) (*Test, error) {
59 | row := db.p.QueryRow(ctx, `SELECT id, created, finished FROM test WHERE id = $1`, id.String())
60 |
61 | var test Test
62 | if err := row.Scan(
63 | &test.ID,
64 | &test.Created,
65 | &test.Finished); err != nil {
66 | if err == pgx.ErrNoRows {
67 | return nil, nil
68 | }
69 |
70 | return nil, err
71 | }
72 |
73 | return &test, nil
74 | }
75 |
76 | func (db *DB) InsertTest(ctx context.Context, id uuid.UUID) error {
77 | _, err := db.p.Exec(ctx, "INSERT INTO test (id) VALUES($1)", id)
78 | return err
79 | }
80 |
81 | func (db *DB) InsertTestResult(ctx context.Context, t *Test, resultType string, addr string, ptr *string) error {
82 | _, err := db.p.Exec(ctx, `
83 | INSERT INTO test_result (test_id, type, addr, ptr)
84 | VALUES($1, $2, $3, $4)
85 | `, t.ID, resultType, addr, ptr)
86 | return err
87 | }
88 |
89 | func (db *DB) TestResults(ctx context.Context, t *Test) ([]*TestResult, error) {
90 | rows, err := db.p.Query(ctx, `
91 | SELECT created, type, addr, ptr
92 | FROM test_result
93 | WHERE test_id = $1
94 | ORDER BY created ASC
95 | `, t.ID)
96 | if err != nil {
97 | return nil, err
98 | }
99 | defer rows.Close()
100 |
101 | var results []*TestResult
102 | for rows.Next() {
103 | var res TestResult
104 | if err = rows.Scan(&res.Created, &res.Type, &res.Addr, &res.Ptr); err != nil {
105 | return nil, err
106 | }
107 |
108 | results = append(results, &res)
109 | }
110 |
111 | return results, nil
112 | }
113 |
114 | func (db *DB) PruneTestResults(ctx context.Context) (int64, error) {
115 | res, err := db.p.Exec(ctx, `
116 | DELETE FROM test
117 | WHERE created < timezone('utc'::text, CURRENT_TIMESTAMP) - '1 day'::interval
118 | `)
119 | if err != nil {
120 | return 0, err
121 | }
122 |
123 | return res.RowsAffected(), nil
124 | }
125 |
126 | func (db *DB) FinishTest(ctx context.Context, t *Test) error {
127 | _, err := db.p.Exec(ctx, `
128 | UPDATE test
129 | SET finished = timezone('utc'::text, CURRENT_TIMESTAMP)
130 | WHERE id = $1
131 | `, t.ID)
132 | return err
133 | }
134 |
135 | func (db *DB) ActiveTests(ctx context.Context, timeout time.Duration) (int64, error) {
136 | var count int64
137 | row := db.p.QueryRow(ctx, `
138 | SELECT count(*)
139 | FROM test
140 | WHERE finished IS NULL
141 | AND created > timezone('utc'::text, CURRENT_TIMESTAMP) - ('1 minute'::interval * $1);
142 | `, int64(timeout.Minutes()))
143 |
144 | err := row.Scan(&count)
145 | return count, err
146 | }
147 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/storage/memory.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | )
11 |
12 | type testWrapper struct {
13 | Test *Test
14 | Results []*TestResult
15 | }
16 |
17 | type Memory struct {
18 | tests map[uuid.UUID]*testWrapper
19 | l sync.Mutex
20 | }
21 |
22 | func NewMemory() *Memory {
23 | return &Memory{tests: map[uuid.UUID]*testWrapper{}}
24 | }
25 |
26 | func (m *Memory) Close() {
27 |
28 | }
29 |
30 | func (m *Memory) Test(ctx context.Context, id uuid.UUID) (*Test, error) {
31 | m.l.Lock()
32 | defer m.l.Unlock()
33 |
34 | tw, ok := m.tests[id]
35 | if !ok {
36 | return nil, nil
37 | }
38 |
39 | copy := *tw.Test
40 | return ©, nil
41 | }
42 |
43 | func (m *Memory) InsertTest(ctx context.Context, id uuid.UUID) error {
44 | m.l.Lock()
45 | defer m.l.Unlock()
46 |
47 | _, ok := m.tests[id]
48 | if ok {
49 | return fmt.Errorf("not found: %s", id)
50 | }
51 |
52 | now := time.Now().UTC()
53 | m.tests[id] = &testWrapper{Test: &Test{ID: id, Created: &now}}
54 | return nil
55 | }
56 |
57 | func (m *Memory) InsertTestResult(ctx context.Context, t *Test, resultType string, addr string, ptr *string) error {
58 | m.l.Lock()
59 | defer m.l.Unlock()
60 |
61 | tw, ok := m.tests[t.ID]
62 | if !ok {
63 | return fmt.Errorf("not found: %s", t.ID)
64 | }
65 |
66 | now := time.Now().UTC()
67 | tw.Results = append(tw.Results, &TestResult{
68 | TestID: tw.Test.ID,
69 | Created: &now,
70 | Type: resultType,
71 | Addr: &addr,
72 | Ptr: ptr,
73 | })
74 | return nil
75 | }
76 |
77 | func (m *Memory) TestResults(ctx context.Context, t *Test) ([]*TestResult, error) {
78 | m.l.Lock()
79 | defer m.l.Unlock()
80 |
81 | tw, ok := m.tests[t.ID]
82 | if !ok {
83 | return nil, fmt.Errorf("not found: %s", t.ID)
84 | }
85 |
86 | var res []*TestResult
87 | for _, tres := range tw.Results {
88 | copy := *tres
89 | res = append(res, ©)
90 | }
91 |
92 | return res, nil
93 | }
94 |
95 | func (m *Memory) PruneTestResults(ctx context.Context) (int64, error) {
96 | m.l.Lock()
97 | defer m.l.Unlock()
98 |
99 | var count int64
100 | for id, tw := range m.tests {
101 | if time.Since(*tw.Test.Created) > time.Hour*24 {
102 | delete(m.tests, id)
103 | count++
104 | }
105 | }
106 |
107 | return count, nil
108 | }
109 |
110 | func (m *Memory) FinishTest(ctx context.Context, t *Test) error {
111 | m.l.Lock()
112 | defer m.l.Unlock()
113 |
114 | tw, ok := m.tests[t.ID]
115 | if !ok {
116 | return fmt.Errorf("not found: %s", t.ID)
117 | }
118 |
119 | now := time.Now().UTC()
120 | tw.Test.Finished = &now
121 | return nil
122 | }
123 |
124 | func (m *Memory) ActiveTests(ctx context.Context, timeout time.Duration) (int64, error) {
125 | m.l.Lock()
126 | defer m.l.Unlock()
127 |
128 | var count int64
129 | for _, tw := range m.tests {
130 | if tw.Test.Finished == nil && time.Since(*tw.Test.Created) < timeout {
131 | count++
132 | }
133 | }
134 |
135 | return count, nil
136 | }
137 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/storage/storage.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "context"
5 | "strings"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 | )
10 |
11 | const (
12 | TestResultDnsQuery = "recv_dns_query"
13 | TestResultLdapBind = "recv_ldap_bind"
14 | TestResultLdapSearch = "recv_ldap_search"
15 | TestResultHttpGet = "recv_http_get"
16 | )
17 |
18 | type Backend interface {
19 | Close()
20 | Test(ctx context.Context, id uuid.UUID) (*Test, error)
21 | InsertTest(ctx context.Context, id uuid.UUID) error
22 | InsertTestResult(ctx context.Context, t *Test, resultType string, addr string, ptr *string) error
23 | TestResults(ctx context.Context, t *Test) ([]*TestResult, error)
24 | PruneTestResults(ctx context.Context) (int64, error)
25 | FinishTest(ctx context.Context, t *Test) error
26 | ActiveTests(ctx context.Context, timeout time.Duration) (int64, error)
27 | }
28 |
29 | type Test struct {
30 | ID uuid.UUID `db:"id"`
31 | Created *time.Time `db:"created"`
32 | Finished *time.Time `db:"finished"`
33 | }
34 |
35 | type TestResult struct {
36 | TestID uuid.UUID `db:"test_id"`
37 | Created *time.Time `db:"created"`
38 | Type string `db:type`
39 | Addr *string `db:ip`
40 | Ptr *string `db:ptr`
41 | }
42 |
43 | func NewBackend(connStr string) (Backend, error) {
44 | if strings.HasPrefix(connStr, "memory://") {
45 | return NewMemory(), nil
46 | }
47 |
48 | return NewDB(connStr)
49 | }
50 |
51 | func (t *Test) Done(timeout time.Duration) bool {
52 | return t.Finished != nil || t.TimedOut(timeout)
53 | }
54 |
55 | func (t *Test) TimedOut(d time.Duration) bool {
56 | return time.Since(*t.Created) > d
57 | }
58 |
59 | func (r *TestResult) Color() string {
60 | if r.Type == TestResultHttpGet {
61 | return "danger"
62 | }
63 |
64 | return "warning"
65 | }
66 |
--------------------------------------------------------------------------------
/cmd/log4shell-tools-server/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Log4Shell Vulnerability Test Tool
5 |
6 |
7 |
8 | {{ if .Test }}
9 | {{ if not (IsTestDone .Test) }}
10 |
11 | {{ end }}
12 | {{ end }}
13 |
14 |
15 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Copied to the clipboard
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
59 | {{ if gt .ActiveTests 0 }}
60 |
({{ .ActiveTests }} tests currently active)
61 | {{ end }}
62 | {{ if .Discontinued }}
63 |
64 |
2023-12-20: Discontinued
65 |
70 |
71 | {{ end }}
72 |
73 | This tool allows you to run a test to check whether one of your
74 | applications is affected by the recent vulnerabilities in log4j:
75 | CVE-2021-44228
77 | and CVE-2021-45046 .
79 | When you hit 'Start', the tool will generate a unique JNDI URI for
80 | you to enter anywhere you suspect it might end up being processed
81 | by log4j. If log4j triggers so much as a DNS lookup, this tool
82 | will tell you about it.
83 |
84 |
85 | You may only use this tool on machines that you have permission
86 | to test on.
87 |
88 | {{ if .Error }}
89 |
90 | {{ .Error }}
91 |
92 | {{ end }}
93 |
106 |
107 |
108 | {{ if .Test }}
109 |
110 |
111 |
112 | {{ if IsTestDone .Test }}
113 | Finished
114 | {{ if IsTestTimedOut .Test }}
115 | (The test timed out)
116 | {{ end }}
117 | {{ else }}
118 |
119 | Waiting...
120 |
121 | Waiting
122 | {{ end }}
123 |
124 | {{ if not (IsTestDone .Test) }}
125 |
Copy the texts below and paste them anywhere you suspect it might end up being processed by log4j:
126 |
LDAP
127 |
128 | {{ if .DNSEnabled }}
129 |
130 | {{ else }}
131 |
132 | {{ end }}
133 | Copy
134 |
135 | {{ if .DNSEnabled }}
136 |
DNS
137 |
138 |
139 | Copy
140 |
141 | {{ end }}
142 | {{ end }}
143 |
147 |
148 |
149 |
150 |
151 | Time
152 | Type
153 | Source
154 | Message
155 |
156 |
157 |
158 | {{ range $val := (GetTestResults .Context .Test) }}
159 |
160 | {{ $val.Created.Format "2006-01-02 15:04:05" }}
161 | {{ $val.Type }}
162 | {{ if $val.Ptr }}{{ $val.Ptr }}{{ else }}{{ $val.Addr }}{{ end }}
163 |
164 | {{ if eq $val.Type "recv_ldap_search" }}
165 | LDAP search query received. At the very least, your log4j deployment supports doing lookups. This can lead to information leakage.
166 | {{ else if eq $val.Type "recv_http_get" }}
167 | GET request for RCE payload payload received.
168 | {{ else if eq $val.Type "recv_dns_query" }}
169 | DNS query received. It is likely that your log4j deployment supports doing lookups. This can lead to information leakage.
170 | {{ end }}
171 |
172 |
173 | {{ end }}
174 |
175 |
176 |
Timestamps are in UTC. Test results are permanently deleted after 24 hours.
177 |
178 |
179 |
180 | {{ end }}
181 |
182 |
183 |
Changelog
184 |
When significant changes are made to the functionality of the tool, I'll post an update here.
185 |
2021-12-14
186 |
187 | The tool now supports detection through DNS. The
188 | jndi:ldap://
URI that is generated for your unique test
189 | ID now also contains a unique *.dns.log4shell.tools name.
190 | With that, the first signs of information leak vulnerability already appear
191 | when log4j performs a DNS lookup, before even connecting to the LDAP
192 | server. This should significantly reduce the number of false
193 | negatives when testing on machines that don't directly have access
194 | to the internet or can't connect to log4shell.tools for some other
195 | reason.
196 |
197 |
198 | Newly generated LDAP JNDI URI's now use this feature by default. There's a dedicated jndi:dns://
option as well.
199 |
200 |
201 |
202 |
203 |
204 |
FAQ
205 |
Here's a short list of frequently asked questions. If yours is not answered here, feel free to reach out.
206 |
What is CVE-2021-44228?
207 |
208 | CVE-2021-44228
209 | is a vulnerability in the popular log4j library by Apache. In the
210 | worst case, it allows bad actors to execute code on any server
211 | where they're able to get log4j to process a malicious log
212 | message.
213 |
214 |
215 | If you're using log4j or a product that depends on log4j, you should act on this immediately.
216 |
217 |
Can I use this tool to test for CVE-2021-45046 as well?
218 |
Yes.
219 |
What can I do to protect myself?
220 |
Please read the official advisory from Apache: https://logging.apache.org/log4j/2.x/security.html#Fixed_in_Log4j_2.15.0 .
221 |
What does this tool do?
222 |
223 | The tool generates a unique ID for you to test with. After you
224 | click start, we'll generate a piece of text for you that looks
225 | similar to this: ${jndi:ldap://*.dns.log4shell.tools:12345/*} .
226 | Copy it and paste it anywhere you suspect it might end up getting
227 | passed through log4j. For example: search boxes, form fields or
228 | HTTP headers.
229 |
230 |
231 | Once an outdated version of log4j sees this string, it will
232 | perform a DNS lookup to get the IP address of
233 | *.dns.log4shell.tools . If this happens, it is considered
234 | the first sign of vulnerability to information leakage. Next, it
235 | will attempt and LDAP search request to
236 | log4shell.tools:12345 . The tool responds with a Java class
237 | description, along with a URL for where to obtain it. Log4j may
238 | even attempt to fetch the class file. The tool will return a 404
239 | and conclude the test.
240 |
241 |
Am I safe if the tool doesn't report anything after the test?
242 |
243 | Not necessarily. If the machine you're testing on does not have
244 | access to the internet or can't reach log4shell.tools for
245 | some other reason, the results will not be accurate. The tool is
246 | only meant to give you a rough assessment of what someone with no
247 | special access to your environment would be able to do.
248 |
249 |
250 | The only way to make sure you're safe, is to start applying
251 | patches.
252 |
253 |
Is there any chance that the tool reports a false positive?
254 |
255 | The DNS lookup detection feature may result in a false positive in
256 | some cases. For example, this can happen if the environment you're
257 | testing has some other tooling that is examining the logs or the
258 | traffic on the network. If the tooling finds anything suspicious,
259 | it may perform a DNS lookup as part of its analysis. This will
260 | lead us to believe that we triggered a lookup in log4j, while that
261 | was actually not the case.
262 |
263 |
Isn't releasing such a tool to the public dangerous?
264 |
265 | I believe in arming the public with the same tools that the bad
266 | actors we're up against already have. Especially in this case
267 | where the vulnerability is so trivial to exploit. Anyone with some
268 | decent Google-fu will be able to find a full PoC (including RCE)
269 | within minutes. The goal is to contribute to leveling the playing
270 | field by allowing anyone to perform a rough assessment of how
271 | vulnerable they are to this log4j vulnerability.
272 |
273 |
274 | Especially if your product runs on a service where you don't have
275 | enough access to update log4j or change its options, a test result
276 | from a tool like this might be enough to get the attention of the
277 | right people.
278 |
279 |
Does this tool perform RCE (remote code execution)?
280 |
281 | No. It runs the exploit right up until your device requests the
282 | RCE payload, then it returns a 404 and concludes the test.
283 |
284 |
What is the privacy policy?
285 |
286 | The tool stores test results, IP addresses and PTR records of the servers
287 | that reach out to it. All test results and any information
288 | collected along with it is automatically permanently deleted
289 | after 24 hours.
290 |
291 |
292 | No information is shared with third parties. To ensure the privacy
293 | of your test results, do not share your unique test ID with anyone
294 | else.
295 |
296 |
Is this tool open source?
297 |
298 | Yes! The code is available on GitHub:
299 | https://github.com/alexbakker/log4shell-tools .
300 |
301 |
Is this tool affiliated with the Apache Software Foundation?
302 |
No.
303 |
I have another question / found an issue. How can I contact you?
304 |
305 | Feel free to send me an
306 | email . If you're reporting a security issue, please don't
307 | discuss it in public before I've had an opportunity to fix it.
308 |
309 |
310 |
311 |
312 |
313 |
318 |
345 |
346 |
347 |
--------------------------------------------------------------------------------
/container.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 |
3 | # This container is only used during development, so we don't bother securely
4 | # configuring PostgreSQL here.
5 |
6 | {
7 | boot.isContainer = true;
8 |
9 | services.postgresql = {
10 | enable = true;
11 | enableTCPIP = true;
12 | settings = {
13 | log_statement = "all";
14 | logging_collector = true;
15 | };
16 | authentication = pkgs.lib.mkOverride 10 ''
17 | local all all trust
18 | hostnossl all all 0.0.0.0/0 trust
19 | '';
20 | initialScript = pkgs.writeText "postgresql-init-script" ''
21 | CREATE ROLE log4shell WITH LOGIN PASSWORD 'log4shell' CREATEDB;
22 | CREATE DATABASE log4shell;
23 | GRANT ALL PRIVILEGES ON DATABASE log4shell TO log4shell;
24 | '';
25 | };
26 |
27 | networking = {
28 | useDHCP = false;
29 | firewall.allowedTCPPorts = [
30 | config.services.postgresql.port
31 | ];
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/example/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | 4.0.0
6 |
7 | me.alexbakker.cve_2021_44228
8 | cve_2021_44228
9 | 1.0-SNAPSHOT
10 | cve_2021_44228
11 |
12 |
13 | UTF-8
14 | 1.7
15 | 1.7
16 |
17 |
18 |
19 |
20 | org.apache.logging.log4j
21 | log4j-api
22 | 2.14.0
23 |
24 |
25 | org.apache.logging.log4j
26 | log4j-core
27 | 2.14.0
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | maven-assembly-plugin
36 | 2.5.5
37 |
38 | ${project.artifactId}-${project.version}-all
39 | false
40 |
41 | jar-with-dependencies
42 |
43 |
44 |
45 | me.alexbakker.cve_2021_44228.App
46 |
47 |
48 |
49 |
50 |
51 | make-assembly
52 | package
53 |
54 | single
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/example/src/main/java/me/alexbakker/cve_2021_44228/App.java:
--------------------------------------------------------------------------------
1 | package me.alexbakker.cve_2021_44228;
2 |
3 | import java.util.logging.Level;
4 |
5 | import org.apache.logging.log4j.LogManager;
6 | import org.apache.logging.log4j.Logger;
7 |
8 | public class App {
9 | private static Logger log = LogManager.getLogger();
10 |
11 | public static void main(String[] args) {
12 | System.out.print("Enter a JNDI URI: ");
13 | String uri = System.console().readLine();
14 | log.error(uri);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1710146030,
9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1712439257,
24 | "narHash": "sha256-aSpiNepFOMk9932HOax0XwNxbA38GOUVOiXfUVPOrck=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "ff0dbd94265ac470dda06a657d5fe49de93b4599",
28 | "type": "github"
29 | },
30 | "original": {
31 | "id": "nixpkgs",
32 | "ref": "nixos-unstable",
33 | "type": "indirect"
34 | }
35 | },
36 | "root": {
37 | "inputs": {
38 | "flake-utils": "flake-utils",
39 | "nixpkgs": "nixpkgs"
40 | }
41 | },
42 | "systems": {
43 | "locked": {
44 | "lastModified": 1681028828,
45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
46 | "owner": "nix-systems",
47 | "repo": "default",
48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
49 | "type": "github"
50 | },
51 | "original": {
52 | "owner": "nix-systems",
53 | "repo": "default",
54 | "type": "github"
55 | }
56 | }
57 | },
58 | "root": "root",
59 | "version": 7
60 | }
61 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Nix flake for log4shell-tools";
3 | inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
4 | inputs.flake-utils.url = "github:numtide/flake-utils";
5 |
6 | outputs = { self, nixpkgs, flake-utils }:
7 | flake-utils.lib.eachDefaultSystem (system:
8 | let
9 | pkgs = import nixpkgs { inherit system; };
10 | in {
11 | defaultPackage =
12 | with pkgs; buildGoModule {
13 | name = "log4shell-tools";
14 | src = ./.;
15 |
16 | vendorHash = "sha256-TjtI04iYvpist+BH7Abrpru5cs0kmC0VJlX1HBT/tjw=";
17 |
18 | subPackages = [ "./cmd/log4shell-tools-server" ];
19 | };
20 | nixosConfigurations.devContainer = nixpkgs.lib.nixosSystem {
21 | inherit system;
22 | modules = [
23 | ./container.nix
24 | ];
25 | };
26 | devShell = with pkgs; mkShell {
27 | buildInputs = [
28 | go
29 | maven
30 | openjdk8
31 | ];
32 | };
33 | }
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/alexbakker/log4shell-tools
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/alexbakker/ldapserver v1.0.2-0.20211212143751-7c2ef6599ffa
7 | github.com/google/uuid v1.3.0
8 | github.com/jackc/pgx/v4 v4.18.1
9 | github.com/julienschmidt/httprouter v1.3.0
10 | github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3
11 | github.com/miekg/dns v1.1.55
12 | github.com/prometheus/client_golang v1.16.0
13 | github.com/sirupsen/logrus v1.9.3
14 | inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a
15 | )
16 |
17 | require (
18 | github.com/beorn7/perks v1.0.1 // indirect
19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
20 | github.com/golang/protobuf v1.5.3 // indirect
21 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect
22 | github.com/jackc/pgconn v1.14.0 // indirect
23 | github.com/jackc/pgio v1.0.0 // indirect
24 | github.com/jackc/pgpassfile v1.0.0 // indirect
25 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect
26 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
27 | github.com/jackc/pgtype v1.14.0 // indirect
28 | github.com/jackc/puddle v1.3.0 // indirect
29 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
30 | github.com/prometheus/client_model v0.4.0 // indirect
31 | github.com/prometheus/common v0.44.0 // indirect
32 | github.com/prometheus/procfs v0.11.0 // indirect
33 | go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
34 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect
35 | golang.org/x/crypto v0.11.0 // indirect
36 | golang.org/x/mod v0.12.0 // indirect
37 | golang.org/x/net v0.12.0 // indirect
38 | golang.org/x/sys v0.10.0 // indirect
39 | golang.org/x/text v0.11.0 // indirect
40 | golang.org/x/tools v0.11.0 // indirect
41 | google.golang.org/protobuf v1.31.0 // indirect
42 | )
43 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
3 | github.com/alexbakker/ldapserver v1.0.2-0.20211212143751-7c2ef6599ffa h1:Tsgs30wcAAx0Cj73NUeqFJXfieZFrVGUtenMr3jGfXU=
4 | github.com/alexbakker/ldapserver v1.0.2-0.20211212143751-7c2ef6599ffa/go.mod h1:3AQlUcdYBjOpq2QfJSifBsPcUoHxwAfDyDzYHRmw5+A=
5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
8 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
9 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
10 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
11 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
12 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
13 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
18 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
19 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
20 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
21 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
22 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
24 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
25 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
26 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
28 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
29 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
30 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
31 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
32 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
33 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
34 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
35 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
36 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
37 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
38 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
39 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
40 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
41 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
42 | github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
43 | github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
44 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
45 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
46 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
47 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
48 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
49 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
50 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
51 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
52 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
53 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
54 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
55 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
56 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
57 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
58 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
59 | github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
60 | github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
61 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
62 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
63 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
64 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
65 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
66 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
67 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
68 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
69 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
70 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
71 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
72 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
73 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
74 | github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
75 | github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
76 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
77 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
78 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
79 | github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
80 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
81 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
82 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
83 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
84 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
85 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
86 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
87 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
88 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
89 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
90 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
91 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
92 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
93 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
94 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
95 | github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 h1:wIONC+HMNRqmWBjuMxhatuSzHaljStc4gjDeKycxy0A=
96 | github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
97 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
98 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
99 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
100 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
101 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
102 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
103 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
104 | github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
105 | github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
106 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
107 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
108 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
109 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
110 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
111 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
112 | github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
113 | github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
114 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
115 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
116 | github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
117 | github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
118 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
119 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
120 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
121 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
122 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
123 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
124 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
125 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
126 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
127 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
128 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
129 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
130 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
131 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
132 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
133 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
134 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
135 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
136 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
137 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
138 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
139 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
140 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
141 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
142 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
143 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
144 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
145 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
146 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
147 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
148 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
149 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
150 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
151 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
152 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
153 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
154 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
155 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
156 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
157 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
158 | go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
159 | go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
160 | go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
161 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
162 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg=
163 | go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
164 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
165 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
166 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
167 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
168 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
169 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
170 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
171 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
172 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
173 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
174 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
175 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
176 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
177 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
178 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
179 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
180 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
181 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
182 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
183 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
184 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
185 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
186 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
187 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
188 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
189 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
190 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
191 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
192 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
193 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
194 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
195 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
196 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
197 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
198 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
199 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
200 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
201 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
202 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
203 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
204 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
205 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
206 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
207 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
208 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
209 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
210 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
211 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
212 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
213 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
214 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
215 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
216 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
217 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
218 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
219 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
220 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
221 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
222 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
223 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
224 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
225 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
226 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
227 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
228 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
229 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
230 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
231 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
232 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
233 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
234 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
235 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
236 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
237 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
238 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
239 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
240 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
241 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
242 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
243 | golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
244 | golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
245 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
246 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
247 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
248 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
249 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
250 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
251 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
252 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
253 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
254 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
255 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
256 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
257 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
258 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
259 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
260 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
261 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
262 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
263 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
264 | inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
265 | inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
266 |
--------------------------------------------------------------------------------