..       :...  ;: :+  X+                                      .x+    .  . ;. .
                                   .         .+  :: ;; X.   :;                                    ;X;    .. ..;...
                         .         :      .....   :+  +      .x                                   .Xx:   ;:...:;:.
                         .         +                X         X      .     .      .                ;X+..;;;; ..;+:
                        ...        :         .      ;:      ;x       .     :                        $X;; ..+... ;;.
                         ...       ..:;;;:.:::       x;  .;X.        :.    ;       .          :    .+xx:  .xx.   ;.:
                      ;.....       .     .....         ;;:          .+     :;       .         :    :. $;   ;+.    ;..
                       .....   ..        ....:           ;          :X     .;        ..       :    +. ;x.;;++;     .::
                      ;.....   .:.  ; .      .           ;         .x;; .   :;        :;     .:    x   X:+;++;:      ;
                       ;::..   .:.  .;X   :;..           ;        .x; X. :  .x;         +:   :. : ;  ;xx:x;;+:.:.    ;.
                       ;::..   .::.  . :  ....           :        x;  ;: ..  +x;.         +;:++XxXX;   ;;X+:;:  ..    ;.
                       ;:.::.  ..:.    ;     .           ..     :x     +. .. .x++:.         ::.;.;    x:xX+;:::.  ..    ;
                       ;:.... .:.::    .:  .;.            ; .;;x:       +XXXXXxx:X;::      .:.+; ..  .x+:;X:::::.  .. ..  :
                       ;:::.:..:.:;     ;.   ..           ;.:x.       .  +;.;;.:x:;x;::....;++: .      .XX:;:::::.   . .:. :.
                       +:::..:.:.:;.   ;.;.:;::.          .x+       ...   :Xx;:XX;;x..:::;+.:+  :;+++XXXX+. .;:::::   . .+; :
                       ;..::....::;+    ;+;...;;           ;+  .            ;   ..:;:;;    :.xXxxxXXXXX:XX;;. :;::::   . .x; ;
                       .  .::...::x;.     .:;..+.           X.           .   .:    ;;;x;..  +Xxx.;xxx;.+;::::  .;::::     ;X;..
                     :   .::....::x .        :xX;.          ;+xxxxxxXXXXXXXX$&x   .:::.       Xxxxxx; :X+::::;;  ;:::.     +;;:
                   .   .;;.......;; ..        :;x:        .;xxxxXXXXXxx..xxxX.                XxxXx  ;:.:::::;;  ;;::.    ;.;x
               ..  .;;;;;......:;;   .         .X;.           Xx     Xxxxxxxx;        .;             xXx;:::::::;; ;;:::   ;;
                  ;..;;+;.... ..;;.   .         :x:.           +;    .:xxxxX:          :.;. .        ;.X+:::::::::;;;;::.  ;:       :.
                :. :   ;........:::.::.          :x.          .x;;;      .  ...           .;   ...::;  xx;:::::::::::+:.:  ;    .:;+
                       .........:.+:+..;          :+.            ;;.:..:;;::::                . ;;   ;  X+:::::::::::;;.: .X:.  ;.
                      ;......:...:;+..x::          .;.             : . ;  ; ;                   ;       ++;:::::::::::x:. X.       :;
                      ......:;...:+:  .+;.          .;..      ....   ;..                               :;;x+;:;:::::::+;.x;     ...  .:
                     :......;:...:;.    ;+.           ::.        .::....                               X; :++;;;;:::::xXX::;:.    ;::. ::
                    :......:;.....+......:+.           .++.       .         +XX;:::::+++XXxxXX        X+    :;;;;+;;::;::::::;;:   +;:.. :
                   ::.....:;......;x::::::;x.       ..   .xX;.    .;       .;;;..      '"*xXX;       X;:      .;++;;;;;::    .;;;.  ::.;; :.
                 ;.......;;........:+X++++;;x:        ..  ..XXX;..  +.      X:::::;;;;;;;;XXx      .X...;        ;;+;;;;;;:     ;:.   ; .;.:
                ........;;..........;.....;++++:.    .....  .+;.:+XX++X;     ;:::::::.:;;+.x      ;.....;;          :+;;;;;;;     .    :  .;:
             .;......:x;...........:+........;;X;;     .......;;.             .;;::...::;x+     .:.. .. .;:           ;; :;;;++        ;:
            ......:;x+.............:x:........:;+X;.     ..xX+.;+.  .. .         :;xxxx;;      ;..::.. .. ...          :;   .:;+;       ;
          :;...:;x;.;:.............:;x;:....:;;;xX+X;..   .xxxxXXXXXX+                       ;................          :;     :;+.    :;
          ;..;;.  :;:.................;;++x++;;;;;;;+X;....xxxXxxxxxxXx+;;:..:xx;          +x..........  . ..:.          :;     .;;;   :;
         ;.;..   .;;...............:::;;+;;:;++;;;;:;;Xx;;:xxxXx+++xX;;;;;;;++x+X;      .xX...... ...... .......          ::      ;;;  .+
        :+;  ;  ;;;..............:...;;;+;:...:.::;+xxx+;:;;        :x;+:...;;....xX+XXX+:.......   ..... .  ...:         :;       ;;  .;
         ;  .  ;;;:.................:;;;;++:;;;;;++;;;++x+   .. . :..    .+;.;...:+;;+x:....    ...  ......... ...         ;.      ;:; ::
             .;;;;.  ...............:;;;;;;;;;;;;;;:;      ...     .  .       ;...;Xxx;;:.. ...  ....  .... .......       .:       ::;:;

