metasploit-credential 6.0.23 → 6.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70002be99f916bdcdb0fe202a8e2684de0cb336ed301cf849724e59955ab88f9
4
- data.tar.gz: 7d9a5389806443237dd2889caf1acd92a836eab7331eb9ebe97cd591f975c70d
3
+ metadata.gz: 4b9e7598b0e2092c89cf53906e4f4a1095d8ab1a0db16909c7b6dfdb34f36126
4
+ data.tar.gz: 05d85b467d16961cd74ddca8d91b9a556917796d063e9220790ebbab5a8fdebf
5
5
  SHA512:
6
- metadata.gz: 772f4d254bbb3faa24f218705f605c0273fe450769315da209567518f52d3f318ea55927a21be482df43584224152624934a4ef1174e8fdc29b35a0707067397
7
- data.tar.gz: ec6628b22248b6fa058ad75a1c1336e1d804596e0ecdaa95a53877ca0abebbe528fca9caf2b257367847d3ad6e5a1ddbd98d1e42fd736e743bb29448bc89ccc1
6
+ metadata.gz: 29e2ef10c016b40770b87a09b91102717c8e65e47592c3774953f804ed95558020aef18abc8556218f413802afbd56798d2f5253e109b5e618de5b35cdaf0f0f
7
+ data.tar.gz: 9b66948b98ed071ddfe1942e73f5454ae9c9f718b352028e344aec532fa9b27f730f62024dde594174f5176d8f6dd3bb54f10649b0f90da148d13f20c8a73804
@@ -39,20 +39,43 @@ module Metasploit::Credential::Creation
39
39
  old_realm_id = nil
40
40
 
41
41
  retry_transaction do
42
- private = Metasploit::Credential::Password.where(data: password).first_or_create!
43
- public = Metasploit::Credential::Public.where(username: username).first_or_create!
44
42
  old_core = Metasploit::Credential::Core.find(core_id)
45
43
  old_realm_id = old_core.realm.id if old_core.realm
44
+ if username.blank? && old_core.public
45
+ username = old_core.public.username
46
+ end
47
+ private = Metasploit::Credential::Password.where(data: password).first_or_create!
48
+ public = Metasploit::Credential::Public.where(username: username).first_or_create!
46
49
  end
47
50
 
48
51
  core = nil
49
52
 
50
53
  retry_transaction do
51
- core = Metasploit::Credential::Core.where(public_id: public.id, private_id: private.id, realm_id: old_realm_id, workspace_id: old_core.workspace_id).first_or_initialize
52
- if core.origin_id.nil?
53
- origin = Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: core_id).first_or_create!
54
- core.origin = origin
54
+ # Create the CrackedPassword origin for this specific originating
55
+ # core first, then look up a Core that is already tied to it.
56
+ # This prevents two different hashes that crack to the same
57
+ # password from collapsing into a single Core where only the
58
+ # first origin link is recorded.
59
+ origin = Metasploit::Credential::Origin::CrackedPassword.where(metasploit_credential_core_id: core_id).first_or_create!
60
+
61
+ core = Metasploit::Credential::Core.find_by(
62
+ origin_type: 'Metasploit::Credential::Origin::CrackedPassword',
63
+ origin_id: origin.id
64
+ )
65
+
66
+ unless core
67
+ core = Metasploit::Credential::Core.where(
68
+ public_id: public.id,
69
+ private_id: private.id,
70
+ realm_id: old_realm_id,
71
+ workspace_id: old_core.workspace_id
72
+ ).first_or_initialize
73
+
74
+ if core.origin_id.nil?
75
+ core.origin = origin
76
+ end
55
77
  end
78
+
56
79
  if opts[:task_id]
57
80
  core.tasks << Mdm::Task.find(opts[:task_id])
58
81
  end
@@ -136,6 +159,17 @@ module Metasploit::Credential::Creation
136
159
  end
137
160
 
138
161
  if opts.has_key?(:username)
