From 681e2c940c8bcf3ed940c94de989bb6e25e867eb Mon Sep 17 00:00:00 2001 From: HD Moore Date: Sun, 18 Oct 2009 01:17:58 +0000 Subject: [PATCH] Fixes #379. Massive rewrite of the MSSQL mixin. This moves everything to TDS 7.0 instead of the old crusty protocol git-svn-id: file:///home/svn/framework3/trunk@7178 4d416f70-5f16-0410-b530-b9f4589650da --- lib/msf/core/exploit/mssql.rb | 697 ++++++++++++++++++---------------- 1 file changed, 377 insertions(+), 320 deletions(-) diff --git a/lib/msf/core/exploit/mssql.rb b/lib/msf/core/exploit/mssql.rb index 5937243430..4a06e61765 100644 --- a/lib/msf/core/exploit/mssql.rb +++ b/lib/msf/core/exploit/mssql.rb @@ -80,108 +80,220 @@ module Exploit::Remote::MSSQL return res end + # + # Send and receive using TDS + # + def mssql_send_recv(req, timeout=15) + sock.put(req) + + # Read the 8 byte header to get the length and status + # Read the length to get the data + # If the status is 0, read another header and more data + + done = false + resp = "" + + while(not done) + head = sock.get_once(8, timeout) + if(not (head and head.length == 8)) + return false + end + + # Is this the last buffer? + if(head[1,1] == "\x01") + done = true + end + + # Grab this block's length + rlen = head[2,2].unpack('n')[0] - 8 + + while(rlen > 0) + buff = sock.get_once(rlen, timeout) + return if not buff + resp << buff + rlen -= buff.length + end + end + + resp + end + + + # + # Encrypt according to the TDS protocol (encode) + # + def mssql_tds_encrypt(pass) + # Convert to unicode, swap 4 bits both ways, xor with 0xa5 + Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*") + end + # # This method connects to the server over TCP and attempts # to authenticate with the supplied username and password # The global socket is used and left connected after auth # - def mssql_login(user='sa', pass='') - + def mssql_login(user='sa', pass='', db='') + disconnect if self.sock connect - - p_hdr = - "\x02\x00\x02\x00\x00\x00\x02\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + pkt = "" + idx = 0 + + pkt << [ + 0x00000000, # Dummy size + 0x71000001, # TDS Version + 0x00000000, # Size + 0x00000007, # Version + rand(1024+1), # PID + 0x00000000, # ConnectionID + 0xe0, # Option Flags 1 + 0x03, # Option Flags 2 + 0x00, # SQL Type Flags + 0x00, # Reserved Flags + 0x00000000, # Time Zone + 0x00000000 # Collation + ].pack('VVVVVVCCCCVV') + + + cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) + uname = Rex::Text.to_unicode( user ) + pname = mssql_tds_encrypt( pass ) + aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) + sname = Rex::Text.to_unicode( rhost ) + dname = Rex::Text.to_unicode( db ) + + idx = pkt.size + 50 # lengths below + + pkt << [idx, cname.length / 2].pack('vv') + idx += cname.length + + pkt << [idx, uname.length / 2].pack('vv') + idx += uname.length + + pkt << [idx, pname.length / 2].pack('vv') + idx += pname.length - p_pk2 = - "\x30\x30\x30\x30\x30\x30\x61\x30\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x20\x18\x81\xb8\x2c\x08\x03"+ - "\x01\x06\x0a\x09\x01\x01\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x73\x71\x75\x65\x6c\x64\x61"+ - "\x20\x31\x2e\x30\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00" + pkt << [idx, aname.length / 2].pack('vv') + idx += aname.length + + pkt << [idx, sname.length / 2].pack('vv') + idx += sname.length + + pkt << [0, 0].pack('vv') + + pkt << [idx, aname.length / 2].pack('vv') + idx += aname.length - p_pk3 = - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x04\x02\x00\x00\x4d\x53\x44"+ - "\x42\x4c\x49\x42\x00\x00\x00\x07\x06\x00\x00"+ - "\x00\x00\x0d\x11\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00" + pkt << [idx, 0].pack('vv') + + pkt << [idx, dname.length / 2].pack('vv') + idx += dname.length + + # The total length has to be embedded twice more here + pkt << [ + 0, + 0, + 0x12345678, + 0x12345678 + ].pack('vVVV') + + pkt << cname + pkt << uname + pkt << pname + pkt << aname + pkt << sname + pkt << aname + pkt << dname + + # Total packet length + pkt[0,4] = [pkt.length].pack('V') + # Embedded packet lengths + pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2 - p_lang = - "\x02\x01\x00\x47\x00\x00\x02\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"+ - "\x00\x00\x00\x00\x00\x00\x30\x30\x30\x00\x00"+ - "\x00\x03\x00\x00\x00" + # Packet header and total length including header + pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt + + resp = mssql_send_recv(pkt) - user = user.slice(0, 29) - pass = pass.slice(0, 29) + info = {:errors => []} + info = mssql_parse_reply(resp,info) - ulen = user.length.chr - plen = pass.length.chr + info[:login_ack] ? true : false + end - user << ("\x00" * (30-user.length)) - pass << ("\x00" * (30-pass.length)) + +=begin + def mssql_login_obsolete(user='sa', pass='') - p_login = p_hdr + user + ulen + pass + plen - p_login << p_pk2 + plen + pass + p_pk3 + disconnect if self.sock + connect - sock.put(p_login) - sock.put(p_lang) + pkt = '' + + host = Rex::Text.rand_text_alpha(rand(8)+1) + proc = Rex::Text.rand_text(rand(8)+1) + appname = Rex::Text.rand_text_alpha(rand(12)+1) + proname = Rex::Text.rand_text_alpha(rand(8)+1) + provers = rand(100) + srvname = "" + rempass = "" + lang = "" + + pkt << [host, host.length].pack('Z30C') + pkt << [user, user.length].pack('Z30C') + pkt << [pass, pass.length].pack('Z30C') + pkt << [proc, proc.length].pack('Z8C') + pkt << Rex::Text.rand_text(6) + pkt << [ + 0x03, # Int2 + 0x00, # Int4 + 0x06, # Ascii + 0x05, # Fload + 0x00, # UseDB + 0x00, # DumpLoad + 0x00, # Interface + 0x00, # Type + 0x00 # SSPI required + ].pack('C*') + pkt << [appname, appname.length].pack('Z30C') + pkt << [srvname, srvname.length].pack('Z30C') + pkt << [rempass, rempass.length].pack('Z255C') + pkt << [0x04020000].pack('N') + pkt << [proname, proname.length].pack('Z10C') + pkt << [provers].pack('N') + pkt << [lang, lang.length].pack('Z30C') + pkt << [0x00].pack('C') # set lang + pkt << [1024].pack('N') # pkt size + pkt << Rex::Text.rand_text(rand(8)+1) # Padding + + pkt = "\x02\x01" + [pkt.length + 8].pack('n') + [rand(0x100)].pack('n') + [rand(0x100)].pack('C') + "\x00" + pkt + + sock.put(pkt) resp = sock.get_once - + if (resp and resp.length > 10 and resp[8,1].unpack('C')[0] == 0xe3) return true end - return false + return false end +=end - def mssql_login_datastore - mssql_login(datastore['MSSQL_USER'], datastore['MSSQL_PASS']) + def mssql_login_datastore(db='') + mssql_login(datastore['MSSQL_USER'], datastore['MSSQL_PASS'], db) end - def mssql_query(sql, doprint=false, opts={}) - info = { :sql => sql } - opts[:timeout] ||= 15 - + def mssql_query(sqla, doprint=false, opts={}) + info = { :sql => sqla } + + opts[:timeout] ||= 15 + + sql = Rex::Text.to_unicode(sqla) pkt = "\x01\x01" + [sql.length + 8].pack('n') + [rand(0x100)].pack('n') + [rand(0x100)].pack('C') + "\x00" + sql - sock.put(pkt) - - resp = sock.get(opts[:timeout]) + resp = mssql_send_recv(pkt, opts[:timeout]) mssql_parse_reply(resp, info) mssql_print_reply(info) if doprint info @@ -191,12 +303,6 @@ module Exploit::Remote::MSSQL mssql_query("xp_cmdshell '#{cmd}'", doprint, opts) end - def mssql_parse_header(header) - type, status, size, chan, pkt, window = header.unpack('CCnnCC') - return [status, size, pkt] - end - - def mssql_print_reply(info) print_status("SQL Query: #{info[:sql]}") @@ -227,300 +333,251 @@ module Exploit::Remote::MSSQL end end - def mssql_parse_reply(resp, info) - info[:errors] = [] + + def mssql_parse_tds_reply(data, info) + info[:errors] ||= [] + info[:colinfos] ||= [] + info[:colnames] ||= [] - data = "" - - status = 0 - while status == 0 - break if not resp - status, size, pkt = mssql_parse_header(resp.slice!(0,8)) - break if not (status and size and pkt) - data << resp.slice!(0,(size-8)) + # Parse out the columns + cols = data.slice!(0,2).unpack('v')[0] + 0.upto(cols-1) do |col_idx| + col = {} + info[:colinfos][col_idx] = col + + col[:utype] = data.slice!(0,2).unpack('v')[0] + col[:flags] = data.slice!(0,2).unpack('v')[0] + col[:type] = data.slice!(0,1).unpack('C')[0] + + case col[:type] + when 48 + col[:id] = :tinyint + + when 52 + col[:id] = :smallint + + when 56 + col[:id] = :rawint + + when 61 + col[:id] = :datetime + + when 34 + col[:id] = :image + col[:max_size] = data.slice!(0,4).unpack('V')[0] + col[:value_length] = data.slice!(0,2).unpack('v')[0] + col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '') + + when 36 + col[:id] = :string + + when 38 + col[:id] = :int + col[:int_size] = data.slice!(0,1).unpack('C')[0] + + when 127 + col[:id] = :bigint + + when 165 + col[:id] = :hex + col[:max_size] = data.slice!(0,2).unpack('v')[0] + + when 173 + col[:id] = :hex # binary(2) + col[:max_size] = data.slice!(0,2).unpack('v')[0] + + when 231,175,167,173,239 + col[:id] = :string + col[:max_size] = data.slice!(0,2).unpack('v')[0] + col[:codepage] = data.slice!(0,2).unpack('v')[0] + col[:cflags] = data.slice!(0,2).unpack('v')[0] + col[:charset_id] = data.slice!(0,1).unpack('C')[0] + + else + col[:id] = :unknown + end + + col[:msg_len] = data.slice!(0,1).unpack('C')[0] + + if(col[:msg_len] and col[:msg_len] > 0) + col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '') + end + info[:colnames] << (col[:name] || 'NULL') end + end + + def mssql_parse_reply(data, info) + info[:errors] = [] until data.empty? token = data.slice!(0,1).unpack('C')[0] case token - when 0xa0 - mssql_parse_column_name(data, info) - when 0xa1 - mssql_parse_column_info(data, info) + when 0x81 + mssql_parse_tds_reply(data, info) when 0xd1 - mssql_parse_row(data, info) + mssql_parse_tds_row(data, info) + when 0xe3 + mssql_parse_env(data, info) when 0x79 mssql_parse_ret(data, info) when 0xfd, 0xfe, 0xff mssql_parse_done(data, info) + when 0xad + mssql_parse_login_ack(data, info) + when 0xab + mssql_parse_info(data, info) when 0xaa mssql_parse_error(data, info) when nil break else - # info[:errors] << "unsupported token: #{token}" + info[:errors] << "unsupported token: #{token}" end end - - info - end - - def mssql_parse_column_name(data, info) - len = data.slice!(0,2).unpack('v')[0] - str = data.slice!(0,len) - info[:colnames] ||= [] - while(not str.empty?) - len = str.slice!(0,1).unpack('C')[0] - col = len == 0 ? 'NULL' : str.slice!(0,len) - info[:colnames] << col - end info end - def mssql_parse_column_info(data, info) - len = data.slice!(0,2).unpack('v')[0] - str = data.slice!(0,len) - - info[:colinfos] ||= [] - - idx = -1 - - while(not str.empty?) - - idx += 1 - - ### int - if( - str[0,5] == "\x07\x00\x20\x00\x38" or - str[0,5] == "\x07\x00\x08\x00\x38" or # int - false - ) - str.slice!(0,5) - info[:colinfos] << [:int, 4] - next - end - - ### smallint - if( - str[0,5] == "\x0d\x00\x09\x00\x26" or - str[0,5] == "\x0d\x00\x21\x00\x26" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:smallint, str.slice!(0,1).unpack('C')[0]] - next - end - - ### smallint2 - if( - str[0,5] == "\x06\x00\x08\x00\x34" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:smallint2, 2] - next - end - - ### image - if( - str[0,9] == "\x14\x00\x21\x00\x22\x00\x10\x00\x00" or - false - ) - str.slice!(0,9) - len = str.slice!(0,2).unpack('v')[0] - nam = str.slice!(0,len) - info[:colinfos] << [:image, 0, nam] - next - end - - ### tinyint - if( - str[0,5] == "\x05\x00\x08\x00\x30" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:tinyint, 1] - next - end - - - ### longint - if( - str[0,6] == "\x19\x00\x20\x00\x6c\x11" or - str[0,6] == "\x19\x00\x21\x00\x6c\x11" or - false - ) - str.slice!(0,6) - info[:colinfos] << [:long, str.slice!(0,2).unpack("v")[0]] - next - end - - ### hex - if(str[0,5] == "\x04\x00\x20\x00\x25") - str.slice!(0,5) - info[:colinfos] << [:hex, str.slice!(0,1).unpack('C')[0]] - next - end - - ### string - if( - str[0,5] == "\x02\x00\x21\x00\x27" or # varchar - str[0,5] == "\x02\x00\x08\x00\x27" or - str[0,5] == "\x02\x00\x01\x00\x27" or - str[0,5] == "\x12\x00\x08\x00\x27" or - str[0,5] == "\x12\x00\x09\x00\x27" or - str[0,5] == "\x04\x00\x09\x00\x25" or # varbinary - str[0,5] == "\x04\x00\x21\x00\x25" or - str[0,5] == "\x04\x00\x09\x00\x25" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:string, str.slice!(0,1).unpack('C')[0]] - next - end - - ### char(x) - if( - str[0,5] == "\x01\x00\x08\x00\x2f" or - str[0,5] == "\x02\x00\x09\x00\x27" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:string, str.slice!(0,1).unpack("C")[0]] - next - end - - ### datetime - if( - str[0,5] == "\x0c\x00\x08\x00\x3d" or - false - ) - str.slice!(0,5) - info[:colinfos] << [:datetime, 8] - next - end - - ### datetime2 - if( - str[0,6] == "\x0f\x00\x21\x00\x6f\x08" or - false - ) - str.slice!(0,6) - info[:colinfos] << [:datetime2, 8] - next - end - - ### binary - if( - str[0,6] == "\x04\x00\x21\x00\x25\x06" or - false - ) - str.slice!(0,6) - info[:colinfos] << [:binary] - next - end - - info[:errors] << "unknown column type: #{info[:colnames][idx]} == #{str.unpack("H*")[0]}" - info[:colinfos] << [:unknown, 6] - break - end - info - end - def mssql_parse_row(data, info) - + def mssql_parse_tds_row(data, info) info[:rows] ||= [] row = [] info[:colinfos].each do |col| - case col[0] - when :int - row << data.slice!(0,4).unpack('V')[0] - - when :smallint - len = data.slice!(0,1).unpack('C')[0] - raw = data.slice!(0,len) - row << raw.unpack('v')[0] - when :smallint2 - row << data.slice!(0,2).unpack('v')[0] + if(data.length == 0) + row << "" + next + end - when :tinyint - row << data.slice!(0,1).unpack('C')[0] - - when :long - - # XXX: completely made up bignum parsing... - - len = data.slice!(0,1).unpack('C')[0] - raw = data.slice!(0,len) - sum = 0 - bit = raw.unpack("C*").reverse - bit.each_index do |idx| - base = (256 ** (bit.length - idx - 1)) - if (base > 0) - sum += base * bit[idx] - else - sum += bit[idx] - end - end - - row << "?#{raw.unpack("H*")[0]}" - + case col[:id] when :hex - len = data.slice!(0,1).unpack('C')[0] - row << ((len > 0) ? data.slice!(0,len) : '') - + str = "" + len = data.slice!(0,2).unpack('v')[0] + if(len > 0 and len < 65535) + str << data.slice!(0,len) + end + row << str.unpack("H*")[0] + when :string - len = data.slice!(0,1).unpack('C')[0] - row << ((len > 0) ? data.slice!(0,len) : '') + str = "" + len = data.slice!(0,2).unpack('v')[0] + if(len > 0 and len < 65535) + str << data.slice!(0,len) + end + row << str.gsub("\x00", '') when :datetime - # XXX: convert to unix time row << data.slice!(0,8).unpack("H*")[0] - - when :datetime2 - # XXX: convert to unix time - len = data.slice!(0,1).unpack('C')[0] - row << data.slice!(0,len).unpack("H*")[0] - - when :image - len = data.slice!(0,1).unpack('C')[0] - row << ((len > 0) ? data.slice!(0,len).unpack("H*")[0] : '') - when :binary + when :rawint + row << data.slice!(0,4).unpack('V')[0] + + when :bigint + row << data.slice!(0,8).unpack("H*")[0] + + when :smallint + row << data.slice!(0, 2).unpack("v")[0] + + when :smallint3 + row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0] + + when :tinyint + row << data.slice!(0, 1).unpack("C")[0] + + when :image + str = '' len = data.slice!(0,1).unpack('C')[0] - row << ((len > 0) ? data.slice!(0,len).unpack("H*")[0] : '') - - when :unknown - len = data.slice!(0,1).unpack('C')[0] - row << "????-" + data.slice!(0,len) if len + str = data.slice!(0,len) if (len and len > 0) + row << str.unpack("H*")[0] + + when :int + len = data.slice!(0, 1).unpack("C")[0] + raw = data.slice!(0, len) if (len and len > 0) + + case len + when 0,255 + row << '' + when 1 + row << raw.unpack("C")[0] + when 2 + row << raw.unpack('v')[0] + when 4 + row << raw.unpack('V')[0] + when 5 + row << raw.unpack('V')[0] # XXX: missing high byte + when 8 + row << raw.unpack('VV')[0] # XXX: missing high dword + else + info[:errors] << "invalid integer size: #{len} #{data[0,16].unpack("H*")[0]}" + end + else + info[:errors] << "unknown column type: #{col.inspect}" end - end + end + info[:rows] << row info - end + end def mssql_parse_ret(data, info) ret = data.slice!(0,4).unpack('N')[0] info[:ret] = ret info end - + def mssql_parse_done(data, info) - status, cmd, rows = data.slice!(0,8).unpack('vvV') + status,cmd,rows = data.slice!(0,8).unpack('vvV') info[:done] = { :status => status, :cmd => cmd, :rows => rows } info end - + def mssql_parse_error(data, info) len = data.slice!(0,2).unpack('v')[0] buff = data.slice!(0,len) errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv') - emsg = buff.slice!(0,elen) + emsg = buff.slice!(0,elen * 2) + emsg.gsub!("\x00", '') + info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}" info end + def mssql_parse_env(data, info) + len = data.slice!(0,2).unpack('v')[0] + buff = data.slice!(0,len) + type = buff.slice!(0,1).unpack('C')[0] + + nval = '' + nlen = buff.slice!(0,1).unpack('C')[0] || 0 + nval = buff.slice!(0,nlen*2).gsub("\x00", '') if nlen > 0 + + oval = '' + olen = buff.slice!(0,1).unpack('C')[0] || 0 + oval = buff.slice!(0,olen*2).gsub("\x00", '') if olen > 0 + + info[:envs] ||= [] + info[:envs] << { :type => type, :old => oval, :new => nval } + info + end + + def mssql_parse_info(data, info) + len = data.slice!(0,2).unpack('v')[0] + buff = data.slice!(0,len) + + errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv') + emsg = buff.slice!(0,elen * 2) + emsg.gsub!("\x00", '') + + info[:infos]||= [] + info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}" + info + end + + def mssql_parse_login_ack(data, info) + len = data.slice!(0,2).unpack('v')[0] + buff = data.slice!(0,len) + info[:login_ack] = true + end + end end