Acunetix AcuSensor Technology is a new security technology that allows you to identify more vulnerabilities than a traditional Web Application Scanner, whilst generating less false positives. In addition it indicates exactly where in your code the vulnerability is and reports also debug information.

Acunetix is a proprietary (and expensive) web application scanner, than can uses a server-side injector to improve vulnerabilities discovery. Burp has a similar feature called the Infiltrator, but unfortunately, it only works on Java and .Net, by patching the bytecode.

This made me wonder how could Acunetix instrument php code, since it's not possible (by default) to override functions in php. This article is about the acusensor from Acunetix 10.5, but it should work for newer versions, since it seems that it isn't updated (nor maintained) that often.

To use the Acusensor, one just has to obtain a file named acu_phpaspect.php from Acunetix, somewhere in its options, put it somewhere accessible on the filesystem by the php process, add the auto_prepend_file="/path/to/acu_phpaspect.php" directive to the php.ini file, and that's it, Acunetix will take advantage of server-side code-instrumentation.

The acu_phpaspect.php code is lightly obfuscated (variables and functions are all a variation of _AAS\d+ , no spaces nor comments, …), and it looks like it isn't really maintained, since it didn't changed that much between Acunetix 9 and the latest available version. It's also missing a lot of potentially dangerous functions, and doesn't support modern (as in "OOP") SQL drivers (With the sole and notable exception of the MySQLi->query method.). I think that I have found a couple of logic bugs that could prevent proper vulnerability discovery, some DoS, but nothing remotely exploitable to pop the server (otherwise I won't be publishing this articles ;)).

Contrary to Burp Infiltrator, there is no bug notice about not deploying this kind of black-magic on production system on one's website. To prevent unintended access, it's using a simple password protection:

<?php if ( strtolower ( basename ( $_SERVER [ "SCRIPT_FILENAME" ])) === strtolower ( basename ( __FILE__ ))) { header ( "HTTP/1.0 404 Not found" ); die (); } $_ENV [ '_AAS0' ] = ( isset ( $_SERVER [ "HTTP_ACUNETIX_ASPECT" ]) && $_SERVER [ "HTTP_ACUNETIX_ASPECT" ] === "enabled" ); if ( $_ENV [ '_AAS0' ]) { $_ENV [ '_AAS0' ] = false ; if ( isset ( $_SERVER [ "HTTP_ACUNETIX_ASPECT_PASSWORD" ])) { $_AAS1 = fopen ( __FILE__ , 'r' ); fseek ( $_AAS1 , - 32 , SEEK_END ); $_ENV [ "_AAS2" ] = stream_get_contents ( $_AAS1 , 32 ); unset ( $_AAS1 ); $_ENV [ '_AAS0' ] = $_SERVER [ "HTTP_ACUNETIX_ASPECT_PASSWORD" ] === $_ENV [ "_AAS2" ]; } } [ … ] __halt_compiler (); 9065 ce82caa61f599de0745ee6191abd

So to trigger its activation, you have to send the right headers ( HTTP_ACUNETIX_ASPECT: enabled and HTTP_ACUNETIX_ASPECT_PASSWORD: password ). It would seem that versions 11 and above automatically issue a password for you (in the demo version, it's always password , or acunetix ).

If you do possess the non-demo version, feel free to send it to me check how the password is generated ;)

The password is stored at the end of the file, as an md5. Surprisingly, the sent password isn't passed to the md5 function before being compared to the expected value: the client is directly sending the hash. The comparison is done in non-constant time, likely allowing intelligent bruteforce attacks, but since php uses a lot of black magic caching, I didn't manage to come with a reliably working proof of concept, on both php5 and php7, but I would be happy to be proved wrong.

There is a third header that can be used to retrieve additional information about the application: ACUNETIX-ASPECT-QUERIES . It takes options separated by ; , currently only two:

filelist : to get a list of every single file (not only .php ones) under the current root. It doesn't check for upward symlinks, and works in a recursive manner.

: to get a list of every single file (not only ones) under the current root. It doesn't check for upward symlinks, and works in a recursive manner. aspectalerts : to get a list of security-sensitive configuration options: display_errors , register_globals , magic_quotes_gpc , allow_url_fopen , allow_url_include , session.use_trans_sid , open_basedir , enable_dl and php_version .

The acu_phpaspect.php file is prepended to every php file, and will, for each file:

Get its content Check if the file is already present in the acusensor's cache (more about that later), and if it is, it'll simply return the content of this cache, instead of pursuing any further. Use the token_get_all php function to tokenize the whole file. For each token, in a switch-case loop, inject wrappers around interesting tokens, like T_INCLUDE / T_INCLUDE_ONCE / T_REQUIRE / T_REQUIRE_ONCE (for arbitrary includes), T_EVAL (for arbitrary code execution), and so on, to monitor when and where they are called, and to inspect their parameters. The later being set to various variations of ACUSTART and ACUEND , with variations to detect file creation/suppression, so that the acusensor can detect how much they are mangled/controllable. The file is eval 'ed, and the output is printed. The execution stops here: the original file is never interpreted.

The acusensor is instrumenting something like 70 functions, from classic ones like preg_replace exec , or system , to exotic ones like sybase_query , sesam_query or maxdb_real_query , but doesn't care about assert , proc_open , extract , ini_set , … it seems that the developers aren't aware of what one can inadvertently do with those function ;)

