rex 2.0.4 → 2.0.5
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/rex/arch/x86.rb +16 -0
- data/lib/rex/constants.rb +1 -0
- data/lib/rex/constants/windows.rb +147 -0
- data/lib/rex/encoder/xdr.rb +3 -2
- data/lib/rex/exceptions.rb +37 -5
- data/lib/rex/exploitation/cmdstager/bourne.rb +9 -1
- data/lib/rex/exploitation/cmdstager/tftp.rb +5 -5
- data/lib/rex/java.rb +3 -0
- data/lib/rex/java/serialization.rb +54 -0
- data/lib/rex/java/serialization/model.rb +20 -0
- data/lib/rex/java/serialization/model/annotation.rb +69 -0
- data/lib/rex/java/serialization/model/block_data.rb +70 -0
- data/lib/rex/java/serialization/model/block_data_long.rb +72 -0
- data/lib/rex/java/serialization/model/class_desc.rb +64 -0
- data/lib/rex/java/serialization/model/contents.rb +156 -0
- data/lib/rex/java/serialization/model/element.rb +44 -0
- data/lib/rex/java/serialization/model/end_block_data.rb +12 -0
- data/lib/rex/java/serialization/model/field.rb +172 -0
- data/lib/rex/java/serialization/model/long_utf.rb +48 -0
- data/lib/rex/java/serialization/model/new_array.rb +225 -0
- data/lib/rex/java/serialization/model/new_class_desc.rb +155 -0
- data/lib/rex/java/serialization/model/new_enum.rb +79 -0
- data/lib/rex/java/serialization/model/new_object.rb +223 -0
- data/lib/rex/java/serialization/model/null_reference.rb +12 -0
- data/lib/rex/java/serialization/model/reference.rb +61 -0
- data/lib/rex/java/serialization/model/reset.rb +12 -0
- data/lib/rex/java/serialization/model/stream.rb +123 -0
- data/lib/rex/java/serialization/model/utf.rb +69 -0
- data/lib/rex/mime/message.rb +9 -14
- data/lib/rex/payloads.rb +1 -0
- data/lib/rex/payloads/meterpreter.rb +2 -0
- data/lib/rex/payloads/meterpreter/patch.rb +136 -0
- data/lib/rex/payloads/win32/kernel/stager.rb +26 -25
- data/lib/rex/post/meterpreter/client.rb +50 -60
- data/lib/rex/post/meterpreter/client_core.rb +18 -25
- data/lib/rex/post/meterpreter/extensions/extapi/adsi/adsi.rb +102 -8
- data/lib/rex/post/meterpreter/extensions/extapi/tlv.rb +24 -14
- data/lib/rex/post/meterpreter/extensions/stdapi/sys/config.rb +18 -0
- data/lib/rex/post/meterpreter/extensions/stdapi/tlv.rb +1 -0
- data/lib/rex/post/meterpreter/packet_dispatcher.rb +1 -1
- data/lib/rex/post/meterpreter/ui/console.rb +1 -1
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/extapi/adsi.rb +43 -1
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/incognito.rb +1 -1
- data/lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/sys.rb +9 -0
- data/lib/rex/proto/dcerpc/svcctl.rb +2 -0
- data/lib/rex/proto/dcerpc/svcctl/packet.rb +304 -0
- data/lib/rex/proto/kademlia.rb +8 -0
- data/lib/rex/proto/kademlia/bootstrap_request.rb +19 -0
- data/lib/rex/proto/kademlia/bootstrap_response.rb +79 -0
- data/lib/rex/proto/kademlia/message.rb +72 -0
- data/lib/rex/proto/kademlia/ping.rb +19 -0
- data/lib/rex/proto/kademlia/pong.rb +41 -0
- data/lib/rex/proto/kademlia/util.rb +22 -0
- data/lib/rex/proto/natpmp/packet.rb +30 -2
- data/lib/rex/proto/quake.rb +3 -0
- data/lib/rex/proto/quake/message.rb +73 -0
- data/lib/rex/proto/smb/client.rb +1 -0
- data/lib/rex/proto/smb/simpleclient.rb +4 -0
- data/lib/rex/proto/sunrpc/client.rb +14 -3
- data/lib/rex/socket/comm/local.rb +10 -7
- data/lib/rex/socket/ssl_tcp_server.rb +79 -40
- data/lib/rex/ui/text/input/readline.rb +33 -6
- data/lib/rex/ui/text/output/file.rb +2 -2
- data/lib/rex/ui/text/output/stdio.rb +70 -14
- data/rex.gemspec +1 -1
- metadata +38 -3
@@ -0,0 +1,8 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'rex/proto/kademlia/bootstrap_request'
|
4
|
+
require 'rex/proto/kademlia/bootstrap_response'
|
5
|
+
require 'rex/proto/kademlia/message'
|
6
|
+
require 'rex/proto/kademlia/ping'
|
7
|
+
require 'rex/proto/kademlia/pong'
|
8
|
+
require 'rex/proto/kademlia/util'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'rex/proto/kademlia/message'
|
4
|
+
|
5
|
+
module Rex
|
6
|
+
module Proto
|
7
|
+
module Kademlia
|
8
|
+
# Opcode for a BOOTSTRAP request
|
9
|
+
BOOTSTRAP_REQUEST = 0x01
|
10
|
+
|
11
|
+
# A Kademlia bootstrap request message
|
12
|
+
class BootstrapRequest < Message
|
13
|
+
def initialize
|
14
|
+
super(BOOTSTRAP_REQUEST)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'rex/proto/kademlia/message'
|
4
|
+
require 'rex/proto/kademlia/util'
|
5
|
+
|
6
|
+
module Rex
|
7
|
+
module Proto
|
8
|
+
module Kademlia
|
9
|
+
# Opcode for a bootstrap response
|
10
|
+
BOOTSTRAP_RESPONSE = 0x09
|
11
|
+
|
12
|
+
# A Kademlia bootstrap response message
|
13
|
+
class BootstrapResponse < Message
|
14
|
+
# @return [String] the ID of the peer that send the bootstrap response
|
15
|
+
attr_reader :peer_id
|
16
|
+
# @return [Integer] the TCP port that the responding peer is listening on
|
17
|
+
attr_reader :tcp_port
|
18
|
+
# @return [Integer] the version of this peer
|
19
|
+
attr_reader :version
|
20
|
+
# @return [Array<Hash<String, Object>>] the peer ID, IP address, UDP/TCP ports and version of each peer
|
21
|
+
attr_reader :peers
|
22
|
+
|
23
|
+
# Constructs a new bootstrap response
|
24
|
+
#
|
25
|
+
# @param peer_id [String] the ID of this peer
|
26
|
+
# @param tcp_port [Integer] the TCP port that this peer is listening on
|
27
|
+
# @param version [Integer] the version of this peer
|
28
|
+
# @param peers [Array<Hash<String, Object>>] the peer ID, IP address, UDP/TCP ports and version of each peer
|
29
|
+
def initialize(peer_id, tcp_port, version, peers)
|
30
|
+
@peer_id = peer_id
|
31
|
+
@tcp_port = tcp_port
|
32
|
+
@version = version
|
33
|
+
@peers = peers
|
34
|
+
end
|
35
|
+
|
36
|
+
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
|
37
|
+
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
|
38
|
+
# and version (1 byte)
|
39
|
+
BOOTSTRAP_PEER_SIZE = 25
|
40
|
+
|
41
|
+
# Builds a bootstrap response from given data
|
42
|
+
#
|
43
|
+
# @param data [String] the data to decode
|
44
|
+
# @return [BootstrapResponse] the bootstrap response if the data is valid, nil otherwise
|
45
|
+
def self.from_data(data)
|
46
|
+
message = Message.from_data(data)
|
47
|
+
# abort if this isn't a valid response
|
48
|
+
return unless message
|
49
|
+
return unless message.type == BOOTSTRAP_RESPONSE
|
50
|
+
return unless message.body.size >= 23
|
51
|
+
bootstrap_peer_id = Rex::Proto::Kademlia.decode_peer_id(message.body.slice!(0, 16))
|
52
|
+
bootstrap_tcp_port, bootstrap_version, num_peers = message.body.slice!(0, 5).unpack('vCv')
|
53
|
+
# protocol says there are no peers and the body confirms this, so just return with no peers
|
54
|
+
if num_peers == 0 && message.body.blank?
|
55
|
+
peers = []
|
56
|
+
else
|
57
|
+
peers_data = message.body
|
58
|
+
# peers data is too long/short, abort
|
59
|
+
return if peers_data.size % BOOTSTRAP_PEER_SIZE != 0
|
60
|
+
peers = []
|
61
|
+
until peers_data.blank?
|
62
|
+
peer_data = peers_data.slice!(0, BOOTSTRAP_PEER_SIZE)
|
63
|
+
peer_id = Rex::Proto::Kademlia.decode_peer_id(peer_data.slice!(0, 16))
|
64
|
+
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
|
65
|
+
peers << {
|
66
|
+
id: peer_id,
|
67
|
+
ip: Rex::Socket.addr_itoa(ip),
|
68
|
+
tcp_port: tcp_port,
|
69
|
+
udp_port: udp_port,
|
70
|
+
version: version
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
BootstrapResponse.new(bootstrap_peer_id, bootstrap_tcp_port, bootstrap_version, peers)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
module Rex
|
4
|
+
module Proto
|
5
|
+
##
|
6
|
+
#
|
7
|
+
# Minimal support for the newer Kademlia protocol, referred to here and often
|
8
|
+
# elsewhere as Kademlia2. It is unclear how this differs from the old protocol.
|
9
|
+
#
|
10
|
+
# Protocol details are hard to come by because most documentation is academic
|
11
|
+
# in nature and glosses over the low-level network details. The best
|
12
|
+
# documents I found on the protocol are:
|
13
|
+
#
|
14
|
+
# http://gbmaster.wordpress.com/2013/05/05/botnets-surrounding-us-an-initial-focus-on-kad/
|
15
|
+
# http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/
|
16
|
+
# http://gbmaster.wordpress.com/2013/11/23/botnets-surrounding-us-performing-requests-sending-out-kademlia2_req-and-asking-contact-where-art-thou/
|
17
|
+
#
|
18
|
+
##
|
19
|
+
module Kademlia
|
20
|
+
# A simple Kademlia message
|
21
|
+
class Message
|
22
|
+
# The header that non-compressed Kad messages use
|
23
|
+
STANDARD_PACKET = 0xE4
|
24
|
+
# The header that compressed Kad messages use, which is currently unsupported
|
25
|
+
COMPRESSED_PACKET = 0xE5
|
26
|
+
|
27
|
+
# @return [Integer] the message type
|
28
|
+
attr_reader :type
|
29
|
+
# @return [String] the message body
|
30
|
+
attr_reader :body
|
31
|
+
|
32
|
+
# Construct a new Message from the provided type and body
|
33
|
+
#
|
34
|
+
# @param type [String] the message type
|
35
|
+
# @param body [String] the message body
|
36
|
+
def initialize(type, body = '')
|
37
|
+
@type = type
|
38
|
+
@body = body
|
39
|
+
end
|
40
|
+
|
41
|
+
# Construct a new Message from the provided data
|
42
|
+
#
|
43
|
+
# @param data [String] the data to interpret as a Kademlia message
|
44
|
+
# @return [Message] the message if valid, nil otherwise
|
45
|
+
def self.from_data(data)
|
46
|
+
return if data.length < 2
|
47
|
+
header, type = data.unpack('CC')
|
48
|
+
if header == COMPRESSED_PACKET
|
49
|
+
fail NotImplementedError, "Unable to handle #{data.length}-byte compressed Kademlia message"
|
50
|
+
end
|
51
|
+
return if header != STANDARD_PACKET
|
52
|
+
Message.new(type, data[2, data.length])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get this Message as a String
|
56
|
+
#
|
57
|
+
# @return [String] the string representation of this Message
|
58
|
+
def to_str
|
59
|
+
[STANDARD_PACKET, @type].pack('CC') + @body
|
60
|
+
end
|
61
|
+
|
62
|
+
# Compares this Message and another Message for equality
|
63
|
+
#
|
64
|
+
# @param other [Message] the Message to compare
|
65
|
+
# @return [Boolean] true iff the two messages have equal types and bodies, false otherwise
|
66
|
+
def ==(other)
|
67
|
+
type == other.type && body == other.body
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'rex/proto/kademlia/message'
|
4
|
+
|
5
|
+
module Rex
|
6
|
+
module Proto
|
7
|
+
module Kademlia
|
8
|
+
# Opcode for a PING request
|
9
|
+
PING = 0x60
|
10
|
+
|
11
|
+
# A Kademlia ping message.
|
12
|
+
class Ping < Message
|
13
|
+
def initialize
|
14
|
+
super(PING)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
require 'rex/proto/kademlia/message'
|
4
|
+
|
5
|
+
module Rex
|
6
|
+
module Proto
|
7
|
+
module Kademlia
|
8
|
+
# Opcode for a PING response
|
9
|
+
PONG = 0x61
|
10
|
+
|
11
|
+
# A Kademlia pong message.
|
12
|
+
class Pong < Message
|
13
|
+
# @return [Integer] the source port from which the PING was received
|
14
|
+
attr_reader :port
|
15
|
+
|
16
|
+
def initialize(port = nil)
|
17
|
+
super(PONG)
|
18
|
+
@port = port
|
19
|
+
end
|
20
|
+
|
21
|
+
# Builds a pong from given data
|
22
|
+
#
|
23
|
+
# @param data [String] the data to decode
|
24
|
+
# @return [Pong] the pong if the data is valid, nil otherwise
|
25
|
+
def self.from_data(data)
|
26
|
+
message = super(data)
|
27
|
+
return if message.type != PONG
|
28
|
+
return if message.body.size != 2
|
29
|
+
Pong.new(message.body.unpack('v')[0])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get this Pong as a String
|
33
|
+
#
|
34
|
+
# @return [String] the string representation of this Pong
|
35
|
+
def to_str
|
36
|
+
super + [@port].pack('v')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
module Rex
|
4
|
+
module Proto
|
5
|
+
module Kademlia
|
6
|
+
# Decodes an on-the-wire representation of a Kademlia peer to its 16-character hex equivalent
|
7
|
+
#
|
8
|
+
# @param bytes [String] the on-the-wire representation of a Kademlia peer
|
9
|
+
# @return [String] the peer ID if valid, nil otherwise
|
10
|
+
def self.decode_peer_id(bytes)
|
11
|
+
peer_id = 0
|
12
|
+
return nil unless bytes.size == 16
|
13
|
+
bytes.unpack('VVVV').map { |p| peer_id = ((peer_id << 32) ^ p) }
|
14
|
+
peer_id.to_s(16).upcase
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO
|
18
|
+
# def encode_peer_id(id)
|
19
|
+
# end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -3,8 +3,6 @@
|
|
3
3
|
#
|
4
4
|
# NAT-PMP protocol support
|
5
5
|
#
|
6
|
-
# by Jon Hart <jhart@spoofed.org>
|
7
|
-
#
|
8
6
|
##
|
9
7
|
|
10
8
|
module Rex
|
@@ -16,6 +14,20 @@ module NATPMP
|
|
16
14
|
[ 0, 0 ].pack('nn')
|
17
15
|
end
|
18
16
|
|
17
|
+
def get_external_address(udp_sock, host, port, timeout=1)
|
18
|
+
vprint_status("#{host}:#{port} - Probing NAT-PMP for external address")
|
19
|
+
udp_sock.sendto(external_address_request, host, port, 0)
|
20
|
+
external_address = nil
|
21
|
+
while (r = udp_sock.recvfrom(12, timeout) and r[1])
|
22
|
+
(ver, op, result, epoch, external_address) = parse_external_address_response(r[0])
|
23
|
+
if external_address
|
24
|
+
vprint_good("#{host}:#{port} - NAT-PMP external address is #{external_address}")
|
25
|
+
break
|
26
|
+
end
|
27
|
+
end
|
28
|
+
external_address
|
29
|
+
end
|
30
|
+
|
19
31
|
# Parse a NAT-PMP external address response +resp+.
|
20
32
|
# Returns the decoded parts of the response as an array.
|
21
33
|
def parse_external_address_response(resp)
|
@@ -23,6 +35,21 @@ module NATPMP
|
|
23
35
|
[ ver, op, result, epoch, Rex::Socket::addr_itoa(addr) ]
|
24
36
|
end
|
25
37
|
|
38
|
+
def map_port(udp_sock, host, port, int_port, ext_port, protocol, lifetime, timeout=1)
|
39
|
+
vprint_status("#{host}:#{port} - Sending NAT-PMP mapping request")
|
40
|
+
# build the mapping request
|
41
|
+
req = map_port_request(int_port, ext_port,
|
42
|
+
Rex::Proto::NATPMP.const_get(datastore['PROTOCOL']), datastore['LIFETIME'])
|
43
|
+
# send it
|
44
|
+
udp_sock.sendto(req, host, datastore['RPORT'], 0)
|
45
|
+
# handle the reply
|
46
|
+
while (r = udp_sock.recvfrom(16, timeout) and r[1])
|
47
|
+
(_, _, result, _, _, actual_ext_port, _) = parse_map_port_response(r[0])
|
48
|
+
return (result == 0 ? actual_ext_port : nil)
|
49
|
+
end
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
26
53
|
# Return a NAT-PMP request to map remote port +rport+/+protocol+ to local port +lport+ for +lifetime+ ms
|
27
54
|
def map_port_request(lport, rport, protocol, lifetime)
|
28
55
|
[ Rex::Proto::NATPMP::Version, # version
|
@@ -39,6 +66,7 @@ module NATPMP
|
|
39
66
|
def parse_map_port_response(resp)
|
40
67
|
resp.unpack("CCnNnnN")
|
41
68
|
end
|
69
|
+
|
42
70
|
end
|
43
71
|
|
44
72
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# -*- coding: binary -*-
|
2
|
+
|
3
|
+
module Rex
|
4
|
+
module Proto
|
5
|
+
##
|
6
|
+
#
|
7
|
+
# Quake 3 protocol, taken from ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt
|
8
|
+
#
|
9
|
+
##
|
10
|
+
module Quake
|
11
|
+
HEADER = 0xFFFFFFFF
|
12
|
+
|
13
|
+
def decode_message(message)
|
14
|
+
# minimum size is header (4) + <command> + <stuff>
|
15
|
+
return if message.length < 7
|
16
|
+
header = message.unpack('N')[0]
|
17
|
+
return if header != HEADER
|
18
|
+
message[4, message.length]
|
19
|
+
end
|
20
|
+
|
21
|
+
def encode_message(payload)
|
22
|
+
[HEADER].pack('N') + payload
|
23
|
+
end
|
24
|
+
|
25
|
+
def getstatus
|
26
|
+
encode_message('getstatus')
|
27
|
+
end
|
28
|
+
|
29
|
+
def getinfo
|
30
|
+
encode_message('getinfo')
|
31
|
+
end
|
32
|
+
|
33
|
+
def decode_infostring(infostring)
|
34
|
+
# decode an "infostring", which is just a (supposedly) quoted string of tokens separated
|
35
|
+
# by backslashes, generally terminated with a newline
|
36
|
+
token_re = /([^\\]+)\\([^\\]+)/
|
37
|
+
return nil unless infostring =~ token_re
|
38
|
+
# remove possibly present leading/trailing double quote
|
39
|
+
infostring.gsub!(/(?:^"|"$)/, '')
|
40
|
+
# remove the trailing \n, if present
|
41
|
+
infostring.gsub!(/\n$/, '')
|
42
|
+
# split on backslashes and group into key value pairs
|
43
|
+
infohash = {}
|
44
|
+
infostring.scan(token_re).each do |kv|
|
45
|
+
infohash[kv.first] = kv.last
|
46
|
+
end
|
47
|
+
infohash
|
48
|
+
end
|
49
|
+
|
50
|
+
def decode_response(message, type)
|
51
|
+
resp = decode_message(message)
|
52
|
+
if /^print\n(?<error>.*)\n?/m =~ resp
|
53
|
+
# XXX: is there a better exception to throw here?
|
54
|
+
fail ::ArgumentError, "#{type} error: #{error}"
|
55
|
+
# why doesn't this work?
|
56
|
+
# elsif /^#{type}Response\n(?<infostring>.*)/m =~ resp
|
57
|
+
elsif resp =~ /^#{type}Response\n(.*)/m
|
58
|
+
decode_infostring(Regexp.last_match(1))
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def decode_status(message)
|
65
|
+
decode_response(message, 'status')
|
66
|
+
end
|
67
|
+
|
68
|
+
def decode_info(message)
|
69
|
+
decode_response(message, 'info')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/rex/proto/smb/client.rb
CHANGED
@@ -66,6 +66,10 @@ attr_accessor :socket, :client, :direct, :shares, :last_share
|
|
66
66
|
|
67
67
|
self.client.spnopt = spnopt
|
68
68
|
|
69
|
+
# In case the user unsets the password option, we make sure this is
|
70
|
+
# always a string
|
71
|
+
pass ||= ''
|
72
|
+
|
69
73
|
ok = self.client.session_setup(user, pass, domain)
|
70
74
|
rescue ::Interrupt
|
71
75
|
raise $!
|
@@ -6,10 +6,21 @@ module Rex
|
|
6
6
|
module Proto
|
7
7
|
module SunRPC
|
8
8
|
|
9
|
+
class RPCError < ::StandardError
|
10
|
+
def initialize(msg = 'RPC operation failed')
|
11
|
+
super
|
12
|
+
@msg = msg
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@msg
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
9
20
|
class RPCTimeout < ::Interrupt
|
10
|
-
|
11
|
-
|
12
|
-
|
21
|
+
def initialize(msg = 'Operation timed out.')
|
22
|
+
@msg = msg
|
23
|
+
end
|
13
24
|
|
14
25
|
def to_s
|
15
26
|
@msg
|