X Ét Ét

1. General

Bài này cho ta một ứng dụng có sử dụng electron và với tên chall thì biết ngay hướng của bài là XSS => RCE thông qua electron

Chall có cấu trúc thư mục như sau:

Với thư mục app là code của electron, code web chính nằm ở server còn bot thì đơn giản là con bot sẽ gọi đến electron

Tóm tắt qua workflow của ứng dụng như sau

  • Trang web có tính năng login, signup

  • Sau khi đăng nhập thì có thể tạo ticket với các data sau

  • Khi tạo ticket ta sẽ được như sau

  • Khi report admin thì code server sẽ santitize title và content để gửi cho bot

  • Tại code xử lý của con bot nó sẽ base64 encode id, title và content lưu vào env, và sau đó gọi đến ứng dụng electron

  • Tại code của electron nó sẽ decode env và load giao diện

2. XSS

Ở trên là cách trang web hoạt động, mình bắt đầu nhảy vào phân tích src

Đầu tiên, trang ticket dùng jinja2 template để load nội dung 1 cách an toàn nên không thể XSS => loại

Tại trang admin.html sẽ load data với options safe do đó trang này có thể bị XSS

Ta có thể load admin.html thông qua route /IsNew. Tuy nhiên để đến được trang này thì phải là request internal, cộng thêm vào đó trang được trang bị CSP

Giá trị CSP của trang

Với script-src 'self' thì ta có thể bypass bằng upload file và trang web cũng có tính năng đó

Để đi đến được admin.html ta sẽ có 2 hướng

  • Thông qua iframe, vì code load iframe chỉ check phần content có tồn tại "http://localhost/tmp/" hay không, do đó mình có thể bypass bằng "http://localhost/tmp/../IsNew?id=<id>/#" và khiến iframe load một ticket bất kỳ. Tuy nhiên XSS được thì không thể RCE được, lý do vì sao thì một lát các bạn sẽ biết

  • Hướng thứ 2 là khiến renderer kích hoạt envent CreateViewer => createNotificationWindow(id) => child.loadURL("http://localhost/IsNew?id="+id) => admin.html => XSS , để làm được điều đó mình phải bypass được check admin

Để bypass check admin thì đơn giản mình chỉ cần tạo user admin có thêm khoảng trắng phía sau, vì khi trang web lưu session sẽ strip username

Điều này khiến username admin hay admin đều có session['username'] là admin

Bypass được admin mình có thể trigger được CreateViewer ,lúc này mình sẽ XSS thông qua title, vì trong electron ta có thể load được internal resource, nên mình sẽ lợi dụng tag meta để load file và trigger XSS.

Để kiểm chứng thì mình sẽ tạo một file html tại máy (giả lập như file upload trên server), và dựng debug ở vscode để test

Tạo ticket chứa payload gọi đến file html của mình

Debug gọi đến electron app, thì mình đã XSS thành công

Bây giờ đã có thể XSS, mình tìm cách để RCE

3. RCE

Mình tham khảo về cách XSS 2 RCE ở bài blog này

Để ý khi window popup thì sẽ set các options sau

Với option nodeIntegrationInSubFrames: false thì ta không thể call ipc từ iframe do đó attack vector XSS từ iframe lúc nảy mình đề cập không thể nào lợi dụng để leo thành RCE được

Tuy nhiên option contextIsolation cũng set là false, do đó main process và processor process có thể dùng chung global content, tuy nhiên không hề có preload nào được load trong event này 😭😭😭

Mình khá là bí đoạn này, sau một hồi search gg về các options được set có thể lợi dụng được gì không. Thì mình tìm được bài này: https://powerofcommunity.net/poc2022/HectorPeralta.pdf

Các bạn tập trung vào slide 49-52. Khi options contextIsolationsandbox là false ta có thể lợi dụng để can thiệp vào quá trình khởi tạo môi trường nodeJS của renderer process

Ta sẽ lợi dụng cơ chế __webpack_require__ để load bất kỳ module nào mà ta mong muốn

Các bạn có thể tìm hiểu thêm về cơ chế load module webpack thông qua bài blog này : https://www.freecodecamp.org/chinese/news/webpack-module-loading/

Nói đơn giản thì webpack là cơ chế giúp load các module của commonJS, mà Nodejs mặc định sẽ sử dụng commonJS để làm module system. Do đó ý tưởng sẽ là ghi đè lại function call của webpack_require trong quá trình module loading, bằng cách check arguments thứ 4 có phải là __webpack_require__ không, sau đó lấy instance của __webpack_require__ và load module child_process để RCE

Ta có đoạn script sau để RCE

<script>

    Function.prototype._call = Function.prototype.call;
    Function.prototype.call = function(arg1,arg2,arg3,arg4) {
        try {
            if (arg4.name == '__webpack_require__') {
            Function.prototype.call = Function.prototype._call;
            delete Function.prototype._call;
            webpack_1 = arg4;
            var cp = {};
            webpack_1.m.module(cp);
            cmd = cp.exports._load('child_process');
            cmd.exec('RCE');
        }} catch(er) {
            console.log(er);
        }
    }
    console.log(Function.prototype.call)
</script>

Tóm lại để exploit ta sẽ:

  • Đăng ký user với tên là admin để bypass check admin

  • Tạo ticket1 để upload malicous html

  • Tạo ticket2, với title là <meta http-equiv="refresh" content="0; url=file:///tmp/<ticket1_uuid>.html">

  • Report ticket2

  • Ghi flag vào /app/server/static và truy cập để đọc flag

Last updated