JSFuck (r20200302판)

문서 조회수 확인중...

1. 개요
2. 원리
3. 예시
3.1. 인터넷 익스플로러용
4. 활용
5. 여담

JSFuck
홈페이지

1. 개요


JavaScript + BrainFuck = JSFuck
난해한 프로그래밍 언어가 아닌, 난해한 프로그래밍 스타일. JavaScript 코드를 JavaScript 문법에서 사용되는 문자 중 단 6가지인 [ ] ( ) ! +만으로 구현할 수 있음에 착안하여 고안된 프로그래밍 스타일이다.

2. 원리


위 6가지 문자 중 단독으로 써서 멀쩡한 건
[]
(어레이 리터럴) 뿐인데, 여기에서 모든 것을 시작하게 된다. 예를 들어 알파벳 문자열
"a"
는 다음의 과정에 의해 얻어오게 된다.
  • ![] = false
배열 자체는 truthy한 값으로 인정되므로, 여기에 부정을 의미하는
!
(느낌표)를 사용하게 되면
false
가 나온다.
  • []+[] = ""
자바스크립트에서 오브젝트 간에
+
(더하기) 연산은 정의되어 있지 않으므로 오브젝트끼리 더하게 되면
.toString()
(문자열로 변환) 처리가 되어 문자열 간의 더하기로 처리된다. 오브젝트로 취급되는 배열을 이용하여
[] + []
를 하게 되면
[].toString() = ""
이므로, 결과적으로
"" + "" = ""
(빈 문자열)이 생성된다.
  • false + "" = "false"
부울 값인
false
에 빈 문자열을 더하게 되면
false.toString() = "false"
이므로
"false"
라는 문자열이 나오게 된다. 앞서 확인한 내용을 조합해보면
![]+([]+[]) = false + "" = "false"
가 된다.
  • !![] = true
false
를 부정하면
true
가 된다.
  • +true = 1
true
앞에 unary plus를 쓰면 숫자형
1
로 변환이 된다. # 앞서 얻어온
!![] = true
의 앞에
+
를 넣은
+!![]
는 숫자
1
이 된다.
  • "false"[1] = "a"
앞서 얻어온 문자열
"false"
의 두 번째 알파벳이 a이므로
"false"[1]
"a"
가 된다. 0부터 세는 것에 주의.
  • 지금까지 파악한 내용들을 모두 이으면
    (![]+([]+[]))[+!![]]
    =
    (false + "")[+true]
    =
    "false"[1]
    =
    "a"
    가 된다. 해냈다!!
이런 식으로 알파벳/숫자를 얻어오고, 각종 내장 오브젝트 등을 이용하여 함수 객체 등에 접근하여 함수를 만들고 또 호출하는 코드를 작성, 최종적으로 자바스크립트 프로그램을 작성하는 것이 가능하다. 실제로 위 홈페이지에 자바스크립트 코드를 입력하면 코드를 위의 6글자로 표현할 수 있도록 치환해서 JSFuck 코드를 작성한다. 간혹 위의 방법만으로는 표현할 수 없는 문자가 등장했을 경우 문자 코드를 이용하는 방법으로 어떻게든 회피하는 것을 볼 수 있다.

3. 예시


다음은 JSFuck 스타일로 작성된
alert(1)
호출 프로그램이다. 아래 텍스트를 복사하여 웹 브라우저의 개발자 도구 콘솔[1] 등에 붙여넣기를 하면 alert 창으로 1이 출력되는 것을 볼 수 있다. 코드 맨 뒤의 ()를 생략하고 개발자 도구 콘솔에 아래 코드를 입력하게 되면 함수가 만들어진 것을 확인할 수 있다.
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[
]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]
])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+
(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+
!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![
]+[])[+!+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]
+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]+[][[]])[
+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!!
[]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+([![
]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[
]+!+[]]+(!![]+[])[+!+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[])[+!+[]]+(![
]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+(!
[]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])
[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]]+[+!+[]]+(
!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[
])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[!+[]+!+[]+[+[]]])()
무려 1,227글자에 달하는 거대한 코드...인 것 같지만, 사실 잘 뜯어보면
[]["filter"]["constructor"]("alert(1)")()
로 줄어드는 것을 알 수 있고, 이는
[].filter.constructor("alert(1)")()
=
Function("alert(1)")()
으로 해석되어 최종적으로는
alert(1)
이 실행된다. 1227글자 중에 무려 1186글자를 문자열 만드는 데 썼다
또 다른 예제
간단한 예제를 통해 살펴보자.
  • 0.1 + 0.2
    =
    0.30000000000000004
number에 number를 더하면 당연히 number이다. 0.3 뒤에 0이 계속 따라붙는 것은 부동소수점의 한계 때문이다.
  • +null
    =
    0
null 앞에 unary plus를 쓰면 0으로 변환된다.
  • [] + []
    =
    ""
빈 배열끼리는 바로 더할 수 없기 때문에
.toString()
처리를 하여 "" + "" = ""이 된다.
  • [][[]]
    = undefined
  • +(![]+[])
    = NaN
  • [] + {}
    =
    "[object Object]"