162
+ # When cracking a hash, the caller may not know the username
163
+ # (e.g. krb5tgs / krb5asrep). Fall back to the originating
164
+ # core's public so each cracked hash gets a distinct Core
165
+ # instead of all of them collapsing into a single BlankUsername
166
+ # Core where only the first origin link is recorded.
167
+ if opts[:username].blank? && opts[:origin_type] == :cracked_password && opts[:originating_core_id]
168
+ originating_core = Metasploit::Credential::Core.find_by(id: opts[:originating_core_id])
169
+ if originating_core&.public
170
+ opts = opts.merge(username: originating_core.public.username)
171
+ end
172
+ end
139
173
  core_opts[:public] = create_credential_public(opts)
140
174
  end
141
175
 
@@ -256,7 +290,20 @@ module Metasploit::Credential::Creation
256
290
 
257
291
  core = nil
258
292
  retry_transaction do
259
- core = Metasploit::Credential::Core.where(private_id: private_id, public_id: public_id, realm_id: realm_id, workspace_id: workspace_id).first_or_initialize
293
+ # When the origin is a CrackedPassword, look up by origin first
294
+ # so that each originating hash gets its own cracked Core.
295
+ # Without this, two different hashes that crack to the same
296
+ # password (and share public/realm/workspace) resolve to a
297
+ # single Core and only the first origin link is recorded.
298
+ if origin.is_a?(Metasploit::Credential::Origin::CrackedPassword)
299
+ core = Metasploit::Credential::Core.find_by(
300
+ origin_type: origin.class.to_s,
301
+ origin_id: origin.id
302
+ )
303
+ end
304
+
305
+ core ||= Metasploit::Credential::Core.where(private_id: private_id, public_id: public_id, realm_id: realm_id, workspace_id: workspace_id).first_or_initialize
306
+
260
307
  if core.origin_id.nil?
261
308
  core.origin = origin
262
309
  end
@@ -3,7 +3,7 @@
3
3
  module Metasploit
4
4
  module Credential
5
5
  # VERSION is managed by GemRelease
6
- VERSION = '6.0.23'
6
+ VERSION = '6.0.24'
7
7
 
8
8
  # @return [String]
9
9
  #
@@ -1 +1 @@
1
- 56490bc7875b183426ab7522d696039f2b53b6a80d12149270b35d9f4566a35fdb2f3877591cdf0397c3278c5a27dc96f45a74737c441d6d4c966d32b93a201a
1
+ a40adeaa65852477620cc75a2031e3330751540828be89231cc907bc82edda8d4d9f438dcf88844adc06d119dd172b75ceca305d75b355c4e8cb7532a6e7114a
@@ -209,6 +209,48 @@ RSpec.describe Metasploit::Credential::Creation do
209
209
  expect( Metasploit::Credential::PostgresMD5.count ).to eq(1)
210
210
  end
211
211
  end
212
+
213
+ context 'when origin is cracked_password and username is blank' do
214
+ let(:named_public) { FactoryBot.create(:metasploit_credential_username) }
215
+ let(:hash_private) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
216
+ let(:manual_origin) { FactoryBot.create(:metasploit_credential_origin_manual) }
217
+
218
+ let!(:originating_core) do
219
+ FactoryBot.create(:metasploit_credential_core,
220
+ public: named_public, private: hash_private,
221
+ workspace: workspace, origin: manual_origin)
222
+ end
223
+
224
+ let(:credential_data) {{
225
+ workspace_id: workspace.id,
226
+ origin_type: :cracked_password,
227
+ originating_core_id: originating_core.id,
228
+ username: '',
229
+ private_data: 'cracked_pw',
230
+ private_type: :password
231
+ }}
232
+
233
+ it 'falls back to the originating core public username instead of creating a BlankUsername' do
234
+ core = test_object.create_credential(credential_data)
235
+ expect(core.public.username).to eq(named_public.username)
236
+ expect(core.public).not_to be_a(Metasploit::Credential::BlankUsername)
237
+ end
238
+
239
+ it 'creates distinct Cores when two different hashes crack to the same password with blank usernames' do
240
+ hash_private2 = FactoryBot.create(:metasploit_credential_nonreplayable_hash)
241
+ named_public2 = FactoryBot.create(:metasploit_credential_username)
242
+ originating_core2 = FactoryBot.create(:metasploit_credential_core,
243
+ public: named_public2, private: hash_private2,
244
+ workspace: workspace, origin: manual_origin)
245
+
246
+ core1 = test_object.create_credential(credential_data)
247
+ core2 = test_object.create_credential(credential_data.merge(originating_core_id: originating_core2.id))
248
+
249
+ expect(core1.id).not_to eq(core2.id)
250
+ expect(core1.public.username).to eq(named_public.username)
251
+ expect(core2.public.username).to eq(named_public2.username)
252
+ end
253
+ end
212
254
  end
