Title: Playing with the acusensor
Date: 2017-04-22 17:00

> 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](https://www.acunetix.com/ordering/))
web application scanner, than can uses a server-side
*injector* to improve vulnerabilities discovery.
[Burp](https://portswigger.net/burp/) has a similar feature
called the [Infiltrator](http://blog.portswigger.net/2016/07/introducing-burp-infiltrator.html),
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](https://secure.php.net/manual/en/function.override-function.php) 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]({static}/files/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.

[![acusensor vuln example]({static}/images/acusensor.png)]({static}/images/acusensor.png)

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 ;)).

# Password protection

Contrary to Burp Infiltrator, there is no [bug
notice](https://portswigger.net/burp/help/infiltrator.html) 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
<?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();9065ce82caa61f599de0745ee6191abd
```

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](https://www.acunetix.com/blog/docs/acusensor-require-password-v11/)
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 <s>send it to me</s>
check how the password is generated ;)

The password is stored at the end of the file, as an [md5](https://en.wikipedia.org/wiki/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 <s>black magic</s> 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.
- `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`.

# Instrumentation

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

1. Get its content
2. 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.
3. Use the [`token_get_all`](https://secure.php.net/manual/en/function.token-get-all.php)
php function to tokenize the whole file.
4. For each token, in a `switch-case` loop, **inject wrappers** around [*interesting* tokens](https://secure.php.net/manual/en/tokens.php),
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.
5. The file is `eval`'ed, and the output is printed.
6. 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
<?php
if (isset ($_GET['a']))
    echo system($_GET['a']);
```

will be instrumented like this:

```php
<?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 <del>mess</del> format is provided at the end of the article.
Since Acunetix is written (mostly) in [Delphi](https://en.wikipedia.org/wiki/Delphi_(programming_language)),
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.

# Caching mechanism

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.

# Client

I wrote a [simple'n'stupid Python client]({static}/files/acusensor_client.py) to play around with the acusensor,
which was greatly improved by [nextgens](http://florent.daigniere.com/).
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
<?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](http://eur-lex.europa.eu/legal-content/EN/TXT/?qid=1435057541496&uri=CELEX:32009L0024)
[here]({static}/files/acu_phpaspect_deobfucated.php).

It would also be a great idea to write some (public) [Burp integration]({static}/images/acuneburp.png) for it. 

# Two years later, February 2019

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.
