こんにちは、8ayacです🐝

2023/03/25 9:00(JST)から2023/03/26 9:00(JST)に行われたLINE CTF 2023において、Web問のOld Palを作りました。

この問題は、477チーム1中67名によって解かれ、最終的なスコアは119点でした。皆さんありがとうございます!

この記事では、この問題の想定解法やポイントを説明します。

問題と解法

問題として配布したファイルは、下記のPerlで書かれたプログラムです。様々な条件を回避する入力を与え、52行目のprint "Congrats! Flag is LINECTF{redacted}"に到達することがゴールだとわかります。

#!/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();
};

21行目から36行目までが基本的な制約で、これらは下記のように整理できます。

  • パスワードは空ではない
  • パスワードは20文字以下であること
  • パスワードは数字、英字、アンダースコア、ハイフン以外の文字が含まれていないこと
  • パスワードは数字、英字、アンダースコアまたはハイフンのいずれかがすべて含まれていること

37行目以降は、想定解以外を防ぐためのフィルターです。

なお、想定解は__LINE__という特別なトークンを使うものです。このトークンは、現在の行を示す値にコンパイルされます。詳しくはhttps://perldoc.perl.org/functions/__LINE__をご覧ください。

各フィルターは、下記のようにまとめられます。

行数 フィルター 説明 フィルターされる文字列の例
L37-L40 $pw =~ /[0-9_-][boxe]/i 2進数、8進数、16進数、指数を使うものをフィルターします。 20230326-0x1, 20230326-0b1など
L41-L44 $pw =~ /AUTOLOAD|BEGIN|CHECK|DESTROY|END|... https://learn.perl.org/docs/keywords.html#functionsのいずれかが含まれる入力をフィルターします。なお、このように特定の関数だけを防ぐEval Injection対策は、実際には脆弱性に繋がりやすいです。 20230325-caller, 20230325-wantarrayなど
L45-L48 $pw =~ /[Mx. squ1ffy]/ quote-like演算子を含む入力をフィルターします。なおfがフィルター対象とされているのは、__FILE__を使用できないようにするためです。 20230325-q--, 20230325-y---, 20230325-__FILE__など

また、以下の部分では、警告が起こった場合にプログラムが終了するようになっています。

use strict;
use warnings;

︙

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

したがって、L51のevalによって評価されるプログラムは警告を出さないように注意しなければなりません。

これらの条件を満たす入力値が、20230326-__LINE__です。このときeval内では__LINE__が51ではなく1になることに注意してください。

あとは、http://104.198.120.186:11006/cgi-bin/main.pl?password=20230326-__LINE__にアクセスすれば、Flagが入手できます。下記のようにcurlコマンドで取ってきても良いです。

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

非想定解

この問題には、下記のような非想定解がありました。

  • 040p20-13324107: 仮数部が8進数表記の浮動小数点数を利用しています。(040p20-13324107=0x20p20-13324107ですが、xが含まれる場合、入力値はフィルターされます。)
  • 20230326-v49: https://perldoc.perl.org/perldata#Version-Stringsを利用しています。

感想と裏話

今回はLINE CTFなので、LINEにかけた問題を作ってみたいという思いがあり、最終的にはこのように__LINE__を使わせる形に落ち着きました。

問題名のOld Palは作問時期にハマっていたカクテルの名前です。本問題が他のヘビーなWeb問を解く前の食前酒になっていたら幸いです。

__LINE__というマジック定数はCやPHPなどにもあるようです。Perlの記法や仕様には不思議なものが多いので、非想定解を事前に潰しきる苦労を考えればPHPを選ぶ方が合理的だったかもしれません。今回は、PalとPerlのダジャレを優先してPerlを選びました。

1 参加チーム総数のうち、1点以上得点したチームの数が477でした。