213
255
 
214
256
  context '#create_credential_and_login' do
@@ -463,6 +505,223 @@ RSpec.describe Metasploit::Credential::Creation do
463
505
 
464
506
  end
465
507
 
508
+ context 'when two different hashes crack to the same password' do
509
+ let(:public_user1) { FactoryBot.create(:metasploit_credential_username) }
510
+ let(:public_user2) { FactoryBot.create(:metasploit_credential_username) }
511
+ let(:hash1) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
512
+ let(:hash2) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
513
+ let(:manual_origin) { FactoryBot.create(:metasploit_credential_origin_manual) }
514
+ let(:cracked_password) { 'cracked_same_password' }
515
+
516
+ let!(:hash_core1) do
517
+ FactoryBot.create(:metasploit_credential_core,
518
+ public: public_user1, private: hash1,
519
+ realm: realm, workspace: workspace, origin: manual_origin)
520
+ end
521
+
522
+ let!(:hash_core2) do
523
+ FactoryBot.create(:metasploit_credential_core,
524
+ public: public_user2, private: hash2,
525
+ realm: realm, workspace: workspace, origin: manual_origin)
526
+ end
527
+
528
+ it 'creates separate Cores for each originating hash' do
529
+ core1 = test_object.create_cracked_credential(
530
+ core_id: hash_core1.id,
531
+ username: public_user1.username,
532
+ password: cracked_password
533
+ )
534
+ core2 = test_object.create_cracked_credential(
535
+ core_id: hash_core2.id,
536
+ username: public_user2.username,
537
+ password: cracked_password
538
+ )
539
+
540
+ expect(core1.id).not_to eq(core2.id)
541
+ end
542
+
543
+ it 'records distinct CrackedPassword origins pointing to each originating core' do
544
+ core1 = test_object.create_cracked_credential(
545
+ core_id: hash_core1.id,
546
+ username: public_user1.username,
547
+ password: cracked_password
548
+ )
549
+ core2 = test_object.create_cracked_credential(
550
+ core_id: hash_core2.id,
551
+ username: public_user2.username,
552
+ password: cracked_password
553
+ )
554
+
555
+ expect(core1.origin).to be_a(Metasploit::Credential::Origin::CrackedPassword)
556
+ expect(core2.origin).to be_a(Metasploit::Credential::Origin::CrackedPassword)
557
+ expect(core1.origin.metasploit_credential_core_id).to eq(hash_core1.id)
558
+ expect(core2.origin.metasploit_credential_core_id).to eq(hash_core2.id)
559
+ end
560
+
561
+ context 'when both hashes share the same username and realm' do
562
+ let(:shared_public) { FactoryBot.create(:metasploit_credential_username) }
563
+
564
+ let!(:hash_core_a) do
565
+ FactoryBot.create(:metasploit_credential_core,
566
+ public: shared_public, private: hash1,
567
+ realm: realm, workspace: workspace, origin: manual_origin)
568
+ end
569
+
570
+ let!(:hash_core_b) do
571
+ FactoryBot.create(:metasploit_credential_core,
572
+ public: shared_public, private: hash2,
573
+ realm: realm, workspace: workspace, origin: manual_origin)
574
+ end
575
+
576
+ it 'reuses the existing Core but creates distinct CrackedPassword origins for each hash' do
577
+ core_a = test_object.create_cracked_credential(
578
+ core_id: hash_core_a.id,
579
+ username: shared_public.username,
580
+ password: cracked_password
581
+ )
582
+ core_b = test_object.create_cracked_credential(
583
+ core_id: hash_core_b.id,
584
+ username: shared_public.username,
585
+ password: cracked_password
586
+ )
587
+
588
+ # Both resolve to the same Core because public/private/realm/workspace match
589
+ expect(core_a.id).to eq(core_b.id)
590
+
591
+ # But distinct CrackedPassword origins are still recorded for each originating hash
592
+ origins = Metasploit::Credential::Origin::CrackedPassword.where(
593
+ metasploit_credential_core_id: [hash_core_a.id, hash_core_b.id]
594
+ )
595
+ expect(origins.count).to eq(2)
596
+ end
597
+ end
598
+ end
599
+
600
+ context 'when username is blank' do
601
+ let(:named_public) { FactoryBot.create(:metasploit_credential_username) }
602
+ let(:hash_private) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
603
+ let(:manual_origin) { FactoryBot.create(:metasploit_credential_origin_manual) }
604
+
605
+ let!(:originating_core) do
606
+ FactoryBot.create(:metasploit_credential_core,
607
+ public: named_public, private: hash_private,
608
+ realm: realm, workspace: workspace, origin: manual_origin)
609
+ end
610
+
611
+ it 'falls back to the originating core public username' do
612
+ core = test_object.create_cracked_credential(
613
+ core_id: originating_core.id,
614
+ username: '',
615
+ password: password
616
+ )
617
+
618
+ expect(core.public.username).to eq(named_public.username)
619
+ end
620
+
621
+ it 'does not create a BlankUsername record for the cracked credential' do
622
+ blank_count_before = Metasploit::Credential::BlankUsername.count
623
+ test_object.create_cracked_credential(
624
+ core_id: originating_core.id,
625
+ username: '',
626
+ password: password
627
+ )
628
+
629
+ # The cracked Core should use the originating core's named username,
630
+ # not create a new BlankUsername
631
+ cracked_core = Metasploit::Credential::Core.last
632
+ expect(cracked_core.public).not_to be_a(Metasploit::Credential::BlankUsername)
633
+ expect(Metasploit::Credential::BlankUsername.count).to eq(blank_count_before)
634
+ end
635
+
636
+ context 'with two different hashes that both have named publics' do
637
+ let(:named_public2) { FactoryBot.create(:metasploit_credential_username) }
638
+ let(:hash_private2) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
639
+
640
+ let!(:originating_core2) do
641
+ FactoryBot.create(:metasploit_credential_core,
642
+ public: named_public2, private: hash_private2,
643
+ realm: realm, workspace: workspace, origin: manual_origin)
644
+ end
645
+
646
+ it 'creates separate Cores each with the correct username from their originating core' do
647
+ core1 = test_object.create_cracked_credential(
648
+ core_id: originating_core.id,
649
+ username: '',
650
+ password: password
651
+ )
652
+ core2 = test_object.create_cracked_credential(
653
+ core_id: originating_core2.id,
654
+ username: '',
655
+ password: password
656
+ )
657
+
658
+ expect(core1.id).not_to eq(core2.id)
659
+ expect(core1.public.username).to eq(named_public.username)
660
+ expect(core2.public.username).to eq(named_public2.username)
661
+ end
662
+ end
663
+ end
664
+
665
+ context 'end-to-end: two different hash types (e.g. krb5tgs and krb5asrep) cracked to the same password' do
666
+ let(:krb5tgs_public) { Metasploit::Credential::Username.create!(username: 'krb5tgs') }
667
+ let(:krb5asrep_public) { Metasploit::Credential::Username.create!(username: 'krb5asrep') }
668
+ let(:krb5tgs_hash) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
669
+ let(:krb5asrep_hash) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
670
+ let(:manual_origin) { FactoryBot.create(:metasploit_credential_origin_manual) }
671
+ let(:cracked_pw) { 'hashcat' }
672
+
673
+ let!(:krb5tgs_core) do
674
+ FactoryBot.create(:metasploit_credential_core,
675
+ public: krb5tgs_public, private: krb5tgs_hash,
676
+ workspace: workspace, origin: manual_origin)
677
+ end
678
+
679
+ let!(:krb5asrep_core) do
680
+ FactoryBot.create(:metasploit_credential_core,
681
+ public: krb5asrep_public, private: krb5asrep_hash,
682
+ workspace: workspace, origin: manual_origin)
683
+ end
684
+
685
+ it 'creates 2 new cracked Cores in addition to the 2 originals' do
686
+ expect {
687
+ test_object.create_cracked_credential(core_id: krb5tgs_core.id, username: '', password: cracked_pw)
688
+ test_object.create_cracked_credential(core_id: krb5asrep_core.id, username: '', password: cracked_pw)
689
+ }.to change { Metasploit::Credential::Core.count }.by(2)
690
+ end
691
+
692
+ it 'links each cracked Core back to its originating hash via CrackedPassword origin' do
693
+ cracked1 = test_object.create_cracked_credential(core_id: krb5tgs_core.id, username: '', password: cracked_pw)
694
+ cracked2 = test_object.create_cracked_credential(core_id: krb5asrep_core.id, username: '', password: cracked_pw)
695
+
696
+ expect(cracked1.origin).to be_a(Metasploit::Credential::Origin::CrackedPassword)
697
+ expect(cracked2.origin).to be_a(Metasploit::Credential::Origin::CrackedPassword)
698
+ expect(cracked1.origin.metasploit_credential_core_id).to eq(krb5tgs_core.id)
699
+ expect(cracked2.origin.metasploit_credential_core_id).to eq(krb5asrep_core.id)
700
+ end
701
+
702
+ it 'uses the originating core username for each cracked Core instead of BlankUsername' do
703
+ cracked1 = test_object.create_cracked_credential(core_id: krb5tgs_core.id, username: '', password: cracked_pw)
704
+ cracked2 = test_object.create_cracked_credential(core_id: krb5asrep_core.id, username: '', password: cracked_pw)
705
+
706
+ expect(cracked1.public.username).to eq('krb5tgs')
707
+ expect(cracked2.public.username).to eq('krb5asrep')
708
+ end
709
+
710
+ it 'allows looking up the cracked password from each originating hash core' do
711
+ test_object.create_cracked_credential(core_id: krb5tgs_core.id, username: '', password: cracked_pw)
712
+ test_object.create_cracked_credential(core_id: krb5asrep_core.id, username: '', password: cracked_pw)
713
+
714
+ # Simulate the lookup that creds display does: find cracked Cores by their CrackedPassword origin
715
+ cracked_cores = Metasploit::Credential::Core.where(origin_type: 'Metasploit::Credential::Origin::CrackedPassword')
716
+ cracked_by_originating_id = cracked_cores.each_with_object({}) do |core, map|
717
+ map[core.origin.metasploit_credential_core_id] = core
718
+ end
719
+
720
+ expect(cracked_by_originating_id[krb5tgs_core.id].private.data).to eq(cracked_pw)
721
+ expect(cracked_by_originating_id[krb5asrep_core.id].private.data).to eq(cracked_pw)
722
+ end
723
+ end
724
+
466
725
  end