pentest-on-stream

The challenge provides us with a zip archive containing the site’s source code and a link to a set of message boxes:

" zip.vim version v33
" Browsing zipfile /home/please/Documents/CTF/jellyctf/pentest-on-stream/pentest_on_stream.zip
" Select a file with cursor and press ENTER

pentest_on_stream/
pentest_on_stream/start_obs.sh
pentest_on_stream/app/
pentest_on_stream/app/server.py
pentest_on_stream/app/chat.html
pentest_on_stream/app/obs_worker.py
pentest_on_stream/app/index.html
pentest_on_stream/app/static/
pentest_on_stream/app/static/oval.svg
pentest_on_stream/app/dono.jpg
pentest_on_stream/app/chat_base.html
pentest_on_stream/app/NunitoSans.woff2
pentest_on_stream/Pipfile
pentest_on_stream/obs-studio/
pentest_on_stream/obs-studio/global.ini
pentest_on_stream/obs-studio/basic/
pentest_on_stream/obs-studio/basic/profiles/
pentest_on_stream/obs-studio/basic/profiles/profile/
pentest_on_stream/obs-studio/basic/profiles/profile/basic.ini
pentest_on_stream/obs-studio/basic/scenes/
pentest_on_stream/obs-studio/basic/scenes/scenes.json
pentest_on_stream/Pipfile.lock
pentest_on_stream/Dockerfile
pentest_on_stream/supervisord.conf

archive contents

initial site

rendered content of the donation site

We can input our name and a message as part of a ‘donation’, which kind of simulates a stream overlay:

loading-dono

some text input - we wait for the site to generate an image

initial-output

My initial thoughts are to test whether the site will dump our unsanitized HTML & Javascript input (especially given the challenge description).

Fortunately, it seems that we can manipulate the HTML content of the remote DOM, but we are unable to get the server to pop an alert; noting the lack of any text content from the message field, it seems likely that there is something happening on the server that isn’t rendered into the image: html-injection

xss-alert-attempt

I want to see if I can get a script to render my injected content to confirm that the server is actually executing scripts in this message box - lets try rendering text into the DOM via a Javascript document.getElementById() function:

<h2>uwu</h2>
<div id="uwu"></div>
<script>
    const uwu = document.getElementById("uwu");
    uwu.innerHTML = "owo";
</script>

injected-from-script

source code

Taking a quick look over the source code, it seems like the backend runs a Docker container running an OBS instance connected via Python to the donation page front-end. Specifically, through the obs_worker.py module, we are calling an HTML template chat_base.html and doing a direct string replacement and finally taking a screenshot of the resulting rendered HTML in a headless browser or something:

import time
import obsws_python as obs
import redis
from rq import Worker, Queue, Connection

listen = ['default']

conn = redis.Redis()
obs_client = obs.ReqClient(host='localhost', port=4455, password='9WIBnsaL6t8Hiors')

with open("/app/chat_base.html", encoding="utf-8") as f:
    chat_html = f.read()

if __name__ == '__main__':
    with Connection(conn):
        worker = Worker(list(map(Queue, listen)))
        worker.work()

def refresh_and_screenshot_chat(name, message, _):
    with open("/app/chat.html", "w", encoding="utf-8") as f:
        f.write(chat_html.replace("__NAME__", name).replace("__MESSAGE__", message))
    obs_client.press_input_properties_button("Browser", "refreshnocache")
    # just dumbly wait for 1 second, there isn't a built in event we can hook to check for page load complete
    time.sleep(1)
    # cl.trigger_hot_key_by_name("OBSBasic.Screenshot")
    screenshot_b64 = obs_client.get_source_screenshot(name="chat", img_format="jpg", width=1280, height=720, quality=90)

    # reset
    with open("/app/chat.html", "w", encoding="utf-8") as f:
        f.write(chat_html)
    # flip a property to force refresh
    rwa = obs_client.get_input_settings(name="Browser").input_settings["restart_when_active"] or False
    obs_client.set_input_settings(name="Browser", settings={"restart_when_active": not rwa}, overlay=True)

    return screenshot_b64.image_data

I was thrown off a bit by the Redis instance here (was there a database angle to consider here?), but it was unimportant.

In the scenes.json file, we also see the flag resides as the name of a scene in the remote OBS instance:

{
    "current_scene": "chat",
    "current_program_scene": "chat",
    "scene_order": [
        {
            "name": "chat"
        },
        {
            "name": "jellyCTF{redacted}"
        }
    ],
    "name": "scenes",

    // ....

Furthermore, we can do a Google search for python library used to interface with OBS, which returns this obsws_python module, which is a Python wrapper for obs-websocket - an OBS websocket server builtin.

flag

This API appears to have a standardised protocol, so I want to try seeing if I can perform some kind of Javascript injection to see if I can reach out to this websocket from the HTML the Python worker inserts into the chat_base.html template.

Going through the aforementioned protocol documentation, we can find a section on request API functions with a handy table of contents to show that we can retrieve a list of scenes via a GetSceneList function. We can’t call these via their Python bindings using Javascipt, but the API has a set of Javascript bindings that might be available to us. We can construct the following script to call the GetSceneList via the API’s Javascript binding:

<div id="uwu"></div>
<script>
    const uwu = document.getElementById("uwu");
    window.obsstudio.getScenes(function (scenes) {
      uwu.innerHTML = scenes;
    });
</script>

Calling this via the site prints our flag:

flag

plsuwu:
gh
sc