Because withdraw reverted, it seems like the entire transaction should have been reverted, but recall that call does not propagate exceptions. It sends a message to another contract, and if that
internal transaction reverts, it just returns 0 to the caller.
So here's what happens:
Exploit calls withdraw.
withdraw calls Exploit's payable fallback function. [call #1]
Exploits payable fallback function calls withdraw again.
withdraw calls Exploits payable fallback function again. [call #2]
That call simply succeeds, because Exploits payable fallback function sees that it has extracted the full 2 ether and just returns.
withdraw checks the result of call #2, sees that it was successful, and then reverts.
call #1 fails, because a revert happened. This passes control back to the caller with a 0 return value indicating failure.
withdraw checks the result of call #1, sees that it failed, and does not do the revert.
The transaction completes successfully.