NASL (Nessus Plugins)
Nessus Attack Scripting Language for writing plugins
I had the misfortune of wanting to build a plugin for Nessus. This turned out to be way more difficult than expected for many reasons. There is no official documentation and only a handful of short blog posts. You learn this proprietary language by looking at examples. That is why I wanted to document what I have learned on this page.
Know what you're in for when wanting to write NASL. If you are still confident, read on.
Getting Started
Nessus by Tenable is a community and professional application that automatically performs some known attacks. It is mostly focused on CVEs with its many plugin implementations that test for these. It is possible to write your own custom plugins in a proprietary language called NASL (Nessus Attack Scripting Language).
These plugins execute when you start a scan and select them. For each host, each time a configured port for the script is found, the script will be executed and can run checks to eventually make a vulnerability report.
Nessus can report one vulnerability per one plugin.
Setup
It is useful to create a simple testing setup locally while developing plugins, before deploying it in the real world. Using Docker, you can create a reproducible and isolated environment where Nessus can run. The following compose file defines a basic Nessus container:
Save the above file somewhere, and run:
After a few moments, the application should become available on https://localhost:8834/ and finish initializing. You should click through the setup this one time and choose Register for Nessus Essentials for a free community version. This configuration will be saved in the Docker nessus
volume the next time you start it.
If you don't have an activation code yet, create one by following the instructions. Then, create an admin account and save the password you input. The Downloading plugins... step will take a long time, after which it will also have to compile all plugins in the background, taking even longer. Have some patience because this only has to be done once.
When everything is completed (tasks in /#/settings/notifications are finished, your terminal should give a progress bar), we can start to customize the setup. When we eventually add our own plugins, the loading process will have to recompile all other plugins as well. It takes way too much time to iterate on an idea, but we can remove some unnecessary plugins for testing.
Removing Standard Plugins
In this step, we will only keep the .nasl
files that are referenced by the standard libraries. At any point, you can add more necessary files from plugins.bak/
back into plugins/
.
Get a shell in your container:
Move all plugins to a backup directory:
Copy all non-
.nasl
files (and optionally your plugin already if you have one):
Recursively copy dependencies:
Settings & Reloading
One important setting that allows unsigned plugins (that we write) to run is the following:
Finally, we can reload the plugins as we set up above with the following commands:
Development Cycle
Eventually, you will write .nasl
files, make slight edits, and want to reload them. To do this, I recommend keeping your plugin in a separate folder nested inside the plugins
folder because they are loaded recursively. You can then replace the entire folder and trigger a plugin reload, which will be much faster after removing the standard plugins.
You can start a test scan with your plugin by pressing New Scan in the UI, and then selecting Advanced Scan to configure which plugins will be used. On the Plugins tab, you can unselect every plugin except your category.
Give the plugin a name, and for testing, you can target host.docker.internal
to point to your local machine. By going to Discovery and Port Scanning, you can also restrict the ports (Port scan range) to only the port you are testing on, to prevent long or unintentional scans.
When pressing Launch, this should hit your testing server with some enumeration requests, and the custom plugin will run if it matches the port/service.
Tip: If your custom plugin did not launch, you may have a non-default port and use a service like Services/www
name inside your script. Service detection must be on for these kinds of names, otherwise, your script won't be triggered.
Go to the Plugins tab and enable the Service detection category.
Plugins
This section explains what is needed to write a plugin, and some tips to enhance the experience.
Plugin Attributes
All scripts have to start with a description
part, where it runs some code while reloading the plugins to set its title, description and other attributes. Below is a minimal example:
All plugins require a unique script_id()
value, otherwise they won't show up. Keep in mind that many IDs are already taken by standard plugins, so take a really high number (eg. 900000+) to make sure it is outside of this range.
Some more attributes can optionally be added to enhance the documentation of your plugin:
Syntax
NASL looks a lot like other programming languages like JavaScript, with if-statements and while-loops being practically identical.
For-loops are the same as in C-like languages, but another foreach
statements exists to ease looping over elements in a list:
Assigning variables without a prefix will make them global by default. If you instead use var
in front of a variable, it will be scoped locally.
Keep this in mind when naming function variables, as it may be easy to accidentally overwrite another global variable with the same name while executing your function!
Values of variables may be integers, strings, booleans, arrays, dictionaries, and more. You can define each one as follows:
You can index arrays and dictionaries with square brackets ([]
):
Strings can be added together to form complex messages. Adding integers to strings automatically converts them. Note that the display()
function doesn't automatically add newlines, which you may want after each message. You can write this special character using the \n
escape, but only inside single-quoted ('
) strings, not in double-quoted ("
) strings:
Function calls as seen above can have unnamed parameters separated by commas (,
). Many other functions also use named parameters which have a key: value
format:
To define your own function that accepts parameters, use the function
keyword. Names between the paratheses are named parameters, and unnamed parameters can be accessed via the special _FCT_ANON_ARGS
variable. All parameters are optional and will be null if they are not given a value by the caller. You must define/include functions before they are called.
Understanding Functions
There is no official documentation about functions available in NASL. There are some built-in functions and ones you can import via include("...")
statements. The best way to understand functions is by looking at examples, and by looking at its source code definition.
We will have to search through all plugins quickly, for which ripgrep
is a great tool:
If we want to understand the http_send_recv3
function, for example, we can search for it recursively with the following command to see its usages:
For a more complete overview of a command's options and where to include
it from, we can look for its definition:
We find many possible options, which you may also search for to find examples. We shouldn't include this .static
file directly in our .nasl
script, but instead, look for a .inc
file:
It appears that http.inc
includes the file and thus the function, together with some additional HTTP utilities. Therefore, you should include http.inc
in your code if you intend to use the function.
Function Definitions
We saw a function definition above of http_send_recv3()
with many different parameters. These in are all named parameters and can be filled in a function call by specifying their name followed by a value, like name: value
.
Some functions also have unnamed parameters, these are less obvious and don't show on the same line as the function name. Instead, in the function body it can reference _FCT_ANON_ARGS
with an index to find the nth unnamed parameter. You can call only these functions with func(arg1, arg2)
syntax. You can search the name and look for the first few lines:
Useful Functions
Below are some more useful functions to get started:
append_element(var, value)
: Append an element (value
) to a list (var
)isnull()
: Check if the first argument isnull
split(sep, keep)
: Split the string in the first argument bysep
into an array.keep
decides to keep the separator in the array as an extra element between each split elementtolower()
: Lowercase the first argumentint()
: Parse a string as an integerjson_read()
: Read the first argument as JSON, returning a parsed object that you can index
Debugging
There is no Visual Studio Code language support for NASL. One language that comes close to its syntax is Ruby, which you should select for the .nasl
extension.
At /opt/nessus/bin/nasl
, there exists a binary that can be used to test a .nasl
script. It will run the code and give you output in the terminal. Note that it won't have access to ports or hosts and is only meant for testing. It is, however, a very useful tool in testing syntax and logic in NASL, without having to go through the whole recompilation hassle for every code change.
For debugging, the display()
function can simply show output in the console. When using the nasl
binary, the first argument is printed to your terminal. When running installed in Nessus, the output can be found inside /opt/nessus/var/nessus/logs/nessusd.dump
, where all debug logging is written.
Similarly, json_write()
is useful for viewing objects in a JSON structure. It may look like this:
Output
The security_report_v4()
function should be used to report vulnerabilities back to Nessus. These will then be displayed in the UI. Below is an example report:
The text in the extra:
parameter is the only way to send dynamic text to the UI. Other information is already statically defined in Plugin Attributes.
Nessus decides its severity by the 'risk_factor'
attribute and severity:
function parameter. Below is a matrix showing how the final value is calculated:
Nessus Setting | none | low | medium | high | critical |
---|---|---|---|---|---|
| INFO | LOW | LOW | LOW | LOW |
| MEDIUM | MEDIUM | MEDIUM | MEDIUM | MEDIUM |
| HIGH | HIGH | HIGH | HIGH | CRITICAL |
Clicking on a vulnerability looks something like this, where the red part is the dynamic output (the extra:
parameter). All the rest are decided by static attributes.
Multiple Plugins
As seen in the last image, only the Output section of a vulnerability report is dynamic. All other information must be decided beforehand while compiling the plugin in the if (description)
section. There are cases where your idea may find multiple different vulnerabilities that should all get a unique title.
While it is simply impossible to alter the title at runtime, you can create multiple plugins that communicate with each other. You will have to write out every possible title and make a unique plugin for it (ideally using a template), which can then communicate to work together.
Plugins are sandboxes, and the only real way for plugins to communicate is through the Knowledge Base (KB). This is a store of keys and values that can be read and written to by any plugin, and will be globally shared. Some existing plugins use this to store which ports are open, which services are detected, or what should be skipped.
One important fact is that the order of plugins is not guaranteed, any plugin may run before another plugin. Plugins run in parallel (5 at a time by default) for maximum efficiency. This can make it difficult to manage a group of plugins as you cannot assign a "main" plugin beforehand. Remember, it may happen that your chosen "main" plugin is 6th in the queue, while 5 other plugins in your group are waiting on the main plugin, creating a deadlock.
To solve this, any plugin must be able to become the main plugin of your group. Using the Knowledge Base on a unique key, your plugins can collectively decide on a main plugin by their unique script ID. We also need to be careful of race conditions as multiple plugins will be accessing the KB in parallel. The following code snippet handles this:
Resources
Below are a few small online resources that were useful while learning about NASL:
/opt/nessus/lib/nessus/plugins
Last updated