위와 같이 빈 오브젝트에
.toString()
처리가 되었다.
  • ++[[]][+[]]+[+[]]
    =
    "10"
    (???)
++[[]][+[]]+[+[]]
는 의도적으로 사람 햇갈리게 만들려고 작성된 코드이긴 하나, 분석하자면 다음과 같다. 주석으로 관련 링크를 달아놓았으니 정말 그 내부까지 궁금한 사람들은 따라가보길 바란다.
  • 연산자
    ++
    +
    보다 우선순위가 높으므로[2] 표면적으로 밖에 위치한
    ++
    +
    를 기준으로 하여 코드를
    ++[[]][+[]]
    ,
    +
    ,
    [+[]]
    의 세 부분으로 이해한다.
  • +[]
    에서의
    +
    는 unary operator로,
    +[]
    +[].toString()
    [3][4][5][6][7]과 동치가 되어
    +""
    가 되고, 빈 문자열
    ""
    에 대한 unary plus 처리 결과는
    0
    [8]이다. ?
  • 위의 분석대로
    +[]
    0
    이므로 원래의 표현은
    ++[[]][0]
    ,
    +
    ,
    [0]
    의 세 부분으로 볼 수 있다.
  • ++[[]][0]
    에서
    ++
    뒤의
    [[]][0]
    는 빈 배열 1개를 담고 있는 배열의 첫 번째 원소라는 의미이므로 그 안의 배열
    []
    와 동치...일 수도 있으나 그렇다고 미리 줄여서
    ++[]
    로 쓰면 ReferenceError가 뜬다. 이는
    ++
    연산자가 레퍼런스를 받아야 하는데,
    []
    단독으로는 어느 변수에도 바인딩이 되어있지 않으므로 레퍼런스가 존재하지 않는 상태이고 따라서
    ++[]
    는 말하자면 갈 곳 잃은 명령어 상태가 된다.[9][10] 따라서
    ++[[]][0]
    ++[]
    로 이해하면 안 된다. ??
  • 다시 돌아와서,
    ++
    의 대상이
    [[]][0]
    라면 이는 배열 안에 있는 배열[11]
    ++
    , 즉
    +1
    을 시키라는 의미가 되며,
    [] + 1
    은 문자열
    "1"
    [12]이 되고, 최종적으로
    ++[[]][0]
    는 배열 안의 배열
    []
    에 1을 더하는 동작을 수행한 뒤에 그 결과값인 숫자
    1
    을 리턴하게 된다.[13] ???
  • 표현은
    1 + [0]
    으로 단순해졌다. 다른 타입 간의 더하기는 toString()을 한 결과물의 합이므로[14]
    "1" + "0"
    "10"
    이 된다.
그냥 저런거 필요 없이 "10"을 만들 땐
+!![]+(+[]+[])
만 하면 된다.
+!![]
가 Number 형의 1이고 괄호 안의
+[]+[]
는 String 형의 "0"이 되고 문자열과 숫자의 결합은 문자열이 되므로 1+"0"은 결과적으로 String형의 "10"이 된다.
[+!![]]+[+[]]
도 가능하다.
[]+[] = ""
을 생각해 보아라
접근방법이 무궁무진(?)하다

3.1. 인터넷 익스플로러용


