从一道毫无人性的刁钻面试题说起

溢出盆景
• 阅读 3833

前言

在 JavaScript 中,你可以不用英文字母与数字,就执行 console.log(1) 吗?

换句话说,就在于代码中不能出现任何英文字母(a-zA-Z)与数字(0-9),除此之外(各种符号)都可以。执行式码之后,会执行 console.log(1),然后在控制台中输出 1

如果你想到可以用什么库或服务之类的东西做到,别急着说出答案。先自己想一下,看看有没有办法自己写出来。如果能从零开始自己写出来,就代表你对 js 这个语言以及各种自动类型转换应该是很熟悉的。

分析几个关键点

要能成功执行题目所要求的的 console.log(1),必须要完成几个关键点:

  1. 找出执行代码的方法
  2. 如何不用字母与数字得出数字的方法
  3. 如何不用字母与数字得到字母的方法

只要这三点都解决了,就能达成题目的要求。

先解决第一点:找出执行代码的方法

找出执行代码的方法

直接 console.log 是不可能的,因为就算你用字符串拼出 console,你也沒办法像 PHP 那样拿字符串来执行函数。

那 eval 呢?evali 里面可以放字符串,可以是就可以。问题是我们也没法使用 eval,因为不能用英文字母。

还有什么方法呢? 还可以用 function constructor: new Function("console.log(1)") 来执行,但问题是我们也不能用 new 这个关键字,所以乍一看也不行,不过不需要 new 也可以,只用 Function("console.log(1)") 就可以创建一个能够执行特定代码的函数。

所以接下来的问题就变成了怎样才能拿到 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. 解决问题时缩小范围的能力,从如何把字符串当作函数执行,再到拼凑出数字和字符串,一步步的缩小问题,子问题解决之后原问题就解决了

从一道毫无人性的刁钻面试题说起


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

从一道毫无人性的刁钻面试题说起


欢迎继续阅读本专栏其它高赞文章:


点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Peter20 Peter20
4年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Stella981 Stella981
4年前
React Hooks实现异步请求实例—useReducer、useContext和useEffect代替Redux方案
<blockquote本文是学习了2018年新鲜出炉的ReactHooks提案之后,针对<strong异步请求数据</strong写的一个案例。注意,本文假设了:<br1.你已经初步了解<codehooks</code的含义了,如果不了解还请移步<ahref"https://reactjs.org/docs/hooksintro.html
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这