Portscanning serves a legitimate role in system
administration/ownership. By confirming exactly what ports a computer
accepts connections on, it's possible to ensure that an operating
system hasn't opened unnecessary ports to the world.
UDP Portscanning in PHP
While TCP port scanning of one's own ports is common, many
underestimate the potential hazards of open UDP ports -- this can lead
to compromise, or indicate that a compromise has occurred. As the nmap
manpage says:
"There is also the cDc Back Orifice backdoor program which hides on
a configurable UDP port on Windows machines. Not to mention the many
commonly vulnerable services that utilize UDP such as snmp, tftp, NFS,
etc."
The single largest impediment to the average home user conducting
port scans of their own machines is the lack of simple software to
conduct the scan for them. Don't get me wrong -- nmap is one of the
best portscanning tools available -- but how comfortable is Joe Average
User going to be using a *nix command line tool? It seems to me that a
tool that only requires Mr. Average User to go to a specific URL and
wait while a PHP script runs a TCP and UDP scan against their machine,
and then returns the results of the scan to them, is just what the
doctor ordered.
With this in mind, I started searching the Internet and found an
implementation of a TCP port scanner that Jim Barcelona had coded and
made available at php wizard.
Great! With half of the work done, I should be able to whip out a
UDP port scanner in no time. I mean, how much harder could coding a UDP
port scanner be? Considerably harder, as it turns out.
TCP Portscanning in PHP
When a TCP socket is created using the fsockopen
function, you specify the IP address of the remote machine and the port
number to which you want to connect. Using the underlying socket
functionality, PHP will then attempt to create a virtual circuit to the
remote machine on the specified port, in order to allow further
communication to occur. If the destination port is unavailable, then
the TCP provider on the remote machine will reject the connection
request and the fsockopen function will be return a boolean value of failed.
So we now have an easy to understand and implement TCP portscanner.
We set up a loop that specifies the minimum and maximum port numbers
that we wish to scan, and within the loop, attempt to open a connection
at the current value of the loop's index. If the attempt to open the
port fails, there's no service at that port. If we're able to
successfully open a socket to that port, then there's a service at that
port which we log in our array of open ports as a key. We then insert
the value for that key with a call to getservbyport,
which will return the unix service that's normally registered at that
port. Lastly, we close the open socket and move onto the next iteration
of our loop.
UDP Portscanning in PHP
When an application wishes to send data over the network using the
UDP protocol, it gives the data to UDP through the assigned port
number, also telling UDP which port on the destination system the data
should be sent to. UDP then creates a UDP message, marking the source
and destination port numbers, which is then passed off to IP for
delivery.
When a UDP packet is received by the destination machine, the UDP
software looks at the packet's header for the destination port number,
and hands the payload off to whatever application has registered as
using that port number. If no application has registered for the
specified port number, then an "ICMP Destination Unreachable: Port
Unreachable" Error Message is returned to the remote machine, and the
payload is discarded.
Creating a UDP socket in PHP is similar to creating a TCP socket, with a couple of differences. You still call the fsockopen function, but you must specify:
- that you want to use the UDP protocol,
- the IP address of the remote computer and
- the port number that you want to connect with.
In contrast to the TCP socket, at this point no connection exists to the remote computer.
function _scanPort ($portNumber) {
$handle = fsockopen($this-> targetIP, $portNumber, &$errno, &$errstr, 2);
if (!$handle) {
echo "$errno : $errstr <br/>";
}
socket_set_timeout ($handle, $this-> timeout);
$write = fwrite($handle,"\x00");
if (!$write) {
echo "error writing to port: $index.<br/>";
next;
}
$startTime = time();
$header = fread($handle, 1);
$endTime = time();
$timeDiff = $endTime - $startTime;
if ($timeDiff >= $this-> timeout) {
fclose($handle);
return 1;
} else {
fclose($handle);
return 0;
}
}
Because a call to read from our UDP socket will block (wait until it receives data), we set a value to the set_socket_timeout function.
This tells the socket to only listen (block) on the socket for a
response from the remote machine for a specific period of time. If that
time is exceeded, then the socket will stop listening, and the code
will continue to run. We'll see how we use this to discern an open port
shortly.
As UDP is connectionless, at this point a virtual circuit is not
setup with the remote machine. Instead, you have to pass the data that
you want to send to the remote machine through the socket using the fwrite function. We then immediately log the time that we begin listening for a response from the remote machine, and use fread to listen to the socket. At this point, one of two things can happen:
- the remote server returns an "ICMP Destination Unreachable: Port Unreachable" Error Message to us, and the fread ends, or
- the socket times out waiting for a response.
In either case, we now log the time that the listening ended, and
derive the total time spent listening by subtracting the end time from
the start time. We compare this number with the socket's timeout value
that we set. If it's less than the timeout value, then the remote
server returned an "ICMP Destination Unreachable: Port Unreachable"
Error Message to us, and we know that the port is closed. If the socket
timed out, then we know one of two other things occurred: the
application at that port was waiting to receive a valid command, or the
packet was lost in transit (UDP doesn't offer guaranteed delivery, and
if a packet is lost en route, we're not going to receive any
information to that effect).
So, we've established a means to discern whether there's anything at
the port (ie. the socket returned before it timed out), and we now need
a way to tell whether a socket timing out was the result of an
application waiting for legitimate data, or the packet was lost in
transit. The easiest way to do this would be to send multiple packets
to that port, and if we get one response back that doesn't timeout,
then we know that there's not an application there and the port is
closed. But how can we do this in a manner that's not too wasteful of
bandwidth, and minimizes the application's run time?
Prior to our full scan of the UDP ports that the class was
instructed to scan, we're going to conduct a smaller port scan very
high in the port range, to minimize the finding of legitimate open
ports, and test the network conditions between the machines. This
initial scan is carried out in our _networkProbe
method. Since most of the open UDP ports that are open are at or below
port 1024, we'll do a scan up in the port 55000 range. Any ports that
time out this high in the port range we can assume did so because of
lost datagrams, and not because we've detected an open port.
function _networkProbe ($noTrials=100, $startPortNumber=55000) {
$endPortNumber = $startPortNumber + $noTrials;
// temporarily set timeout to 2 seconds. we'll modify this with the
// data that we get from this method
$this-> timeout = 2;
// setup a for loop to scan the ports
for ($portNumber = $startPortNumber; $portNumber < $endPortNumber;
$portNumber++) {
$startTime = $this-> _getmicrotime();
$result = $this-> _scanPort($portNumber);
$endTime = $this-> _getmicrotime();
$timeDiff = $endTime - $startTime;
if (!$result) {
$responsesArray[] = $timeDiff;
$totalTime += $timeDiff;
}
}
$noResponses = count($responsesArray);
// if more than 40% of the datagrams timed out, abort the scan
if ($noResponses < (.6 * $noTrial)) {
echo "The connection is losing too many packets. Scan aborted. <br/>";
exit;
}
$averageResponseTime = $this-> _calcAvgResponseTime ($noResponses,
$totalTime);
$standardDeviation = $this-> _calcStdrDeviation ($responsesArray);
// calculate the timeout value
$timeoutValue = ceil($averageResponseTime + 4 * $standardDeviation);
// calculate number of cleanup iterations we'll need
// percentFalsePositive is the % of datagrams that we sent in
// the trial that timed out
$percentFalsePositives = ($noTrials - $noResponses)/$noTrials;
// percentResponses is the % of datagrams that we sent in the trial
// that returned (eg -- didn't timeout)
$percentResponses = $noResponses/$noTrials;
// calculate the total number of ports to be scanned in the
// real scan
$portRange = $this-> maxPort - $this-> minPort + 1;
// estFalsePositives is the estimated number of false positives we
// anticipate getting from the real scan
$estFalsePositives = $portRange * $percentFalsePositives;
$this-> cleanupIterations = $this->
_calcNoIterations ($estFalsePositives, $percentResponses, $portRange);
if ($this-> debug == 1) {
echo "<br/>";
echo "total time $totalTime<br/>";
echo "timeout value: " . $this-> timeout . "<br/>";
echo "cleanup iterations: " . $this-> cleanupIterations . "<br/>";
echo "<br/>";
flush();
}
}
So we now know:
- the number of packets that were lost from our sample,
- the total number of packets that were sent and
- the average time that it we had to listen for each packet that did respond without blocking.
With the number of packets lost from our sample, the total packets
sent with our initial scan, and the range of how many ports we're going
to scan in the main scan, we can calculate the number of iterations
that we'll have to run, and retest the open ports detected, to
eliminate false positives. The formula that we use to do this is an
exponential decay logarithm and it's functionallity can be found in the
_calcNoIterations method of the class.
We're also going to use the average response time of the fread calls
that returned an "ICMP Destination Unreachable: Port Unreachable" Error
Message and didn't block, to calculate the standard deviation of these
individual times. We multiply the standard deviation by a factor of
four (four sigma) and add it to our average response time. This allows
us to minimize the timeout value, and still be reasonably certain that
we're not eliminating too many scans that would've returned had it not
timed out. At this point, this check is actually superfluous given that
the set_socket_timeout value can't be set to a value of
less than one second, which is where most of the derived timeout values
are going to be. However, if the socket timeout value is ever modified
to accept values of less than one second, we can anticipate a runtime
decrease up to a factor of five in eliminating the ports that don't
have a service on them.
Using the UDP Portscanning Class
The UDP Port Scanning Class does an excellent job of abstracting the
complexities of UDP port scanning. Use of the class is simplicity
itself: include the class and new up an object based on the class
that's passing it the the target ip address, and optionally, the port
to start scanning at, the port to end scanning at, and whether the
object should output information as it conducts the scan. By default,
the start port is set to 1, the end port is set to 1024, and output is
on.
Then call the objects doScan method and assign its output to a variable that will hold the results of the scan as an array. Here's an example:
include ('classes/udpPortScanner.inc');
$udpScanner = new udpPortScanner("$REMOTE_ADDR");
$ports = $udpScanner-> doScan();
if (count($ports) == 0) {
echo "no open udp ports detected.<br/>";
} else {
echo "open udp ports:<br/>";
foreach ($ports as $portNumber => $service) {
echo "$portNumber ($service)<br/>";
}
}
Closing
There are two modifications that I have to make and another that I'd
like to make. First, the calculated value of the number of iterations
to eliminate false positives resulted in a number that is too low if
the network conditions are good, and I was getting ports returned that
were not, in fact, open. I modified the code to bump the minimum number
of cleanup iterations up to a value of five in order to compensate for
this.
A UDP port scanner is heavily reliant upon the network conditions
that exist between the two machines. In testing, I had friends in both
Australia and South Africa (I'm located in the U.S.) volunteer to be
scanned, and the scanner was having a really difficult time detecting
closed ports. This was because the network conditions were leading to a
very large number of packets being lost in transit, and very long
runtimes were encountered. In defense of the scanner, an nmap scan of
the first 1024 UDP ports on the host in South Africa took nearly an
hour, indicating that this is endemic to UDP port scanning, not to this
scanner's implementation. With this in mind, it became obvious that
some of the network connections were just too bad to complete a scan in
a reasonable amount of time. Because of this, I modified the code to
abort if, during the initial network testing scan, more than 40% of the
packets were lost. Feel free to eliminate or modify this value, but be
warned that scanning over a bad network span can take a considerable
amount of time.
Finally, I'd also like to collect information as to what ports Microsoft places its various services on, to complement the getservbyport
function, which will only return services that are mapped to the
traditional ports on a *nix box. I could then specify which Microsoft
service is running on a port, and in a later version, indicate how
comprimising this is, and describe methods to disable the service and
close the port if it's not in use.
|