@@ -132,6 +132,28 @@ class ErrorMessage < BinData::Record
132132
133133 uint32 :status_code
134134 opc_ua_string :reason
135+
136+ # Decode a body that arrives only once the server has decided it cannot
137+ # answer, so it is read as leniently as it can be: the two fields are taken
138+ # one at a time, and a Reason that will not decode still leaves the
139+ # StatusCode reportable. An ERR is the server's only account of why it gave
140+ # up, and half of one is worth more than none.
141+ #
142+ # @param body [String] the two fields, without any framing.
143+ # @return [Array(Integer, String), Array(Integer, nil), Array(nil, nil)] the
144+ # StatusCode and Reason, either of which is nil when it could not be read.
145+ def self . decode ( body )
146+ raw = body . to_s . b
147+ return [ nil , nil ] if raw . bytesize < 4
148+
149+ reason = begin
150+ Rex ::Proto ::OpcUa ::Types ::OpcUaString . read ( raw . byteslice ( 4 ..-1 ) . to_s ) . snapshot
151+ rescue ::IOError , ::BinData ::ValidityError
152+ nil
153+ end
154+
155+ [ raw . byteslice ( 0 , 4 ) . unpack1 ( 'V' ) , reason ]
156+ end
135157 end
136158
137159 # One framed message as it came off the wire. The body excludes the header.
@@ -160,10 +182,12 @@ def intermediate?
160182
161183 # Frames and reassembles OPC-UA TCP messages over a socket.
162184 #
163- # The only thing required of the socket is get_once(length, timeout), which is
164- # what makes this testable without a network: Msf::Exploit::Remote::Tcp#sock
165- # satisfies it and so does a test double. Writing is deliberately not part of
166- # this class, since building a request is the business of the layer above.
185+ # The only thing required of the socket is get_once(length, timeout) and put,
186+ # which is what makes this testable without a network:
187+ # Msf::Exploit::Remote::Tcp#sock satisfies it and so does a test double.
188+ #
189+ # Building the body of a request belongs to the layer above; what belongs here
190+ # is the header that wraps it, so that a caller cannot get MessageSize wrong.
167191 class MessageStream
168192 # Seconds allowed per read when the caller gives no timeout of its own.
169193 DEFAULT_TIMEOUT = 5
@@ -181,6 +205,22 @@ def initialize(sock, timeout: DEFAULT_TIMEOUT)
181205 @timeout = timeout
182206 end
183207
208+ # Frame a message and write it. MessageSize counts the header, so it is
209+ # computed here rather than trusted from the caller.
210+ #
211+ # Nothing this library sends needs more than one chunk: a Hello, an
212+ # OpenSecureChannel and a GetEndpoints request are all small, and the
213+ # SendBufferSize a server may impose is at least 8192 bytes.
214+ #
215+ # @param message_type [String] a MessageType value.
216+ # @param body [String] everything that follows the 8 byte header.
217+ # @param chunk_type [String] a ChunkType value.
218+ # @return [Integer] the number of bytes written.
219+ def send_message ( message_type , body , chunk_type : ChunkType ::FINAL )
220+ raw = body . to_s . b
221+ @sock . put ( ( message_type + chunk_type ) . b + [ HEADER_LEN + raw . bytesize ] . pack ( 'V' ) + raw )
222+ end
223+
184224 # Read exactly len bytes, accumulating across reads. A single read is not
185225 # guaranteed to return the full amount, and a GetEndpoints response carrying
186226 # server certificates routinely spans several segments.
@@ -307,22 +347,15 @@ def abort_error(body)
307347 Error ::AbortError . new ( **status_and_reason ( body . byteslice ( SECURE_MSG_PREFIX_LEN ..-1 ) . to_s ) )
308348 end
309349
310- # Decode the StatusCode and Reason that an ERR body and an abort body both
311- # carry, in the same two fields in the same order.
312- #
313- # Either arrives only once the server has decided it cannot answer, so a
314- # body that will not decode is still reported as the failure it is rather
315- # than being replaced by a complaint about the decode; the StatusCode is
316- # simply left unknown.
350+ # The StatusCode and Reason that an ERR body and an abort body both carry, in
351+ # the same two fields in the same order.
317352 #
318353 # @param body [String] the bytes of the two fields.
319- # @return [Hash] keyword arguments for the exception, empty when the body
320- # could not be decoded.
354+ # @return [Hash] keyword arguments for the exception.
321355 def status_and_reason ( body )
322- err = ErrorMessage . read ( body )
323- { status_code : err . status_code . snapshot , reason : err . reason . snapshot }
324- rescue ::IOError , ::BinData ::Error
325- { }
356+ status_code , reason = ErrorMessage . decode ( body )
357+
358+ { status_code : status_code , reason : reason }
326359 end
327360 end
328361end
0 commit comments