Overview
This document describes how to extend an Acunetix On-Premises installation with custom vulnerability checks. Even though a default installation of Acunetix already includes thousands of carefully crafted vulnerability checks, users with specific requirements and customized environments may benefit from extending scan coverage and functionality by adding their own, custom-tailored checks.
The scripting language used by Acunetix is easy to pick up for anybody with even basic knowledge of the languages found within the JavaScript, ECMAScript, or TypeScript families. This means that developers of custom vulnerability checks will not be required to learn yet another scripting language from scratch – Acunetix embraces and builds upon well-established design patterns and extends them with DAST-specific features where needed.
In order to develop a custom vulnerability check, a basic understanding of how Acunetix web vulnerability scans works internally is required. Consider each scan as a sequence of three separate stages:
- Discovery,
- Analysis, and
- Testing.
During the first stage, Discovery, Acunetix attempts to learn as much about the scanned web site as possible.
To do so, Acunetix initially browses the site just like a human visitor would, but it then also looks at the source code of each page to discover references to less obvious resources. Acunetix uses various techniques to coerce the web server into disclosing as much information about itself as is reasonable, and it uses all of this information to launch the second stage: Analysis.
During the Analysis stage, Acunetix takes all the previously gathered information and processes it. Acunetix identifies the most promising ways to test for vulnerabilities, prioritizes them accordingly, and passes the results to the third stage: Testing.
Each of the many vulnerability checks in Acunetix is implemented in the form of a script. During the Testing stage, all of these scripts are invoked inside a virtual environment, within which they can access information collected during the two previous stages. The custom vulnerability checks which you are about to create will be treated in much the same way as native checks, with both types sharing the same environment and relying upon the same information.
To better control when a script should be invoked, and what information it should have access to, each script can be placed in one of two categories: target or httpdata. Both will be explained in the sections below.
Script Categories
Scripts in the target category will be invoked once per target. In most cases, this simply means once per scan, but if you make use of the Allowed Hosts feature, target scripts will be run for each allowed host as well. Target scripts are useful for one-time tasks, such as checking whether the scan target exposes a certain resource or testing how the server responds to a specific request.
Scripts in the httpdata category will be invoked once per response. “Response” refers to the HTTP responses Acunetix elicits from the server during the previous two stages, Discovery and Analysis. For example, the user may have pointed Acunetix to http://example/index.html, which contained a link to http://example/subpage.html. Acunetix would have requested the first page, /index.html, followed by the second, /subpage.html, and received valid responses for both. All scripts in the httpdata category would then be invoked for both pages, one at a time. During each such invocation, every script will have access to response-specific properties such as the page contents and the response headers. On top of that, scripts will also have access to the HTTP request that caused the HTTP response.
Example:
The page http://example/index.html, to which the Acunetix scanner was pointed, consists of:
This is index.html. <a href="subpage.html">Link to subpage.html</a>
The linked page, http://example/subpage.html, contains the following text:
This is subpage.html.
After discovering these two pages, Acunetix will invoke the script demo.js. The script was stored in the httpdata directory, and it therefore belongs to the httpdata category. For the purpose of this example, the script consists of only the following two lines of code, which assign the request URI and the response body to a variable, and then write the value of the variable to a log file:
let out = `URI: "${scriptArg.http.request.uri}" / Response body: "${scriptArg.http.response.body}"`;
ax.log(ax.LogLevelInfo, out);
At the end of the scan, the debug log files will contain the following two entries, both put there by the above script:
INFO /httpdata/demo.js 9580 URI: "/index.html" / Response body: "This is index.html. <a href="subpage.html">Link to subpage.html</a>"
INFO /httpdata/demo.js 1464 URI: "/subpage.html" / Response body: "This is subpage.html."
A more interesting example, which demonstrates the processing of responses by searching them for potentially sensitive comments, can be found inside the sample script %PROGRAMDATA\Acunetix\shared\custom-scripts\httpdata\demo.js (Windows) and /home/acunetix/.acunetix/data/custom-scripts/httpdata/demo.js (Linux).
All custom vulnerability checks have to be stored inside the Acunetix custom-scripts directory. On a default installation of Acunetix on Windows, the full path of the directory is C:\ProgramData\Acunetix\shared\custom-scripts, and /home/acunetix/.acunetix/data/custom-scripts on Linux. Within this directory, scripts can be placed in either the target or the httpdata subdirectory, thereby assigning them to one of the previously described categories. Regardless of the chosen category, checks must be stored as files with the extension .js (eg. demo.js).
Enabling Custom Vulnerability Checks
Acunetix allows for the execution of custom vulnerability checks on a per-scan basis. To enable custom scripts for a given scan, simply navigate to Scan Types, click Add New Profile, and enable Custom Scripts.
Any of the built-in checks may be enabled as well, as Acunetix will automatically prevent them from interfering with each other. Once saved, the newly created scan type can be selected when launching a scan:
Further Reading
To further assist you with the development of custom vulnerability checks, the custom-scripts directory contains native.d.ts, defining all the interfaces and properties supported by the Acunetix custom script engine.
Processing data within the script environment
As explained in the Overview section, scripts have access to all information relevant to the context they have been invoked in. This information can be accessed through the scriptArg interface, which exposes all available data through the following properties:
- scriptArg.target: Contains information about the scan target. Supported properties are host (string), port (string), secure (boolean) and ip (string array). Example: scriptArg.target.host
- scriptArg.location: When accessed from within an httpdata script, this object contains the strings path and name of the location object the script was invoked on. For target scripts, path and name will return ‘/’ and ” (empty), respectively. Example: scriptArg.location.name
- scriptArg.http: Exposes an ax.http.Job object which contains the HTTP request/response pair the script was invoked on. Examples: scriptArg.http.request.uri, scriptArg.http.response.body
Sending Requests and Receiving Responses
The HTTP Job object (ax.http.Job) provides an interface to a built in HTTP client. It allows you to issue a customized HTTP request, and to receive the server’s response.
Example:
let job = ax.http.job();
job.hostname = 'www.example.com';
job.request.uri = '/';
ax.http.execute(job).sync();
The ax.http.Job object, which in the above example was returned by ax.http.job(), has a number of settings. These settings allow you to customize the request as needed.
Initially, you will likely want to provide basic connection parameters, such as the destination host to which your request should be sent, as well as the TCP destination port on which to connect, and whether or not a secure (TLS/SSL) connection should be established:
- hostname: Mandatory. The host or domain name of the server to connect to. Example: www.example.com
- secure: Optional. Whether or not the connection should use HTTPS and be secured by TLS/SSL. Can be set to either true or false, with the default being false.
- port: Optional. The TCP destination port on which to connect. This setting is optional, and its default value is 80, unless secure is set to true, in which case port will default to 443. The port must be set as a string: job.port = “8080”;
- timeout: Optional. The number of milliseconds after which a pending, incomplete connection attempt should be considered as unsuccessful, and cancelled. Must be passed as numeric value. Default: 30000. Example: job.timeout = 10000;
- retries: Optional. The number of times a request which has timed out should be retransmitted. Must be passed as numeric value. Default: 3. Example: job.retries = 1;
Once these connection parameters have been defined, it is time to set up the HTTP request itself. The ax.http.Job.Request object exposes all required properties:
- uri: The URI to request. Default: /. Example: job.request.uri = ‘/path/’;’
- method: Usually ‘GET’ or ‘POST’, but arbitrary values are supported. The default value is ‘GET’.
- body: The request body to send.
- addHeader(name: string, value: string): Allows for the insertion of custom request headers. Example: job.request.addHeader(‘X-Header’, ‘Value’)
The request itself can be sent by passing the Job object to ax.http.execute(), as shown in the example above.
After sending the request, and before attempting to process the response, it is recommended to verify that the required connection was established successfully. The error property of the Job object contains information about possible errors, and can be used to ensure no such errors occurred:
if (!job.error){
// Proceed
}
HTTP Response Handling
Similar to the previously described Request property, the Job object exposes information about the received HTTP response in the ax.http.Job.Response object, which exposes its information through the following properties:
- version: The HTTP version the server responded with. Example: job.response.version might return the string “HTTP/1.1”
- status: The numeric HTTP status code. Common status codes are 200, 301, and 404. An explanation of status codes can be found at https://developer.mozilla.org/en-US/docs/Web/HTTP/Status.
- reason: A string representation of the status code. Example: OK
- headers: The ax.http.Job.Response.Headers interface gives you convenient access to the received response headers. If a simple text representation of all response headers is preferable, the toString() function can be used: job.response.headers.toString(). To process individual headers, has(name: string): boolean will return information about whether a response header with the given name exists. If so, get(name: string): string may be used to retrieve its value. Example: if(job.response.headers.has(‘Content-Type’)) ax.log(ax.LogLevelInfo, job.response.headers.get(‘content-type’)) – note that header names are not case-sensitive
- body: The response body
A sample script demonstrating the use of ax.http.Job can be found at %PROGRAMDATA\Acunetix\shared\custom-scripts\target\demo.js (on Windows) and /home/acunetix/.acunetix/data/custom-scripts/target/demo.js (on Linux). This sample script will retrieve the Acunetix blog’s XML newsfeed, extract information about the blog, and issue a vulnerability alert displaying the titles of the three most recent blog posts. To activate this script, you will need to:
- remove the first line containing: /// disabled: true
- change each line containing: if(false) to if(true)
Logging
During the development of custom checks, you may want to log information for debugging purposes. The ax.log() method can be used to add log entries:
ax.log(level: number, data: any)
- level defines the severity of the log entry. Its primary purpose is to aid in the processing of larger log files by allowing you to sort or filter entries based on severity. Possible values are ax.LogLevelInfo, ax.LogLevelWarning, and ax.LogLevelError. Alternatively, these log levels may be specified by their numeric equivalents; 1, 2, and 3, respectively.
- data is the data to be logged; usually a string or a number.
Example:
ax.log(ax.LogLevelInfo, ’something Happened.')
In order to access the resulting log files, you will need to enable target debugging prior to launching a scan of the target. The corresponding option can be set in the target’s Advanced tab and is named Debug scans for this target. The location of the log files can be found in the Events tab after the scan has completed.
Issuing a Vulnerability Alert
Custom scripts can report vulnerabilities and other issues, and the reported item will be treated by Acunetix just like a natively implemented vulnerability alert: it will be displayed within the user interface, and, depending on your configuration, may be submitted to connected issue trackers.
In order to report a vulnerability during an Acunetix scan, scripts have to invoke the addVuln() method, which is exposed by the scanState interface:
scanState.addVuln({ typeId: 'custom.xml', location: scriptArg.location, http: scriptArg.http, details: "(Information provided by custom vulnerability check)", });
As demonstrated in this example, the addVuln() method expects an object which contains information about the reported issue. This object supports the following properties:
- typeId: Mandatory. This should always be set to custom.xml to specify that the vulnerability alert is the result of a custom script, which will automatically flag the vulnerability as a high priority item with the highest possible CVSS value.
- location: Mandatory if path has not been set. The expected value is an ax.state.Location object pointing to the location where the vulnerability was found, or the location it is affecting. In most cases it will be sufficient to simply pass scriptArg.location, as this will automatically reference the location pertaining to the current script invocation.
- path: Mandatory if location has not been set. The function and purpose of path are similar to that of location. The difference is that path can be a simple string, and as such, there is no requirement for this path to be stored in an ax.state.Location object. While this makes it considerably easier to set an arbitrary location, you will have to ensure that the path actually exists and that the Acunetix scanner is aware of it – if not, there is no way for Acunetix to determine correct location, and the vulnerability will be assigned to the default location / (root) instead.
- http: Optional. This property can be used to pass the HTTP request/response pair which led to the vulnerability alert being issued. The HTTP request/response pair will then be displayed within the Acunetix user interface alongside other information about the vulnerability. Whenever possible, http should be passed, as it will likely be of great value when attempting to reproduce, mitigate, and resolve an issue. Inside httpdata scripts, it will often be sufficient to pass scriptArg.http, which contains information related to the HTTP response which caused the current script invocation. In cases where an issue was discovered through the use of a manually created HTTP Job object (ax.http.Job), this object should be passed instead. An example of how to pass manually created HTTP Job objects can be found in %PROGRAMDATA\Acunetix\shared\custom-scripts\target\demo.js (on Windows) and /home/acunetix/.acunetix/data/custom-scripts/target/demo.js (on Linux).
- details: Optional. This string parameter can be set to whatever information the author of the script deems to be useful, such as additional details about the reported issue, or information about what led to the discovery of the issue in the first place.
- parameter: Optional. If a discovered issue can be tied to a specific parameter, such as a particular GET query parameter, it may be specified here. This will allow the Acunetix user interface to display the supplied parameter name in the vulnerability overview next to the related vulnerability alert.
The below image shows an alert issued for the scan target https://localhost:8080/, with scanState.addVuln() setting path to /a/b/c/ and param to MyParam:
Adding Unknown and Hidden Locations to Scans
When you are scanning a target containing pages or endpoints which are not referenced anywhere on the site, you can use custom scripting to manually teach Acunetix about these hidden locations. Doing to will cause Acunetix to process them as part of the previously discussed stages (discovery, analysis, testing).
The scanState interface exposes the hintLinks() function, which expects an array of strings containing the URIs you want Acunetix to become aware of:
scanState.hintLinks(links: string[])
Imagine scanning a web site which consists of only two pages, /index.php and /random7502345240832.php. The former does not reference the latter, and due to the random nature of the latter’s filename, it is unlikely to be discovered when pointing the scanner to /index.php. If you still want the second page to be tested for vulnerabilities, simply add the following code to your custom script:
scanState.hintLinks(['/random7502345240832.php']);
Note that the added URIs have to be absolute (ie. starting with a slash indicating root), and within scope of the current scan. If URIs are out of scope, they will be ignored.
Helper Functions
When processing HTML requests and responses, you may need to handle HTML entities. The Acunetix script engine provides two functions to encode reserved HTML characters to entities, and vice versa:
function ax.util.htmlEncode(input: string): string;
function ax.util.htmlDecode(input: string): string;
Example:
let testString = 'This is <b>bold</b> text';
let encoded = ax.util.htmlEncode(testString);
ax.log(ax.LogLevelInfo, `Encoded string: ${encoded}`);
let decoded = ax.util.htmlDecode(encoded);
ax.log(ax.LogLevelInfo, `Decoded string: ${decoded}`);
/*
Expected output:
INFO /httpdata/demo.js 7320 Encoded string: This is <b>bold</b> text
INFO /httpdata/demo.js 7320 Decoded string: This is <b>bold</b> text
*/
In addition, the Acunetix script engine allows you to encode strings to base64, and to decode base64 encoded strings:
function ax.util.base64Decode(input: string): string;
function ax.util.base64Encode(input: string): string;
A sample script demonstrating the use of these helper functions can be found at %PROGRAMDATA\Acunetix\shared\custom-scripts\target\demo.js (on Windows) and /home/acunetix/.acunetix/data/custom-scripts/target/demo.js (on Linux).
XML Parser
The Acunetix custom script engine contains an XML parser, which was specifically designed to assist you with the processing of HTTP responses consisting of XML.
The ax.struct.parseXml(data: string) function accepts a string as the data parameter, and returns a DOMDocument object. This happens regardless of whether or not data could be parsed as valid XML. If an error occurs, the error property of the resulting DOMDocument object will be set to true. This allows you to easily verify whether the supplied data was successfully parsed before attempting to process the returned object:
// Attempt to parse a string as XML
let parsedXML = ax.struct.parseXml('This is not XML');
// Determine whether string could be parsed as XML
if(parsedXML.error === true){
// error; unable to parse string as XML
}
else if (parsedXML.error === false){
// success; continue processing parsed XML
}
After a string has been parsed as XML, its tags, as well as the data stored within its tags and attributes, can be accessed through
- the getElementsByTagName() function (which returns a DOMNodeList),
- the .text property, and
- the getAttribute() function,
respectively.
This is demonstrated in the following example:
var xmlString = `<outer>
<inner my-attribute="attribute-value">tag-value</inner>
</outer>`;
let parsedXML = ax.struct.parseXml(xmlString);
if (!parsedXML.error)
{
let outer = parsedXML.getElementsByTagName('outer')[0]; // [Object]
let inner = outer.getElementsByTagName('inner')[0]; // [Object]
let innerText = outer.getElementsByTagName('inner')[0].text; // "tag-value"
let attributeValue = inner.getAttribute('my-attribute'); // "attribute-value"
ax.log(ax.LogLevelInfo, 'inner.text: ' +inner.text); // "tag-value"
ax.log(ax.LogLevelInfo, 'attributeValue: '+attributeValue);// "attribute-value"
}
Note that every DOMNodeList element has a length property, indicating the number of XML elements stored within it. This allows you to easily loop through the tags in more complex and even unknown XML documents.
A full example of how to dynamically parse an external XML file can be found in the sample script %PROGRAMDATA\Acunetix\shared\custom-scripts\target\demo.js (on Windows) and /home/acunetix/.acunetix/data/custom-scripts/target/demo.js (on Linux).
Get the latest content on web security
in your inbox each week.