Open c4-bot-10 opened 10 months ago
141345 marked the issue as duplicate of #239
0xean changed the severity to 2 (Med Risk)
0xean marked the issue as satisfactory
Hi @0xean,
I believe this report should be labeled as high
severity since it demonstrates a way for an attacker to directly steal and freeze user assets. In addition, I would like to bring some details to your attention that may have been overlooked. My report discusses an exploit path which leverages 3 distinct bugs. Below I will list the primary
reports that have been labeled for each of these bugs:
Primary for bug #1
(see description in my report): 466
escrow contract
. Note regarding bug #2
(see description in my report):
bug #2
is not dependent on bug #3
(see details from my report). As described in my report, the root cause for these two bugs are distinct and not dependent on eachother. Therefore, I think It would be more appropriate to revert to the initial evaluation of 179 (label 179 as primary for this distinct root cause). Primary for bug #3
(see description in my report): 239
Further description and lesser impacts of bug #3
section in my report) and have also provided an additional POC for this exploit (see gist in report). However, since the main exploit path
in my report demonstrates how to steal any and multiple rented NFTs at once (ERC721/ERC1155), without the need for the attacker to initially own any NFTs and create an active rental, I believe the main exploit path
from my report can be considered of greater impact
than the exploit demonstrated in the above issues (also discussed in my report).Based on the information above, I would like to ask you to please re-evaluate this report. I believe this report can potentially be handled in one of two ways:
Identification and demonstration of the maximum achievable impact of the root cause
(for each root cause identified above) by combining the 3 root causes, this report can be labeled as primary and all other reports mentioned above (including their duplicates) can be considered duplicates of this report (severity will not be the same between all duplicates since most fail to demonstrate high
severity impacts). I appreciate you taking the time to read my comment.
Edit: Tagging the sponsor in case they would like to observe, confirm the POC provided in my report.
@Alec1017
0xean marked the issue as not a duplicate
0xean marked the issue as duplicate of #385
@0xJCN - good news, it is high since a dupe of #385 and will be upgraded along with all other dupes here.
Also good news is that I think this should be in the report.
0xean changed the severity to 3 (High Risk)
Hi @0xean,
Thank you for taking the time to address the issues in my previous comments.
I believe all duplicates of this report show a lesser
impact than what this report demonstrates. For this reason, I believe each duplicate should receive some amount of partial credit since they do not fulfill the requisites for a full mark. Here are the requisites for a full mark
as defined in the supreme-court-decisions-fall-2023:
The requisites of a full mark report are:
- Identification and demonstration of the root cause
- Identification and demonstration of the maximum achievable impact of the root cause
This report demonstrates how an attacker can steal any, and multiple, NFTs (ERC721 & ERC1155) at once. Additionally, the attacker does not need to own any NFTs prior or create any rental orders themself prior to the exploit (rental orders are created during the exploit and immediately stopped in the same tx). For reference, the current duplicates (the ones that do not have 25% partial credit) demonstrate how an attacker can only steal ERC1155 tokens. Moreover, for this lesser impact the attacker must first own an ERC1155 of the same tokenId as the victim and must also create a rental order prior to the exploit.
Based on the information above, I believe the duplicates of this report fail to fulfill the second requisite for a full mark: Identification and demonstration of the maximum achievable impact of the root cause
.
Thank you for taking the time to read my additional comment.
@0xean The maximum impact this report demonstrates is that actively rented NFTs can be stolen and therefore affected rentals are freezed, and this impact is actually already demonstrated in a lot of the duplicates of this issue.
Here are a few examples but not limited to:
I believe all duplicates of this report show a lesser impact than what this report demonstrates.
Therefore, I don't think this is true.
@stalinMacias
This report demonstrates how an attacker can steal any, and multiple, NFTs (ERC721 & ERC1155) at once. Additionally, the attacker does not need to own any NFTs prior or create any rental orders themself prior to the exploit (rental orders are created during the exploit and immediately stopped in the same tx). For reference, the current duplicates (the ones that do not have 25% partial credit) demonstrate how an attacker can only steal ERC1155 tokens. Moreover, for this lesser impact the attacker must first own an ERC1155 of the same tokenId as the victim and must also create a rental order prior to the exploit.
This paragraph explains the justification for my statement. In my opinion, the impacts differ as I explained above. I believe the above statement also explains that the exploit in this report has a higher likely-hood of occurring than the duplicates. The range of affected NFTs is also greater. This report specifies the theft of ERC1155 and ERC721 tokens. The requirements for the attacker are also less for this exploit: attacker does not need to own any NFTs prior to exploit. Meanwhile, the duplicates require the attacker to own ERC1155 tokens of the same tokenId as the token they are stealing, and the attacker must have an active rental for that ERC1155.
0xean marked the issue as selected for report
0xean marked the issue as primary issue
Alec1017 (sponsor) confirmed
Lines of code
https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L265-L302 https://github.com/re-nft/smart-contracts/blob/3ddd32455a849c3c6dc3c3aad7a33a6c9b44c291/src/policies/Stop.sol#L313-L363
Vulnerability details
Bug Description
The main exploit presented in this report takes advantage of multiple bugs. Some of these bugs on their own can be viewed as having their own "lesser" impacts. However, this report can be considered as my demonstration of the
greatest impact
for all the bugs described below:stopRent
/stopRentBatch
functions do not validate the existence of the suppliedRentalOrder
until after the lender is sent the specified NFT. Therefore, a non-existentRentalOrder
can be supplied and then created during the callback to the lender (onERC721Received/onERC1155Received
). (Successful exploitation of this bug is dependent on the other bugs below)metadata.expiration > block.timestamp
. This enables the above bug to be utilized, as a required signature is able to be retrieved beforehand so that the order creation and fulfillment process can be executed atomically for multiple orders. (Successful exploitation of this bug is not dependent on the other bugs mentioned)Signer.sol
are not compliant with EIP-712. This can be leveraged with the above bugs to obtain a valid signature and use this signature with multiple orders that have differentmetadata.orderType
values. This also allows arbitrarysafe wallet
addresses to be included in theRentalOrder
struct supplied to thestop
functions. (Successful exploitation of this bug is not dependent on the other bugs mentioned)Combined, the above vulnerabilities allow the theft and freezing of any and/or all NFTs currently being rented out (active or expired, as long as the rental has not been stopped). The rental payments for the affected rentals will also be frozen in the
escrow
contract. The outline of thisBug Description
section will be as follows:Main exploit path
In order to thoroughly detail this exploit path I will provide my explanation as a means to prove that the following actions are possible:
RentalOrder
can be passed into astop
function and pass validation. The only requirement for this exploit is that theRentalOrder.orderType
specifies aPAY
order andmsg.sender == RentalOrder.lender
.RentalOrder
that pertain to the rental order (rented NFT) being exploited are theRentalOrder.items
andRentalOrder.rentalWallet
fields. Theitems
field will specify the rented NFT that therentalWallet
(victim) curently owns. All other fields can differ.RentalOrder
can be created and fulfilled during thereclamation
process in which the rented NFT gets transferred from therentalWallet
(victim) to theRentalOrder.lender
(specified by exploiter). This can be done during theonERC721Received
/onERC1155Received
callback to theRentalOrder.lender
. Note that during the creation and fulfillment process we will specify ourattacker safe
as theRentPayload.fulfillment
so that ourattacker safe
will receive the stolen NFT specified in theitems
field.RentalOrder
has been created in storage, the next state-modifyingsettlePayment
andremoveRentals
function calls will succeed. The first call will result in theRentalOrder.lender
receiving back the ERC20 token payment specified in thisRentalOrder
, and the second call will remove this newly createdRentalOrder
from storage.removeRentals
call the computedorderHash
(this is used to identify a specific rental order) does not take into account theRentalOrder.rentalWallet
. Therefore, this allowed us to supply the victim'ssafe wallet
instead of ourattacker safe
in thestop
function calls and ultimately produce the necessaryorderHash
that corresponds to our newly createdRentalOrder
.When a rental is stopped the
stopRent
function is invoked (thestopRentBatch
function can also be used and has similar functionality). ARentalOrder
struct is supplied to this function and the only validation for this struct is performed in the_validateRentalCanBeStoped
internal function on line 267 ofStop::stopRent
:Stop::stopRent#L265-L267
Stop::_validateRentalCanBeStoped#L126-L154
As we can see above, only the
orderType
,endTimestamp
, andlender
fields of theRentalOrder
struct are used in order to validate whether or not a rental order can be stoppped. If a non-existentRentalOrder
is supplied to thestopRent
function, this check will pass if theorderType
is aPAY
order and themsg.sender
for this call is thelender
.Next, a
rentalAssetUpdates
bytes array is constructed with respect to the NFTs specified in theRentalOrder.items
field and thesafe wallet
specified in theRentalOrder.rentalWallet
field. ThisrentalAssetsUpdate
array contains information that ties together therentalWallet
anditems
fields supplied and is used to update storage when theRentalOrder
is finally stopped (removed from storage).Stop::stopRent#L272-L282
Note that the information in the
rentalAssetsUpdates
pertains to the rental order (rented NFT) that we are exploiting. No validation or state changes occur during this construction and therefore the execution continues to line 288:Stop::stopRent#L288-L290
The above lines of code are simple to bypass. If our supplied
RentalOrder
does not specifyhooks
, then the code on lines 288 - 290 will be skipped.Next, the reclamation process is initiated:
Stop::stopRent#L292-L293
Stop::_reclaimRentedItem#L166-L177
The above code will initiate a
delegate call
via theRentalOrder.rentalWallet
(victim). This call will invoke the following code:Reclaimer::reclaimRentalOrder#L89-L99
Reclaimer.sol#L32-L49
As we can see above, the
reclaimRentalOrder
function will be invoked by theRentalOrder.rentalWallet
via a delegate call and this will result in the target NFT being transferred out of thevictim safe
and to our specifiedlender
address.For this exploit, the
lender
address supplied will be a contract (attackerContract
) and therefore aonERC721Received
/onERC1155Received
callback will be performed via this contract. During this callback, the order that will correspond to the suppliedRentalOrder
(currently non-existent) will be created and fulfilled. Here is a brief description of the steps that occur during this callback:attackerContract
creates an on-chainPAY
order (acting as lender). This order specifies the stolen NFT as the NFT to rent out.attackerContract
creates a complimentary on-chainPAYEE
order (acting as renter). This order mirrors thePAY
order previously created.attackerContract
fulfills the orders viaSeaport::matchAdvancedOrders
(acting as renter). Note that this will require a server-side signature to be retrieved beforehand. I will explain this further, along with other prerequisites for this exploit, in a later section.attacker safe
and ourattackerContract
has sentx
amount of ERC20 tokens to theescrow
contract for this rental order. (x
can be as little as1 wei
). Our newly createdRentalOrder
is then hashed and recorded in storage.During our callback the
Create::validateOrder
function will be called bySeaport
for our specifiedPAY
order (this will also occur for our complimentaryPAYEE
order, but thePAYEE
call does not result in any state changes). During this call theRentalOrder
for our newly created order will be constructed and then the hash of thatRentalOrder
will be stored in storage, indicating an active rental:Create::_rentFromZone#L579-L595
The above code shows how the
RentalOrder
is constructed. Notice that theRentalOrder
struct contains arentalWallet
field. This struct is then passed into the_deriveRentalOrderHash
function in order to derive theorderHash
. TheorderHash
is then stored in storage via theStorage::addRentals
call on line 595. Lets observe how thisorderHash
is computed:Signer::_deriveRentalOrderHash#L181-L193
According to the above code, the
RentalOrder.rentalWallet
field is not considered when creating the EIP-712 hash for theRentalOrder
struct. Therefore, theRentalOrder.rentalWallet
can be any address and the_deriveRentalOrderHash
will produce a "correct"orderHash
as long as all other fields pertain to an actual active order.After the actions in the callback are performed, execution in the
stopRent
function continues to line 296:Stop::stopRent#L295-L296
PaymentEscrow::settlePayment#L320-L329
As we can see above, all the fields of the
RentalOrder
supplied, that are used in the_settlePayment
function, pertain to our newly createdRentalOrder
. Remember, the only field that does not pertain to this new order is theRentalOrder.rentalWallet
field, which points to thevictim safe
that was the previous owner of the stolen NFT. Therefore, execution in this function call will continue as expected for anyPAY
order: thelender
andrenter
willl receive theirpro-rata
share of the rental payment that was sent to thisescrow
contract during fulfillment. Reminder: theattackerContract
is both thelender
andrenter
for this new order.Finally, the
RentalOrder
supplied will be removed from storage:Stop::stopRent#L299-L302
First, the
orderHash
for the suppliedRentalOrder
is retrieved via the_deriveRentalOrderHash
internal function. As mentioned previously, theRentalOrder.rentalWallet
is ignored when computing theorderHash
and therefore the computedorderHash
will correctly pertain to our newly created order (this is despite the fact that the suppliedRentalOrder.rentalWallet
points to thevictim safe
and not ourattacker safe
).The
_convertToStatic
internal function on line 301 ofStop::stopRent
will simply create aRentalAssetUpdate[]
array which contains arentalId
that pertains the thevictim safe
and details of the target NFT.The
Storage::removeRentals
function will then be called:Storage::removeRentals#L216-L233
As we can see above, our
orderHash
is removed from storage on line 225. And the state update on line 233 only pertains to the victim's rental order. Note: When the victim's rental order was created therentedAssets[asset.rentalId]
was incremented byasset.amount
and here the mapping is simply being decremented byasset.amount
. Theattacker safe
is now the owner of the target NFT. Since theorderHash
corresponding to theRentalOrder
, that theattackerContract
created, was removed from storage during this call tostopRent
, the stolen NFT will remain frozen in theattacker safe
(only rentals with anorderHash
stored in storage can be stopped). In addition, the rental payment for the exploitedRentalOrder
will remain frozen in theescrow
contract since that rental order can also not be stopped. This is due to the fact that the affected order'ssafe wallet
no longer owns the NFT. Thus, the reclamation process will fail when the NFT is attempted to be transferred out of thesafe wallet
(theattacker safe
is now the owner of these NFT). A malicious actor is able to perform this exploit to steal and affect multiple NFTs (and their respective rental orders) by utilizing thestopRentBatch
function.Prerequisites for the main exploit
Now I will discuss the prerequisites for this exploit. The first being that our
attackerContract
will require a valid server-side signature in order to fulfill thePAY
andPAYEE
orders created during the callback. Note:PAY
orders require the fulfiller to also create a complimentaryPAYEE
order. The fulfiller is then expected to receive signatures for both of these orders and supply them together to theSeaport::matchAdvancedOrders
function in order to fulfill both thePAY
andPAYEE
orders. Let us observe the digest that theserver-side signer
signs in order to create a valid signature:Create::validateOrder#L759-L763
Line 761 will return the digest and then the
_recoverSignerFromPayload
internal function will recover the signing address via the digest and the suppliedsignature
. Zooming in on the_deriveRentPayloadHash
function we will observe that this should return the EIP-712 hash of theRentPayload
struct.Signer::_deriveRentPayloadHash#L248-L260
Below is the
RentPayload
struct:RentalStructs.sol#L154-L159
However, we will notice that the derived hash of the
OrderMetadata
struct computed in the_deriveOrderMetadataHash
internal function is not EIP-712 compliant:Signer::_deriveOrderMetadataHash#L218-L237
Below is the
OrderMetadata
struct:RentalStructs.sol#L50-L59
As we can see above, the
orderType
andemittedExtraData
fields are not considered when deriving the EIP-712 hash for theOrderMetadata
struct. Since thisOrderMetadata
struct is present in theRentPayload
struct, which is used as the signature digest, then theorderType
can be changed and the derivation of theRentPayload
EIP-712 hash will be "correct" (as long as therentDuration
andhooks
fields are the correct). In addition, we will also notice that theOrderMetadata
struct contains the only information in this digest that pertains to the order that the fulfiller is signing. Specifically, only therentDuration
and thehooks
will be considered. The other fields in theRentPayload
struct are supplied and therefore defined via the front-end when the signature is being created.It is important to note that when a
PAY
order is stopped (via one of thestop
functions), theendTimestamp
(which is dependent on therentDuration
) is not considered when thelender
is the one stopping the order (whenlender == msg.sender
). Since our main exploit requires that ourattackerContract
atomically creates and fulfills aPAY
/PAYEE
order, therentDuration
ultimately does not matter since ourattackerContract
is thelender
and will be calling thestopRent
function directly (attackerContract == lender == msg.sender
). Therefore, the only loose requirement for our main exploit is that thehooks
field is empty (this is mostly for convenience and to keep the execution flow minimal). Thus, we are able to create a valid signature via the front-end by utilizing virtually any order (theorderType
does not matter either since this field is not considered in the derivation of the EIP-712 hash of theOrderMetadata
struct). However, you may have noticed that when creating the signature we must specify arenter
as theRentPayload.intendedFulfiller
and asafe wallet
as theRentPayload.fulfillment
. This leads us to our second prerequisite:When an order is fulfilled the
RentPayload.intendedFulfiller
and theRentPayload.fulfillment
fields are validated:Create::_rentFromZone#L537-L538
Create::_isValidSafeOwner#L647-L655
As we can see above, the
RentPayload.fulfillment.recipient
must be a validsafe wallet
deployed via the protocol and theRentPayload.fulfiller
(seaportPayload.fulfiller == RentPayload.fulfiller
) must be an owner of thatsafe wallet
. Therefore, before we obtain a signature for our exploit we must first create ourattackerContract
, deploy a safe for our exploit (attacker safe
), and ensure that ourattackerContract
is an owner of that safe. Fortunately, the process of deploying asafe wallet
is permissionless and we are able to specify theowners
that we would like to have initialized for our deployed safe:Factory::deployRentalSafe#L138-L145
Therefore, we can call the above function and supply our
attackerContract
as anowner
during this function call. This will result in ourattacker safe
being created and ourattackerContract
being an owner of this safe. Then we can create a valid fulfillment signature via the front-end (using any order) and specify ourattackerContract
as theRentPayload.fulfiller
and ourattacker safe
as theRentPayload.fulfillment
. Now we have a valid signature that we are able to use for our exploit. Note that our exploit requires us to create aPAY
order andPAY
orders require a complimentaryPAYEE
order to be created and fulfilled as well. Therefore, we will need to provide a valid signature for ourPAY
order fulfillment and ourPAYEE
order fulfillment. As mentioned previously, since the derived EIP-712 hash of theOrderMetadata
struct, that is signed by theserver-side signer
, does not take into account theorderType
field, we are able to obtain one valid signature for ourattackerContract
and modify theorderType
as we please to utilize it for ourPAY
andPAYEE
orders. Performing these actions before we initiate our main exploit will result in the actions detailed above in theMain exploit path
section to execute successfully.Further description and lesser impacts of bug
#2
The predictable signature digest allows some server-side restrictions to be bypassed.
Current server-side restrictions that the sponsor has identified:
startAmount
andendAmount
must be the sameAs explained in the previous section, the EIP-712 hash of the
RentPayload
is the signature digest that theserver-side signer
will sign and this digest + signature are then used to validate order fulfillments during theCreate::validateOrder
call. We have shown that theRentPayload.metadata
is the only field that pertains to the specific order that is being fulfilled (all other fields in theRentPayload
are supplied during the signing process via the front-end and pertain to the fulfiller). However, thismetadata
field contains generic details of a rental order that can possibly be similar/identical to other rental orders:RentalStructs.sol#L50-L59
Therefore, if a user wishes to bypass a server-side restriction (using the ones above as an example) that does not validate fields in the
OrderMetadata
struct, then the user can simply create a temporary order via the front-end with valid parameters, obtain a signature for that order via the front-end, cancel their initial order viaSeaport::incrementCounter
, create another on-chain order viaSeaport::validate
(using restricted parameters), and use the previously obtained signature during the fulfillment of this restricted order. This vulnerability is of lesser impact due to the fact that the impact is vague asserver-side restrictions
are arbitrary and can be changed, removed, added, etc...Further description and lesser impacts of bug
#3
The following functions have been identified to be non-compliant with EIP-712: the
_deriveRentalOrderHash
,_deriveRentPayloadHash
, and the_deriveOrderMetadataHash
functions. However, only the_deriveRentPayloadHash
and_deriveOrderMetadataHash
functions can result in 3rd party integrators being unable to properly integrate with reNFT (Medium impact). To further explain, the_deriveRentPayloadHash
is used to derive the signature digest that theserver-side signer
signed to validate order fulfillment. At the present moment, it seems reNFT uses this function in order to derive the signature digest (Seen in their tests. Also none of their tests would pass if they derived the "correct" EIP-712 hashes). Therefore, if 3rd party integrators supply a "correct" EIP-712 digest (RentPayload
hash) to theserver-side signer
(perhaps via an SDK), then the resulting signature will not be considered valid during the fulfillment of an order:Create::validateOrder#L760-L763
Additionally, the
_deriveOrderMetadataHash
function is used to compare the EIP-712OrderMetadata
hash to theOrderComponents.zoneHash
. Note that the documentation defines thezoneHash
as the EIP-712 hashed version of theOrderMetadata
struct. Observe the following quote from thecreating-a-rental.md
documentation:According to the above information, the creator of the order must also use the
_deriveOrderMetadataHash
to derive the "wrong" EIP-712 hash and supply this as thezoneHash
when creating (signing) the seaport order. If the creator instead uses the "correct" EIP-712 hash, then the following comparison performed during order fulfillment will result in a revert:Create::_rentFromZone#L534-L535
Create::_isValidOrderMetadata#L635-L637
Finally, the non-compliance of the
_deriveRentalOrderHash
can also result in a differenthigh
impact than the main impact described in this report. This impact can be achieved solely by exploiting this bug (not dependent on other bugs) and will allow a malicious actor to steal ERC1155 tokens (not possible for ERC721 tokens) that are actively rented, as well as freeze the rental payment of the affectedRentalOrder
. Since this impact only affects ERC1155 tokens and requires the exploiter to first obtain ERC1155 tokens (of equivalent and equal value) as the rental order that is being exploited, this impact is considered "lesser" than the the main impact showcased in this report (although this impact still results in the direct theft and freezing of assets and therefore I believe it would be classified as ahigh
as well). Below is a brief description of the vulnerability:A malicious actor will need to create a
RentalOrder
that has an identicalRentalOrder.items
field compared to thetarget RentalOrder
that they will be exploiting. Therefore, if the targetRentalOrder
specifies the rental of5
ERC1155 tokens ofid == 0
, then the malicious actor will have to also obtain5
ERC1155 tokens ofid == 0
and create a rental order for those tokens. The malicious actor will then call thestopRent
function and supply theirRentalOrder
. However, they will leverage the bug inside of the_deriveRentalOrderHash
internal function and modify theirRentalOrder.rentalWallet
field to point to thetarget RentalOrder.rentalWallet
(the victim'ssafe wallet
). For simplicity we will assume that the malicious actor'sRentalOrder
has expired and is therefore allowed to be stopped.When the execution in the
stopRent
function reaches the reclamation process, the5
ERC1155 tokens ofid == 0
will be sent fromRentalOrder.rentalWallet
(victim'ssafe wallet
) to the malicious actor (RentalOrder.lender
). Similar to the previous explanations, thesettlement
process will proceed as expected for any expired order. The nextStorage::removeRentals
call will also succeed as usual since theorderHash
that is removed from storage is derived using the_deriveRentalOrderHash
function and since this function does not consider therentalWallet
, the producedorderHash
will correctly be associated with the malicious actor'sRentalOrder
(despite therentalWallet
pointing to the victim'ssafe wallet
). However, the maliciousRentalOrder
must have the sameitems
field as the victim's rental order due to the fact that theitems
field is considered when deriving theorderHash
:Signer::_deriveRentalOrderHash#L181-L186
Therefore, this exploit can only occur for ERC1155 tokens since the malicious actor would need to first create a
RentalOrder
for the same ERC1155 tokens (amount and tokenId) as the victim'sRentalOrder
. This is not possible for ERC721 tokens since they are non-fungible. The result of this lesser impact is that the malicious actor stole the ERC1155 tokens from the victim, froze the victim's rental payment, and the malicious actor's originally rented ERC1155 tokens will remain frozen in theirsafe wallet
(this is due to the fact that the malicious actor'sorderhash
has been removed from storage and therefore can no longer be stopped/reclaimed).Note: Since the "lesser" impact above (see gist) can still be considered of
high
severity/impact I will include an additional PoC for this exploit at the end of theProof of Concept
section, in the form of a gist.Main Impact
A malicious actor is able to steal any actively rented NFT using the exploit path detailed in the first section. The capital requirements for the exploiter are as follows: 1) Have at least
1 wei
of an ERC20 that will be used to create a maliciousRentalOrder
(the exploiter will receive these funds back at the end of the exploit). 2) create and deploy aattackerContract
and asafe wallet
(gas costs). 3) Additional gas costs will be incurred if exploiting severalvictim RentalOrders
at once viastopRentBatch
.This will result in the specified NFT to be stolen from the victim's
safe wallet
and transferred to the attacker'ssafe wallet
. Since the attacker's rental order was atomically created and stopped during this function call, no one will be able to callstopRent
for thismalicious RentalOrder
, rendering the stolen NFT frozen in the attacker'ssafe wallet
. Additionally, thevictim RentalOrders
will not be able to be stopped upon expiration since the reclamation process that occurs during thestopRent
function call will fail (victim safe wallet
no longer is the owner of the stolen NFT). This will result in the rental payment for thisvictim RentalOrder
to be frozen in theescrow
contract. An exploiter is able to prepare this exploit for multiplevictim RentalOrders
and steal multiple NFTs in one transaction viastopRentBatch
.Proof of Concept
In order to provide more context for the PoC provided below, I will detail the steps that are occuring in the PoC:
attackerContract
is created.x
amount of wei (for an arbitrary ERC20) is sent to ourattackerContract
.x == num_of_victim_orders
. These funds will be used when theattackerContract
programatically creates the malicious rental orders (ourattackerContract
will behave aslender
andrenter
).safe wallet
is created (attacker safe
) and ourattackerContract
is initialized as an owner for that safe.OrderMetadata
struct from one of the target orders that were initially created (in practice the order used can be any order).Order
structs were created and partially filled with values that pertain to thePAY
andPAYEE
orders that ourattackerContract
will create on-chain viaSeaport::validate
. The only fields that are not set during this process are theOfferItem
orConsiderationItem
fields that specify the ERC721 or ERC1155 token that we will be stealing (these are dyanmic and therefore are determined during the callback functions). TheOrder
structs are then stored in theattackerContract
for later use.AdvancedOrder
structs that are required for the fulfillment transaction (i.e.Seaport::matchAdvancedOrders
). TheAdvancedOrder
structs are then stored in theattackerContract
for later use.Fulfillment
struct (required during theSeaport::matchAdvancedOrders
function call) is pre-constructed. This struct willl remain the same for all of our order creations. This struct is also stored in theattackerContract
for later use.RentalOrder.seaportOrderHash
(this is the same as theorderHash
that is computed for ourOrder
during the call toSeaport::validate
) is pre-computed viaSeaport::getOrderHash
. Note that thisorderHash
needs to be computed with a completeOrder
struct. Since ourOrder
structs were only partially filled during step 6, we will use theRentalOrder.items
field of the targetRentalOrder
to retrieve the details of the ERC721/ERC1155 token(s) that we will be stealing. We will then use these details to temporarily construct our actualOrder
struct and therefore pre-compute the requiredorderHash
.items
field of thevictim RentalOrder
, we will create our maliciousRentalOrder
.attackerContract
will initiate a call tostopRent
and pass in our maliciousRentalOrder
. However, we will first setRentalOrder.rentalWallet
equal to therentalWallet
of our victimRentalOrder
(thesafe wallet
that we will be stealing the NFT from).RentalOrders
by looping over the orders and computing the necessary maliciousRentalOrders
. This time, instead of theattackerContract
initiating a call tostopRent
, a call tostopRentBatch
will be initiated and an array of the maliciousRentalOrders
will be supplied.attacker safe
is now the owner of all the stolen NFTs and the victimRentalOrders
can no longer be stopped (even after their expiration).Place the following test inside of the
/test/integration/
directory and run test withforge test --mc StealAllRentedNFTs_POC
The below PoC showcases how an attacker can leverage all the bugs described in this report to steal multiple actively rented NFTs via
stopRentBatch
.Additional PoC for "lesser"
high
severity/impact vulnerability achieved via only exploiting bug#2
: reNFT-StealERC1155Tokens_PoC.mdTools Used
manual
Recommended Mitigation Steps
By addressing the non-compliant EIP-712 functions we can mitigate the main exploit path described in this report. I would suggest the following changes:
I would also suggest that the
RentPayload
, which is the signature digest of the server-side signer, be modified to include a field that is unique to the specific order that is being signed. This would disable users from obtaining a single signature and utilizing that signature arbitrarily (for multiple fulfillments/fulfill otherwise "restricted" orders).Assessed type
Other