Lets take an example. A script like this:

<?php if ( isset ( $_GET [ 'a' ])) echo system ( $_GET [ 'a' ]);

will be instrumented like this:

<?php if ( isset ( $_GET [ _AAS91 ( "/tmp/acusensor/test.php" , 2 , " \$ _GET" , 'a' )])) echo _AAS86 ( "/tmp/acusensor/test.php" , 3 , "system" , Array ( $_GET [ _AAS91 ( "/tmp/acusensor/test.php" , 3 , " \$ _GET" , 'a' )])); return true ;

Communication between the acusensor and acunetix is done by means of embedded comments of the form <!--ACUASPECT:the_data_encoded_in_base64--> , with the payload encoded in a len | value or type_len | type | len | value serialization format:

$ ~/dev/acusensor curl '127.0.0.1:8080/index.php?a=uptime' -H 'Acunetix-Aspect: enabled' -H 'Acunetix-Aspect-Password: 9065ce82caa61f599de0745ee6191abd' <!--ACUASPECT:MDAwMDAwMDRQT05HbjAwMDAwMDAwMDAwMDAwMDBu--> 15 :32:27 up 5 :17, 9 users, load average: 0 .55, 0 .40, 0 .39 15 :32:27 up 5 :17, 9 users, load average: 0 .55, 0 .40, 0 .39<!--ACUASPECT:MDAwMDAwMEFWYXJfQWNjZXNzYTAwMDAwMDAyMDAwMDAwMDNHRVQwMDAwMDAwMWEwMDAwMDAyNS9ob21lL2p2b2lzaW4vZGV2L2FjdXNlbnNvci9pbmRleC5waHAwMDAwMDAwM24wMDAwMDAwQVZhcl9BY2Nlc3NhMDAwMDAwMDIwMDAwMDAwM0dFVDAwMDAwMDAxYTAwMDAwMDI1L2hvbWUvanZvaXNpbi9kZXYvYWN1c2Vuc29yL2luZGV4LnBocDAwMDAwMDA0bjAwMDAwMDBCU3lzX0NvbW1hbmRhMDAwMDAwMDEwMDAwMDAwNnVwdGltZTAwMDAwMDI1L2hvbWUvanZvaXNpbi9kZXYvYWN1c2Vuc29yL2luZGV4LnBocDAwMDAwMDA1YTAwMDAwMDAxMDAwMDAwMTQic3lzdGVtIiB3YXMgY2FsbGVkLjAwMDAwMDBBVmFyX0FjY2Vzc2EwMDAwMDAwMjAwMDAwMDAzR0VUMDAwMDAwMDFiMDAwMDAwMjUvaG9tZS9qdm9pc2luL2Rldi9hY3VzZW5zb3IvaW5kZXgucGhwMDAwMDAwMDduMDAwMDAwMEFWYXJfQWNjZXNzYTAwMDAwMDAyMDAwMDAwMDNHRVQwMDAwMDAwMWMwMDAwMDAyNS9ob21lL2p2b2lzaW4vZGV2L2FjdXNlbnNvci9pbmRleC5waHAwMDAwMDAwQW4 = -->

