在 JavaScript 中,你能夠不用英文字母與數字,就執行 console.log(1)
嗎?
換句話說,就在於代碼中不能出現任何英文字母(a-zA-Z)與數字(0-9),除此以外(各類符號)均可以。執行式碼以後,會執行 console.log(1)
,而後在控制檯中輸出 1
。php
若是你想到能夠用什麼庫或服務之類的東西作到,別急着說出答案。先本身想一下,看看有沒有辦法本身寫出來。若是能從零開始本身寫出來,就表明你對 js 這個語言以及各類自動類型轉換應該是很熟悉的。前端
要能成功執行題目所要求的的 console.log(1)
,必需要完成幾個關鍵點:node
只要這三點都解決了,就能達成題目的要求。git
先解決第一點:找出執行代碼的方法程序員
直接 console.log
是不可能的,由於就算你用字符串拼出 console
,你也沒辦法像 PHP 那樣拿字符串來執行函數。github
那 eval 呢?evali 裏面能夠放字符串,能夠是就能夠。問題是咱們也無法使用 eval,由於不能用英文字母。面試
還有什麼方法呢? 還能夠用 function constructor: new Function("console.log(1)")
來執行,但問題是咱們也不能用 new
這個關鍵字,因此乍一看也不行,不過不須要 new
也能夠,只用 Function("console.log(1)")
就能夠建立一個可以執行特定代碼的函數。segmentfault
因此接下來的問題就變成了怎樣才能拿到 function constructor,只要能拿到就有機會數組
在 JS 中能夠用 .Constructor
拿到某個對象的構造函數,例如 "".constructor
就會獲得:ƒ String() { [native code] }
,若是你有一個函數,就能拿到 function constructor,像這樣:(()=>{}).constructor
,在這個問題中咱們不能直接用 .constructor
,應該用:(()=>{})['constructor']
。瀏覽器
若是不支持 ES6 ,不能用箭頭函數怎麼辦,還有辦法獲得一個函數嗎?
有,並且很容易,就是各類內置函數,例如說 []['fill']['constructor']
,其實就是 [].fill.constructor
,或者是 ""['slice']['constructor']
,也能夠拿到 function constructor,因此這不是個問題,就算沒有箭頭函數也不要緊。
一開始咱們指望的代碼是這樣:Function('console.log(1)')()
,用前面的方法改寫的話,應該把前面的 Function
替換成 (()=>{})['constructor']
,變成 (()=>{})['constructor']('console.log(1)')()
只要想辦法拼湊出這段代碼問題就解決了。如今咱們解決了第一個問題:找到執行函數的方法。
接下來的數字就比較簡單了.
這裏的關鍵在與 js 的強制多態,若是你有看過 js 類型轉換的文章,或許會記得 {} + []
能夠得出 0
這個數字。
假設你不知道這個,我來解釋一下:利用 !
這個運算符,能夠獲得 false
,例如 1[]
或者 !{}
均可以得出 false
。而後兩個 false
相加就可獲得 0
:![] + ![]
,以此類推,既然 ![]
是 false
,那前面再加一個 !
,!![]
就是 true
,因此 ![] + !![]
就等於 false + true
,也就是 0 + 1
,結果就是 1
。
或者用更簡短的方法,用來 +[]
也能夠利用自動類型轉換獲得 0
這個結果,那麼 +!![]
就是 1
。
有了 1 以後,就能夠獲得全部數字了,只要一直不斷暴力相加就好了,若是不想這樣作,也能夠利用位運算 <<
>>
或者是乘號,好比說要湊出 8
,就是 1 << 3
,或者是 2 << 2
,要湊出 2 就是(+!![])+(+!![])
,因此 (+!![])+(+!![]) << (+!![])+(+!![])
就是 8
,只須要四個 1
就好了,不須要本身加 8 次。
不過如今能夠先不考慮長度,只須要考慮能不能湊出來就好了,只要能得出 1 就足夠了。
最後就是要想辦法湊出字符串了,或者說要獲得 (()=>{})['constructor']('console.log(1)')()
中的每個字符。
怎樣才能獲得字符呢?答案是和數字同樣,即強制多態。
上面說過 ![]
能夠獲得 false
,那在後面加一個空字符串:![] + ''
,不就能夠獲得 "false"
了嗎?這樣就能夠拿到 a, e, f, l, s 這五個字符。例如 (![] + '')[1]
就是 a
,爲了方便紀錄,咱們來寫一小段代碼:
const mapping = { a: "(![] + '')[1]", e: "(![] + '')[4]", f: "(![] + '')[0]", l: "(![] + '')[2]", s: "(![] + '')[3]", }
那既然有了false
,那麼拿到 true
也不是什麼難事了,!![] + ''
能夠獲得 true
,如今把代碼改爲:
const mapping = { a: "(![] + '')[1]", e: "(![] + '')[4]", f: "(![] + '')[0]", l: "(![] + '')[2]", r: "(!![] + '')[1]", s: "(![] + '')[3]", t: "(!![] + '')[0]", u: "(!![] + '')[2]", }
而後再用一樣的方法,用 ''+{}
能夠獲得 "[object Object]"
(或是你要用神奇的 []+{}
也行),如今代碼能夠更新成這樣:
const mapping = { a: "(![] + '')[1]", b: "(''+{})[2]", c: "(''+{})[5]", e: "(![] + '')[4]", f: "(![] + '')[0]", j: "(''+{})[3]", l: "(![] + '')[2]", o: "(''+{})[1]", r: "(!![] + '')[1]", s: "(![] + '')[3]", t: "(!![] + '')[0]", u: "(!![] + '')[2]", }
從數組或是對象取一個不存在的屬性會返回 undefined
,再把 undefined
加上字串,就能夠拿到字串的 undefined
,就像這樣:[][{}]+''
,能夠獲得 undefined
。
拿到以後,咱們的轉換表就變得更加完整了:
const mapping = { a: "(![] + '')[1]", b: "(''+{})[2]", c: "(''+{})[5]", d: "([][{}]+'')[2]", e: "(![] + '')[4]", f: "(![] + '')[0]", i: "([][{}]+'')[5]", j: "(''+{})[3]", l: "(![] + '')[2]", n: "([][{}]+'')[1]", o: "(''+{})[1]", r: "(!![] + '')[1]", s: "(![] + '')[3]", t: "(!![] + '')[0]", u: "(!![] + '')[2]", }
看一下轉換表,再看看咱們的目標字符串:(()=>{})['constructor']('console["log"](1)')()
,稍微比對一下,就會發現要湊出 constructor
是沒有問題的,要湊出 console
也是沒問題的,但是就惟獨缺了 log 的 g
,目前咱們的轉換表裏面沒有這個字符。
因此還須要從某個地方把 g
拿出來,才能拼湊出咱們想要的字符串。或者也能夠換個方法,用其餘方式拿到字符。
我一開始想到兩個方法,第一個是利用進制轉換,把數字用 toString
轉成字符串時能夠帶一個參數 radix
,表明這個數字要轉換成多少進制,像是 (10).toString(16)
就會獲得 a,由於 10 進制的 10
就是 16 進制的 a
。
英文字母一共 26 個,數字有 10 個,因此只要用 (10).toString(36)
就能獲得 a
,用 (16).toString(36)
就能夠獲得 g
了,能夠用這個方法獲得全部的英文字母。但是問題來了, toString
自己也有 g
,但如今咱們沒有,因此這方法行不通。
另外一個方法是用 base64,JS 有兩個內置函數:btoa
跟 atob
,btoa
是把一個字符串編碼爲 base64,例如 btoa('abc')
會獲得 YWJj
,而後再用 atob('YWJj')
解碼就會獲得 abc
。
只要想辦法讓 base64 編碼後的結果有 g
就好了,能夠寫代碼去跑,也能夠本身慢慢試,幸運的是 btoa(2)
能獲得 Mg==
這個字符串。因此 btoa(2)[1]
的結果就是 g
了。
不過下一個問題又來了,怎樣執行 btoa
?同樣只能經過上面的 function constructor:(()=>{})['constructor']('return btoa(2)[1]')()
,此次每個字符都湊得出來。
能夠結合上面的 mapping,寫一小段簡單的代碼來幫助作轉換,目標是把一個字符串轉成沒有字符的形式:
const mapping = { a: "(![] + '')[1]", b: "(''+{})[2]", c: "(''+{})[5]", d: "([][{}]+'')[2]", e: "(![] + '')[4]", f: "(![] + '')[0]", i: "([][{}]+'')[5]", j: "(''+{})[3]", l: "(![] + '')[2]", n: "([][{}]+'')[1]", o: "(''+{})[1]", r: "(!![] + '')[1]", s: "(![] + '')[3]", t: "(!![] + '')[0]", u: "(!![] + '')[2]", } const one = '(+!![])' const zero = '(+[])' function transformString(input) { return input.split('').map(char => { // 先假設數字只會有個位數,比較好作轉換 if (/[0-9]/.test(char)) { if (char === '0') return zero return Array(+char).fill().map(_ => one).join('+') } if (/[a-zA-Z]/.test(char)) { return mapping[char] } return `"${char}"` }) // 加上 () 保證執行順序 .map(char => `(${char})`) .join('+') } const input = 'constructor' console.log(transformString(input))
輸出是:
((''+{})[5])+((''+{})[1])+(([][{}]+'')[1])+((![] + '')[3])+((!![] + '')[0])+((!![] + '')[1])+((!![] + '')[2])+((''+{})[5])+((!![] + '')[0])+((''+{})[1])+((!![] + '')[1])
能夠再寫一個函數只轉換數字,把數字去掉:
function transformNumber(input) { return input.split('').map(char => { // 先假設數字只會有個位數,比較好作轉換 if (/[0-9]/.test(char)) { if (char === '0') return zero let newChar = Array(+char).fill().map(_ => one).join('+') return`(${newChar})` } return char }) .join('') } const input = 'constructor' console.log(transformNumber(transformString(input)))
獲得的結果是:
((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] + '')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])
把結果丟給 console 執行,發現獲得的值就是 constructor
沒錯。因此綜合以上代碼,回到剛剛那一段:(()=>{})['constructor']('return btoa(2)[1]')()
,要獲得轉換完的結果就是:
const con = transformNumber(transformString('constructor')) const fn = transformNumber(transformString('return btoa(2)[1]')) const result = `(()=>{})[${con}](${fn})()` console.log(result)
結果很長就不貼了,但確實能獲得一個 g
。
在繼續以前,先把代碼改一下,增長一個能直接轉換代碼的函數:
function transform(code) { const con = transformNumber(transformString('constructor')) const fn = transformNumber(transformString(code)) const result = `(()=>{})[${con}](${fn})()` return result; } console.log(transform('return btoa(2)[1]'))
好了,到這裏其實已經接很近終點了,只有一件事尚未解決,那就是 btoa
是 WebAPI,瀏覽器纔有,node.js 並無這個函數,因此想要作得更漂亮,就必須找到其餘方式來產生 g
這個字符。
回憶一下一開始所提的,用 function.constructor
能夠拿到 function constructor,以此類推,用 ''['constructor']
能夠拿到 string constructor,只要再加上一個字串,就能夠拿到 string constructor 的內容了!
像是這樣:''['constructor'] + ''
,獲得的結果是:"function String() { [native code] }"
,一會兒就多了一堆字符串可用,而咱們牽腸掛肚的 g
就是:(''['constructor'] + '')[14]
。
因爲咱們的轉換器目前只能支持一位數的數字(由於作起來簡單),咱們改爲:(''['constructor'] + '')[7+7]
,能夠寫成這樣:
mapping['g'] = transform(`return (''['constructor'] + '')[7+7]`)
經歷過千辛萬苦以後,終於湊出了最麻煩的 g
這個字符,結合咱們剛剛寫好的轉換器,就能夠順利產生 console.log(1)
去除掉字母與數字後的版本:
const mapping = { a: "(![] + '')[1]", b: "(''+{})[2]", c: "(''+{})[5]", d: "([][{}]+'')[2]", e: "(![] + '')[4]", f: "(![] + '')[0]", i: "([][{}]+'')[5]", j: "(''+{})[3]", l: "(![] + '')[2]", n: "([][{}]+'')[1]", o: "(''+{})[1]", r: "(!![] + '')[1]", s: "(![] + '')[3]", t: "(!![] + '')[0]", u: "(!![] + '')[2]", } const one = '(+!![])' const zero = '(+[])' function transformString(input) { return input.split('').map(char => { // 先假設數字只會有個位數,比較好作轉換 if (/[0-9]/.test(char)) { if (char === '0') return zero return Array(+char).fill().map(_ => one).join('+') } if (/[a-zA-Z]/.test(char)) { return mapping[char] } return `"${char}"` }) // 加上 () 保證執行順序 .map(char => `(${char})`) .join('+') } function transformNumber(input) { return input.split('').map(char => { // 先假設數字只會有個位數,比較好作轉換 if (/[0-9]/.test(char)) { if (char === '0') return zero let newChar = Array(+char).fill().map(_ => one).join('+') return`(${newChar})` } return char }) .join('') } function transform(code) { const con = transformNumber(transformString('constructor')) const fn = transformNumber(transformString(code)) const result = `(()=>{})[${con}](${fn})()` return result; } mapping['g'] = transform(`return (''['constructor'] + '')[7+7]`) console.log(transform('console.log(1)'))
最後的代碼:
(()=>{})[((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] + '')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])](((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+((![] + '')[((+!![])+(+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![])+(+!![]))])+(".")+((![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![]))])+((()=>{})[((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] + '')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])](((!![] + '')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] + '')[((+!![])+(+!![]))])+((!![] + '')[((+!![]))])+(([][{}]+'')[((+!![]))])+(" ")+("(")+("'")+("'")+("[")+("'")+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((''+{})[((+!![]))])+(([][{}]+'')[((+!![]))])+((![] + '')[((+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((!![] + '')[((+!![]))])+((!![] + '')[((+!![])+(+!![]))])+((''+{})[((+!![])+(+!![])+(+!![])+(+!![])+(+!![]))])+((!![] + '')[(+[])])+((''+{})[((+!![]))])+((!![] + '')[((+!![]))])+("'")+("]")+(" ")+("+")+(" ")+("'")+("'")+(")")+("[")+((+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![]))+("+")+((+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![])+(+!![]))+("]"))())+("(")+((+!![]))+((+!![])+(+!![]))+((+!![])+(+!![])+(+!![]))+(")"))()
用了 1800 個字符,成功寫出了只有:[
,],
(,
),{
,}
,"
,'
,+
,!
,=
,>
這 12 個字符的程序,而且可以順利執行 console.log(1)
。
而由於咱們已經能夠順利拿到 String 這幾個字了,因此就能夠用以前提過的位轉換的方法,獲得任意小寫字符,像是這樣:
mapping['S'] = transform(`return (''['constructor'] + '')[9]`) mapping['g'] = transform(`return (''['constructor'] + '')[7+7]`) console.log(transform('return (35).toString(36)')) // z
那要怎樣拿到任意大寫字符,或甚至任意字符呢?我也有想到幾種方式。
若是想拿到任意字符,能夠經過 String.fromCharCode
,或是寫成另外一種形式:""['constructor']['fromCharCode']
,就能夠拿到任意字符。但是在這以前要先想辦法拿到大寫的 C
,這個就要再想一下了。
除了這條路,還有另一條,那就是靠編碼,例如說 '\u0043'
其實就是大寫的 C 了,因此我本來以為能夠透過這種方法來湊,但我試了一下是不行的,像是 console.log("\u0043")
會印出 C 沒錯,可是 console.log(("\u00" + "43"))
就會直接噴一個錯誤給你,看來編碼沒有辦法這樣拼起來(仔細想一想發現滿合理的)。
除了這條路,還有另一個方法,那就是依靠編碼,例如說 '\u0043'
其實就是大寫的 C
,因此我本來覺得能夠經過這種方法來湊,但試了一下是不行的,像是 console.log("\u0043")
會印出 C
沒錯,可是 console.log(("\u00" + "43"))
就會直接報一個錯誤,看來編碼沒有辦法這樣拼起來。不過仔細想一想仍是很合理的。
最後寫出來的那個轉換的函數其實並不完整,沒有辦法執行任意代碼碼,沒有繼續作完是由於 jsfuck 這個庫已經寫得很清楚了,在 README 裏面詳細了描述它的轉換過程,並且最後只用了 6 個字符而已,真的很佩服。
在它的代碼當中也能夠看出是怎樣轉換的,大寫 C
的部分是用了一個 String 上名爲 italics
的函數,能夠產生 <i></i>
,以後再調用 escape
,就會獲得 %3Ci%3E%3C/i%3E
,而後就獲得大寫 C
了。
有些人可能會說我平時寫 BUG 寫得好好的,搞這些亂七八糟的有什麼用,但這樣作的重點並不在於最後的結果,而是在訓練幾個東西: