codesake 0.0.1 → 0.15.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/History.md +40 -0
- data/README.md +25 -3
- data/Rakefile +22 -2
- data/bin/codesake +36 -0
- data/codesake.gemspec +3 -0
- data/features/codesake_complains_if_missing_target.feature +8 -0
- data/features/codesake_process_jsp_file.feature +88 -0
- data/features/codesake_process_text_file.feature +23 -0
- data/features/step_definition/codesake_steps.rb +164 -0
- data/features/support/env.rb +1 -0
- data/lib/codesake.rb +8 -3
- data/lib/codesake/cli.rb +90 -0
- data/lib/codesake/engine/core.rb +10 -0
- data/lib/codesake/engine/generic.rb +12 -0
- data/lib/codesake/engine/jsp.rb +165 -0
- data/lib/codesake/engine/text.rb +36 -0
- data/lib/codesake/kernel.rb +39 -0
- data/lib/codesake/utils/files.rb +25 -0
- data/lib/codesake/utils/secrets.rb +45 -0
- data/lib/codesake/version.rb +2 -1
- data/spec/cli_spec.rb +65 -0
- data/spec/engine_core_spec.rb +45 -0
- data/spec/file_utils_spec.rb +59 -0
- data/spec/jsp_engine_spec.rb +114 -0
- data/spec/kernel_spec.rb +63 -0
- data/spec/secrets_utils_spec.rb +79 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/text_engine_spec.rb +72 -0
- metadata +92 -3
@@ -0,0 +1 @@
|
|
1
|
+
require 'aruba/cucumber'
|
data/lib/codesake.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
require "codesake/version"
|
2
|
+
require "codesake/cli"
|
3
|
+
require "codesake/kernel"
|
4
|
+
require "codesake/utils/files"
|
5
|
+
require "codesake/utils/secrets"
|
6
|
+
require "codesake/engine/text"
|
7
|
+
require "codesake/engine/generic"
|
8
|
+
require "codesake/engine/core"
|
9
|
+
require "codesake/engine/jsp"
|
2
10
|
|
3
|
-
module Codesake
|
4
|
-
# Your code goes here...
|
5
|
-
end
|
data/lib/codesake/cli.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'trimmy'
|
3
|
+
|
4
|
+
module Codesake
|
5
|
+
class Cli
|
6
|
+
attr_reader :options
|
7
|
+
attr_reader :targets
|
8
|
+
|
9
|
+
def parse(command_line)
|
10
|
+
@options = {}
|
11
|
+
|
12
|
+
return {:vulnerabilities=>:all} if (command_line.nil?) or (command_line.send(:empty?))
|
13
|
+
|
14
|
+
begin
|
15
|
+
option_parser =OptionParser.new do |opts|
|
16
|
+
executable_name = File.basename($PROGRAM_NAME)
|
17
|
+
opts.banner =
|
18
|
+
"codesake v#{Codesake::VERSION} - (C) 2012 - paolo@armoredcode.com\nReviews one or more source file for security issues.\n\nUsage #{executable_name} [options] sources\n"
|
19
|
+
# opts.on("-h", "--help") do
|
20
|
+
# @options[:help] = true
|
21
|
+
# end
|
22
|
+
|
23
|
+
opts.on("-v", "--version", "Show codesake version") do
|
24
|
+
@options[:version] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-V", "--verbose", "Be verbose") do
|
28
|
+
@options[:verbose] = true
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-k KEYWORDS", "--add-keys", "Add the command separated list of strings as reserved keywords") do |val|
|
32
|
+
@options[:keywords] = val.trim.split(",")
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-o TARGET", "--output", "Write output to file, to json string or to db usin SQLite3") do |val|
|
36
|
+
@options[:output]=:screen
|
37
|
+
val=val.trim
|
38
|
+
@options[:output]=val.to_sym if (val.to_sym == :file) or (val.to_sym == :json) or (val.to_sym == :db)
|
39
|
+
end
|
40
|
+
opts.on("-C", "--confirmed-vulnerabilities", "Show only confirmed vulnerabilities") do
|
41
|
+
@options[:vulnerabilities] = :confirmed
|
42
|
+
end
|
43
|
+
opts.on("-A", "--all-vulnerabilities", "Show all vulnerabilities found [default]") do
|
44
|
+
@options[:vulnerabilities] = :all
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
rest = option_parser.parse(command_line)
|
50
|
+
|
51
|
+
@targets = []
|
52
|
+
@targets = build_target_list(rest[0].split(" ")) if expect_targets? and (! rest.empty?) and (! rest[0].nil?)
|
53
|
+
@options[:vulnerabilities] = :all if @options[:vulnerabilities].nil?
|
54
|
+
rescue OptionParser::InvalidOption => e
|
55
|
+
@options={:error=>true, :message=>e.message}
|
56
|
+
end
|
57
|
+
@options
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
def is_good_target?(target)
|
62
|
+
(!Dir.glob(target).empty?) or File.exists?(target) or File.directory?(target)
|
63
|
+
end
|
64
|
+
|
65
|
+
def has_errors?
|
66
|
+
(@options[:error])
|
67
|
+
end
|
68
|
+
|
69
|
+
def error_message
|
70
|
+
@options[:message] if has_errors?
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
private
|
75
|
+
def expect_targets?
|
76
|
+
(! @options[:help] ) and ( ! @options[:version])
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def build_target_list(target_list)
|
81
|
+
ret = []
|
82
|
+
target_list.each do |target|
|
83
|
+
ret << {:target=>target, :valid=>is_good_target?(target)}
|
84
|
+
end
|
85
|
+
|
86
|
+
ret
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'codesake/engine/core'
|
2
|
+
|
3
|
+
module Codesake
|
4
|
+
module Engine
|
5
|
+
class Jsp
|
6
|
+
include Codesake::Utils::Files
|
7
|
+
include Codesake::Utils::Secrets
|
8
|
+
include Codesake::Engine::Core
|
9
|
+
|
10
|
+
FALSE_POSITIVES = ["request.getContextPath()", "request.getLocalName()", "request.getLocalPort()"]
|
11
|
+
|
12
|
+
|
13
|
+
attr_reader :imports
|
14
|
+
attr_reader :attack_entrypoints
|
15
|
+
attr_reader :reflected_xss
|
16
|
+
attr_reader :cookies
|
17
|
+
|
18
|
+
def initialize(filename, options)
|
19
|
+
@filename = filename
|
20
|
+
@options = options
|
21
|
+
|
22
|
+
read_file
|
23
|
+
load_secrets
|
24
|
+
end
|
25
|
+
|
26
|
+
def analyse
|
27
|
+
ret = []
|
28
|
+
@reserved_keywords = find_reserved_keywords
|
29
|
+
@imports = find_imports
|
30
|
+
@attack_entrypoints = find_attack_entrypoints
|
31
|
+
@reflected_xss = find_reflected_xss
|
32
|
+
@cookies = find_cookies
|
33
|
+
|
34
|
+
@reserved_keywords.each do |secret|
|
35
|
+
ret << "reserved keyword found: \"#{secret[:matcher]}\" (#{@filename}@#{secret[:line]})"
|
36
|
+
end
|
37
|
+
@imports.each do |import|
|
38
|
+
ret << "imported package found: \"#{import[:package]}\""
|
39
|
+
end
|
40
|
+
|
41
|
+
@attack_entrypoints.each do |entry|
|
42
|
+
ret << "attack entrypoint found: parameter \"#{entry[:param]}\" stored in \"#{entry[:var]}\" (#{@filename}@#{entry[:line]})"
|
43
|
+
end
|
44
|
+
@reflected_xss.each do |entry|
|
45
|
+
ret << "suspicious reflected xss found: \"#{entry[:var]}\" (#{@filename}@#{entry[:line]})\"" if entry[:false_positive] and @options[:vulnerabilities] == :all
|
46
|
+
ret << "reflected xss found: \"#{entry[:var]}\" (#{@filename}@#{entry[:line]})\"" if ! entry[:false_positive]
|
47
|
+
end
|
48
|
+
|
49
|
+
@cookies.each do |c|
|
50
|
+
ret << "cookie \"#{c[:name]}\" found with value: \"#{c[:value]}\" (#{@filename}@#{c[:line]})"
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
ret
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
private
|
60
|
+
# redefined
|
61
|
+
def find_reserved_keywords
|
62
|
+
|
63
|
+
ret = []
|
64
|
+
|
65
|
+
@file_content.each_with_index do |l, i|
|
66
|
+
l = l.unpack("C*").pack("U*")
|
67
|
+
@secrets.each do |s|
|
68
|
+
ret << {:line=> i+1, :matcher=>s } if l.trim.include?(s)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ret
|
73
|
+
end
|
74
|
+
|
75
|
+
def find_cookies
|
76
|
+
ret = []
|
77
|
+
|
78
|
+
@file_content.each_with_index do |l, i|
|
79
|
+
l = l.unpack("C*").pack("U*")
|
80
|
+
m = /Cookie (.*?) = new Cookie \("(.*?)",(.*?)\)/.match(l);
|
81
|
+
ret << {:line => i+1, :var => m[1].trim, :name => m[2].trim.gsub("\"", ""), :value => m[3].trim.gsub("\"", "")} unless m.nil?
|
82
|
+
|
83
|
+
m = /Cookie (.*?) = new Cookie\("(.*?)",(.*?)\)/.match(l);
|
84
|
+
ret << {:line => i+1, :var => m[1].trim, :name => m[2].trim.gsub("\"", ""), :value => m[3].trim.gsub("\"", "")} unless m.nil?
|
85
|
+
|
86
|
+
|
87
|
+
m = /(.*?) = new Cookie \("(.*?)",(.*?)\)/.match(l);
|
88
|
+
ret << {:line => i+1, :var => m[1].trim, :name => m[2].trim.gsub("\"", ""), :value => m[3].trim.gsub("\"", "")} unless m.nil?
|
89
|
+
|
90
|
+
|
91
|
+
end
|
92
|
+
ret
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
def find_reflected_xss
|
98
|
+
ret = []
|
99
|
+
@file_content.each_with_index do |l, i|
|
100
|
+
# <%=avar%> #=> /<%=(\w+)%>/.match(a)[1] = avar
|
101
|
+
l = l.unpack("C*").pack("U*")
|
102
|
+
m = /<%=(.*?)%>/.match(l)
|
103
|
+
ret << {:line => i+1, :var=> m[1].trim, :false_positive=>Codesake::Engine::Jsp.is_false_positive?(m[1].trim)} unless m.nil?
|
104
|
+
|
105
|
+
m = /out\.println\((.*?)\)/.match(l)
|
106
|
+
ret << {:line => i+1, :var=> m[1].trim, :false_positive=>Codesake::Engine::Jsp.is_false_positive?(m[1].trim)} unless m.nil?
|
107
|
+
|
108
|
+
m = /out\.print\((.*?)\)/.match(l)
|
109
|
+
ret << {:line => i+1, :var=> m[1].trim, :false_positive=>Codesake::Engine::Jsp.is_false_positive?(m[1].trim)} unless m.nil?
|
110
|
+
|
111
|
+
m = /out\.write\((.*?)\)/.match(l)
|
112
|
+
ret << {:line => i+1, :var=> m[1].trim, :false_positive=>Codesake::Engine::Jsp.is_false_positive?(m[1].trim)} unless m.nil?
|
113
|
+
|
114
|
+
m = /out\.writeln\((.*?)\)/.match(l)
|
115
|
+
ret << {:line => i+1, :var=> m[1].trim, :false_positive=>Codesake::Engine::Jsp.is_false_positive?(m[1].trim)} unless m.nil?
|
116
|
+
end
|
117
|
+
|
118
|
+
ret
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.is_false_positive?(var)
|
122
|
+
FALSE_POSITIVES.include?(var)
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def find_attack_entrypoints
|
127
|
+
ret = []
|
128
|
+
@file_content.each_with_index do |l, i|
|
129
|
+
l = l.unpack("C*").pack("U*")
|
130
|
+
m = /request.getParameter\((.*?)\)/.match(l)
|
131
|
+
ret << {:line => i+1, :param => m[1].trim.gsub("\"", ""), :var => variable_from_line(l) } unless m.nil?
|
132
|
+
|
133
|
+
m = /request.getParameterValues\((.*?)\)/.match(l)
|
134
|
+
ret << {:line => i+1, :param => m[1].trim.gsub("\"", ""), :var => variable_from_line(l) } unless m.nil?
|
135
|
+
|
136
|
+
m = /request.getAttribute\((.*?)\)/.match(l)
|
137
|
+
ret << {:line => i+1, :param => m[1].trim.gsub("\"", ""), :var => variable_from_line(l) } unless m.nil?
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
ret
|
142
|
+
end
|
143
|
+
def variable_from_line(line)
|
144
|
+
ret = ""
|
145
|
+
left_operand = line.split('=')[0]
|
146
|
+
l = left_operand.split
|
147
|
+
l[l.size - 1]
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
def find_imports
|
153
|
+
ret = []
|
154
|
+
@file_content.each_with_index do |l, i|
|
155
|
+
l = l.unpack("C*").pack("U*")
|
156
|
+
m = /<%@page import="(.*?)"%>/.match(l)
|
157
|
+
ret << {:line => i+1, :package=>m[1].trim} unless m.nil?
|
158
|
+
end
|
159
|
+
|
160
|
+
ret
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'codesake/engine/core'
|
2
|
+
|
3
|
+
module Codesake
|
4
|
+
module Engine
|
5
|
+
class Text
|
6
|
+
include Codesake::Utils::Files
|
7
|
+
include Codesake::Utils::Secrets
|
8
|
+
include Codesake::Engine::Core
|
9
|
+
|
10
|
+
attr_reader :reserved_keywords
|
11
|
+
|
12
|
+
def initialize(filename)
|
13
|
+
@filename = filename
|
14
|
+
@raw_results = nil
|
15
|
+
|
16
|
+
read_file
|
17
|
+
load_secrets
|
18
|
+
end
|
19
|
+
|
20
|
+
def analyse
|
21
|
+
ret = []
|
22
|
+
@reserved_keywords = find_reserved_keywords
|
23
|
+
@reserved_keywords.each do |secret|
|
24
|
+
ret << "reserved keyword found: \"#{secret[:matcher]}\" (#{@filename}@#{secret[:line]})"
|
25
|
+
end
|
26
|
+
|
27
|
+
ret
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.is_txt?(filename)
|
31
|
+
(File.extname(filename).empty? or File.extname(filename) == ".txt" or File.extname(filename) == ".conf" or File.extname(filename) == ".rc" or File.extname(filename) == ".bak" or File.extname(filename) == ".old" )
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Codesake
|
4
|
+
class Kernel
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_reader :engine
|
8
|
+
|
9
|
+
NONE = 0
|
10
|
+
TEXT = 1
|
11
|
+
JSP = 2
|
12
|
+
UNKNOWN = -1
|
13
|
+
|
14
|
+
def choose_engine(filename, options)
|
15
|
+
|
16
|
+
|
17
|
+
engine = nil
|
18
|
+
|
19
|
+
case detect(filename)
|
20
|
+
when TEXT
|
21
|
+
engine = Codesake::Engine::Text.new(filename)
|
22
|
+
when NONE
|
23
|
+
engine = Codesake::Engine::Generic.new(filename)
|
24
|
+
when JSP
|
25
|
+
engine = Codesake::Engine::Jsp.new(filename, options)
|
26
|
+
end
|
27
|
+
engine
|
28
|
+
end
|
29
|
+
|
30
|
+
def detect(filename)
|
31
|
+
return NONE if filename.nil? or filename.empty?
|
32
|
+
return TEXT if Codesake::Engine::Text.is_txt?(filename)
|
33
|
+
return JSP if (File.extname(filename) == ".jsp")
|
34
|
+
|
35
|
+
|
36
|
+
return UNKNOWN
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Codesake
|
2
|
+
module Utils
|
3
|
+
module Files
|
4
|
+
|
5
|
+
attr_accessor :filename
|
6
|
+
attr_reader :file_content
|
7
|
+
|
8
|
+
def read_file
|
9
|
+
@file_content = []
|
10
|
+
@file_content = File.readlines(@filename) if File.exists?(@filename)
|
11
|
+
end
|
12
|
+
|
13
|
+
def lines
|
14
|
+
@file_content.count
|
15
|
+
end
|
16
|
+
|
17
|
+
def lines_of_comment
|
18
|
+
0
|
19
|
+
end
|
20
|
+
def loc
|
21
|
+
0
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Codesake
|
2
|
+
module Utils
|
3
|
+
module Secrets
|
4
|
+
DEFAULT_SECRETS = ["secret", "password", "username", "login", "xxx", "fixme", "fix", "todo", "passwd"]
|
5
|
+
attr_accessor :secrets
|
6
|
+
attr_accessor :reserved_keywords
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
load_secrets
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_secrets
|
13
|
+
@secrets = DEFAULT_SECRETS
|
14
|
+
end
|
15
|
+
|
16
|
+
def add(word)
|
17
|
+
@secrets << word
|
18
|
+
@secrets
|
19
|
+
end
|
20
|
+
|
21
|
+
def reserved?(word)
|
22
|
+
@secrets.include?(word)
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_reserved_keywords
|
26
|
+
|
27
|
+
ret = []
|
28
|
+
|
29
|
+
@file_content.each_with_index do |l, i|
|
30
|
+
l = l.unpack("C*").pack("U*")
|
31
|
+
l.split.each do |tok|
|
32
|
+
# ret << {:line=> i+1, :matcher=>tok, :source_line=>l} if @secrets.include?(tok.downcase)
|
33
|
+
ret << {:line=> i+1, :matcher=>tok } if @secrets.include?(tok.downcase)
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
ret
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|