diff --git a/.github/labeler.yml b/.github/labeler.yml index c8d85e5b..7fd724ab 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -29,6 +29,12 @@ - src/output_builder/** - src/models/output_builder.cr +🏷️ tagger: +- changed-files: + - any-glob-to-any-file: + - src/taggers/** + - src/models/tag.cr + πŸ’Š spec: - changed-files: - any-glob-to-any-file: spec/** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cda9a67f..1ddaae5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - crystal-version: ['1.8.0', '1.9.0', '1.10.0', '1.11.0'] + crystal-version: ['1.10.0', '1.11.0'] steps: - uses: actions/checkout@v3 - uses: MeilCli/setup-crystal-action@v4 diff --git a/README.md b/README.md index 3d60773f..36b8c566 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@
-

Noir is an attack surface detector that identifies endpoints by static analysis.

+

Attack surface detector that identifies endpoints by static analysis.

+

+ + + + + + +

+ +

+ Key Features β€’ + Available Support Scope β€’ + Installation β€’ + Usage β€’ + Contributing +

+ ## Key Features - Automatically identify language and framework from source code. - Find API endpoints and web pages through code analysis. @@ -10,7 +27,10 @@ - That provides structured data such as JSON and YAML for identified Attack Surfaces to enable seamless interaction with other tools. Also provides command line samples to easily integrate and collaborate with other tools, such as curls or httpie. ## Available Support Scope -### Endpoint's Entities + +
+ Endpoint's Entities + - Path - Method - Param @@ -19,7 +39,10 @@ - Protocol (e.g ws) - Details (e.g The origin of the endpoint) -### Languages and Frameworks +
+ +
+ Languages and Frameworks | Language | Framework | URL | Method | Param | Header | Cookie | WS | |----------|-------------|-----|--------|-------|--------|--------|----| @@ -45,8 +68,10 @@ | C# | ASP.NET MVC | βœ… | X | X | X | X | X | | JS | Next | X | X | X | X | X | X | +
-### Specification +
+ Specification | Specification | Format | URL | Method | Param | Header | WS | |------------------------|---------|-----|--------|-------|--------|----| @@ -55,6 +80,9 @@ | OAS 3.0 | JSON | βœ… | βœ… | βœ… | βœ… | X | | OAS 3.0 | YAML | βœ… | βœ… | βœ… | βœ… | X | | RAML | YAML | βœ… | βœ… | βœ… | βœ… | X | +| HAR | JSON | βœ… | βœ… | βœ… | βœ… | X | + +
## Installation ### Homebrew (macOS) @@ -115,6 +143,11 @@ Usage: noir --no-color Disable color output --no-log Displaying only the results + Tagger: + -T, --use-all-taggers Activates all taggers for full analysis coverage + --use-taggers VALUES Activates specific taggers (e.g., --use-taggers hunt,etc) + --list-taggers Lists all available taggers + Deliver: --send-req Send results to a web request --send-proxy http://proxy.. Send results to a web request via an HTTP proxy @@ -140,54 +173,54 @@ Usage: noir Example ```bash -noir -b . -u https://testapp.internal.domains +noir -b . -u https://testapp.internal.domains -T ``` -![](https://github.com/noir-cr/noir/assets/13212227/40d09acf-e250-4ea9-a84b-d9251a2d5147) +![](https://github.com/noir-cr/noir/assets/13212227/4e69da04-d585-4745-9cc7-ef6e69e193b0) JSON Result ``` -noir -b . -u https://testapp.internal.domains -f json +noir -b . -u https://testapp.internal.domains -f json -T ``` ```json -[ - ... - { - "headers": [], +{ + "url": "https://testapp.internal.domains/query", "method": "POST", "params": [ { - "name": "article_slug", - "param_type": "json", - "value": "" - }, - { - "name": "X-API-KEY", - "value":"", - "param_type":"header" + "name": "my_auth", + "value": "", + "param_type": "cookie", + "tags": [] }, { - "name": "auth", - "param_type": "cookie", - "value": "" + "name": "query", + "value": "", + "param_type": "form", + "tags": [ + { + "name": "sqli", + "description": "This parameter may be vulnerable to SQL Injection attacks.", + "tagger": "Hunt" + } + ] } ], - "protocol": "http", - "url": "https://testapp.internal.domains/comments", "details": { "code_paths": [ { - "path": "app_source/testapp.cr", - "line": 3 + "path": "spec/functional_test/fixtures/crystal_kemal/src/testapp.cr", + "line": 8 } ] - } + }, + "protocol": "http", + "tags": [] } -] ``` -### Contributing +## Contributing Noir is open-source project and made it with ❀️ if you want contribute this project, please see [CONTRIBUTING.md](./CONTRIBUTING.md) and Pull-Request with cool your contents. diff --git a/SECURITY.md b/SECURITY.md index 034e8480..c98a5bdd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,21 +1,19 @@ # Security Policy -## Supported Versions +## Reporting a Vulnerability -Use this section to tell people about which versions of your project are -currently being supported with security updates. +Found a security issue? Let us know so we can fix it. -| Version | Supported | -| ------- | ------------------ | -| 5.1.x | :white_check_mark: | -| 5.0.x | :x: | -| 4.0.x | :white_check_mark: | -| < 4.0 | :x: | +### How to Report -## Reporting a Vulnerability +* **For general security concerns**, please open a [GitHub issue](https://github.com/noir-cr/noir/issues). Use the `πŸ›‘οΈ security` label and describe the issue in as much detail as you can. This helps us to understand and address the problem more effectively. +* **For sensitive matters**, we encourage you to directly email the [noir team members](https://github.com/orgs/noir-cr/people). Handling these issues discreetly is vital for everyone's safety. + +### Our Team + +Beyond being passionate open source contributors, we are also seasoned Red Team security engineers. Our dual expertise means we're not only ready but also keen to address any security issues you might identify. Consider us your approachable security allies. Whether you notice something minor or more significant, we encourage you to get in touch. Open dialogue is key to us, and we're here to address any security concerns you might haveβ€”together. -Use this section to tell people how to report a vulnerability. +## Conclusion +Your vigilance and willingness to report security issues are what help keep our project robust and secure. We appreciate the time and effort you put into making our community a safer place. Remember, no concern is too small; we're here to listen and act. Together, we can ensure a secure environment for all our users and contributors. Thank you for being an essential part of our project's security. -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +Thank you for your support in maintaining the security and integrity of our project! \ No newline at end of file diff --git a/shard.lock b/shard.lock index 3211fdbc..bdf4e9a2 100644 --- a/shard.lock +++ b/shard.lock @@ -4,6 +4,10 @@ shards: git: https://github.com/mamantoha/crest.git version: 1.3.11 + har: + git: https://github.com/neuralegion/har.git + version: 1.2.0 + http-client-digest_auth: git: https://github.com/mamantoha/http-client-digest_auth.git version: 0.6.0 diff --git a/shard.yml b/shard.yml index e72751c7..bb2aa871 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: noir -version: 0.13.0 +version: 0.14.0 authors: - hahwul @@ -12,7 +12,9 @@ targets: dependencies: crest: github: mamantoha/crest + har: + github: NeuraLegion/har -crystal: 1.8.2 +crystal: ~> 1.10 license: MIT diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 90ec266b..3ecae15f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,10 +1,10 @@ name: noir base: core20 -version: 0.13.0 +version: 0.14.0 summary: Attack surface detector that identifies endpoints by static analysis. description: | - Noir is your ally in the quest for digital fortification. - A cutting-edge attack surface detector, it unveils hidden endpoints through meticulous static analysis. + Noir is an open-source project specializing in identifying attack surfaces for enhanced whitebox security testing and security pipeline. + This includes the capability to discover API endpoints, web endpoints, and other potential entry points within source code for thorough security analysis. grade: stable # must be 'stable' to release into candidate/stable channels confinement: strict # use 'strict' once you have the right plugs and slots @@ -23,7 +23,7 @@ parts: curl -fsSL https://crystal-lang.org/install.sh | sudo bash snapcraftctl pull shards install - shards build --release + shards build --release --no-debug --production cp ./bin/noir $SNAPCRAFT_PART_INSTALL/ snapcraftctl build build-packages: diff --git a/spec/functional_test/fixtures/crystal_kemal/custom_public/2.html b/spec/functional_test/fixtures/crystal_kemal/custom_public/2.html new file mode 100644 index 00000000..e69de29b diff --git a/spec/functional_test/fixtures/crystal_kemal/public/1.html b/spec/functional_test/fixtures/crystal_kemal/public/1.html new file mode 100644 index 00000000..e69de29b diff --git a/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr b/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr index 787f08ac..0b9a46f2 100644 --- a/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr +++ b/spec/functional_test/fixtures/crystal_kemal/src/testapp.cr @@ -10,8 +10,16 @@ post "/query" do env.params.body["query"].as(String) end +get "/token" do + env.params.body["client_id"].as(String) + env.params.body["redirect_url"].as(String) + env.params.body["grant_type"].as(String) +end + ws "/socket" do |socket| socket.send "Hello from Kemal!" end +public_folder "custom_public" + Kemal.run diff --git a/spec/functional_test/fixtures/har/example.har b/spec/functional_test/fixtures/har/example.har new file mode 100644 index 00000000..89e6d01f --- /dev/null +++ b/spec/functional_test/fixtures/har/example.har @@ -0,0 +1,153 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Firefox", + "version": "123.0.1" + }, + "browser": { + "name": "Firefox", + "version": "123.0.1" + }, + "pages": [ + { + "id": "page_1", + "pageTimings": { + "onContentLoad": -1, + "onLoad": -1 + }, + "startedDateTime": "2024-03-17T00:17:31.653+09:00", + "title": "https://www.hahwul.com/" + } + ], + "entries": [ + { + "startedDateTime": "2024-03-17T00:17:31.653+09:00", + "request": { + "bodySize": 0, + "method": "GET", + "url": "https://www.hahwul.com/", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "Host", + "value": "www.hahwul.com" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0" + }, + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + } + ], + "cookies": [ + { + "name": "_ga", + "value": "GA1.1.1310623768.1671977578" + }, + { + "name": "_ga_N9SYSZ280B", + "value": "GS1.1.1710602187.53.0.1710602187.0.0.0" + } + ], + "queryString": [], + "headersSize": 690 + }, + "response": { + "status": 304, + "statusText": "", + "httpVersion": "HTTP/2", + "headers": [ + { + "name": "date", + "value": "Sat, 16 Mar 2024 15:17:31 GMT" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "cache-control", + "value": "max-age=600" + }, + { + "name": "etag", + "value": "W/\"65f5937e-aadc\"" + }, + { + "name": "expires", + "value": "Sat, 16 Mar 2024 13:59:28 GMT" + }, + { + "name": "age", + "value": "258" + }, + { + "name": "x-served-by", + "value": "cache-icn1450027-ICN" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "3" + }, + { + "name": "x-timer", + "value": "S1710602252.651544,VS0,VE1" + }, + { + "name": "vary", + "value": "Accept-Encoding" + }, + { + "name": "x-fastly-request-id", + "value": "f40aba25bfeacafe5eb1bce11729a353344a30c3" + }, + { + "name": "X-Firefox-Spdy", + "value": "h2" + } + ], + "cookies": [], + "content": { + "mimeType": "text/plain", + "size": 0, + "text": "\n\n......\n\n\n" + }, + "redirectURL": "", + "headersSize": 382, + "bodySize": 382 + }, + "cache": { + "afterRequest": { + "expires": "1709911195", + "lastFetched": "1710602189", + "fetchCount": "2", + "_dataSize": "9437", + "_lastModified": "1710602191", + "_device": "" + } + }, + "timings": { + "blocked": 0, + "dns": 0, + "connect": 0, + "ssl": 0, + "send": 0, + "wait": 7, + "receive": 0 + }, + "time": 7, + "_securityState": "secure", + "serverIPAddress": "185.199.108.153", + "connection": "443", + "pageref": "page_1" + } + ] + } +} \ No newline at end of file diff --git a/spec/functional_test/func_spec.cr b/spec/functional_test/func_spec.cr index b5b13a73..a912e8fe 100644 --- a/spec/functional_test/func_spec.cr +++ b/spec/functional_test/func_spec.cr @@ -128,4 +128,8 @@ class FunctionalTester def app @app end + + def set_url(url) + @app.options[:url] = url + end end diff --git a/spec/functional_test/testers/crystal_kemal_spec.cr b/spec/functional_test/testers/crystal_kemal_spec.cr index 5359e6fe..7da85e4c 100644 --- a/spec/functional_test/testers/crystal_kemal_spec.cr +++ b/spec/functional_test/testers/crystal_kemal_spec.cr @@ -7,9 +7,16 @@ extected_endpoints = [ Param.new("query", "", "form"), Param.new("my_auth", "", "cookie"), ]), + Endpoint.new("/token", "GET", [ + Param.new("grant_type", "", "form"), + Param.new("redirect_url", "", "form"), + Param.new("client_id", "", "form"), + ]), + Endpoint.new("/1.html", "GET"), + Endpoint.new("/2.html", "GET"), ] FunctionalTester.new("fixtures/crystal_kemal/", { :techs => 1, - :endpoints => 3, + :endpoints => 6, }, extected_endpoints).test_all diff --git a/spec/functional_test/testers/har_spec.cr b/spec/functional_test/testers/har_spec.cr new file mode 100644 index 00000000..2e71940e --- /dev/null +++ b/spec/functional_test/testers/har_spec.cr @@ -0,0 +1,19 @@ +require "../func_spec.cr" + +extected_endpoints = [ + Endpoint.new("https://www.hahwul.com/", "GET", [ + Param.new("Host", "www.hahwul.com", "header"), + Param.new("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0", "header"), + Param.new("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "header"), + Param.new("_ga", "GA1.1.1310623768.1671977578", "cookie"), + Param.new("_ga_N9SYSZ280B", "GS1.1.1710602187.53.0.1710602187.0.0.0", "cookie"), + ]), +] + +instance = FunctionalTester.new("fixtures/har/", { + :techs => 1, + :endpoints => 1, +}, extected_endpoints) + +instance.set_url "https://www.hahwul.com" +instance.test_all diff --git a/spec/unit_test/tagger/tagger_spec.cr b/spec/unit_test/tagger/tagger_spec.cr new file mode 100644 index 00000000..e2ea14b6 --- /dev/null +++ b/spec/unit_test/tagger/tagger_spec.cr @@ -0,0 +1,56 @@ +require "../../../src/tagger/tagger" + +describe "Tagger" do + it "hunt_tagger" do + noir_options = default_options() + extected_endpoints = [ + Endpoint.new("/api/me", "GET", [ + Param.new("q", "", "query"), + Param.new("query", "", "query"), + Param.new("filter", "", "query"), + Param.new("X-Forwarded-For", "", "header"), + ]), + Endpoint.new("/api/sign_ups", "POST", [ + Param.new("url", "", "cookie"), + Param.new("command", "", "cookie"), + Param.new("role", "", "cookie"), + ]), + ] + NoirTaggers.run_tagger(extected_endpoints, noir_options, "hunt") + extected_endpoints.each do |endpoint| + endpoint.params.each do |param| + case param.name + when "query" + param.tags.each do |tag| + tag.name.should eq("sqli") + end + when "url" + param.tags.each do |tag| + tag.name.should eq("ssrf") + end + when "role" + param.tags.each do |tag| + tag.name.should eq("sqli") + end + end + end + end + end + + it "oauth_tagger" do + noir_options = default_options() + extected_endpoints = [ + Endpoint.new("/token", "GET", [ + Param.new("client_id", "", "query"), + Param.new("grant_type", "", "query"), + Param.new("code", "", "query"), + ]), + ] + NoirTaggers.run_tagger(extected_endpoints, noir_options, "oauth") + extected_endpoints.each do |endpoint| + endpoint.tags.each do |tag| + tag.name.should eq("oauth") + end + end + end +end diff --git a/src/analyzer/analyzer.cr b/src/analyzer/analyzer.cr index 09a82483..d6b09e9f 100644 --- a/src/analyzer/analyzer.cr +++ b/src/analyzer/analyzer.cr @@ -13,6 +13,7 @@ def initialize_analyzers(logger : NoirLogger) analyzers["go_echo"] = ->analyzer_go_echo(Hash(Symbol, String)) analyzers["go_fiber"] = ->analyzer_go_fiber(Hash(Symbol, String)) analyzers["go_gin"] = ->analyzer_go_gin(Hash(Symbol, String)) + analyzers["har"] = ->analyzer_har(Hash(Symbol, String)) analyzers["java_armeria"] = ->analyzer_armeria(Hash(Symbol, String)) analyzers["java_jsp"] = ->analyzer_jsp(Hash(Symbol, String)) analyzers["java_spring"] = ->analyzer_java_spring(Hash(Symbol, String)) diff --git a/src/analyzer/analyzers/analyzer_crystal_kemal.cr b/src/analyzer/analyzers/analyzer_crystal_kemal.cr index 7914606f..f93a390a 100644 --- a/src/analyzer/analyzers/analyzer_crystal_kemal.cr +++ b/src/analyzer/analyzers/analyzer_crystal_kemal.cr @@ -2,6 +2,10 @@ require "../../models/analyzer" class AnalyzerCrystalKemal < Analyzer def analyze + # Variables + is_public = true + public_folders = [] of String + # Source Analysis begin Dir.glob("#{@base_path}/**/*") do |path| @@ -24,6 +28,25 @@ class AnalyzerCrystalKemal < Analyzer last_endpoint.push_param(param) end end + + if line.includes? "serve_static false" || "serve_static(false)" + is_public = false + end + + if line.includes? "public_folder" + begin + splited = line.split("public_folder") + public_folder = "" + + if splited.size > 1 + public_folder = splited[1].gsub("(", "").gsub(")", "").gsub(" ", "").gsub("\"", "").gsub("'", "") + if public_folder != "" + public_folders << public_folder + end + end + rescue + end + end end end end @@ -32,6 +55,29 @@ class AnalyzerCrystalKemal < Analyzer logger.debug e end + # Public Dir Analysis + if is_public + begin + Dir.glob("#{@base_path}/public/**/*") do |file| + next if File.directory?(file) + real_path = "#{@base_path}/public/".gsub(/\/+/, '/') + relative_path = file.sub(real_path, "") + @result << Endpoint.new("/#{relative_path}", "GET") + end + + public_folders.each do |folder| + Dir.glob("#{@base_path}/#{folder}/**/*") do |file| + next if File.directory?(file) + relative_path = get_relative_path(@base_path, file) + relative_path = get_relative_path(folder, relative_path) + @result << Endpoint.new("/#{relative_path}", "GET") + end + end + rescue e + logger.debug e + end + end + result end diff --git a/src/analyzer/analyzers/analyzer_har.cr b/src/analyzer/analyzers/analyzer_har.cr new file mode 100644 index 00000000..b4cf32a1 --- /dev/null +++ b/src/analyzer/analyzers/analyzer_har.cr @@ -0,0 +1,68 @@ +require "../../models/analyzer" + +class AnalyzerHar < Analyzer + def analyze + locator = CodeLocator.instance + har_files = locator.all("har-path") + + if har_files.is_a?(Array(String)) && @url != "" + har_files.each do |har_file| + if File.exists?(har_file) + data = HAR.from_file(har_file) + logger.debug "Open #{har_file} file" + data.entries.each do |entry| + if entry.request.url.includes? @url + path = entry.request.url.to_s.gsub(@url, "") + endpoint = Endpoint.new(path, entry.request.method) + + entry.request.query_string.each do |query| + endpoint.params << Param.new(query.name, query.value, "query") + end + + is_websocket = false + entry.request.headers.each do |header| + endpoint.params << Param.new(header.name, header.value, "header") + if header.name == "Upgrade" && header.value == "websocket" + is_websocket = true + end + end + + entry.request.cookies.each do |cookie| + endpoint.params << Param.new(cookie.name, cookie.value, "cookie") + end + + post_data = entry.request.post_data + if post_data + params = post_data.params + mime_type = post_data.mime_type + param_type = "body" + if mime_type == "application/json" + param_type = "json" + end + if params + params.each do |param| + endpoint.params << Param.new(param.name, param.value.to_s, param_type) + end + end + end + + details = Details.new(PathInfo.new(har_file, 0)) + endpoint.set_details(details) + if is_websocket + endpoint.set_protocol "ws" + end + @result << endpoint + end + end + end + end + end + + @result + end +end + +def analyzer_har(options : Hash(Symbol, String)) + instance = AnalyzerHar.new(options) + instance.analyze +end diff --git a/src/detector/detector.cr b/src/detector/detector.cr index 33266ebd..37bcf295 100644 --- a/src/detector/detector.cr +++ b/src/detector/detector.cr @@ -22,6 +22,7 @@ def detect_techs(base_path : String, options : Hash(Symbol, String), logger : No DetectorGoEcho, DetectorGoFiber, DetectorGoGin, + DetectorHar, DetectorJavaArmeria, DetectorJavaJsp, DetectorJavaSpring, diff --git a/src/detector/detectors/har.cr b/src/detector/detectors/har.cr new file mode 100644 index 00000000..44cc53ae --- /dev/null +++ b/src/detector/detectors/har.cr @@ -0,0 +1,29 @@ +require "../../models/detector" +require "../../utils/json" +require "../../utils/yaml" +require "../../models/code_locator" +require "har" + +class DetectorHar < Detector + def detect(filename : String, file_contents : String) : Bool + if (filename.includes? ".har") || (filename.includes? ".json") + if valid_json? file_contents + begin + data = HAR.from_string(file_contents) + if data.version.to_s.includes? "1." + locator = CodeLocator.instance + locator.push("har-path", filename) + return true + end + rescue + end + end + end + + false + end + + def set_name + @name = "har" + end +end diff --git a/src/models/endpoint.cr b/src/models/endpoint.cr index a3838281..c79b7d73 100644 --- a/src/models/endpoint.cr +++ b/src/models/endpoint.cr @@ -4,26 +4,30 @@ require "yaml" struct Endpoint include JSON::Serializable include YAML::Serializable - property url, method, params, protocol, details + property url, method, params, protocol, details, tags def initialize(@url : String, @method : String) @params = [] of Param @details = Details.new @protocol = "http" + @tags = [] of Tag end def initialize(@url : String, @method : String, @details : Details) @params = [] of Param @protocol = "http" + @tags = [] of Tag end def initialize(@url : String, @method : String, @params : Array(Param)) @details = Details.new @protocol = "http" + @tags = [] of Tag end def initialize(@url : String, @method : String, @params : Array(Param), @details : Details) @protocol = "http" + @tags = [] of Tag end def set_details(@details : Details) @@ -33,6 +37,10 @@ struct Endpoint @protocol = protocol end + def add_tag(tag : Tag) + @tags << tag + end + def push_param(param : Param) @params << param end @@ -54,11 +62,16 @@ end struct Param include JSON::Serializable include YAML::Serializable - property name, value, param_type + property name, value, param_type, tags # param_type can be "query", "json", "form", "header", "cookie" def initialize(@name : String, @value : String, @param_type : String) + @tags = [] of Tag + end + + def add_tag(tag : Tag) + @tags << tag end end @@ -111,3 +124,12 @@ struct EndpointReference def initialize(@endpoint : Endpoint, @metadata : Hash(Symbol, String)) end end + +struct Tag + include JSON::Serializable + include YAML::Serializable + property name, description, tagger + + def initialize(@name : String, @description : String, @tagger : String) + end +end diff --git a/src/models/noir.cr b/src/models/noir.cr index fb0137b4..d2fb7ebf 100644 --- a/src/models/noir.cr +++ b/src/models/noir.cr @@ -1,5 +1,6 @@ require "../detector/detector.cr" require "../analyzer/analyzer.cr" +require "../tagger/tagger.cr" require "../deliver/*" require "../output_builder/*" require "./endpoint.cr" @@ -91,6 +92,22 @@ class NoirRunner @endpoints = analysis_endpoints options, @techs, @logger optimize_endpoints combine_url_and_endpoints + + # Run tagger + if @options[:all_taggers] == "yes" + @logger.info "Running all taggers." + NoirTaggers.run_tagger @endpoints, @options, "all" + if @is_debug + NoirTaggers.get_taggers.each do |tagger| + @logger.debug "Tagger: #{tagger}" + end + end + elsif @options[:use_taggers] != "" + @logger.info "Running #{@options[:use_taggers]} taggers." + NoirTaggers.run_tagger @endpoints, @options, @options[:use_taggers] + end + + # Run deliver deliver end diff --git a/src/models/output_builder.cr b/src/models/output_builder.cr index fd9600c8..705009c1 100644 --- a/src/models/output_builder.cr +++ b/src/models/output_builder.cr @@ -38,6 +38,7 @@ class OutputBuilder final_body = "" final_headers = [] of String final_cookies = [] of String + final_tags = [] of String is_json = false first_query = true first_form = true @@ -73,6 +74,12 @@ class OutputBuilder if param.param_type == "json" is_json = true end + + if param.tags.size > 0 + param.tags.each do |tag| + final_tags << tag.name + end + end end if is_json @@ -88,13 +95,19 @@ class OutputBuilder end end - @logger.debug "Baked endpoint #{final_url} with #{final_body} body and #{final_headers.size} headers." + @logger.debug "Baked endpoints" + @logger.debug " + Final URL: #{final_url}" + @logger.debug " + Body: #{final_body}" + @logger.debug " + Headers: #{final_headers}" + @logger.debug " + Cookies: #{final_cookies}" + @logger.debug " + Tags: #{final_tags}" { url: final_url, body: final_body, header: final_headers, cookie: final_cookies, + tags: final_tags.uniq, body_type: is_json ? "json" : "form", } end diff --git a/src/models/tagger.cr b/src/models/tagger.cr new file mode 100644 index 00000000..51ab59bd --- /dev/null +++ b/src/models/tagger.cr @@ -0,0 +1,30 @@ +require "./logger" + +class Tagger + @logger : NoirLogger + @options : Hash(Symbol, String) + @is_debug : Bool + @is_color : Bool + @is_log : Bool + @name : String + + def initialize(options : Hash(Symbol, String)) + @is_debug = str_to_bool(options[:debug]) + @options = options + @is_color = str_to_bool(options[:color]) + @is_log = str_to_bool(options[:nolog]) + @name = "" + + @logger = NoirLogger.new @is_debug, @is_color, @is_log + end + + def name + @name + end + + def perform(endpoints : Array(Endpoint)) : Array(Endpoint) + # After inheriting the class, write an action code here. + + endpoints + end +end diff --git a/src/noir.cr b/src/noir.cr index a1d09736..a3b6beb9 100644 --- a/src/noir.cr +++ b/src/noir.cr @@ -6,7 +6,7 @@ require "./options.cr" require "./techs/techs.cr" module Noir - VERSION = "0.13.0" + VERSION = "0.14.0" end noir_options = default_options() @@ -33,6 +33,21 @@ OptionParser.parse do |parser| noir_options[:nolog] = "yes" end + parser.separator "\n Tagger:".colorize(:blue) + parser.on "-T", "--use-all-taggers", "Activates all taggers for full analysis coverage" { |_| noir_options[:all_taggers] = "yes" } + parser.on "--use-taggers VALUES", "Activates specific taggers (e.g., --use-taggers hunt,oauth)" { |var| noir_options[:use_taggers] = var } + parser.on "--list-taggers", "Lists all available taggers" do + puts "Available taggers:" + techs = NoirTaggers.get_taggers + techs.each do |tagger, value| + puts " #{tagger.to_s.colorize(:green)}" + value.each do |k, v| + puts " #{k.to_s.colorize(:blue)}: #{v}" + end + end + exit + end + parser.separator "\n Deliver:".colorize(:blue) parser.on "--send-req", "Send results to a web request" { |_| noir_options[:send_req] = "yes" } parser.on "--send-proxy http://proxy..", "Send results to a web request via an HTTP proxy" { |var| noir_options[:send_proxy] = var } diff --git a/src/options.cr b/src/options.cr index f323d82c..764c7a29 100644 --- a/src/options.cr +++ b/src/options.cr @@ -19,6 +19,8 @@ def default_options :url => "", :use_filters => "", :use_matchers => "", + :all_taggers => "no", + :use_taggers => "", } noir_options diff --git a/src/output_builder/common.cr b/src/output_builder/common.cr index a0ee7475..3ca81ac1 100644 --- a/src/output_builder/common.cr +++ b/src/output_builder/common.cr @@ -31,6 +31,16 @@ class OutputBuilderCommon < OutputBuilder r_buffer += "\n β—‹ body: #{r_body}" end + tags = baked[:tags] + endpoint.tags.each do |tag| + tags << tag.name.to_s + end + + if tags.size > 0 + r_tags = tags.join(" ").colorize(:light_magenta).toggle(@is_color) + r_buffer += "\n β—‹ tags: #{r_tags}" + end + if @options[:include_path] == "yes" details = endpoint.details if details.code_paths && details.code_paths.size > 0 diff --git a/src/tagger/tagger.cr b/src/tagger/tagger.cr new file mode 100644 index 00000000..26030ff0 --- /dev/null +++ b/src/tagger/tagger.cr @@ -0,0 +1,43 @@ +require "./taggers/*" +require "../models/tagger" + +module NoirTaggers + HasTaggers = { + hunt: { + name: "HuntParam Tagger", + desc: "Identifies common parameters vulnerable to certain vulnerability classes", + runner: HuntParamTagger, + }, + oauth: { + name: "OAuth Tagger", + desc: "Identifies OAuth endpoints", + runner: OAuthTagger, + }, + } + + def self.get_taggers + HasTaggers + end + + def self.run_tagger(endpoints : Array(Endpoint), options : Hash(Symbol, String), use_taggers : String) + tagger_list = [] of Tagger # This will hold instances of taggers + + # Define taggers by creating instances + # Assuming HuntParamTagger is defined and is the only tagger + HasTaggers.each_value do |tagger| + if tagger[:runner].class.to_s == "Class" + instance = tagger[:runner].new(options) + tagger_list << instance + end + end + + # Parsing use_taggers + use_taggers_arr = use_taggers.split(",") + use_taggers_arr = use_taggers_arr.map(&.strip) + + # Run taggers + tagger_list.each do |tagger| + tagger.perform(endpoints) if use_taggers_arr.includes?(tagger.name) || use_taggers_arr.includes?("all") + end + end +end diff --git a/src/tagger/taggers/hunt_param.cr b/src/tagger/taggers/hunt_param.cr new file mode 100644 index 00000000..c6486b42 --- /dev/null +++ b/src/tagger/taggers/hunt_param.cr @@ -0,0 +1,58 @@ +require "../../models/tagger" +require "../../models/endpoint" + +class HuntParamTagger < Tagger + TAG_DEFINITIONS = { + "ssti" => { + "words" => ["template", "preview", "id", "view", "activity", "name", "content", "redirect"], + "description" => "This parameter may be vulnerable to Server Side Template Injection (SSTI) attacks.", + }, + "ssrf" => { + "words" => ["dest", "redirect", "uri", "path", "continue", "url", "window", "next", "data", "reference", "site", "html", "val", "validate", "domain", "callback", "return", "page", "feed", "host", "port", "to", "out", "view", "dir", "show", "navigation", "open"], + "description" => "This parameter may be vulnerable to Server Side Request Forgery (SSRF) attacks.", + }, + "sqli" => { + "words" => ["id", "select", "report", "role", "update", "query", "user", "name", "sort", "where", "search", "params", "process", "row", "view", "table", "from", "sel", "results", "sleep", "fetch", "order", "keyword", "column", "field", "delete", "string", "number", "filter"], + "description" => "This parameter may be vulnerable to SQL Injection attacks.", + }, + "idor" => { + "words" => ["id", "user", "account", "number", "order", "no", "doc", "key", "email", "group", "profile", "edit", "report"], + "description" => "This parameter may be vulnerable to Insecure Direct Object Reference (IDOR) attacks.", + }, + "file-inclusion" => { + "words" => ["file", "document", "folder", "root", "path", "pg", "style", "pdf", "template", "php_path", "doc"], + "description" => "This parameter may be vulnerable to File Inclusion attacks.", + }, + "debug" => { + "words" => ["access", "admin", "dbg", "debug", "edit", "grant", "test", "alter", "clone", "create", "delete", "disable", "enable", "exec", "execute", "load", "make", "modify", "rename", "reset", "shell", "toggle", "adm", "root", "cfg", "config"], + "description" => "This parameter may be vulnerable to Debug method exploits.", + }, + "command-injection" => { + "words" => ["daemon", "host", "upload", "dir", "execute", "download", "log", "ip", "cli", "cmd"], + "description" => "This parameter may be vulnerable to Command Injection attacks.", + }, + } + + def initialize(options : Hash(Symbol, String)) + super + @name = "hunt" + end + + def perform(endpoints : Array(Endpoint)) + tagger = {} of String => Hash(String, Array(String) | String) + TAG_DEFINITIONS.each do |key, value| + tagger[key] = {"words" => value["words"], "description" => value["description"]} + end + + endpoints.each do |endpoint| + endpoint.params.each do |param| + TAG_DEFINITIONS.each do |k, v| + if v["words"].includes? param.name + tag = Tag.new(k, v["description"].to_s, "Hunt") + param.add_tag(tag) + end + end + end + end + end +end diff --git a/src/tagger/taggers/oauth.cr b/src/tagger/taggers/oauth.cr new file mode 100644 index 00000000..7697b7fd --- /dev/null +++ b/src/tagger/taggers/oauth.cr @@ -0,0 +1,33 @@ +require "../../models/tagger" +require "../../models/endpoint" + +class OAuthTagger < Tagger + WORDS = ["grant_type", "code", "redirect_uri", "redirect_url", "client_id", "client_secret"] + + def initialize(options : Hash(Symbol, String)) + super + @name = "oauth" + end + + def perform(endpoints : Array(Endpoint)) + endpoints.each do |endpoint| + tmp_params = [] of String + + endpoint.params.each do |param| + tmp_params.push param.name.to_s + end + + words_set = Set.new(WORDS) + tmp_params_set = Set.new(tmp_params) + intersection = words_set & tmp_params_set + + # Check that at least three parameters match. + check = intersection.size.to_i >= 3 + + if check + tag = Tag.new("oauth", "Suspected OAuth endpoint for granting 3rd party access.", "Oauth") + endpoint.add_tag(tag) + end + end + end +end diff --git a/src/techs/techs.cr b/src/techs/techs.cr index ed1bdf5b..ea543b85 100644 --- a/src/techs/techs.cr +++ b/src/techs/techs.cr @@ -5,6 +5,11 @@ module NoirTechs :language => "Crystal", :similar => ["kemal", "crystal-kemal", "crystal_kemal"], }, + :crystal_lucky => { + :framework => "Lucky", + :language => "Crystal", + :similar => ["lucky", "crystal-lucky", "crystal_lucky"], + }, :cs_aspnet_mvc => { :framework => "ASP.NET MVC", :language => "C#", @@ -30,6 +35,10 @@ module NoirTechs :language => "Go", :similar => ["gin", "go-gin", "go_gin"], }, + :har => { + :format => ["JSON"], + :similar => ["har"], + }, :java_armeria => { :framework => "Armeria", :language => "Java",