Hacking

NMAP Scripting Example

Dejan Lukan
January 10, 2013 by
Dejan Lukan

1. Nmap API

When writing Nmap NSE scripts, we of course need to have a way to talk to the Nmap API, which provides us with various advanced features so we don't have to write those features ourselves. We can't do everything in LUA language that is used for writing NSE scripts, but we also need a way to talk to the Nmap API. For example: we need a way to check the information that was already gathered about the scanning host or network when running a script. This can be achieved by passing certain arguments to the NSE script within the action function. Let's present an example of the banner.nse action function:

What should you learn next?

What should you learn next?

From SOC Analyst to Secure Coder to Security Manager — our team of experts has 12 free training plans to help you hit your goals. Get your free copy now.

[plain]

action = function( host, port )

local out = grab_banner(host, port)

return output( out )

end

[/plain]

We can see that the action function accepts two parameters, the host and port. The host argument passed into the action function contains information about the host we're about to scan while the port argument contains the port about to be scanned. We can then use the host and port objects in the NSE script to request specific information that we require.

The host object has the following members:

- host.ip: the IP address of the target host

- host.name: the reverse DNS of the target host

- host.targetname: the name of the host specified in the Nmap command

- host.directly_connected: specifies if the scanning host is on the same local network as the scanned host

- host.mac_addr: the MAC address of the target host (only available if host.directly_connected is true)

- host.max_addr_src: our own MAC address

- host.interface: our own network interface

- host.interface_mtu: maximum transmission unit of our own interface

- host.bin_ip: the IP address of the target host

- host.bin_ip_src: our own IP address

- host.times: contains the RTT, SRTT, RTTVAR and timeout information about the target host

The port object has the following members:

- port.number: the port number of the target host

- port.protocol: the protocol of the target host

- port.service: the service running on the target host

- port.version: additional information about the service running on the target host

- port.state: what state the target port is in: open, open|filtered, filtered, closed.

When writing NSE scripts, we also have direct access to Nsock, which is the Nmap socket library. We can use the Nsock library to perform parallel non-blocking operations, which provides a powerful mechanism that allows running several scripts at the same time efficiently. With the Nsock library, we can achieve something like the following scenario: we can create a socket that connects to a specific IP address and port, send or receive some data, and then close the socket at the end. We can create a socket with the new_socket function call, which returns a new socket object that has the following methods:

- send: send some data to the socket

- receive: receive some data from the socket

- close: close the socket

- set_timeout: sets the timeout value to a socket

- receive_bytes

- receive_lines

- receive_buf

Within Nsock, we can also create raw sockets which are handled with libpcap, but we won't go into the details right now. Let's just say the following: the difference between a normal socket and a raw socket is that a normal socket operates right above the TCP or UDP protocol, while a raw socket operates right above the wire (so we need to provide our own Ethernet, IP, TCP/UDP, application headers).

To print some data on the screen when the script is done executing, we need to form a table that contains special keys, which are used to automatically format and print the data on the screen. The LUA is automatically converted to a string for normal output which we can see when we're running Nmap. Each nested table gets a new level of indentation, which is honored when the output from the script is being printed on the screen.

There's also a special table called the registry, which holds the variables from all scripts. This is a global storing place that can be used by all scripts to store and retrieve values, which effectively gives us the ability to share data between different scripts. The values in the registry are stored only for the current run of the Nmap program. When writing a key to the global registry, we need to ensure that the key is unique so we don't accidentally overwrite the value of some other script if the key is the same. When a certain script depends upon some other script's output value, we must declare that script as a dependency for the initial script, which ensures the correct execution order of the scripts.

2. An Example

In this example, we've written a script that should parse all the available HTTP methods that can be run on a specific HTTP server. We can do that by executing the following requests and reading the response from the server:

[plain]

OPTIONS / HTTP/1.0

[/plain]

When doing this with an NSE script, we need to create a socket that will connect to the target host on port 80 (or some other port), and send the "OPTIONS / HTTP/1.0" command string terminated by CRLF. The server should then respond with all the available HTTP method options that it supports. An example of running that request on a host www.gentoo.org is presented below:

[plain]

# telnet www.gentoo.org 80

Trying 89.16.167.134...

Connected to www.gentoo.org.

Escape character is '^]'.