467
726
 
468
727
  context '#create_credential_origin_import' do
@@ -981,6 +1240,100 @@ RSpec.describe Metasploit::Credential::Creation do
981
1240
  expect(core.tasks).to include(task)
982
1241
  end
983
1242
 
1243
+ context 'when origin is a CrackedPassword' do
1244
+ let(:manual_origin) { FactoryBot.create(:metasploit_credential_origin_manual) }
1245
+ let(:hash1) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
1246
+ let(:hash2) { FactoryBot.create(:metasploit_credential_nonreplayable_hash) }
1247
+ let(:cracked_pw) { FactoryBot.create(:metasploit_credential_password) }
1248
+ let(:pub1) { FactoryBot.create(:metasploit_credential_username) }
1249
+ let(:pub2) { FactoryBot.create(:metasploit_credential_username) }
1250
+ let(:rlm) { FactoryBot.create(:metasploit_credential_realm) }
1251
+ let(:ws) { FactoryBot.create(:mdm_workspace) }
1252
+
1253
+ let!(:hash_core1) do
1254
+ FactoryBot.create(:metasploit_credential_core,
1255
+ public: pub1, private: hash1,
1256
+ realm: rlm, workspace: ws, origin: manual_origin)
1257
+ end
1258
+
1259
+ let!(:hash_core2) do
1260
+ FactoryBot.create(:metasploit_credential_core,
1261
+ public: pub2, private: hash2,
1262
+ realm: rlm, workspace: ws, origin: manual_origin)
1263
+ end
1264
+
1265
+ it 'creates separate Cores when CrackedPassword origins have different publics' do
1266
+ origin1 = Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: hash_core1.id)
1267
+ origin2 = Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: hash_core2.id)
1268
+
1269
+ core1 = test_object.create_credential_core(
1270
+ origin: origin1,
1271
+ public: pub1,
1272
+ private: cracked_pw,
1273
+ realm: rlm,
1274
+ workspace_id: ws.id
1275
+ )
1276
+ core2 = test_object.create_credential_core(
1277
+ origin: origin2,
1278
+ public: pub2,
1279
+ private: cracked_pw,
1280
+ realm: rlm,
1281
+ workspace_id: ws.id
1282
+ )
1283
+
1284
+ expect(core1.id).not_to eq(core2.id)
1285
+ expect(core1.origin_id).to eq(origin1.id)
1286
+ expect(core2.origin_id).to eq(origin2.id)
1287
+ end
1288
+
1289
+ it 'returns the existing Core when called again with the same CrackedPassword origin' do
1290
+ origin1 = Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: hash_core1.id)
1291
+
1292
+ core_first = test_object.create_credential_core(
1293
+ origin: origin1,
1294
+ public: pub1,
1295
+ private: cracked_pw,
1296
+ realm: rlm,
1297
+ workspace_id: ws.id
1298
+ )
1299
+ core_second = test_object.create_credential_core(
1300
+ origin: origin1,
1301
+ public: pub1,
1302
+ private: cracked_pw,
1303
+ realm: rlm,
1304
+ workspace_id: ws.id
1305
+ )
1306
+
1307
+ expect(core_first.id).to eq(core_second.id)
1308
+ end
1309
+
1310
+ it 'looks up by origin before falling back to public/private/realm/workspace' do
1311
+ origin1 = Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: hash_core1.id)
1312
+
1313
+ core = test_object.create_credential_core(
1314
+ origin: origin1,
1315
+ public: pub1,
1316
+ private: cracked_pw,
1317
+ realm: rlm,
1318
+ workspace_id: ws.id
1319
+ )
1320
+
1321
+ # Calling again with the same origin returns the same core
1322
+ # even though the lookup by origin happens first
1323
+ core_again = test_object.create_credential_core(
1324
+ origin: origin1,
1325
+ public: pub1,
1326
+ private: cracked_pw,
1327
+ realm: rlm,
1328
+ workspace_id: ws.id
1329
+ )
1330
+
1331
+ expect(core.id).to eq(core_again.id)
1332
+ expect(core.origin).to be_a(Metasploit::Credential::Origin::CrackedPassword)
1333
+ expect(core.origin.metasploit_credential_core_id).to eq(hash_core1.id)
1334
+ end
1335
+ end
1336
+
984
1337
  end
985
1338
 
986
1339
  context '#create_credential_login' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metasploit-credential
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.23
4
+ version: 6.0.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Metasploit Hackers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-21 00:00:00.000000000 Z
11
+ date: 2026-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: metasploit-concern