파일 시그니처 검증
벌써 4번째 방어 로직이었나..? 파일 시그니처 검증이다. (자꾸 시그너처로 쓰는데 시그니처가 맞는거겠지?)
파일 시그니처는 어떠한 임의의 파일의 형식을 판별하기 위해 사용되는 특정 바이너리 값을 말한다. 파일 매직 넘버라고도 하며 여기서 다뤄볼거는 엄밀히 말해 헤더 시그니처 검증이다. 푸터 쪽을 검증할 수도 있다.
파일 타입 | 바이너리 | decoded text |
png | 89 50 4E 47 0D 0A 1A 0A | ‰PNG.... |
jpeg | FF D8 FF D0 / FF D8 FF D1 | ÿØÿà / ÿØÿá |
gif | 47 49 46 38 37 61 / 47 49 46 38 39 61 | GIF87a / GIF89a |
25 50 44 46 2D | %PDF- | |
webp | 52 49 46 46 ?? ?? ?? ?? 57 45 42 50 | RIFF????WEBP |
이 외에도 파일 포맷이 다양한 만큼 당연히 시그니처도 그만큼 많다. 우선 뭔가 많이 사용될 것만 같고(?) 현재 노트북에 보이는 파일 타입 위주로만 아주 간략하게 작성해보았다.
아래는 Hexa editor 파일로 몇 가지 타입을 구경해본 결과!
시그니처를 검증하는 코드는 아래와 같다.
function fileSignature($file) {
$open_file = fopen($file, 'rb');
$header = fread($open_file, 8);
fclose($open_file);
// JPEG 시그니처 검사
if (substr(bin2hex($header), 0, 6) == 'ffd8ff') {
return 'JPEG';
}
// PNG 시그니처 검사
if (bin2hex($header) == '89504e470d0a1a0a') {
return 'PNG';
}
// PDF 시그니처 검사
if (substr(bin2hex($header), 0, 8) == '25504446') {
return 'PDF';
}
return 'unknown';
}
간략하게 설명하자면 파일을 바이너리 읽기 모드로 오픈한 후 오픈된 파일을 8바이트를 읽어 들여 header 변수에 저장한다. 8바이트를 읽어 들이는 이유는 단지 PNG 시그니처 검사가 위 코드에서 8바이트로 제일 길기 때문이다. webp 시그니처 검사도 있었다면 12바이트를 읽었을 것.
아무튼 그렇게 png, jpeg, pdf 시그니처만 검사하는 간단한 코드이다. jpeg는 FF D8 FF E0 / FF D8 FF E1 두 가지가 있어서 그냥 간단하게 앞 3바이트만 검사했다. 그리고 시그니처가 맞으면 그에 관련된 포맷을 반환한다. 맞지 않을 시 unknown 반환.
그 이후로 unknown이 리턴되었다면 업로드 허용하지 않고 허용하는 포맷이 리턴된다면 업로드 시키면 된다.
지금까지 우회했던 방식으로 우회를 시도해봐도 시그니처를 검사하기 때문에 파일의 확장자를 우회하던 content type을 우회하던 간에 검증단에서 막히게 될 것이다.
역시 우회를 하기 위해서는 이번엔 시그니처 검증을 통과해야만 한다.
그럼 업로드에 전혀 문제없는 jpg 파일 뒤에 몰래 악의적인 코드를 삽입하면 우회가 되겠지 으헤헿ㅔ
정상 jpg 파일 뒤에 one-line webshell 코드를 삽입했다. 그러면 헤더 시그니처 검증을 해도 허용될 것이다.
하지만 아뿔싸! 생각을 해보니 jpg로 올렸기 때문에 실행할 방법이 없다. 현재 공부하는 건 파일 시그니처만 검증한다는 가정이기 때문에 데이터는 그대로 한 채 확장자를 php로 올려주면 된다.
업로드한 green.php 파일이 뭔가 이상하게 깨져있는데 원래 그림 파일이었던 것을 php 파일로 그냥 변환시켜버리니까 php 코드를 제외한 나머지는 깨져보이는게 당연한 것이다. php 코드는 정상 작동중이니 poc 코드 대충 때려 넣어주면 된다.
성공!