1011 lines
29 KiB
Lua
1011 lines
29 KiB
Lua
--- A minimalistic PPPoE (Point-to-point protocol over Ethernet)
|
|
-- library, implementing basic support for PPPoE
|
|
-- Discovery and Configuration requests. The PPPoE protocol is ethernet based
|
|
-- and hence does not use any IPs or port numbers.
|
|
--
|
|
-- The library contains a number of classes to support packet creation,
|
|
-- parsing and sending/receiving responses. The classes are:
|
|
-- o LCP - Contains classes to build and parse PPPoE LCP requests and
|
|
-- responses.
|
|
--
|
|
-- o PPPoE - Contains classes to build and parse PPPoE requests and
|
|
-- responses.
|
|
--
|
|
-- o Comm - Contains some basic functions for sending and receiving
|
|
-- LCP and PPPoE requests and responses.
|
|
--
|
|
-- o Helper- The Helper class serves as the main interface between scripts
|
|
-- and the library.
|
|
--
|
|
--
|
|
-- @author Patrik Karlsson <patrik@cqure.net>
|
|
--
|
|
|
|
local rand = require "rand"
|
|
local nmap = require "nmap"
|
|
local packet = require "packet"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local table = require "table"
|
|
_ENV = stdnse.module("pppoe", stdnse.seeall)
|
|
|
|
|
|
EtherType = {
|
|
PPPOE_DISCOVERY = 0x8863,
|
|
PPPOE_SESSION = 0x8864,
|
|
}
|
|
|
|
-- A Class to handle the Link Control Protocol LCP
|
|
LCP = {
|
|
|
|
ConfigOption = {
|
|
|
|
RESERVED = 0,
|
|
MRU = 1,
|
|
AUTH_PROTO = 3,
|
|
QUAL_PROTO = 4,
|
|
MAGIC_NUMBER = 5,
|
|
PROTO_COMPR = 7,
|
|
ACFC = 8,
|
|
|
|
-- Value has already been encoded, treat it as a byte stream
|
|
RAW = -1,
|
|
|
|
-- Creates a new config option
|
|
-- @param option number containing the configuration option
|
|
-- @param value containing the configuration option value
|
|
-- @param raw string containing the configuration options raw value
|
|
-- @return o new instance of ConfigOption
|
|
new = function(self, option, value, raw)
|
|
local o = {
|
|
option = option,
|
|
value = value,
|
|
raw = raw,
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the ConfigOption
|
|
-- class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of ConfigOption
|
|
parse = function(data)
|
|
local opt, pos, len = {}, 1, 0
|
|
opt.option, len, pos = string.unpack("BB", data, pos)
|
|
opt.raw, pos = string.unpack("c" .. ( len - 2 ), data, pos)
|
|
|
|
-- MRU
|
|
if ( 1 == opt.option ) then
|
|
opt.value = string.unpack(">I2", opt.raw)
|
|
end
|
|
return LCP.ConfigOption:new(opt.option, opt.value, opt.raw)
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
-- MRU
|
|
if ( self.raw ) then
|
|
return string.pack(">BB", self.option, #self.raw + 2) .. self.raw
|
|
elseif( 1 == self.option ) then
|
|
return string.pack(">BBI2", 1, 4, self.value)
|
|
else
|
|
error( ("Unsupported configuration option %d"):format(self.option) )
|
|
end
|
|
end,
|
|
},
|
|
|
|
-- A class to hold multiple ordered config options
|
|
ConfigOptions = {
|
|
|
|
new = function(self, options)
|
|
local o = {
|
|
options = options or {},
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Adds a new config option to the table
|
|
-- @param option instance of ConfigOption
|
|
add = function(self, option)
|
|
table.insert(self.options, option)
|
|
end,
|
|
|
|
-- Gets a config option by ID
|
|
-- @param opt number containing the configuration option to retrieve
|
|
-- @return v instance of ConfigOption
|
|
getById = function(self, opt)
|
|
for _, v in ipairs(self.options) do
|
|
if ( v.option == opt ) then
|
|
return v
|
|
end
|
|
end
|
|
end,
|
|
|
|
-- Returns all config options in an ordered table
|
|
-- @return tab table containing all configuration options
|
|
getTable = function(self)
|
|
local tab = {}
|
|
for _, v in ipairs(self.options) do
|
|
table.insert(tab, v)
|
|
end
|
|
return tab
|
|
end,
|
|
|
|
|
|
-- Parses a byte stream and builds a new instance of the ConfigOptions
|
|
-- class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of ConfigOption
|
|
parse = function(data)
|
|
local options = LCP.ConfigOptions:new()
|
|
local pos, opt, opt_val, len
|
|
|
|
repeat
|
|
opt, len, pos = string.unpack(">BB", data, pos)
|
|
if ( 0 == opt ) then break end
|
|
opt_val, pos = string.unpack("c"..len, data, (pos - 2))
|
|
options:add(LCP.ConfigOption.parse(opt_val))
|
|
until( pos == #data )
|
|
return options
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
local str = ""
|
|
for _, v in ipairs(self.options) do
|
|
str = str .. tostring(v)
|
|
end
|
|
return str
|
|
end,
|
|
|
|
},
|
|
|
|
ConfigOptionName = {
|
|
[0] = "Reserved",
|
|
[1] = "Maximum receive unit",
|
|
[3] = "Authentication protocol",
|
|
[4] = "Quality protocol",
|
|
[5] = "Magic number",
|
|
[7] = "Protocol field compression",
|
|
[8] = "Address and control field compression",
|
|
},
|
|
|
|
Code = {
|
|
CONFIG_REQUEST = 1,
|
|
CONFIG_ACK = 2,
|
|
CONFIG_NAK = 3,
|
|
TERMINATE_REQUEST = 5,
|
|
TERMINATE_ACK = 6,
|
|
},
|
|
|
|
-- The LCP Header
|
|
Header = {
|
|
|
|
-- Creates a new instance of the LCP header
|
|
-- @param code number containing the LCP code of the request
|
|
-- @param identifier number containing the LCP identifier
|
|
new = function(self, code, identifier)
|
|
local o = {
|
|
code = code,
|
|
identifier = identifier or 1,
|
|
length = 0,
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
|
|
-- Parses a byte stream and builds a new instance of the Header class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of ConfigOption
|
|
parse = function(data)
|
|
local header = LCP.Header:new()
|
|
header.code, header.identifier, header.length = string.unpack(">BBI2", data)
|
|
return header
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
return string.pack(">BBI2", self.code, self.identifier, self.length)
|
|
end,
|
|
|
|
},
|
|
|
|
ConfigRequest = {
|
|
|
|
-- Creates a new instance of the ConfigRequest class
|
|
-- @param identifier number containing the LCP identifier
|
|
-- @param options table of <code>LCP.ConfigOption</code> options
|
|
-- @return o instance of ConfigRequest
|
|
new = function(self, identifier, options)
|
|
local o = {
|
|
header = LCP.Header:new(LCP.Code.CONFIG_REQUEST, identifier),
|
|
options = LCP.ConfigOptions:new(options)
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the ConfigRequest
|
|
-- class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of ConfigRequest
|
|
parse = function(data)
|
|
local req = LCP.ConfigRequest:new()
|
|
req.header = LCP.Header.parse(data)
|
|
req.options = LCP.ConfigOptions.parse(data:sub(#tostring(req.header) + 1))
|
|
return req
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
self.header.length = 4 + #tostring(self.options)
|
|
return tostring(self.header) .. tostring(self.options)
|
|
end,
|
|
},
|
|
|
|
ConfigNak = {
|
|
|
|
-- Creates a new instance of the ConfigNak class
|
|
-- @param identifier number containing the LCP identifier
|
|
-- @param options table of <code>LCP.ConfigOption</code> options
|
|
-- @return o instance of ConfigNak
|
|
new = function(self, identifier, options)
|
|
local o = {
|
|
header = LCP.Header:new(LCP.Code.CONFIG_NAK, identifier),
|
|
options = LCP.ConfigOptions:new(options),
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
self.header.length = 4 + #tostring(self.options)
|
|
return tostring(self.header) .. tostring(self.options)
|
|
end,
|
|
},
|
|
|
|
ConfigAck = {
|
|
|
|
-- Creates a new instance of the ConfigAck class
|
|
-- @param identifier number containing the LCP identifier
|
|
-- @param options table of <code>LCP.ConfigOption</code> options
|
|
-- @return o instance of ConfigNak
|
|
new = function(self, identifier, options)
|
|
local o = {
|
|
header = LCP.Header:new(LCP.Code.CONFIG_ACK, identifier),
|
|
options = LCP.ConfigOptions:new(options),
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the ConfigAck class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of ConfigRequest
|
|
parse = function(data)
|
|
local ack = LCP.ConfigAck:new()
|
|
ack.header = LCP.Header.parse(data)
|
|
ack.options = LCP.ConfigOptions.parse(data:sub(#tostring(ack.header) + 1))
|
|
return ack
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
self.header.length = 4 + #tostring(self.options)
|
|
return tostring(self.header) .. tostring(self.options)
|
|
end,
|
|
|
|
},
|
|
|
|
TerminateRequest = {
|
|
|
|
-- Creates a new instance of the TerminateRequest class
|
|
-- @param identifier number containing the LCP identifier
|
|
-- @return o instance of ConfigNak
|
|
new = function(self, identifier, data)
|
|
local o = {
|
|
header = LCP.Header:new(LCP.Code.TERMINATE_REQUEST, identifier),
|
|
data = data or "",
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Converts the class instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
self.header.length = 4 + #self.data
|
|
return tostring(self.header) .. self.data
|
|
end,
|
|
}
|
|
|
|
}
|
|
|
|
-- The PPPoE class
|
|
PPPoE = {
|
|
|
|
-- Supported PPPoE codes (requests/responses)
|
|
Code = {
|
|
SESSION_DATA = 0x00,
|
|
PADO = 0x07,
|
|
PADI = 0x09,
|
|
PADR = 0x19,
|
|
PADS = 0x65,
|
|
PADT = 0xa7,
|
|
},
|
|
|
|
-- Support PPPoE Tag types
|
|
TagType = {
|
|
SERVICE_NAME = 0x0101,
|
|
AC_NAME = 0x0102,
|
|
HOST_UNIQUE = 0x0103,
|
|
AC_COOKIE = 0x0104,
|
|
},
|
|
|
|
-- Table used to convert table IDs to Names
|
|
TagName = {
|
|
[0x0101] = "Service-Name",
|
|
[0x0102] = "AC-Name",
|
|
[0x0103] = "Host-Uniq",
|
|
[0x0104] = "AC-Cookie",
|
|
},
|
|
|
|
|
|
Header = {
|
|
|
|
-- Creates a new instance of the PPPoE header class
|
|
-- @param code number containing the PPPoE code
|
|
-- @param session number containing the PPPoE session
|
|
-- @return o instance of Header
|
|
new = function(self, code, session)
|
|
local o = {
|
|
version = 1,
|
|
type = 1,
|
|
code = code,
|
|
session = session or 0,
|
|
length = 0,
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of Header
|
|
parse = function(data)
|
|
local header = PPPoE.Header:new()
|
|
local vertyp
|
|
vertyp, header.code, header.session, header.length = string.unpack(">BBI2I2", data)
|
|
header.version = (vertyp >> 4)
|
|
header.type = (vertyp & 0x0F)
|
|
return header
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
local vertype = (self.version << 4) + self.type
|
|
return string.pack(">BBI2I2", vertype, self.code, self.session, self.length)
|
|
end,
|
|
|
|
|
|
},
|
|
|
|
-- The TAG NVP Class
|
|
Tag = {
|
|
|
|
-- Creates a new instance of the Tag class
|
|
-- @param tag number containing the tag type
|
|
-- @param value string/number containing the tag value
|
|
-- @return o instance of Tag
|
|
new = function(self, tag, value)
|
|
local o = { tag = tag, value = value or "" }
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
return string.pack(">I2s2", self.tag, self.value)
|
|
end,
|
|
},
|
|
|
|
PADI = {
|
|
|
|
-- Creates a new instance of the PADI class
|
|
-- @param tags table of <code>PPPoE.Tag</code> instances
|
|
-- @param value string/number containing the tag value
|
|
-- @return o instance of ConfigNak
|
|
new = function(self, tags)
|
|
local c = rand.random_string(8)
|
|
|
|
local o = {
|
|
header = PPPoE.Header:new(PPPoE.Code.PADI),
|
|
tags = tags or {
|
|
PPPoE.Tag:new(PPPoE.TagType.SERVICE_NAME),
|
|
PPPoE.Tag:new(PPPoE.TagType.HOST_UNIQUE, c)
|
|
}
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
local tags = ""
|
|
for _, tag in ipairs(self.tags) do
|
|
tags = tags .. tostring(tag)
|
|
end
|
|
self.header.length = #tags
|
|
return tostring(self.header) .. tags
|
|
end,
|
|
|
|
},
|
|
|
|
PADO = {
|
|
|
|
-- Creates a new instance of the PADO class
|
|
-- @return o instance of PADO
|
|
new = function(self)
|
|
local o = { tags = {} }
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of PADO
|
|
parse = function(data)
|
|
local pado = PPPoE.PADO:new()
|
|
pado.header = PPPoE.Header.parse(data)
|
|
local pos = #tostring(pado.header) + 1
|
|
pado.data = data:sub(pos)
|
|
|
|
repeat
|
|
local tag, decoded, raw
|
|
tag, raw, pos = string.unpack(">I2s2", pos)
|
|
if ( PPPoE.TagDecoder[tag] ) then
|
|
decoded = PPPoE.TagDecoder[tag](raw)
|
|
else
|
|
stdnse.debug1("PPPoE: Unsupported tag (%d)", tag)
|
|
end
|
|
local t = PPPoE.Tag:new(tag, raw)
|
|
t.decoded = decoded
|
|
table.insert(pado.tags, t)
|
|
until( pos >= #data )
|
|
|
|
return pado
|
|
end,
|
|
},
|
|
|
|
PADR = {
|
|
|
|
-- Creates a new instance of the PADR class
|
|
-- @param tags table of <code>PPPoE.Tag</code> instances
|
|
-- @return o instance of PADR
|
|
new = function(self, tags)
|
|
local o = {
|
|
tags = tags or {},
|
|
header = PPPoE.Header:new(PPPoE.Code.PADR)
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
local tags = ""
|
|
for _, tag in ipairs(self.tags) do
|
|
tags = tags .. tostring(tag)
|
|
end
|
|
self.header.length = #tags
|
|
return tostring(self.header) .. tags
|
|
end,
|
|
|
|
},
|
|
|
|
PADS = {
|
|
|
|
-- Creates a new instance of the PADS class
|
|
-- @return o instance of PADS
|
|
new = function(self)
|
|
local o = { tags = {} }
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of PADS
|
|
parse = function(data)
|
|
local pads = PPPoE.PADS:new()
|
|
pads.header = PPPoE.Header.parse(data)
|
|
local pos = #tostring(pads.header) + 1
|
|
pads.data = data:sub(pos)
|
|
return pads
|
|
end,
|
|
|
|
},
|
|
|
|
PADT = {
|
|
|
|
-- Creates a new instance of the PADT class
|
|
-- @param session number containing the PPPoE session
|
|
-- @return o instance of PADT
|
|
new = function(self, session)
|
|
local o = { header = PPPoE.Header:new(PPPoE.Code.PADT) }
|
|
setmetatable(o, self)
|
|
o.header.session = session
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of PADI
|
|
parse = function(data)
|
|
local padt = PPPoE.PADT:new()
|
|
padt.header = PPPoE.Header.parse(data)
|
|
return padt
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
return tostring(self.header)
|
|
end,
|
|
},
|
|
|
|
SessionData = {
|
|
|
|
-- Creates a new instance of the SessionData class
|
|
-- @param session number containing the PPPoE session
|
|
-- @param data string containing the LCP data to send
|
|
-- @return o instance of ConfigNak
|
|
new = function(self, session, data)
|
|
local o = {
|
|
data = data or "",
|
|
header = PPPoE.Header:new(PPPoE.Code.SESSION_DATA)
|
|
}
|
|
setmetatable(o, self)
|
|
o.header.session = session
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Parses a byte stream and builds a new instance of the class
|
|
-- @param data string containing raw bytes to parse
|
|
-- @return o instance of SessionData
|
|
parse = function(data)
|
|
local sess = PPPoE.SessionData:new()
|
|
sess.header = PPPoE.Header.parse(data)
|
|
local pos = #tostring(sess.header) + 1 + 2
|
|
sess.data = data:sub(pos)
|
|
return sess
|
|
end,
|
|
|
|
-- Converts the instance to string
|
|
-- @return string containing the raw config option
|
|
__tostring = function(self)
|
|
-- 2 for the encapsulation
|
|
self.header.length = 2 + 4 + #self.data
|
|
return tostring(self.header) .. "\xC0\x21" .. self.data
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
-- A bunch of tag decoders
|
|
PPPoE.TagDecoder = {}
|
|
PPPoE.TagDecoder.decodeHex = stdnse.tohex
|
|
PPPoE.TagDecoder.decodeStr = function(data) return data end
|
|
PPPoE.TagDecoder[PPPoE.TagType.SERVICE_NAME] = PPPoE.TagDecoder.decodeStr
|
|
PPPoE.TagDecoder[PPPoE.TagType.AC_NAME] = PPPoE.TagDecoder.decodeStr
|
|
PPPoE.TagDecoder[PPPoE.TagType.AC_COOKIE] = PPPoE.TagDecoder.decodeHex
|
|
PPPoE.TagDecoder[PPPoE.TagType.HOST_UNIQUE] = PPPoE.TagDecoder.decodeHex
|
|
|
|
-- The Comm class responsible for communication with the PPPoE server
|
|
Comm = {
|
|
|
|
-- Creates a new instance of the Comm class
|
|
-- @param iface string containing the interface name
|
|
-- @param src_mac string containing the source MAC address
|
|
-- @param dst_mac string containing the destination MAC address
|
|
-- @return o new instance of Comm
|
|
new = function(self, iface, src_mac, dst_mac)
|
|
local o = {
|
|
iface = iface,
|
|
src_mac = src_mac,
|
|
dst_mac = dst_mac,
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
-- Sets up the pcap receiving socket
|
|
-- @return status true on success
|
|
connect = function(self)
|
|
self.socket = nmap.new_socket()
|
|
self.socket:set_timeout(10000)
|
|
|
|
local mac = stdnse.format_mac(self.src_mac)
|
|
|
|
-- let's set a filter on PPPoE we can then check what packet is ours,
|
|
-- based on the HOST_UNIQUE tag, if we need to
|
|
self.socket:pcap_open(self.iface, 1500, false, "ether[0x0c:2] == 0x8863 or ether[0x0c:2] == 0x8864 and ether dst " .. mac)
|
|
return true
|
|
end,
|
|
|
|
-- Sends a packet
|
|
-- @param data class containing the request to send
|
|
-- @return status true on success, false on failure
|
|
send = function(self, data)
|
|
local eth_type = ( data.header.code == PPPoE.Code.SESSION_DATA ) and 0x8864 or 0x8863
|
|
local ether = self.dst_mac .. self.src_mac .. string.pack(">I2", eth_type)
|
|
local p = packet.Frame:new(ether .. tostring(data))
|
|
|
|
local sock = nmap.new_dnet()
|
|
if ( not(sock) ) then
|
|
return false, "Failed to create raw socket"
|
|
end
|
|
|
|
local status = sock:ethernet_open(self.iface)
|
|
-- we don't actually need to do this as the script simply crashes
|
|
-- if we don't have the right permissions at this point
|
|
if ( not(status) ) then
|
|
return false, "Failed to open raw socket"
|
|
end
|
|
|
|
status = sock:ethernet_send(p.frame_buf)
|
|
if ( not(status) ) then
|
|
return false, "Failed to send data"
|
|
end
|
|
sock:ethernet_close()
|
|
return true
|
|
end,
|
|
|
|
-- Receive a response from the server
|
|
-- @return status true on success, false on failure
|
|
-- @return response class containing the response or
|
|
-- err string on error
|
|
recv = function(self)
|
|
local status, _, l2, l3 = self.socket:pcap_receive()
|
|
-- if we got no response, just return false as there's
|
|
-- probably not really an error
|
|
if ( not(status) ) then
|
|
return false, "Did not receive any packets"
|
|
end
|
|
|
|
local header = PPPoE.Header.parse(l3)
|
|
local p = packet.Frame:new(l2..l3)
|
|
|
|
-- there's probably a more elegant way of doing this
|
|
if ( EtherType.PPPOE_DISCOVERY == p.ether_type ) then
|
|
if ( header.code == PPPoE.Code.PADO ) then
|
|
local pado = PPPoE.PADO.parse(l3)
|
|
pado.mac_srv = p.mac_src
|
|
return true, pado
|
|
elseif ( header.code == PPPoE.Code.PADS ) then
|
|
local pads = PPPoE.PADS.parse(l3)
|
|
return true, pads
|
|
elseif ( header.code == PPPoE.Code.PADT ) then
|
|
local pads = PPPoE.PADT.parse(l3)
|
|
return true, pads
|
|
end
|
|
elseif ( EtherType.PPPOE_SESSION == p.ether_type ) then
|
|
return true, PPPoE.SessionData.parse(l3)
|
|
end
|
|
return false, ("Received unsupported response, can't decode code (%d)"):format(header.code)
|
|
end,
|
|
|
|
-- Does an "exchange", ie, sends a request and waits for a response
|
|
-- @param data class containing the request to send
|
|
-- @return status true on success, false on failure
|
|
-- @return response class containing the response or
|
|
-- err string on error
|
|
exch = function(self, data)
|
|
local status, err = self:send(data)
|
|
if ( not(status) ) then
|
|
return false, err
|
|
end
|
|
local retries, resp = 3, nil
|
|
|
|
repeat
|
|
status, resp = self:recv()
|
|
if ( data.header and 0 == data.header.session ) then
|
|
return true, resp
|
|
elseif ( data.header and data.header.session == resp.header.session ) then
|
|
return true, resp
|
|
end
|
|
retries = retries - 1
|
|
until(retries == 0)
|
|
|
|
return false, "Failed to retrieve proper PPPoE response"
|
|
end,
|
|
|
|
}
|
|
|
|
-- The Helper class is the main script interface
|
|
Helper = {
|
|
|
|
-- Creates a new instance of Helper
|
|
-- @param iface string containing the name of the interface to use
|
|
-- @return o new instance on success, nil on failure
|
|
new = function(self, iface)
|
|
local o = {
|
|
iface = iface,
|
|
|
|
-- set the LCP identifier to 0
|
|
identifier = 0,
|
|
}
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
|
|
if ( not(nmap.is_privileged()) ) then
|
|
return nil, "The PPPoE library requires Nmap to be run in privileged mode"
|
|
end
|
|
|
|
-- get src_mac
|
|
local info = nmap.get_interface_info(iface)
|
|
if ( not(info) or not(info.mac) ) then
|
|
return nil, "Failed to get source MAC address"
|
|
end
|
|
o.comm = Comm:new(iface, info.mac)
|
|
return o
|
|
end,
|
|
|
|
-- Sets up the pcap socket for listening and does some other preparations
|
|
-- @return status true on success, false on failure
|
|
connect = function(self)
|
|
return self.comm:connect()
|
|
end,
|
|
|
|
|
|
-- Performs a PPPoE discovery initiation by sending a PADI request to the
|
|
-- ethernet broadcast address
|
|
-- @return status true on success, false on failure
|
|
-- @return pado instance of PADO on success, err string on failure
|
|
discoverInit = function(self)
|
|
local padi = PPPoE.PADI:new()
|
|
self.comm.dst_mac = ("\xFF"):rep(6)
|
|
local status, err = self.comm:send(padi)
|
|
if ( not(status) ) then
|
|
return false, err
|
|
end
|
|
-- wait for a pado
|
|
local pado, retries = nil, 3
|
|
|
|
repeat
|
|
status, pado = self.comm:recv()
|
|
if ( not(status) ) then
|
|
return status, pado
|
|
end
|
|
retries = retries - 1
|
|
until( pado.tags or retries == 0 )
|
|
if ( not(pado.tags) ) then
|
|
return false, "PADO response contained no tags"
|
|
end
|
|
|
|
local pado_host_unique
|
|
for _, tag in ipairs(pado.tags) do
|
|
if ( PPPoE.TagType.HOST_UNIQUE == tag.tag ) then
|
|
pado_host_unique = tag.raw
|
|
end
|
|
end
|
|
|
|
-- store the tags for later use
|
|
self.tags = pado.tags
|
|
self.comm.dst_mac = pado.mac_srv
|
|
|
|
if ( pado_host_unique and
|
|
pado_host_unique ~= padi.tags[PPPoE.TagType.HOST_UNIQUE] ) then
|
|
-- currently, we don't handle this, we probably should
|
|
-- in order to do so, we need to split the function exch
|
|
-- to recv and send
|
|
return false, "Got incorrect answer"
|
|
end
|
|
|
|
return true, pado
|
|
end,
|
|
|
|
-- Performs a Discovery Request by sending PADR to the PPPoE ethernet
|
|
-- address
|
|
-- @return status true on success, false on failure
|
|
-- @return pads instance of PADS on success
|
|
discoverRequest = function(self)
|
|
|
|
-- remove the AC-Name tag if there is one
|
|
local function getTag(tag)
|
|
for _, t in ipairs(self.tags) do
|
|
if ( tag == t.tag ) then
|
|
return t
|
|
end
|
|
end
|
|
end
|
|
|
|
local taglist = {
|
|
PPPoE.TagType.SERVICE_NAME,
|
|
PPPoE.TagType.HOST_UNIQUE,
|
|
PPPoE.TagType.AC_COOKIE
|
|
}
|
|
|
|
local tags = {}
|
|
for _, t in ipairs(taglist) do
|
|
if ( getTag(t) ) then
|
|
table.insert(tags, getTag(t))
|
|
end
|
|
end
|
|
|
|
local padr = PPPoE.PADR:new(tags)
|
|
local status, pads = self.comm:exch(padr)
|
|
|
|
if ( status ) then
|
|
self.session = pads.header.session
|
|
end
|
|
|
|
return status, pads
|
|
end,
|
|
|
|
-- Attempts to specify a method for authentication
|
|
-- If the server responds with another method it's NAK:ed and we try to set
|
|
-- our requested method instead. If this fails, we return a failure
|
|
-- @param method string containing one of the following methods:
|
|
-- <code>MSCHAPv1</code>, <code>MSCHAPv2</code> or <code>PAP</code>
|
|
-- @return status true on success, false on failure
|
|
-- err string containing error message on failure
|
|
setAuthMethod = function(self, method)
|
|
|
|
local AuthMethod = {
|
|
methods = {
|
|
{ name = "EAP", value = "\xC2\x27" },
|
|
{ name = "MSCHAPv1", value = "\xC2\x23\x80" },
|
|
{ name = "MSCHAPv2", value = "\xC2\x23\x81" },
|
|
{ name = "PAP", value = "\xC0\x23" },
|
|
}
|
|
}
|
|
|
|
AuthMethod.byName = function(name)
|
|
for _, m in ipairs(AuthMethod.methods) do
|
|
if ( m.name == name ) then
|
|
return m
|
|
end
|
|
end
|
|
end
|
|
|
|
AuthMethod.byValue = function(value)
|
|
for _, m in ipairs(AuthMethod.methods) do
|
|
if ( m.value == value ) then
|
|
return m
|
|
end
|
|
end
|
|
end
|
|
|
|
local auth_data = ( AuthMethod.byName(method) and AuthMethod.byName(method).value )
|
|
if ( not(auth_data) ) then
|
|
return false, ("Unsupported authentication mode (%s)"):format(method)
|
|
end
|
|
|
|
self.identifier = self.identifier + 1
|
|
|
|
-- First do a Configuration Request
|
|
local options = { LCP.ConfigOption:new(LCP.ConfigOption.MRU, 1492) }
|
|
local lcp_req = LCP.ConfigRequest:new(self.identifier, options)
|
|
local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
|
|
local status, resp = self.comm:exch(sess_req)
|
|
|
|
if ( not(status) or PPPoE.Code.SESSION_DATA ~= resp.header.code ) then
|
|
return false, "Unexpected packet type was received"
|
|
end
|
|
|
|
-- Make sure we got a Configuration Request in return
|
|
local lcp_header = LCP.Header.parse(resp.data)
|
|
if ( LCP.Code.CONFIG_REQUEST ~= lcp_header.code ) then
|
|
return false, ("Unexpected packet type was received (%d)"):format(lcp_header.code)
|
|
end
|
|
|
|
local config_req = LCP.ConfigRequest.parse(resp.data)
|
|
if ( not(config_req.options) ) then
|
|
return false, "Failed to retrieve any options from response"
|
|
end
|
|
|
|
local auth_proposed = config_req.options:getById(LCP.ConfigOption.AUTH_PROTO)
|
|
|
|
if ( auth_proposed.raw ~= auth_data ) then
|
|
local options = { LCP.ConfigOption:new(LCP.ConfigOption.AUTH_PROTO, nil, auth_data) }
|
|
local lcp_req = LCP.ConfigNak:new(self.identifier, options)
|
|
local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
|
|
local status, resp = self.comm:exch(sess_req)
|
|
|
|
if ( not(status) or PPPoE.Code.SESSION_DATA ~= resp.header.code ) then
|
|
return false, "Unexpected packet type was received"
|
|
end
|
|
|
|
-- Make sure we got a Configuration Request in return
|
|
local lcp_header = LCP.Header.parse(resp.data)
|
|
if ( LCP.Code.CONFIG_REQUEST ~= lcp_header.code ) then
|
|
return false, ("Unexpected packet type was received (%d)"):format(lcp_header.code)
|
|
end
|
|
|
|
config_req = LCP.ConfigRequest.parse(resp.data)
|
|
|
|
-- if the authentication methods match, send an ACK
|
|
if ( config_req.options:getById(LCP.ConfigOption.AUTH_PROTO).raw == auth_data ) then
|
|
-- The ACK is essential the Config Request, only with a different code
|
|
-- Do a dirty attempt to just replace the code and send the request back as an ack
|
|
self.identifier = self.identifier + 1
|
|
|
|
local lcp_req = LCP.ConfigAck:new(config_req.header.identifier, config_req.options:getTable())
|
|
local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
|
|
local status, resp = self.comm:send(sess_req)
|
|
|
|
return true
|
|
end
|
|
|
|
return false, "Authentication method was not accepted"
|
|
end
|
|
|
|
|
|
return false, "Failed to negotiate authentication mechanism"
|
|
end,
|
|
|
|
-- Sends a LCP Terminate Request and waits for an ACK
|
|
-- Attempts to do so 10 times before aborting
|
|
-- @return status true on success false on failure
|
|
close = function(self)
|
|
local tries = 10
|
|
repeat
|
|
if ( 0 == self.session ) then
|
|
break
|
|
end
|
|
local lcp_req = LCP.TerminateRequest:new(self.identifier)
|
|
local sess_req = PPPoE.SessionData:new(self.session, tostring(lcp_req))
|
|
local status, resp = self.comm:exch(sess_req)
|
|
if ( status and resp.header and resp.header.code ) then
|
|
if ( PPPoE.Code.SESSION_DATA == resp.header.code ) then
|
|
local lcp_header = LCP.Header.parse(resp.data)
|
|
if ( LCP.Code.TERMINATE_ACK == lcp_header.code ) then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
tries = tries - 1
|
|
until( tries == 0 )
|
|
|
|
self.comm:exch(PPPoE.PADT:new(self.session))
|
|
|
|
return true
|
|
end,
|
|
|
|
}
|
|
|
|
return _ENV;
|