OPTIONS / HTTP/1.0

HTTP/1.1 200 OK

Date: Thu, 27 Sep 2012 21:11:04 GMT

Server: Apache

Allow: GET,HEAD,POST,OPTIONS

Content-Length: 0

Connection: close

Content-Type: text/html; charset=utf-8

Connection closed by foreign host.
[/plain]

We used the program telnet to connect to the host www.gentoo.org on port 80 and executed the "OPTIONS / HTTP/1.0" command. The request was accepted and the response said that the valid HTTP methods are GET, HEAD, POST and OPTIONS. Now we need to make an NSE script that will do the same thing.

When starting to write the NSE script, we must first define the fields description, categories, dependencies, license and author. This can be seen below:

[plain]

description = [[

Attempts to find the HTTP methods available on the target HTTP server.

]]

author = "Dejan Lukan"

license = "GPL 2.0"

categories = {"default"}

[/plain]

The rule of the script should decide if the script will be executed or not. If we don't receive the right host and port information that are passed into the script, we can simply quit the execution of the script and do nothing. In the rule function, we need to take a look at the host and port number and decide whether that specified port is indeed TCP and open (usually the HTTP port is 80). A script must contain one of the following rules that determine if the script will be run or not: prerule, hostrule, portrule or postrule. In our case we can use portrule to determine whether the port number is opened and running HTTP service. We can do this with the following code:

[plain]

-- returns true if port is likely to be HTTP, false otherwise

portrule = shortport.http

[/plain]

This will return true whenever Nmap thinks that the port is using HTTP protocol. After that, we only need to specify the action rules, which contain the actions to be done when the script is run. We won't present the action separately, but let's present the whole script, which can be seen below:

[plain]

local http = require "http"

local nmap = require "nmap"

local stdnse = require "stdnse"

local shortport = require "shortport"

local table = require "table"

description = [[

Attempts to find the HTTP methods available on the target HTTP server.

]]

author = "Dejan Lukan"

license = "GPL 2.0"

categories = {"default"}

-- returns true if port is likely to be HTTP, false otherwise
portrule = shortport.http

action = function(host, port)
local out = {}

-- make the "OPTIONS" request
response = http.generic_request(host, port, "OPTIONS", "/")

-- form the output

table.insert(out, string.format("Request : OPTIONS / HTTP/1.0"))

table.insert(out, string.format("Host : %s (%s)", host.ip, host.name))

table.insert(out, string.format("Port : %s", port.number))

table.insert(out, string.format("Allowed Methods : %s", response.header['allow']))

return stdnse.format_output(true, out)

end

[/plain]

The above script is run whenever the used port is determined to be using HTTP protocol. In such cases, the script will first send the "OPTIONS" HTTP method to the server host. Afterwards, it will construct the output by inserting specific values to the table object. At the end, the output is presented with the stdnse.format_output command.

When testing the script, we should use the command below which also specifies the debugging options and print enough information to let us know what's happening. This is especially important when something goes wrong and the script isn't working as expected.

[plain]

# nmap --script=/home/user/http_options.nse www.gentoo.org -p 80 <strong>--script-trace -dd</strong>

[/plain]

Whenever the script is run, the following is outputted:

[plain]
# nmap --script=/home/eleanor/testing/http_options.nse www.gentoo.org -p 80