IE에서는 문자열 추출 파트에서 어긋나기 때문에 오류가 발생한다.부분적으로 실행해보면 'constructor', 'alert(1)'이 되어야 할 것이 'ninstruntir', 'alertr1('와 같이 나타난다. 이는 위의 JSFuck 변환기에서 c, o 등의 문자열을
[]['filter']+[]
=
[].filter.toString()
에서 추출하기 때문인데[15], 이 결과가 파이어폭스 등 타 브라우저에서는
"function filter() {
    [native code]
}"
인 데 반해, IE에서는
"
function filter() {
    [native code]
}
"
로 출력되기 때문이다. 잘 보면 앞뒤에 개행 문자가 붙어 있어 글자가 하나씩 밀리는 것을 볼 수 있다. 엣지에서는 이런 문제가 나타나지 않는다.
이론적으로는
[]['filter']+[]
가 등장하는 부분을
([]['filter']+[])['trim']()
=
[].filter.toString().trim()
으로 고쳐서 파이어폭스와 인터넷 익스플로러 모두 지원하는 코드를 만들 수는 있으나, JSFuck에서 'm'을 만들려면
Number.toString()
을 거쳐야 하고, 이때 위의 문제가 발생하므로 이는 불가능하다.
[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[
]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]][([][(![]+[])[+[]]+([![]]+[][[]
])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+
(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[
]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]
+(!![]+[])[+!+[]]])[[+!+[]]+[+!+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+
[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+([![]]
+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+
!+[]]+(!![]+[])[+!+[]]]+[])[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]
+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![
]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[[+!+[]]+[+!+[]]]+(!![]+[])[+!+[]]]((
![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]
+(!![]+[])[+[]]+(![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+
[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[[!+[]+!+
[]]+[+!+[]]]+[+!+[]]+(!![]+[][(![]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+
[])[!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]])[[!
+[]+!+[]]+[+!+[]]])()
1,253자로 더 늘어난 것은 문제가 발생한, 문자열을 추출하는 숫자에 해당하는 부분을 고쳤기 때문이다.

4. 활용


XSS 공격 벡터로 활용될 수 있다. 입력값이 필터링 되는 경우, 특히 알파벳 자체가 필터링 되는 경우에는 특수 문자만을 이용해서 XSS 코드를 삽입해야 하는데, 앞에서도 설명했듯이 자바스크립트는 오직 특수 문자만을 이용해서 코드를 실행시키는 것이 가능하기에 이를 활용하여 공격할 수 있다.
하지만 JSFuck 자체를 이용해 공격하기에는 다소 무리가 있는데 코드가 너무 길어지기 때문이다. 그래서 활용 가능한 특수 문자들[16]을 이용해 직접 코드를 만들어 공격하는 게 일반적이다. JSFuck 자체를 이용한다기보다는 이러한 자바스크립트 성질들을 이용한다고 보면 된다.
워게임이나 CTF에서 간혹 이러한 방식을 이용한 XSS 공격 문제가 출제되고는 한다.

5. 여담


아래의 표에서 볼 수 있듯이 자바스크립트 내부적으로 바로 만들 수 없는 특수문자[17], 특히 한글에 대해서 취약하다.
JSFuck이 6가지 문자로만 표현되는 것에 착안해 코드 한 글자당 $$log_2 6$$비트의 공간을 차지한다고 치고 (UTF-8 기준으로) 원문과 JSFuck 치환 결과의 크기를 비교하면 다음과 같다. 위 홈페이지에서 원하는 문장을 치고 Eval Source(코드로 취급) 체크를 해제하면 실제로 몇 글자로 치환되는지 확인할 수 있다.
원문
(바이트 수)
JSFuck 글자 수
(바이트 수)
원문 대비 용량
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
(446바이트)
104,711글자
(약 33.04KB)
약 75.86배
다람쥐 헌 쳇바퀴에 타고파.
(37바이트)
116,477글자
(약 36.75KB)
1017.19배
나무위키, 여러분이 가꾸어 나가는 지식의 나무.
(64바이트)
201,606글자
(약 63.62KB)
1017.86배
K
(1바이트)
8,312글자
(약 8.12KB)
8312배
또한 재미있는 사실로 63을 표현하는 데는 48자[18]가 필요하고, 48을 표현하는 데에는 63자[19]가 필요하다.
이름이 왠지 자바스크립트에게 좆까(...)를 시전하기 위한 듯 하다.

[1] 아니면 주소창에
javascript:
라고 치고 그 뒤에 붙여넣고 엔터 쳐도 된다.
[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence[3] unary plus 연산자는 ToNumber() 처리를 한다고 한다. 따라서 ToNumber()를 확인해야 한다.[4] ToNumber()는 오브젝트(배열은 오브젝트 타입이다)에 대해 ToPrimitive() 처리를 한 결과물로 ToNumber() 처리를 한다고 하니 ToPrimitive()를 봐야 한다.[5] ToPrimitive() 처리를 위해서는 DefaultValue 내부 메소드를 참조해야 한다.[6] 배열에 대한 DefaultValue는 toString() 처리라고 한다.[7] 배열에 대한 toString()의 결과물은 .join() 명령 호출 결과가 되므로
""
가 된다.
[8] 빈 문자열에 대한 ToNumber() 결과
0
이며, unary plus 연산자는 ToNumber() 처리한 결과를 리턴하므로 최종적으로 숫자
0
이 된다.
[9]
++
자신의 파라미터를 PutValue()에 넣는다고 한다.
[10] 배열 리터럴
[]
단독으로는 primitive로 취급되므로 레퍼런스가 아니다. 따라서 PutValue() 처리를 하려고 하면 ReferenceError를 낸다.
[11] 배열 안에 있는 배열을
[0]
이라는 배열 접근자를 이용하여 꺼냈으므로 이는 레퍼런스가 생성된 상태이다.
[12] 다른 타입 간의 더하기이므로 toString() 처리 후 두 문자열을 붙이는 것이 된다.[13] ++ 연산자의 리턴 값은 ToNumber() 처리를 한 숫자이므로 앞서 받아온 문자열
"1"
은 숫자
1
이 된다.
[14] http://www.ecma-international.org/ecma-262/5.1/#sec-11.6.1[15] 사실 'o', '('는 효율성 문제 때문에 각각 앞에 문자열 'true', 'false'를 붙여서 사용한다.[16] 예를 들어 변수명으로 $나 _를 사용할 수 있다.[17] 심지어 몇몇 대문자도 여기에 들어간다! 예를 들어 소문자 a는 15글자면 만들 수 있지만, 대문자 A를 만들려면 726글자가 필요하며, 심지어 대문자 K는 8,312글자가 필요하다. 소문자 중에서는 z가 1,807자로 가장 많은 글자 수를 필요로 한다.[18]
[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]
[19]
[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]