一道有趣的CTF赛题-unicode引发的WebAssembly与js交互问题

ibukifalling 2022-02-21 10:46:00

0x00 前言

在前几天的DiceCTF里,遇到了一道关于WebAssembly的题目。解题时利用Unicode字符的转换问题,使前端js脚本与WebAssembly进行交互时出现异常,以达到XSS的效果。

解题时也了解了一些关于WebAssembly的知识,算是有所收获

0x01 WebAssembly介绍

在看题目之前,我们先来了解一下WebAssembly是什么

按照WebAssembly官网的介绍:

WebAssembly/wasm WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式

WebAssembly 有一套完整的语义,实际上 wasm 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率

WebAssembly 运行在一个沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在web环境中,WebAssembly将会严格遵守同源策略以及浏览器安全策略。

WebAssembly 设计了一个非常规整的文本格式用来、调试、测试、实验、优化、学习、教学或者编写程序。可以以这种文本格式在web页面上查看wasm模块的源码。

WebAssembly 在 web 中被设计成无版本、特性可测试、向后兼容的。WebAssembly 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。当然,WebAssembly 不仅可以运行在浏览器上,也可以运行在非web环境下。

因为官方介绍说的不太人话,人类比较难读懂,所以再贴一段别的地方找的介绍:

WebAssembly(缩写为 wasm)是一种使用非 JavaScript 代码,并使其在浏览器中运行的方法。这些代码可以是 C、C++ 或 Rust 等。它们会被编译进你的浏览器,在你的 CPU 上以接近原生的速度运行。这些代码的形式是二进制文件,你可以直接在 JavaScript 中将它们当作模块来用。

概括一下重点:WebAssembly与js一样可以在浏览器上运行,而且由于其更“底层”,所以会有运行速度上的优势。

速度优势也是WebAssembly的主要卖点。

0x02 解题

[DiceCTF2022]blazingfast

题目主页是一个WebAssembly实现的大小写转换器,可以把输入的句子转换成大小写交替的形式

还给出了一个adminbot页面,可以让带有bot访问url,不过必须符合/^https:\/\/blazingfast\.mc\.ax\//的正则,也就是只能访问上面说的那个大小写转换器

可以从给出的源码里看到flag就在bot的localStorage中

所以本题的思路很清晰:通过xss,让bot向我们发送flag就行了。

业务逻辑

本题中直接给出了WebAssembly的c语言源码

int length, ptr = 0;
char buf[1000];

void init(int size) {
    length = size;
    ptr = 0;
}

char read() {
    return buf[ptr++];
}

void write(char c) {
    buf[ptr++] = c;
}

int mock() {
    for (int i = 0; i < length; i ++) {
        if (i % 2 == 1 && buf[i] >= 65 && buf[i] <= 90) {
            buf[i] += 32;
        }

        if (buf[i] == '<' || buf[i] == '>' || buf[i] == '&' || buf[i] == '"') {
            return 1;
        }
    }

    ptr = 0;

    return 0;
}

同时通过网页中的js代码可以看到与WebAssembly的交互过程

let blazingfast = null;

function mock(str) {
    blazingfast.init(str.length);

    if (str.length >= 1000) return 'Too long!';

    for (let c of str.toUpperCase()) {
        if (c.charCodeAt(0) > 128) return 'Nice try.';
        blazingfast.write(c.charCodeAt(0));
    }

    if (blazingfast.mock() == 1) {
        return 'No XSS for you!';
    } else {
        let mocking = '', buf = blazingfast.read();

        while(buf != 0) {
            mocking += String.fromCharCode(buf);
            buf = blazingfast.read();
        }

        return mocking;
    }
}

function demo(str) {
    document.getElementById('result').innerHTML = mock(str);
}

WebAssembly.instantiateStreaming(fetch('/blazingfast.wasm')).then(({ instance }) => {   
    blazingfast = instance.exports;

    document.getElementById('demo-submit').onclick = () => {
        demo(document.getElementById('demo').value);
    }

    let query = new URLSearchParams(window.location.search).get('demo');

    if (query) {
        document.getElementById('demo').value = query;
        demo(query);
    }
})

大致的功能实现逻辑如下:

前端js获取用户输入的字符串,以该字符串的长度初始化wasm程序的length值
-->js将用户输入转换成大写后写入wasm的buf数组
-->wasm程序将buf数组中的字符转成大小写交替的形式
-->js读取wasm的buf数组,将字符串输出在界面中

有三个过滤点:
字符串长度不能超过1000
字符的unicode编码值不能超过128
不能含有< > & "

其中主要是第三个过滤点防止了我们进行XSS,后续考虑怎么绕过这个过滤

代码审计

我们细看wasm源码中的mock函数部分

int mock() {
    for (int i = 0; i < length; i ++) {
        if (i % 2 == 1 && buf[i] >= 65 && buf[i] <= 90) {
            buf[i] += 32;
        }

        if (buf[i] == '<' || buf[i] == '>' || buf[i] == '&' || buf[i] == '"') {
            return 1;
        }
    }

    ptr = 0;

    return 0;
}

对字符串的处理进行到length处就停止了,但是js代码在读取的时候

while(buf != 0) {
    mocking += String.fromCharCode(buf);
    buf = blazingfast.read();
}

却是一直读取到\0为止。

也就是说,如果我们能构造一个特殊字符串,使length的值比传给wasm的字符串的真实长度要小,就能使length之后的字符不被检查

for (let c of str.toUpperCase()) {
    if (c.charCodeAt(0) > 128) return 'Nice try.';
    blazingfast.write(c.charCodeAt(0));
}

虽然这里限制了字符的unicode值不能大于128,但是多亏了这个toUpperCase方法,我们能够利用一些奇技淫巧来进行下一步操作

神奇的Unicode大小写转换

这篇文章提到:某些unicode字符会在大小写转换时被转成英文字母。

更神奇的是,甚至有的unicode字符,会在转换后变成两个甚至三个英文字母!

'ß'.toLowerCase() // 'ss'
'ß'.toLowerCase() === 'SS'.toLowerCase() // true

XSS

如果我们在输入的字符串中添加这些特殊字符,js获取并告知wasm的长度就会比转换成大写后字符串的真实长度要小,从而绕过过滤!

虽然payload会全部被转成大写,不过我们可以通过实体编码的方式解决

alert(1)

最后使用一个观感极佳(?)的payload成功获取flag


0x03 总结

比赛时临时学了一下wasm,不过好在对wasm的考察程度并不深,最后靠一点运气和灵感做出来了。比赛时也找到一些其他的关于wasm安全的资料,提到了一些关于wasm的pwn技巧,这里就不作过多延展了

可能是国际比赛的原因,还吃了网络的亏……一开始在自己的服务器上监听,笑死,根本打不通,还以为是自己payload写的有问题,改了半天。换了好几个平台试,最后用一开始的payload打通了……

这道题从本质上来说,还是接口调用时的处理没有做好才导致问题出现

最后,再默念一遍——开发和安全缺一不可!

评论

ibukifalling

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

密码学 文章:13 篇
memcache安全 文章:1 篇
事件分析 文章:223 篇
后门 文章:39 篇
Windows安全 文章:88 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录