Starting Nmap 6.01 ( http://nmap.org ) at 2012-10-01 22:05 CEST

Nmap scan report for www.gentoo.org (89.16.167.134)

Host is up (0.051s latency).

PORT STATE SERVICE

80/tcp open http

| http_options:

| Request : OPTIONS / HTTP/1.0

| Host : 89.16.167.134 (www.gentoo.org)

| Port : 80

|_ Allowed Methods : GET,HEAD

Nmap done: 1 IP address (1 host up) scanned in 0.31 seconds
[/plain]

From this we can see that the Nmap script returned only HTTP methods GET and HEAD even though the previous telnet session said that GET, HEAD, POST, and OPTIONS methods are supported. To check what's happening, we can use the --script-trace and -dd options. The important output is presented below:

[plain]

NSE: TCP 192.168.1.2:57257 > 89.16.167.134:80 | CONNECT

NSE: TCP 192.168.1.2:57257 > 89.16.167.134:80 | 00000000: 4f 50 54 49 4f 4e 53 20 2f 20 48 54 54 50 2f 31 OPTIONS / HTTP/1

00000010: 2e 31 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 .1 Connection:

00000020: 63 6c 6f 73 65 0d 0a 55 73 65 72 2d 41 67 65 6e close User-Agen

00000030: 74 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 t: Mozilla/5.0 (

00000040: 63 6f 6d 70 61 74 69 62 6c 65 3b 20 4e 6d 61 70 compatible; Nmap

00000050: 20 53 63 72 69 70 74 69 6e 67 20 45 6e 67 69 6e Scripting Engin

00000060: 65 3b 20 68 74 74 70 3a 2f 2f 6e 6d 61 70 2e 6f e; http://nmap.o

00000070: 72 67 2f 62 6f 6f 6b 2f 6e 73 65 2e 68 74 6d 6c rg/book/nse.html

00000080: 29 0d 0a 48 6f 73 74 3a 20 77 77 77 2e 67 65 6e ) Host: www.gen

00000090: 74 6f 6f 2e 6f 72 67 0d 0a 0d 0a too.org

[/plain]

We can see that Nmap is actually executing the "OPTIONS / HTTP/1.1" command and not "OPTIONS / HTTP/1.0". It seems that the http.generic_request doesn't support the use of HTTP/1.0 http version. But not to worry, we can also use raw socket to send arbitrary requests. The new script that uses a raw socket to execute arbitrary requests can be seen below:

[plain]

local http = require "http"

local nmap = require "nmap"

local stdnse = require "stdnse"

local shortport = require "shortport"

local table = require "table"

description = [[

Attempts to find the HTTP methods available on the target HTTP server.

]]

author = "Dejan Lukan"

license = "GPL 2.0"

categories = {"default"}

-- returns true if port is likely to be HTTP, false otherwise
portrule = shortport.http

action = function(host, port)
local out = {}

-- make the "OPTIONS / HTTP/1.0" request

local socket = nmap.new_socket()

socket:connect(host, port)

socket:send("OPTIONS / HTTP/1.0rnrn")

s,response = socket:receive()

socket:close()

-- form the output

table.insert(out, string.format("Request : OPTIONS / HTTP/1.0"))

table.insert(out, string.format("Host : %s (%s)", host.ip, host.name))

table.insert(out, string.format("Port : %s", port.number))

table.insert(out, string.match(response, "Allow: [^r]*rn"));

return stdnse.format_output(true, out)
end

[/plain]

After running the script, we get the output as presented below:

[plain]
# nmap --script=/home/user/testing/http_options.nse www.gentoo.org -p 80

Starting Nmap 6.01 ( http://nmap.org ) at 2012-10-01 22:12 CEST

Nmap scan report for www.gentoo.org (89.16.167.134)

Host is up (0.052s latency).

PORT STATE SERVICE

80/tcp open http

| http_options2:

| Request : OPTIONS / HTTP/1.0

| Host : 89.16.167.134 (www.gentoo.org)

| Port : 80

|_ Allow: GET,HEAD,POST,OPTIONS

Nmap done: 1 IP address (1 host up) scanned in 0.30 seconds
[/plain]

Now we can see that we get the right options: GET, HEAD, POST and OPTIONS. Our Nmap script is complete.

3. Conclusion

We've seen how to actually write a NSE script that actually does something. In our case, this was merely executing an "OPTIONS / HTTP/1.0" command, but we could have easily written something more complicated.

Further Resources:

What should you learn next?

What should you learn next?

From SOC Analyst to Secure Coder to Security Manager — our team of experts has 12 free training plans to help you hit your goals. Get your free copy now.

Nmap's commands and usage options

Dejan Lukan
Dejan Lukan

Dejan Lukan is a security researcher for InfoSec Institute and penetration tester from Slovenia. He is very interested in finding new bugs in real world software products with source code analysis, fuzzing and reverse engineering. He also has a great passion for developing his own simple scripts for security related problems and learning about new hacking techniques. He knows a great deal about programming languages, as he can write in couple of dozen of them. His passion is also Antivirus bypassing techniques, malware research and operating systems, mainly Linux, Windows and BSD. He also has his own blog available here: http://www.proteansec.com/.