Basic session support for the web console

git-svn-id: file:///home/svn/framework3/trunk@4344 4d416f70-5f16-0410-b530-b9f4589650da
unstable
HD Moore 2007-02-10 18:07:08 +00:00
parent f4fd1051da
commit d0f3f574b0
11 changed files with 597 additions and 168 deletions

View File

@ -10,9 +10,9 @@ class ConsoleController < ApplicationController
def index
# Work around rails stupidity
if(not $webrick_hooked)
if(not $webrick_hooked_console)
$webrick.mount_proc("/_session") do |req, res|
$webrick.mount_proc("/_console") do |req, res|
res['Content-Type'] = "text/javascript"
@ -22,11 +22,12 @@ class ConsoleController < ApplicationController
out = ''
tsp = Time.now.to_i
prompt_old = console.prompt
# Poll the console output for 15 seconds
while( tsp + 15 > Time.now.to_i and out.length == 0)
while( tsp + 15 > Time.now.to_i and out.length == 0 and console.prompt == prompt_old)
out = console.read()
select(nil, nil, nil, 0.25)
select(nil, nil, nil, 0.10)
end
out = out.unpack('C*').map{|c| sprintf("%%%.2x", c)}.join
@ -42,7 +43,7 @@ class ConsoleController < ApplicationController
end
end
$webrick_hooked = true
$webrick_hooked_console = true
end
cid = params[:id]

View File

@ -1,3 +1,8 @@
#
# Author: Metasploit LLC
# Description: The AJAX console controller of msfweb
#
class SessionsController < ApplicationController
layout 'windows'
@ -9,5 +14,57 @@ class SessionsController < ApplicationController
end
def interact
end
# Work around rails stupidity
if(not $webrick_hooked_session)
$webrick.mount_proc("/_session") do |req, res|
res['Content-Type'] = "text/javascript"
m = req.path_info.match(/cid=(\d+)/)
if (m and m[1] and $msfweb.sessions[m[1].to_i])
cid = m[1].to_i
$msfweb.connect_session(cid)
out = ''
tsp = Time.now.to_i
# Poll the session output for 15 seconds
while( tsp + 15 > Time.now.to_i and out.length == 0)
out = $msfweb.read_session(cid)
select(nil, nil, nil, 0.10)
end
out = out.unpack('C*').map{|c| sprintf("%%%.2x", c)}.join
script = "// Metasploit Web Session Data\n"
script += "var ses_update = unescape('#{out}');\n"
res.body = script
else
res.body = '// Invalid session ID'
end
end
$webrick_hooked_session = true
end
cid = params[:id].to_i
$msfweb.connect_session(cid)
if(params[:cmd])
if (params[:cmd].strip.length > 0)
$msfweb.write_session(cid, params[:cmd] + "\n")
end
script = "// Metasploit Web Session Data\n"
script += "var ses_update = unescape('');\n"
send_data(script, :type => "text/javascript")
end
end
end

View File

@ -1,2 +1,50 @@
<h1>Sessions#interact</h1>
<p>Find me in app/views/sessions/interact.rhtml</p>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="eng">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<meta name="Author" content="LMH (lmh@info-pull.com), Metasploit LLC" />
<meta name="Copyright" content="(c) 2006, LMH (lmh@info-pull.com), (c) 2006-2007 Metasploit LLC" />
<title>msfweb v.3 - console demo</title>
<% ["prototype","effects","controls", "window", "application", "session"].each do |js| %>
<%= javascript_include_tag js %><% end %>
<%= stylesheet_link_tag "msfsession" %>
</head>
<body onload="session_init(<%=params[:id]%>)">
<div id="session_window">
<div id="session_output">
</div>
<table id="session_command_bar" border=0 padding=4 cellspacing=0 width='100%'>
<tr>
<td
nowrap='true'
valign='top'
id="session_prompt"
>
&gt;&gt;&nbsp;
</td>
<td nowrap='true' width='100%'>
<textarea
id="session_input"
class="input"
wrap="off"
onkeydown="return session_keydown(event)"
onkeypress="return session_keypress(event)"
rows="1"
></textarea>
</td>
</tr>
</table>
</div>
</body>
</html>

View File

