interactsh 0.9.7 → 1.0.1
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 +4 -4
- data/lib/interactsh/client.rb +86 -0
- data/lib/interactsh/crypto.rb +54 -0
- data/lib/interactsh/http_client.rb +83 -0
- data/lib/interactsh/utils.rb +45 -0
- data/lib/interactsh.rb +14 -89
- metadata +11 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa30bc8120b5dca23aca543290f232fc2e0d0f89205884832e4a92474b7abc0c
|
4
|
+
data.tar.gz: b17b95e91e0fd89616c9d8a904169253bb1889591f974e02bece19f4a21221c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd77bd629127c5321154a37865697d0f8a0fb96da96f7424a74ccfee06a7cb5c7f42852cc71487794fb457ace76fbc0119e420a8b45dcc35a8ab35ae216a2b1c
|
7
|
+
data.tar.gz: ee792e8d85619409a261042f8d876849a1696f981a0cb6c8706288c059ad8ce6e2c4b4c7942033a5811d7af0d5939aee42ba9800932c262d43aa0c026fb81792
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactsh
|
4
|
+
# Main client class for Interactsh interaction
|
5
|
+
class Client
|
6
|
+
attr_reader :public_key_encoded, :secret, :server, :random_data, :rsa, :token
|
7
|
+
|
8
|
+
# Creates a new Interactsh client
|
9
|
+
#
|
10
|
+
# @param server [String] The Interactsh server to use (default: 'oast.me')
|
11
|
+
# @param token [String, nil] Optional authentication token
|
12
|
+
def initialize(server = 'oast.me', token = nil)
|
13
|
+
@rsa = OpenSSL::PKey::RSA.new(2048)
|
14
|
+
@public_key = @rsa.public_key.to_pem
|
15
|
+
@public_key_encoded = Base64.strict_encode64(@public_key)
|
16
|
+
|
17
|
+
@secret = Utils.generate_uuid
|
18
|
+
@random_data = Utils.generate_random_string(13)
|
19
|
+
|
20
|
+
@server = server
|
21
|
+
@token = token
|
22
|
+
@http_client = HttpClient.new(server, token)
|
23
|
+
@crypto = Crypto.new(@rsa)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Generates a new domain for interaction testing
|
27
|
+
#
|
28
|
+
# @return [String] The generated domain name for interaction testing
|
29
|
+
def new_domain
|
30
|
+
correlation_id = Xid.new.to_s
|
31
|
+
register(correlation_id)
|
32
|
+
|
33
|
+
"#{correlation_id}#{random_data}.#{server}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Polls the server for interaction data for a given host
|
37
|
+
#
|
38
|
+
# @param host [String] The host to poll data for
|
39
|
+
# @return [Array] Array of interaction data or empty array if polling failed
|
40
|
+
def poll(host)
|
41
|
+
correlation_id = host[0..19]
|
42
|
+
response = @http_client.make_poll_request(correlation_id, secret)
|
43
|
+
|
44
|
+
return [] unless @http_client.response_successful?(response)
|
45
|
+
|
46
|
+
data = JSON.parse(response.body)
|
47
|
+
parse_poll_data(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Parses and decrypts poll data
|
53
|
+
#
|
54
|
+
# @param data [Hash] The poll data to parse
|
55
|
+
# @return [Array] The decoded interaction data
|
56
|
+
def parse_poll_data(data)
|
57
|
+
decoded_data = []
|
58
|
+
|
59
|
+
return decoded_data if data.empty? || !data['data'] || data['data'].empty?
|
60
|
+
|
61
|
+
data['data'].each do |enc_data|
|
62
|
+
decoded_data << @crypto.decrypt_data(data['aes_key'], enc_data)
|
63
|
+
end
|
64
|
+
|
65
|
+
decoded_data
|
66
|
+
end
|
67
|
+
|
68
|
+
# Registers a correlation ID with the Interactsh server
|
69
|
+
#
|
70
|
+
# @param correlation_id [String] The correlation ID to register
|
71
|
+
# @return [void]
|
72
|
+
def register(correlation_id)
|
73
|
+
data = {
|
74
|
+
'public-key': public_key_encoded,
|
75
|
+
'secret-key': secret,
|
76
|
+
'correlation-id': correlation_id
|
77
|
+
}
|
78
|
+
|
79
|
+
response = @http_client.make_register_request(data)
|
80
|
+
|
81
|
+
return if response && response.code.to_i == 200
|
82
|
+
|
83
|
+
puts '[!] Interactsh - Problem with domain registration'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactsh
|
4
|
+
# Cryptographic operations for Interactsh
|
5
|
+
class Crypto
|
6
|
+
def initialize(rsa)
|
7
|
+
@rsa = rsa
|
8
|
+
end
|
9
|
+
|
10
|
+
# Decrypts interaction data using the provided AES key
|
11
|
+
#
|
12
|
+
# @param aes_key [String] The encrypted AES key
|
13
|
+
# @param enc_data [String] The encrypted data
|
14
|
+
# @return [Hash] The decrypted interaction data
|
15
|
+
def decrypt_data(aes_key, enc_data)
|
16
|
+
decrypted_aes_key = decrypt_aes_key(aes_key)
|
17
|
+
decrypt_payload(decrypted_aes_key, enc_data)
|
18
|
+
rescue StandardError => e
|
19
|
+
puts "[!] Interactsh - Error decrypting data: #{e.message}"
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Decrypts the AES key using RSA
|
26
|
+
#
|
27
|
+
# @param aes_key [String] The encrypted AES key
|
28
|
+
# @return [String] The decrypted AES key
|
29
|
+
def decrypt_aes_key(aes_key)
|
30
|
+
pkey = OpenSSL::PKey::RSA.new(@rsa)
|
31
|
+
encrypted_aes_key = Base64.urlsafe_decode64(aes_key)
|
32
|
+
JOSE::JWA::PKCS1.rsaes_oaep_decrypt(
|
33
|
+
OpenSSL::Digest::SHA256,
|
34
|
+
encrypted_aes_key,
|
35
|
+
pkey
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Decrypts the payload using the decrypted AES key
|
40
|
+
#
|
41
|
+
# @param decrypted_aes_key [String] The decrypted AES key
|
42
|
+
# @param enc_data [String] The encrypted data
|
43
|
+
# @return [Hash] The decrypted payload as a hash
|
44
|
+
def decrypt_payload(decrypted_aes_key, enc_data)
|
45
|
+
secretdata = Base64.decode64(enc_data)
|
46
|
+
decipher = OpenSSL::Cipher.new('aes-256-cfb')
|
47
|
+
decipher.decrypt
|
48
|
+
decipher.key = decrypted_aes_key
|
49
|
+
|
50
|
+
# The data minus the size of the IV (first 16 bytes)
|
51
|
+
JSON.parse((decipher.update(secretdata) + decipher.final)[16..])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactsh
|
4
|
+
# HTTP client for Interactsh API
|
5
|
+
class HttpClient
|
6
|
+
def initialize(server, token)
|
7
|
+
@server = server
|
8
|
+
@token = token
|
9
|
+
end
|
10
|
+
|
11
|
+
# Makes the HTTP request to poll for interaction data
|
12
|
+
#
|
13
|
+
# @param correlation_id [String] The correlation ID to poll for
|
14
|
+
# @param secret [String] The secret key
|
15
|
+
# @return [Net::HTTPResponse] The HTTP response
|
16
|
+
def make_poll_request(correlation_id, secret)
|
17
|
+
uri = URI.parse("https://#{@server}/poll?id=#{correlation_id}&secret=#{secret}")
|
18
|
+
http = setup_http_client(uri)
|
19
|
+
|
20
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
21
|
+
apply_headers(request)
|
22
|
+
|
23
|
+
http.request(request)
|
24
|
+
rescue StandardError => e
|
25
|
+
puts "[!] Interactsh - HTTP request error: #{e.message}"
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Makes the HTTP request to register a correlation ID
|
30
|
+
#
|
31
|
+
# @param data [Hash] The data to send
|
32
|
+
# @return [Net::HTTPResponse] The HTTP response
|
33
|
+
def make_register_request(data)
|
34
|
+
uri = URI.parse("https://#{@server}/register")
|
35
|
+
http = setup_http_client(uri)
|
36
|
+
|
37
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
38
|
+
apply_headers(request, 'Content-Type' => 'application/json')
|
39
|
+
request.body = data.to_json
|
40
|
+
|
41
|
+
http.request(request)
|
42
|
+
rescue StandardError => e
|
43
|
+
puts "[!] Interactsh - HTTP request error: #{e.message}"
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks if the HTTP response was successful
|
48
|
+
#
|
49
|
+
# @param response [Net::HTTPResponse, nil] The HTTP response
|
50
|
+
# @return [Boolean] True if response is successful, false otherwise
|
51
|
+
def response_successful?(response)
|
52
|
+
if !response || response.code.to_i != 200
|
53
|
+
puts '[!] Interactsh - Problem with data recovery'
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Sets up an HTTP client
|
62
|
+
#
|
63
|
+
# @param uri [URI] The URI to connect to
|
64
|
+
# @return [Net::HTTP] The configured HTTP client
|
65
|
+
def setup_http_client(uri)
|
66
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
67
|
+
http.use_ssl = (uri.scheme == 'https')
|
68
|
+
http
|
69
|
+
end
|
70
|
+
|
71
|
+
# Applies headers to an HTTP request
|
72
|
+
#
|
73
|
+
# @param request [Net::HTTPRequest] The request to apply headers to
|
74
|
+
# @param additional_headers [Hash] Additional headers to apply
|
75
|
+
# @return [void]
|
76
|
+
def apply_headers(request, additional_headers = {})
|
77
|
+
headers = additional_headers.dup
|
78
|
+
headers['Authorization'] = @token if @token
|
79
|
+
|
80
|
+
headers.each { |key, value| request[key] = value }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Interactsh
|
4
|
+
# Utility methods for Interactsh
|
5
|
+
module Utils
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# Generates a random string of specified length
|
9
|
+
#
|
10
|
+
# @param length [Integer] Length of the random string
|
11
|
+
# @return [String] Random alphanumeric string
|
12
|
+
def generate_random_string(length)
|
13
|
+
charset = Array('a'..'z') + Array(0..9)
|
14
|
+
Array.new(length) { charset.sample }.join
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generates a RFC4122 version 4 UUID
|
18
|
+
#
|
19
|
+
# @return [String] The generated UUID
|
20
|
+
def generate_uuid
|
21
|
+
random_bytes = create_uuid_bytes
|
22
|
+
hex = random_bytes.map { |b| b.to_s(16).rjust(2, '0') }.join
|
23
|
+
format_uuid(hex)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates the byte array for UUID with proper version and variant
|
27
|
+
#
|
28
|
+
# @return [Array] Array of bytes for UUID
|
29
|
+
def create_uuid_bytes
|
30
|
+
bytes = Array.new(16) { rand(0..255) }
|
31
|
+
# Set version (4) and variant (RFC4122)
|
32
|
+
bytes[6] = (bytes[6] & 0x0F) | 0x40 # version 4
|
33
|
+
bytes[8] = (bytes[8] & 0x3F) | 0x80 # variant RFC4122
|
34
|
+
bytes
|
35
|
+
end
|
36
|
+
|
37
|
+
# Formats a hex string as a UUID with hyphens
|
38
|
+
#
|
39
|
+
# @param hex [String] The hex string to format
|
40
|
+
# @return [String] The formatted UUID string
|
41
|
+
def format_uuid(hex)
|
42
|
+
[hex[0..7], hex[8..11], hex[12..15], hex[16..19], hex[20..31]].join('-')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/interactsh.rb
CHANGED
@@ -1,89 +1,14 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
|
4
|
-
require '
|
5
|
-
require 'jose'
|
6
|
-
require '
|
7
|
-
require 'base64'
|
8
|
-
require 'json'
|
9
|
-
require 'ruby_xid'
|
10
|
-
require '
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(server = 'oast.me', token = nil)
|
17
|
-
@rsa = OpenSSL::PKey::RSA.new(2048)
|
18
|
-
@public_key = @rsa.public_key.to_pem
|
19
|
-
@public_key_encoded = Base64.encode64(@public_key)
|
20
|
-
|
21
|
-
@secret = SecureRandom.uuid
|
22
|
-
@random_data = Array.new(13) { (Array('a'..'z') + Array(0..9)).sample }.join
|
23
|
-
|
24
|
-
@server = server
|
25
|
-
@token = token
|
26
|
-
end
|
27
|
-
|
28
|
-
def new_domain
|
29
|
-
correlation_id = Xid.new.to_s
|
30
|
-
register(correlation_id)
|
31
|
-
|
32
|
-
"#{correlation_id}#{random_data}.#{server}"
|
33
|
-
end
|
34
|
-
|
35
|
-
def poll(host)
|
36
|
-
correlation_id = host[0..19]
|
37
|
-
headers = {}
|
38
|
-
headers['Authorization'] = token if token
|
39
|
-
|
40
|
-
response = Typhoeus.get(File.join(server, "/poll?id=#{correlation_id}&secret=#{secret}"), headers: headers)
|
41
|
-
unless response&.code == 200
|
42
|
-
puts '[!] Interactsh - Problem with data recovery'
|
43
|
-
return
|
44
|
-
end
|
45
|
-
|
46
|
-
datas = JSON.parse(response.body)
|
47
|
-
parse_poll_datas(datas)
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def parse_poll_datas(datas)
|
53
|
-
decoded_datas = []
|
54
|
-
|
55
|
-
unless datas.empty?
|
56
|
-
datas['data'].each do |enc_data|
|
57
|
-
decoded_datas << decrypt_data(datas['aes_key'], enc_data)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
decoded_datas
|
62
|
-
end
|
63
|
-
|
64
|
-
def register(correlation_id)
|
65
|
-
data = { "public-key": public_key_encoded, "secret-key": secret, "correlation-id": correlation_id }.to_json
|
66
|
-
|
67
|
-
headers = { 'Content-Type' => 'application/json' }
|
68
|
-
headers['Authorization'] = token if token
|
69
|
-
|
70
|
-
response = Typhoeus.post(File.join(server, '/register'), body: data, headers: headers)
|
71
|
-
return if response.code == 200
|
72
|
-
|
73
|
-
puts '[!] Interactsh - Problem with domain registration'
|
74
|
-
end
|
75
|
-
|
76
|
-
def decrypt_data(aes_key, enc_data)
|
77
|
-
pkey = OpenSSL::PKey::RSA.new(rsa)
|
78
|
-
encrypted_aes_key = Base64.urlsafe_decode64(aes_key)
|
79
|
-
decrypted_aes_key = JOSE::JWA::PKCS1.rsaes_oaep_decrypt(OpenSSL::Digest::SHA256, encrypted_aes_key, pkey)
|
80
|
-
|
81
|
-
secretdata = Base64.decode64(enc_data)
|
82
|
-
decipher = OpenSSL::Cipher.new('aes-256-cfb')
|
83
|
-
decipher.decrypt
|
84
|
-
decipher.key = decrypted_aes_key
|
85
|
-
|
86
|
-
# The data minus the size of the IV
|
87
|
-
JSON.parse((decipher.update(secretdata) + decipher.final)[16..])
|
88
|
-
end
|
89
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Gem dependencies
|
4
|
+
require 'openssl'
|
5
|
+
require 'jose'
|
6
|
+
require 'stringio'
|
7
|
+
require 'base64'
|
8
|
+
require 'json'
|
9
|
+
require 'ruby_xid'
|
10
|
+
require 'net/http'
|
11
|
+
require 'uri'
|
12
|
+
|
13
|
+
# Internal dependencies
|
14
|
+
Dir[File.join(__dir__, 'interactsh/*.rb')].each { |file| require file }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: interactsh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua MARTINELLE
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jose
|
@@ -50,41 +50,8 @@ dependencies:
|
|
50
50
|
- - ">="
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: 1.0.7
|
53
|
-
-
|
54
|
-
|
55
|
-
requirement: !ruby/object:Gem::Requirement
|
56
|
-
requirements:
|
57
|
-
- - "~>"
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: 0.2.0
|
60
|
-
type: :runtime
|
61
|
-
prerelease: false
|
62
|
-
version_requirements: !ruby/object:Gem::Requirement
|
63
|
-
requirements:
|
64
|
-
- - "~>"
|
65
|
-
- !ruby/object:Gem::Version
|
66
|
-
version: 0.2.0
|
67
|
-
- !ruby/object:Gem::Dependency
|
68
|
-
name: typhoeus
|
69
|
-
requirement: !ruby/object:Gem::Requirement
|
70
|
-
requirements:
|
71
|
-
- - "~>"
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
version: '1.4'
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: 1.4.0
|
77
|
-
type: :runtime
|
78
|
-
prerelease: false
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
80
|
-
requirements:
|
81
|
-
- - "~>"
|
82
|
-
- !ruby/object:Gem::Version
|
83
|
-
version: '1.4'
|
84
|
-
- - ">="
|
85
|
-
- !ruby/object:Gem::Version
|
86
|
-
version: 1.4.0
|
87
|
-
description:
|
53
|
+
description: Ruby client library for Interactsh - a tool for detecting out-of-band
|
54
|
+
interactions
|
88
55
|
email:
|
89
56
|
- contact@jomar.fr
|
90
57
|
executables: []
|
@@ -92,6 +59,10 @@ extensions: []
|
|
92
59
|
extra_rdoc_files: []
|
93
60
|
files:
|
94
61
|
- lib/interactsh.rb
|
62
|
+
- lib/interactsh/client.rb
|
63
|
+
- lib/interactsh/crypto.rb
|
64
|
+
- lib/interactsh/http_client.rb
|
65
|
+
- lib/interactsh/utils.rb
|
95
66
|
homepage: https://github.com/JoshuaMart/Interactsh-Library
|
96
67
|
licenses:
|
97
68
|
- MIT
|
@@ -99,6 +70,7 @@ metadata:
|
|
99
70
|
source_code_uri: https://github.com/JoshuaMart/Interactsh-Library
|
100
71
|
homepage_uri: https://github.com/JoshuaMart/Interactsh-Library
|
101
72
|
github_repo: https://github.com/JoshuaMart/Interactsh-Library
|
73
|
+
rubygems_mfa_required: 'true'
|
102
74
|
post_install_message:
|
103
75
|
rdoc_options: []
|
104
76
|
require_paths:
|
@@ -107,14 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
79
|
requirements:
|
108
80
|
- - ">="
|
109
81
|
- !ruby/object:Gem::Version
|
110
|
-
version: 3.1.
|
82
|
+
version: 3.1.3
|
111
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
84
|
requirements:
|
113
85
|
- - ">="
|
114
86
|
- !ruby/object:Gem::Version
|
115
87
|
version: '0'
|
116
88
|
requirements: []
|
117
|
-
rubygems_version: 3.
|
89
|
+
rubygems_version: 3.5.22
|
118
90
|
signing_key:
|
119
91
|
specification_version: 4
|
120
92
|
summary: Interactsh Ruby Library
|