從一道毫無人性的刁鑽面試題提及

前言

在 JavaScript 中,你能夠不用英文字母與數字,就執行 console.log(1) 嗎?

換句話說,就在於代碼中不能出現任何英文字母(a-zA-Z)與數字(0-9),除此以外(各類符號)均可以。執行式碼以後,會執行 console.log(1),而後在控制檯中輸出 1php

若是你想到能夠用什麼庫或服務之類的東西作到,別急着說出答案。先本身想一下,看看有沒有辦法本身寫出來。若是能從零開始本身寫出來,就表明你對 js 這個語言以及各類自動類型轉換應該是很熟悉的。前端

分析幾個關鍵點

要能成功執行題目所要求的的 console.log(1),必需要完成幾個關鍵點:node

  1. 找出執行代碼的方法
  2. 如何不用字母與數字得出數字的方法
  3. 如何不用字母與數字獲得字母的方法

只要這三點都解決了,就能達成題目的要求。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 有兩個內置函數:btoaatobbtoa 是把一個字符串編碼爲 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 寫得好好的,搞這些亂七八糟的有什麼用,但這樣作的重點並不在於最後的結果,而是在訓練幾個東西:

  1. 對於 js 語言的熟悉度,咱們用了不少類型轉換和內置方法來拼湊東西,可能有些是你歷來沒聽到過的。
  2. 解決問題時縮小範圍的能力,從如何把字符串看成函數執行,再到拼湊出數字和字符串,一步步的縮小問題,子問題解決以後原問題就解決了

173382ede7319973.gif


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章


歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索