The first encoded part is simply to signal to Acunetix that the Acusensor is here and working, it decodes as 00000004PONGn0000000000000000n . This part is only used by the wizard: it's completely ignored (and doesn't even have to be present) for the actual scan.

The second one is a bit more complex; a concatenation of structures, indicating the type of alert, the filename, human-readable message, the line number, parameters, …

0000000AVar_Accessa0000000200000003GET00000001a00000025/home/jvoisin/dev/acusensor/index.php00000003n0000000AVar_Accessa0000000200000003GET00000001a00000025/home/jvoisin/dev/acusensor/index.php00000004n0000000BSys_Commanda0000000100000006uptime00000025/home/jvoisin/dev/acusensor/index.php00000005a0000000100000014"system" was called.0000000AVar_Accessa0000000200000003GET00000001b00000025/home/jvoisin/dev/acusensor/index.php00000007n0000000AVar_Accessa0000000200000003GET00000001c00000025/home/jvoisin/dev/acusensor/index.php0000000An

A simple state machine implementation to decode this mess format is provided at the end of the article. Since Acunetix is written (mostly) in Delphi, it shouldn't be prone to buffer-overflow, but I haven't checked what's happening when some fields are longer/shorter than what they're supposed to be.

To avoid systematically reparsing every file, the acusensor stores instrumented files in a cache folder, either the return value of sys_get_temp_dir , realpath($_ENV['TMP']) , realpath($_ENV['TMPDIR']) , realpath($_ENV['TEMP']) , tempnam(md5(uniqid(rand(), false)), "") (also know as military-grade randomness generation), and in files named _AAS166 .md5($source_code . $filename) . Those files are never removed, aren't ending with .php , and are readable by everyone.

I wrote a simple'n'stupid Python client to play around with the acusensor, which was greatly improved by nextgens. It is able to bruteforce the acusensor password, grab directory listing, show php configuration variables, and of course, find some vulnerabilities:

$ python client.py http://127.0.0.1:8080/ [ + ] Checking if the acusensor is enabled on http://127.0.0.1:8080/ [ * ] Acusensor detected ( password: 9065ce82caa61f599de0745ee6191abd ) [ + ] Trying to get controllable variables at /: - $_GET [ "a" ] - $_GET [ "b" ] [ + ] Sending payloads for each variable... - $_GET [ "a" ] - $_GET [ "b" ] [ + ] Vulnerabilities: - "unserialize" was called. in /home/jvoisin/dev/acusensor/index.php:8 with $_GET [ "b" ] set to "ACUSTART" as parameter ( Unserialize ) - "system" was called. in /home/jvoisin/dev/acusensor/index.php:5 with $_GET [ "a" ] set to "ACUSTART" as parameter ( Sys_Command ) $

when run on this file:

<?php $c = 'id' ; if ( isset ( $_GET [ 'a' ])) $c = $_GET [ 'a' ]; echo system ( $c ); if ( isset ( $_GET [ 'b' ])) echo unserialize ( $_GET [ 'b' ]);

You can download the deobfuscated version of the acusensor for research purposes here.

It would also be a great idea to write some (public) Burp integration for it.

This is an update of the blogpost, since I gave a quick (private) talk about the Acusensor, I checked if anything changed in two years.

The only changes are related to the configuration settings checks. For example, display_error used to trigger an alert if it was set to 1 , now it does if it's different from 0 . Issues about on / On / 1 not being correctly detected have also been fixed.

I'm a bit disappointed that Acunetix doesn't invest a bit more into this technology, because I'm quite sure that with some improvements, it could become a game-changer.