Hello, I'm 8ayac 🐝

I created the Web challenge Old Pal for LINE CTF 2023, held from March 25, 2023 at 09:00 JST to March 26, 2023 at 09:00 JST.

Out of a field of 477 teams1, 67 participants solved this challenge, and its final score was 119 points. Thank you, everyone!

This post explains the intended solution and the key points of the challenge.

The challenge and its solution

The following Perl program was distributed with the challenge. The goal is to provide input that gets past the various checks and reaches print "Congrats! Flag is LINECTF{redacted}" on line 52.

#!/usr/bin/perl
use strict;
use warnings;

use CGI;
use URI::Escape;


$SIG{__WARN__} = \&warn;
sub warn {
    print("Hacker? :(");
    exit(1);
}


my $q = CGI->new;
print "Content-Type: text/html\n\n";


my $pw = uri_unescape(scalar $q->param("password"));
if ($pw eq '') {
    print "Hello :)";
    exit();
}
if (length($pw) >= 20) {
    print "Too long :(";
    die();
}
if ($pw =~ /[^0-9a-zA-Z_-]/) {
    print "Illegal character :(";
    die();
}
if ($pw !~ /[0-9]/ || $pw !~ /[a-zA-Z]/ || $pw !~ /[_-]/) {
    print "Weak password :(";
    die();
}
if ($pw =~ /[0-9_-][boxe]/i) {
    print "Do not punch me :(";
    die();
}
if ($pw =~ /AUTOLOAD|BEGIN|CHECK|DESTROY|END|INIT|UNITCHECK|abs|accept|alarm|atan2|bind|binmode|bless|break|caller|chdir|chmod|chomp|chop|chown|chr|chroot|close|closedir|connect|cos|crypt|dbmclose|dbmopen|defined|delete|die|dump|each|endgrent|endhostent|endnetent|endprotoent|endpwent|endservent|eof|eval|exec|exists|exit|fcntl|fileno|flock|fork|format|formline|getc|getgrent|getgrgid|getgrnam|gethostbyaddr|gethostbyname|gethostent|getlogin|getnetbyaddr|getnetbyname|getnetent|getpeername|getpgrp|getppid|getpriority|getprotobyname|getprotobynumber|getprotoent|getpwent|getpwnam|getpwuid|getservbyname|getservbyport|getservent|getsockname|getsockopt|glob|gmtime|goto|grep|hex|index|int|ioctl|join|keys|kill|last|lc|lcfirst|length|link|listen|local|localtime|log|lstat|map|mkdir|msgctl|msgget|msgrcv|msgsnd|my|next|not|oct|open|opendir|ord|our|pack|pipe|pop|pos|print|printf|prototype|push|quotemeta|rand|read|readdir|readline|readlink|readpipe|recv|redo|ref|rename|require|reset|return|reverse|rewinddir|rindex|rmdir|say|scalar|seek|seekdir|select|semctl|semget|semop|send|setgrent|sethostent|setnetent|setpgrp|setpriority|setprotoent|setpwent|setservent|setsockopt|shift|shmctl|shmget|shmread|shmwrite|shutdown|sin|sleep|socket|socketpair|sort|splice|split|sprintf|sqrt|srand|stat|state|study|substr|symlink|syscall|sysopen|sysread|sysseek|system|syswrite|tell|telldir|tie|tied|time|times|truncate|uc|ucfirst|umask|undef|unlink|unpack|unshift|untie|use|utime|values|vec|wait|waitpid|wantarray|warn|write/) {
    print "I know eval injection :(";
    die();
}
if ($pw =~ /[Mx. squ1ffy]/i) {
    print "You may have had one too many Old Pal :(";
    die();
}


if (eval("$pw == 20230325")) {
    print "Congrats! Flag is LINECTF{redacted}"
} else {
    print "wrong password :(";
    die();
};

Lines 21 through 36 impose the basic restrictions, which can be summarized as follows.

  • The password must not be empty.
  • The password must be no longer than 20 characters.
  • The password must contain no characters other than digits, letters, underscores, and hyphens.
  • The password must contain at least one digit, at least one letter, and an underscore or a hyphen.

The filters from line 37 onward are intended to prevent unintended solutions.

The intended solution uses the special token __LINE__. This token is compiled into a value indicating the current line. See https://perldoc.perl.org/functions/__LINE__ for details.

The filters can be summarized as follows.

Lines Filter Description Examples of filtered strings
L37–L40 $pw =~ /[0-9_-][boxe]/i Filters expressions using binary, octal, hexadecimal, or exponential notation. 20230326-0x1, 20230326-0b1, etc.
L41–L44 $pw =~ /AUTOLOAD|BEGIN|CHECK|DESTROY|END|... Filters input containing any of the functions listed at https://learn.perl.org/docs/keywords.html#functions. Blocking only selected functions as a defense against eval injection tends to leave vulnerabilities in practice. 20230325-caller, 20230325-wantarray, etc.
L45–L48 $pw =~ /[Mx. squ1ffy]/ Filters input containing quote-like operators. The character f is filtered to prevent the use of __FILE__. 20230325-q--, 20230325-y---, 20230325-__FILE__, etc.

The following part of the program also causes it to exit when a warning occurs.

use strict;
use warnings;

︙

$SIG{__WARN__} = \&warn;
sub warn {
    print("Hacker? :(");
    exit(1);
}

The program evaluated by eval on line 51 must therefore avoid producing any warnings.

An input that meets these conditions is 20230326-__LINE__. Note that __LINE__ evaluates to 1 inside eval here, not 51.

Accessing http://104.198.120.186:11006/cgi-bin/main.pl?password=20230326-__LINE__ then reveals the flag. You can also retrieve it with curl as follows.

$ curl http://104.198.120.186:11006/cgi-bin/main.pl?password=20230326-__LINE__ 
Congrats! Flag is LINECTF{3e05d493c941cfe0dd81b70dbf2d972b}

Unintended solutions

The challenge had the following unintended solutions.

Thoughts and background

Since this was LINE CTF, I wanted to create a challenge with a play on LINE, and eventually settled on having solvers use __LINE__.

Old Pal is the name of a cocktail I was into while writing the challenge. I hope this challenge served as an aperitif before the heavier Web challenges.

The magic constant __LINE__ also appears to exist in C, PHP, and other languages. Perl has plenty of unusual syntax and behavior, so PHP might have been the more practical choice given the effort involved in ruling out unintended solutions. This time, I chose Perl for the Pal/Perl pun.

1 Of all participating teams, 477 scored at least one point.