@ -1,13 +1,24 @@
<table cellpadding="0" cellspacing="0" border="0">
<% if(@sessions.length > 0) %>
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<thead>
<tr>
<th>ID</th>
<th>Description</th>
<th width="10">ID</th>
<th width="40">Target</th>
<th width="60">Payload</th>
<th width="60">Exploit</th>
</tr>
</thead>
<tbody>
<% @sessions.each_pair do |n,m| %>
<tr><td><%= n %></td><td><%= m %></td></tr>
<tr>
<td><%= n %></td>
<td><a href="/sessions/interact/<%= n %>"><%= m.tunnel_peer %></a></td>
<td><%= m.via_payload %></td>
<td><%= m.via_exploit %></td>
<% end %>
</tbody>
</table>
<% else %>
There are no active sessions, go exploit something ;-)
<% end %>

View File

@ -55,7 +55,8 @@ require 'msf/base'
$msfweb = Msf::Ui::Web::Driver.new({'LogLevel' => 5})
$msframework = $msfweb.framework
$webrick = nil
$webrick_hooked = false
$webrick_hooked_console = false
$webrick_hooked_session = false
module WEBrickHooker
def initialize(*args)

View File

@ -54,7 +54,7 @@ function console_refocus() {
}
function console_read() {
new Ajax.Updater("console_update", '/_session/cid=' + console_id, {
new Ajax.Updater("console_update", '/_console/cid=' + console_id, {
asynchronous:true,
evalScripts:true,
onComplete:console_read_output
@ -85,7 +85,7 @@ function console_read_output(req) {
console_update_output(req);
// Reschedule the console reader
setTimeout(console_read, 1000);
setTimeout(console_read, 1);
}
function console_update_output(req) {

View File

@ -0,0 +1,183 @@
/*
* Copyright (c) 2006 LMH <lmh[at]info-pull.com>
* Added to Metasploit under the terms of the Metasploit Framework License v1.2
* Additions Copyright (C) 2006-2007 Metasploit LLC
*/
var session_id;
var session_history = new Array(); // Commands history
var session_hindex = 0; // Index to current command history
var session_input; // Object to console input
var session_output; // Object to console output
var session_prompt; // Object to console prompt
var session_status;
var session_cmdbar;
// Placeholders
var ses_prompt = "";
var ses_update = "";
var ses_tabbed = "";
// Internal commands
var cmd_internal =
{
clear:function() {
session_output.innerHTML = '';
}
};
function status_busy() {
session_input.style.color = "red";
}
function status_free() {
session_input.style.color = "white";
}
// This function is based on the excellent example:
// http://tryruby.hobix.com/js/mouseApp.js
function keystroke_block(e) {
e.cancelBubble=true;
e.returnValue = false;
if (window.event && !window.opera) e.keyCode=0;
if (e.stopPropagation) e.stopPropagation();
if (e.preventDefault) e.preventDefault();
return false;
}
function session_refocus() {
session_input.blur();
session_input.focus();
}
function session_read() {
new Ajax.Updater("session_update", '/_session/cid=' + session_id, {
asynchronous:true,
evalScripts:true,
onComplete:session_read_output
});
}
function session_printline(s, type) {
if ((s=String(s))) {
var n = document.createElement("div");
// IE has to use innerText
if (n.innerText != undefined) {
n.innerText = s;
// Firefox uses createTextNode
} else {
n.appendChild(document.createTextNode(s));
}
n.className = type;
session_output.appendChild(n);
return n;
}
}
function session_read_output(req) {
// Call the console updated
session_update_output(req);
// Reschedule the console reader
setTimeout(session_read, 1);
}
function session_update_output(req) {
try { eval(req.responseText); } catch(e){ alert(req.responseText); }
status_free();
if (ses_update.length > 0) {
session_printline(ses_update, 'output_line');
}
session_refocus();
}
function session_keypress(e) {
if (e.keyCode == 13) { // enter
session_input.value = (session_input.value.replace(/^ +/,'')).replace(/ +$/,'');
// ignore duplicate commands in the history
if(session_history[session_history.length-1] != session_input.value) {
session_history.push(session_input.value);
session_hindex = session_history.length - 1;
}
session_printline("\n" + session_input.value, 'output_line')
if(cmd_internal[session_input.value]) {
cmd_internal[session_input.value]();
session_input.value = "";
session_input.focus();
return keystroke_block(e);
}
status_busy();
new Ajax.Updater("session_update", document.location, {
asynchronous:true,
evalScripts:true,
parameters:"cmd=" + escape(session_input.value),
onComplete:session_update_output
});
session_input.value = "";
session_input.focus();
return keystroke_block(e);
}
}
function session_keydown(e) {
if (e.keyCode == 38) { // up
// TODO: place upper cmd in history on session_input.value
session_input.value = session_history[session_hindex];
if (session_hindex > 0) {
session_hindex--;
}
return keystroke_block(e);
} else if (e.keyCode == 40) { // down
if (session_hindex < session_history.length - 1) {
session_hindex++;
}
session_input.value = session_history[session_hindex];
return keystroke_block(e);
}
}
function session_init(cid) {
session_id = cid;
session_input = document.getElementById("session_input");
session_output = document.getElementById("session_output");
session_prompt = document.getElementById("session_prompt");
session_status = document.getElementById("session_status");
session_cmdbar = document.getElementById("session_command_bar");
session_refocus();
status_free();
session_read();
return true;
}

View File

@ -10,8 +10,8 @@ html,body {
width: 100%;
height: 100%;
background: #000000;
color: #fff;
font-family: monospace;
color: #eeeeee;
font-family: fixed;
font-size: 12px;
}
@ -48,7 +48,7 @@ html,body {
.input {
font: inherit;
font-family: monospace;
font-family: fixed;
background: #000000;
border: 0;
color: white;
@ -61,7 +61,7 @@ html,body {
color: white;
font: inherit;
text-align: right;
font-family: monospace;
font-family: fixed;
font-size: 12px;
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2006, LMH <lmh@info-pull.com>
* All Rights Reserved.
*
* Standards compliant:
* Valid, warning-free CSS: http://jigsaw.w3.org/css-validator
*/
html,body {
width: 100%;
height: 100%;
background: #000000;
color: #eeeeee;
font-family: fixed;
font-size: 12px;
}
#session_window {
background: #000000;
padding: 1em;
}
#session_output {
width: 100%;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
background: #000000;
}
.output_line {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
#input {
width: 100%;
border: none;
padding: 0;
background: #000000;
}
.input {
font: inherit;
font-family: fixed;
background: #000000;
border: 0;
color: white;
width: 600px;
overflow: hidden;
font-size: 12px;
}
.prompt {
color: white;
font: inherit;
text-align: right;
font-family: fixed;
font-size: 12px;
}
#session_command_bar {
background: #000000;
}
#session_status {
color: #fff;
font-family: monospace;
font-size: 12px;
text-transform: smallcaps;
}

155
lib/msf/ui/web/console.rb Normal file
View File

@ -0,0 +1,155 @@
module Msf
module Ui
module Web
###
#
# This class implements a console instance for use by the web interface
#
###
class WebConsole
attr_accessor :pipe
attr_accessor :console
attr_accessor :console_id
attr_accessor :last_access
attr_accessor :framework
attr_accessor :thread
class WebConsolePipe < Rex::IO::BidirectionalPipe
attr_accessor :input
attr_accessor :output
attr_accessor :prompt
attr_accessor :killed
def intrinsic_shell?
true
end
def supports_readline
false
end
def _print_prompt
end
#
# Wrapper methods around input pipe
#
def close
self.pipe_input.close
end
def put(*args)
self.pipe_input.put(*args)
end
def gets
self.pipe_input.gets
end
def pgets
self.pipe_input.gets
end
def eof?
self.pipe_input.eof?
end
def fd(*args)
# Remove the following line to enable full sessions via the console
# We really should just hook the on_session() instead...
raise ::RuntimeError, "Session interaction should be performed via the Sessions tab"
self.pipe_input.fd(*args)
end
def sysread(*args)
self.pipe_input.sysread(*args)
end
end
#
# Provides some overrides for web-based consoles
#
module WebConsoleShell
def supports_color?
false
end
end
def initialize(framework, console_id)
# Configure the framework
self.framework = framework
# Configure the ID
self.console_id = console_id
# Create a new pipe
self.pipe = WebConsolePipe.new
self.pipe.input = self.pipe.pipe_input
# Create a read subscriber
self.pipe.create_subscriber('msfweb')
# Initialize the console with our pipe
self.console = Msf::Ui::Console::Driver.new(
'msf',
'>',
{
'Framework' => self.framework,
'LocalInput' => self.pipe,
'LocalOutput' => self.pipe,
'AllowCommandPassthru' => false,
}
)
self.console.extend(WebConsoleShell)
self.thread = Thread.new { self.console.run }
update_access()
end
def update_access
self.last_access = Time.now
end
def read
update_access
self.pipe.read_subscriber('msfweb')
end
def write(buf)
update_access
self.pipe.write_input(buf)
end
def execute(cmd)
self.console.run_single(cmd)
end
def prompt
self.pipe.prompt
end
def tab_complete(cmd)
self.console.tab_complete(cmd)
end
def shutdown
self.pipe.killed = true
self.pipe.close
self.thread.kill
end
end
end
end
end

View File

@ -8,151 +8,8 @@ module Ui
module Web
require 'rex/io/bidirectional_pipe'
require 'msf/ui/web/console'
###
#
# This class implements a console instance for use by the web interface
#
###
class WebConsole
attr_accessor :pipe
attr_accessor :console
attr_accessor :console_id
attr_accessor :last_access
attr_accessor :framework
attr_accessor :thread
class WebConsolePipe < Rex::IO::BidirectionalPipe
attr_accessor :input
attr_accessor :output
attr_accessor :prompt
attr_accessor :killed
def intrinsic_shell?
true
end
def supports_readline
false
end
def _print_prompt
end
#
# Wrapper methods around input pipe
#
def close
self.pipe_input.close
end
def put(*args)
self.pipe_input.put(*args)
end
def gets
self.pipe_input.gets
end
def pgets
self.pipe_input.gets
end
def eof?
self.pipe_input.eof?
end
def fd(*args)
raise ::RuntimeError, "Session interaction should be performed via the Sessions tab"
self.pipe_input.fd(*args)
end
def sysread(*args)
self.pipe_input.sysread(*args)
end
end
#
# Provides some overrides for web-based consoles
#
module WebConsoleShell
def supports_color?
false
end
end
def initialize(framework, console_id)
# Configure the framework
self.framework = framework
# Configure the ID
self.console_id = console_id
# Create a new pipe
self.pipe = WebConsolePipe.new
self.pipe.input = self.pipe.pipe_input
# Create a read subscriber
self.pipe.create_subscriber('msfweb')
# Initialize the console with our pipe
self.console = Msf::Ui::Console::Driver.new(
'msf',
'>',
{
'Framework' => self.framework,
'LocalInput' => self.pipe,
'LocalOutput' => self.pipe,
'AllowCommandPassthru' => false,
}
)
self.console.extend(WebConsoleShell)
self.thread = Thread.new { self.console.run }
update_access()
end
def update_access
self.last_access = Time.now
end
def read
update_access
self.pipe.read_subscriber('msfweb')
end
def write(buf)
update_access
self.pipe.write_input(buf)
end
def execute(cmd)
self.console.run_single(cmd)
end
def prompt
self.pipe.prompt
end
def tab_complete(cmd)
self.console.tab_complete(cmd)
end
def shutdown
self.pipe.killed = true
self.pipe.close
self.thread.kill
end
end
###
#
@ -164,6 +21,7 @@ class Driver < Msf::Ui::Driver
attr_accessor :framework # :nodoc:
attr_accessor :consoles # :nodoc:
attr_accessor :sessions # :nodoc:
attr_accessor :last_console # :nodoc:
ConfigCore = "framework/core"
@ -181,8 +39,8 @@ class Driver < Msf::Ui::Driver
# Set the passed options hash for referencing later on.
self.opts = opts
# Initalize the consoles set
self.consoles = {}
self.sessions = {}
# Initialize configuration
Msf::Config.init
@ -195,10 +53,6 @@ class Driver < Msf::Ui::Driver
# Initialize the console count
self.last_console = 0
# Give the comm an opportunity to set up so that it can receive
# notifications about session creation and so on.
# Comm.setup(framework)
end
def create_console
@ -228,6 +82,48 @@ class Driver < Msf::Ui::Driver
end
end
def write_session(id, buf)
ses = self.framework.sessions[id]
return if not ses
ses.user_input.put(buf)
end
def read_session(id)
ses = self.framework.sessions[id]
return if not ses
ses.user_output.read_subscriber('session_reader')
end
# Detach the session from an existing input/output pair
def connect_session(id)
# Ignore invalid sessions
ses = self.framework.sessions[id]
return if not ses
# Has this session already been detached?
return if ses.user_output.has_subscriber?('session_reader')
# Create a new pipe
spipe = WebConsole::WebConsolePipe.new
spipe.input = spipe.pipe_input
# Create a read subscriber
spipe.create_subscriber('session_reader')
# Replace the input/output handles
ses.user_input = spipe.input
ses.user_output = spipe
Thread.new do
ses.interact
end
end
def sessions
self.framework.sessions
end
#
# Stub
#