Hi, tụi mình là team 3r0th3r CC. Đợt vừa rồi team mình đã đứng hạng 17 vòng Tứ Kết của cuộc thi Digital Dragons CTF và cũng clear được hết tất cả challenge
Đây sẽ là bài writeup của vòng đó đồng thời cũng là bài debut của bọn mình <3
Nhìn sơ qua, ta có thể thấy được đây chính là trang cho phép ping tới các host. Như mọi khi thì mình bật Burp Suite lên, intercept request rồi test thôi :3
Sau khi ngồi mò tí thì mình nhận ra có vài ký tự đặc biệt không bị lọc như:
1
$\;.
Với kinh nghiệm có được từ các giải CTF trước, không quá khó để đoán ra đây là vuln Command Injection
1
;ls
Nhiệm vụ của ta đơn giản chỉ là tìm xem file chứa flag nằm ở đâu rồi đọc nó thôi
Tuy nhiên, vấn đề lúc này lại phát sinh, dấu cách bị lọc nên ta không thể thêm option -al vào lệnh ls được. Để giải quyết thì mình đã thêm $IFS thay cho dấu cách để bypass
1
;ls$IFS-al
Bây giờ, mình đã biết flag nằm trong file .flag.txt. Nhưng tới lúc đọc flag thì mình nhận ra lệnh cat cũng bị lọc mất =))
Sau một hồi nghiên cứu, nói thẳng ra là search Google, mình đã tìm ra cách. Cụ thể là ta chỉ cần thêm \ vào kế mỗi ký tự của lệnh là sẽ bypass được
Đề này đại loại bảo rằng mình phải tìm cái kho báu được giấu trong lòng ngôi đền bí ẩn gì đó. Mà mình ngồi test sơ thì chẳng thu được gì nên mình xem source luôn để phân tích được sâu hơn
functionopenTab(tabName) { var tabContents = document.getElementsByClassName('tab-content'); for (var i = 0; i < tabContents.length; i++) { tabContents[i].classList.remove('active'); } var tabs = document.getElementsByClassName('tab'); for (var i = 0; i < tabs.length; i++) { tabs[i].classList.remove('active'); } document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); }
/* Remove /api/debug in production */
Hồi đầu mình không để ý lắm nên cứ ngồi test ở endpoint /search xem có bị dính lỗi SQL Injection không, mãi sau nhìn xuống dưới mới biết là có /api/debug nằm ở dòng cuối ._.
Lúc này mình thử gửi request GET để check và server trả về 405 METHOD NOT ALLOWED. Vì vậy mình đã chuyển sang POST request xong lại nhận được 400 BAD REQUEST :)
Đọc lại file script.js, có thể thấy server sử dụng json, vì vậy nên ta cần phải đổi thành Content-Type: application/json mới được
Nhiệm vụ lúc này là làm sao để lên được admin, tuy nhiên thì mọi chuyện không hề đơn giản
Sau khi nghiên cứu, search Google, mình thấy dạng này khá giống vuln NoSQL Injection nên đã test vài payload nhưng không thành công
Nguyên lý là ta sẽ biến nó thành 1 mảng, trong đó sẽ có chứa cả value mà mình mong muốn và value hợp lệ, từ đó qua mặt hệ thống, và cách này thành công thật
2 challs này cơ bản là giống nhau vì đề có lỗ hổng… (chắc vậy idk :v)
Việc của ta là Extract file .ova
Vì file .ova là dạng nén bao gồm file mô tả OVF, file đĩa thường có định dạng .vmdk, file .mf để đảm bảo tính toàn vẹn Trong đó .vmdk là 1 tệp chứa các đĩa (tui đã sử dụng chỗ này để grep flag :b -> do không bị mã hóa nên khá ez)
Sau khi Extract vào trong tệp đó sẽ thấy các tệp đã nói trên, tiếp theo đổi đuôi .vmdk -> .zip và Extract tiếp thôi
Sau đó ta sẽ tìm thấy địa chỉ IP khá đáng ngờ, truy cập vào ta sẽ thấy thêm 1 file onedrive.zip
Tải file đó xuống và giải nén ra, flag sẽ nằm ở file mail.php :b
1 2 3 4 5 6 7 8 9
<?php
$email = array("phish@digidragonsctf.com"); //PUT YOUR EMAIL HERE!!! $telegramTOKEN = "12345678:5ab7fbc37fe19710e6e764bdfb931969"; //PUT YOUR TELEGRAM TOKEN HERE!!! PS : Hex value is your flag $telegramID = "12345678"; //PUT YOUR ID HERE!!!
?>
OSINT
ddcScapeG0at24
Đầu tiên, challenge cho ta 1 cái username ddcScapeG0at24. Sử dụng tool instantusername hoặc 1 số tool như sherlock, ta sẽ tìm được tài khoản GitHub
Mọi thứ đều hướng tới tài khoản LinkedIn, tuy nhiên tới đây lại là hẻm cụt. Team mình đã dành ra gần 3 tiếng vô nghĩa chỉ để ngồi mò xem có thể moi được gì từ cái acc này hay không .__.
Xong bọn mình lại mò về GitHub. Lúc này vài member trong team phát hiện ra email trong repo tại commit này
1
ddcScapeG0at24@gmail.com
Nên tụi mình đã sử dụng tool Epieos để trích xuất thông tin. Sau một lúc ngồi mò thì tụi mình tìm được Calendar
Khi bấm sang September 2024 để xem, tụi mình thấy 1 trang web và thông tin để đăng nhập
Giờ thì chỉ cần đăng nhập vào lấy flag nữa thôi là xong
FLAG: flag{a432a9312d26242a97984f23308e49b6}
Reverse Engineering
Happiness
Đầu tiên thì mình luôn kiểm tra xem file nó là gì
1 2
$ file "./happiness-dist" happiness-dist: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a6fe990565d61a0ed6aa417f41ac043a300556b, for GNU/Linux 4.4.0, with debug_info, not stripped
Nó là một file ELF 64-bit
Sau khi chạy file thì ta thấy nó hiện ra một giao diện GUI đăng nhập để nhập Username và Password
Sau khi quăng vô IDA và lướt sơ qua hàm main thì có thể thấy một hàm có tên là on_button_clicked.
Cụ thể, g_signal_connect_data là một hàm được sử dụng để kết nối một tín hiệu (signal) với một callback function (hàm sẽ được gọi khi tín hiệu đó được phát ra). Trong trường hợp này:
button: Là đối tượng mà bạn đang kết nối tín hiệu. Trong trường hợp này, đó là một nút (button). "clicked": Là tên của tín hiệu. Tín hiệu "clicked" sẽ được phát ra khi người dùng nhấp vào nút. on_button_clicked: Là tên của hàm callback sẽ được gọi khi tín hiệu "clicked" được phát ra. Đây là hàm mà bạn cần viết để định nghĩa hành vi khi nút bị nhấp. grid: Đây là dữ liệu bạn muốn truyền vào hàm callback on_button_clicked. Nó có thể là bất kỳ dữ liệu nào mà bạn cần trong hàm callback. 0LL, 0LL: Đây là các flags và dữ liệu người dùng khác, thường được đặt mặc định thành 0LL nếu không cần thiết sử dụng. Dòng mã này sẽ khiến hàm on_button_clicked được gọi khi nút button được nhấp, và grid sẽ được truyền vào hàm đó.
Tóm cái váy lại, dòng trên là xử lý sự kiện khi nhấp vào nút Login
Xem mã giả của hàm on_button_clicked có thể thấy được nó lấy dữ liệu đầu vào của Username và Password thông qua username_entry và password_entry và được lưu vào biến username và password
Tiếp tục lướt xuống dưới có thể thấy được nó đang kiểm tra xem username có phải là “admin” hay không. Nếu đúng thì nó sẽ sao chép chuỗi admin vào biến admin_xored và thực hiện một đoạn giải mã encrypted_flag (cờ bị mã hoá). Đoạn code mã hoá nhìn sơ qua thì cũng không có gì khó, chỉ đơn giản là sử dụng phép xor để xor chuỗi admin với 0xDE sau đó lấy encrypted_flag xor với admin_xored ta sẽ được decrypted_flag và đây cũng chính là flag mà ta cần tìm.
Đến bước này có rất nhiều cách để tìm flag và cách nhanh nhất của mình là quăng vô gdb đặt breakpoint tại if ( !strcmp(password, decrypted_flag) ) sau đó chạy chương trình bằng lệnh run hoặc r. Nhập username là admin và password có thể để trống rồi ấn login.
$ gdb "happiness-dist" --q GEF for linux ready, type `gef' to start, `gef config' to configure 88 commands loaded and 5 functions added for GDB 13.2 in 0.00ms using Python engine 3.12 Reading symbols from happiness-dist... gef➤ b* 0x0000555555556a8e Breakpoint 1 at 0x555555556a8e: file happiness.c, line 86. gef➤ r Starting program: /home/kali/CTF/DDC/Rev/Happiness/happiness-dist [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff5c006c0 (LWP 11118)] [New Thread 0x7ffff52006c0 (LWP 11119)] [New Thread 0x7fffefe006c0 (LWP 11120)]
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.070: Theme parsing error: gtk.css:2057:20: '' is not a valid color name
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.070: Theme parsing error: gtk.css:2058:16: '' is not a valid color name
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.071: Theme parsing error: gtk.css:2534:38: value 34 out of range. Must be from 0.0 to 1.0
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.074: Theme parsing error: gtk.css:4993:38: value 34 out of range. Must be from 0.0 to 1.0
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.077: Theme parsing error: gtk.css:7646:38: value 34 out of range. Must be from 0.0 to 1.0
(happiness-dist:11109): Gtk-WARNING **: 02:15:10.078: Theme parsing error: gtk.css:7785:51: value 34 out of range. Must be from 0.0 to 1.0 [New Thread 0x7fffef4006c0 (LWP 11121)] [New Thread 0x7fffeea006c0 (LWP 11130)]
Không như bài trước là sử dụng mã hoá xor đơn giản và kiểm tra username trước thì bài này lại sử dụng mã hoá RC4 và kiểm tra serial và username cùng lúc. Sau khi nhập đúng serial và username thì nó thực hiện giải mã encrypted_string bằng mã hoá RC4 với key là entered_serial cũng có nghĩa là serial của người dùng nhập vào và lưu ở dataa.
Để giải quyết bài này thì ta phải tìm được serial cũng chính là key để giải mã flag. Để ý trước khi kiểm tra điều kiện có một dòng gọi hàm strtoull và gán cho user_serial trông rất khả nghi
user_serial = strtoull(entered_serial, 0LL, 10);
strtoull: Là một hàm chuẩn trong thư viện C, có chức năng chuyển đổi một chuỗi ký tự (được biểu diễn dưới dạng số) thành một số nguyên không dấu kiểu unsigned long long.
Vậy có nghĩa là nếu như serial là ký tự chữ cái thì nó sẽ trả về 0 và nếu là chữ số thì sẽ chuyển thành một số nguyên không dấu. Vậy ta có thể chắc chắn rằng serial là một số bất kỳ nào đó mà không phải chữ cái :()
Thật ra trên thực tế là quăng vô gdb luôn chứ không phân tích như trên =))
Bây giờ hãy vô gdb và đặt breakpoint tại 0x00005555555568ad <+669>: cmp rax,QWORD PTR [rbp-0x80] tương ứng với ( *(_QWORD *)&len[4] == user_serial rồi run. Sau đó nhập username là Hacker và serial là một số bất kỳ
Trong mã assembly trên thì QWORD PTR [rbp-0x80] là nơi lưu trữ số serial mà người dùng đã nhập vào, còn rax chứa số serial cần so sánh với số serial người dùng nhập. Sử dụng lệnh p/d $rax để hiển thị số serial cần tìm. Việc còn lại là nhập serial để lấy flag thôi!!!
Có thể thấy hệ thống gọi 4 hàm nhưng trong đó 3 hàm đầu là thư viện chuẩn để setup thao tác đầu vào đầu ra của người dùng nên ta sẽ bỏ qua. Hướng sự chú ý tới hàm cuối cùng là open_portal vào xem bên trong hàm này chứa những gì:
printf("You found an unknown stone.\nPlease enter the stone's name:\n > "); __isoc99_scanf("%s", stone); printf("Inspecting the stone ***"); printf(stone); puts("***"); if ( SECRET_KEY == -559038737 ) { puts("The portal opens to a new world!"); win(); } else { puts("The stone remains inert."); } }
Ta sẽ phát hiện ra hàm win(). Đây là hàm mà đề bài yêu cầu chúng ta khai thác và lấy flag trong này. Đọc code chúng ta thấy ở đây:
1
printf(stone);
Đây là vuln Format String cho phép người dùng kiểm soát đầu vào và được đọc từ hàm scanf
Khi mình sử dụng lệnh checksec để kiểm tra những biện pháp bảo mật. Mình nhận ra tất cả đều đã bị tắt hoặc không có
1 2 3 4 5 6
RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments
Và rồi mình thấy rằng có một cách khác để ta có thể gọi hàm win() đơn giản hơn mà không cần phải exploit Format String, đó là sử dụng kỹ thuật ret2win (BoF). Đây là một kỹ thuật khai thác phổ biến nhằm chuyển hướng luồng thực thi của chương trình sang một hàm cụ thể
Đầu tiên chúng ta cần xác định vị trí của hàm win()
Tiếp đến chuyển địa chỉ vừa có sang dạng little-edian ta có:
Hiện tại trong team vẫn còn đang khá thiếu người chơi mảng này nên phần này tụi mình sẽ không nói đến các kỹ năng và lý thuyết chuyên sâu nhé :3
Independence
Trong bài này ta sẽ được đưa cho một file Python chứa một hàm dùng để mã hoá (encrypt) và tạo ra keygen cùng với một file out.txt chứa văn bản đã mã hoá
defencrypt(m, pubkey, privkey): g, p = pubkey x, _ = privkey h = pow(g, x, p) C = [] while m: y = getRandomRange(2, p) c1 = pow(g, y, p) y = (y<<1) | (m & 1) c2 = pow(h, y, p) C += [(c1, c2)] m >>= 1 return C
defkeygen(nbits=512): p = getStrongPrime(nbits) g = getRandomRange(2, p) x = getRandomRange(2, p) pub = (g, p) priv = (x, p) return pub, priv
pub, priv = keygen() m = bytes_to_long(FLAG) c = encrypt(m, pub, priv)
withopen('out.txt', 'w') as f: f.write(f'{pub = }\n') f.write(f'out = {str(c)}')
Hàm encrypt dựa và pubkey (khoá chung) và privkey (khoá riêng tư) để mã hoá m (văn bản) Sau khi dành vài tiếng để mò trên Google và sự trợ giúp của ChatGPT thì mình phát hiện ra chương trình trên là legendre symbol và dưới đây là script lấy flag:
from Crypto.Util.number import * from sympy.ntheory import legendre_symbol
# Đọc thông tin từ pubkey và ciphertext p = 9581257592556018473305786754018994054986440370491067910997313283399579058244765977967617476919486211692103485121526918608638896652486174462300514168144287 g = 5464549774190809852923763408523051958716400587576327799474715226373287205246183801056700913652415087121976663782311766735601091617825037804761387911068511
out = [ ... # thay trong out.txt ]
m_bits = []
# Khôi phục từng bit của thông điệp for c1, c2 in out: # Tính g^2x mod p g2x = pow(c1, 2, p) # Tính Legendre symbol legendre = legendre_symbol(c2, p)
# Nếu Legendre symbol là 1, thì bit là 0; ngược lại là 1 if legendre == 1: m_bits.append(0) else: m_bits.append(1)
# Xây dựng lại m từ các bit m = 0 for bit inreversed(m_bits): m = (m << 1) | bit
# Chuyển đổi m thành chuỗi flag flag = long_to_bytes(m) print(flag.decode('utf-8'))
FLAG: flag{f6b7b38f75cab050b57b2bf2a2b92bef}
MenofCulture
Đây là chall lỏ nhất trong giải này vì lúc đầu BTC chỉ cho mỗi file đã được mã hoá cùng với một file pub và mãi đến khi gần cuối mới quăng source ra một cách rất phong cách :)))
Nhìn qua file pub.pem thì có thể biết rằng đây là RSA
1 2 3 4 5 6
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnthVx5zkZF+UEva8CWMjFFK/d gF7zEZooTUWRbM3MfRDdLIXal9W8PhJpT8RnPfeJGAtA4PVuUfDbw/23+j5fFpTH 18W1Oa7PEa7YCJVdrjpG2ef7TXwGmpSXkUqTx8zhDu7Hw9biXSxBiGvZApTOSLeX IgHSiEIUbKu43RsCiQIDAQAB -----END PUBLIC KEY-----
Bây giờ hãy dùng python hoặc SSH để coi trong đây có gì nào!!!
Ở đây mình dùng Python
1 2 3 4 5 6 7 8 9
from Crypto.PublicKey import RSA
# Đọc khóa công khai từ file withopen("pub.pem", "r") as f: key = RSA.import_key(f.read())
n = key.n e = key.e print(f"n = {n}\ne = {e}")
Sau đó ta sẽ thu được kết quả như sau:
1 2
n = 162713183540670273925360771290754389689114786355448853241093028636518592961121037232646775711743110514409738823813415192804425847227123676570595264815143540806320729587612450134959312352754888089223236634317216147289460381334931873855694863560717350586193771526660796357658134609347867121461366800623337144969 e = 65537
Tới đây thì tụi mình bị ngơ và không biết nên đi tiếp như nào
Sau một vài tiếng lo lắng rớt top trong sự bất lực thì một senpai đã xuất hiện và giúp đỡ tụi mình. Và mình và team cũng cảm ơn senpai đó rất nhiều vì đã giúp team mình giải được chall này <3
Tạo mảng numpy 2 chiều với kích thước height, width, số 0 (màu đen)
1 2 3 4 5 6 7 8 9 10
for i inrange(height): for j inrange(width): index = i * width + j if index >= len(data): image_data[i, j] = 0 else: if data[index] == ' ': image_data[i, j] = 0 elif data[index] in ('$', '&', '%'): image_data[i, j] = 255
Duyệt qua từng hàng và cột của mảng
index sẽ tính toán vị trí ký tự trong chuỗi data
Nếu index vượt quá chiều dài của chuỗi data sẽ đặt pixel đó thành 0 (màu đen)
Ngược lại, index là khoảng trắng đặt pixel thành 0 (màu đen), nếu có ký tự $, &, % sẽ đặt pixel thành 255 (màu trắng)
Cảm ơn các bạn đã đọc bài viết của chúng mình. Vì lúc tụi mình giải có vài challenge quên lưu lại đề cộng với đây là lần đầu tụi mình làm blog với nhau nên sẽ còn thiếu sót. Tụi mình sẽ cố gắng hơn vào lần sau hehe :3