Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests fail with 'possible traversal attack' #99

Open
craftyguy opened this issue Feb 10, 2020 · 19 comments
Open

Tests fail with 'possible traversal attack' #99

craftyguy opened this issue Feb 10, 2020 · 19 comments
Assignees

Comments

@craftyguy
Copy link

I'm trying to build/test unshield on Alpine Linux (with musl libc), and all of the bundled tests fail. I've included the output from running the following manually (as found in the baldur gate test sh script):

./build/src/unshield -D 3 -d extract1 x data1.cab

Output:

unshield_fail.log

@twogood twogood self-assigned this Feb 11, 2020
@twogood
Copy link
Owner

twogood commented Feb 11, 2020

Seems we use some GNU extension to realpath that probably is not available in Alpine:

  /* use GNU extension to return non-existing files to real_output_directory */
  realpath(output_directory, real_output_directory);
  realpath(filename, real_filename);
  if (real_filename == NULL || strncmp(real_filename,
                                       real_output_directory,
                                       strlen(real_output_directory)) != 0)
  {
    fprintf(stderr, "\n\nExtraction failed.\n");
    fprintf(stderr, "Possible directory traversal attack for: %s\n", filename);
    fprintf(stderr, "To be placed at: %s\n\n", real_filename);
    success = false;
    goto exit;
  }

GNU extensions
If the call fails with either EACCES or ENOENT and resolved_path is not NULL, then the prefix of path that is not readable or does not exist is returned in resolved_path.

@twogood
Copy link
Owner

twogood commented Feb 11, 2020

Please try with the latest master @craftyguy !

@craftyguy
Copy link
Author

@twogood works great, thanks for the quick fix!

I'm packaging this for Alpine Linux, is there any chance you could tag a new release so I can package a specific release rather than 'master'?

@craftyguy
Copy link
Author

Oof, spoke too soon. One test is failing and referencing #42

/home/pmos/build/src/twogood-unshield-f097b07 # UNSHIELD=$(pwd)/build/src/unshield bash run-tests.sh
Running test ./test/v0/wireplay.sh...succeeded
Running test ./test/v0/the-feeble-files-spanish.sh...succeeded
Running test ./test/v0/baldurs_gate_patch_v1_1_4315_international.sh...succeeded
Running test ./test/v0/avigomanager.sh...succeeded
Running test ./test/v5/CVE-2015-1386/CVE-2015-1386.sh...Cabinet: /home/pmos/build/src/twogood-unshield-f097b07/test/v5/CVE-2015-1386/data1.cab
  extracting: extract1/Bovine_Files/../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../tmp/moo
 --------  -------
          1 files
unshield vulnerable to CVE-2015-1386
See https://github.com/twogood/unshield/issues/42
FAILED with code 2

That issue seems to indicate this was resolved for debian on some earlier version of unshield, but since it has to do with some traversal stuff maybe it's not fully resolved for Alpine/musl libc?

@craftyguy
Copy link
Author

craftyguy commented Feb 11, 2020

FWIW, realpath does appear to be in POSIX (and therefore supported by musl): https://pubs.opengroup.org/onlinepubs/9699919799/

A simple 'hello world' compiles/runs with musl:

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char b[PATH_MAX];
    char *r = realpath("test.c", b);
    if (r) {
        printf("%s.\n", b);
    } else {
        perror("realpath");
        exit(EXIT_FAILURE);
    }
    return 0;
}
~ $ gcc test.c -o test
~ $ ldd test
        /lib/ld-musl-aarch64.so.1 (0)
        libc.musl-aarch64.so.1 => /lib/ld-musl-aarch64.so.1 (0)
~ $ ./test
/home/pmos/test.c.

@twogood
Copy link
Owner

twogood commented Feb 11, 2020

Yes, realpath() is POSIX but there is a GNU extension to support files that do not exist, see bottom of my comment #99 (comment)

@craftyguy
Copy link
Author

@twogood oops, I missed that somehow when I first read your comment, sorry about that!

So there must be something else causing that test to fail when built with musl?

@twogood
Copy link
Owner

twogood commented Feb 11, 2020

Well on master I disabled the path traversal check unless you are on GNU libc. "Damned if you, damned if you don't..." ;)

@twogood
Copy link
Owner

twogood commented Feb 11, 2020

I will create a proper release when we have sorted this out. I want a path traversal check but I don't want to reimplement realpath :)

@craftyguy
Copy link
Author

I see. There was a patch submitted for #42 that doesn't appear to rely on GNU extensions to realpath (1027922), but comment in that merge request seems to suggest that approach doesn't work. I don't know enough about this stuff to be able to help much, and definitely don't want to re-introduce a CVE into this project by recommending the wrong thing :(

@twogood twogood reopened this Feb 12, 2020
@twogood
Copy link
Owner

twogood commented Feb 12, 2020

Unfortunately it still seems to rely on the GNU extension to realpath but this must have been an issue on Alpine for much bigger projects than unshield...

@craftyguy
Copy link
Author

@twogood I thought about this some more, and talked with some folks on the #musl IRC channel to see what they thought such a check would look like ¹

Are there any cases where having a '..' in the path for an object in a .cab is valid? If there's no case for that, then it seems like 1) checking that the directory to extract into is writable for the current user, and 2) that the file path of the file to extract does not contain any '..'

¹ A (somewhat biased) response was along the lines of "using realpath this way is clearly not the intended purpose of realpath, in glibc or otherwise" (paraphrasing is mine), but I'd rather focus on solving the issue and not derailing the discussion to focus on that bit :)

@twogood
Copy link
Owner

twogood commented Feb 16, 2020

I don't know if .. is valid but of course, it's pretty easy to add a check for that.

@wdlkmpx
Copy link
Contributor

wdlkmpx commented May 19, 2021

I just came across this issue, I cross compiled a musl static unshield

The problem with musl is that there's no way to identify it, no macros, the musl devs are insane.

@wdlkmpx
Copy link
Contributor

wdlkmpx commented May 20, 2021

Guys let's collect .cab files, all the InstallShield files I've checked so far do not contain / or \..\. / is an invalid char in Windows, can't be present in paths.

The paths seem to follow a fixed pattern, no special commands for relative paths, I mean: no \..\or ..\ (at the start)

Cabinet: data1.cab
     6332  Xp64Driver-C1_SWE\brimb3ex.cat
    31173  Xp64Driver-C1_SWE\brprbh3e.cat
     6332  Xp64Driver-C1_NOR\brimb3ex.cat
    31173  Xp64Driver-C1_NOR\brprbh3e.cat
    47672  PCFAX_DOC_FRE\PC_FAX32.chm
    12313  PCFAX_DOC_FRE\BrotherAtYourLogo.jpg
    19746  PCFAX_DOC_FRE\PCFAXsetup.jpg
    24028  PCFAX_DOC_FRE\Phone.jpg
    15942  PCFAX_DOC_FRE\Phonesmall.jpg
    21238  PCFAX_DOC_FRE\driver.jpg
    18056  PCFAX_DOC_FRE\enablePCfax.jpg
     3947  PCFAX_DOC_FRE\howtousebrotherpc.htm
    11498  PCFAX_DOC_FRE\simplesmall.jpg
    31326  <Support>Swedish String Tables\StringTable-001d-Swedish.ips
    25570  <Support>Portuguese (Standard) String Tables\StringTable-0816-Portuguese (Standard).ips
   143111  Twainx64_UK\BRS06LNG.chm
    81920  Twainx64_UK\BrTwdLNG.dll

The only surprise I've seem so far is <Support>....

Please share your .cab files with me, I'll implement a fix without realpath, just simple logic with strings. Remember these .cab files are created by a Windows application for MS Windows, and the paths don't contain special 'commands'.

Need a repository to store .cab files for testing

@wdlkmpx
Copy link
Contributor

wdlkmpx commented May 20, 2021

I produced an extra workaround for CVE-2015-1386 that will fix a test failure with everything other than gcc+linux
See commit diff & message wdlkmpx@46a9173
My repo has interesting stuff

unshield with a static musl:

# file src/unshield
src/unshield: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

# ./run-tests.sh 
------------------------------------------------
UNSHIELD=/mnt/sda3/pkg2/unshield_static/unshield/src/unshield
------------------------------------------------
Running test ./test/v0/avigomanager.sh...succeeded
Running test ./test/v0/baldurs_gate_patch_v1_1_4315_international.sh...succeeded
Running test ./test/v0/the-feeble-files-spanish.sh...succeeded
Running test ./test/v0/wireplay.sh...succeeded
Running test ./test/v5-CVE-2015-1386/CVE-2015-1386.sh...succeeded

@twogood
Copy link
Owner

twogood commented May 20, 2021

Good workaround!

@wdlkmpx
Copy link
Contributor

wdlkmpx commented May 20, 2021

I optmized the change I made to extract_file() in the commit linked above,
Buit I don't want to trigger a new Travis CI build in the Pull Request until I have new important changes to commit, so I'll paste the diff here so you can analyze it

The app(s) that create InstallShield CAB files, will never insert something like \..\ or /../ in the path, so it's pretty safe to assume it's malicious. It's not a TAR file that really contains 'commands' to relative paths. I'll inspect as many cab files as I can to verify my claim... the realpath stuff might not be necessary...

diff --git a/src/unshield.c b/src/unshield.c
index ebbc4a3..77c19ea 100644
--- a/src/unshield.c
+++ b/src/unshield.c
@@ -400,15 +400,15 @@ static int is_traversal_attack (const char *name)
 static bool extract_file(Unshield* unshield, const char* prefix, int index)
 {
   bool success = false;
-  char* dirname;
-  char* filename;
+  char* dirname = NULL;
+  char* filename = NULL;
   const char* origdir  = NULL;
   const char* basename = NULL;
-  char* p;
+  char* p = NULL;
   int directory = unshield_file_directory(unshield, index);
   size_t path_max;
-  char* real_output_directory;
-  char* real_filename;
+  char* real_output_directory = NULL;
+  char* real_filename = NULL;
   int traversal_attack = 0;
 
   #ifdef PATH_MAX
@@ -419,17 +419,6 @@ static bool extract_file(Unshield* unshield, const char* prefix, int index)
       path_max = 4096;
   #endif
 
-  real_output_directory = malloc(path_max);
-  real_filename = malloc(path_max);
-  dirname = malloc(path_max);
-  filename = malloc(path_max);
-  if (real_output_directory == NULL || real_filename == NULL)
-  {
-    fprintf(stderr,"Unable to allocate memory.");
-    success=false;
-    goto exit;
-  }
-
   /*
      filename: dir2extract1/Language_Independant/Override/xan6.CRE
             -d output_dir           prefix       origdir  basename
@@ -448,6 +437,17 @@ static bool extract_file(Unshield* unshield, const char* prefix, int index)
      goto exit;
   }
 
+  real_output_directory = malloc(path_max);
+  real_filename = malloc(path_max);
+  dirname = malloc(path_max);
+  filename = malloc(path_max);
+  if (real_output_directory == NULL || real_filename == NULL)
+  {
+    fprintf(stderr,"Unable to allocate memory.");
+    success=false;
+    goto exit;
+  }
+
   if(strlen(output_directory) < path_max-1)
   {
     strncpy(dirname, output_directory,path_max-1);
@@ -589,23 +589,25 @@ exit:
   if (traversal_attack)
   {
     fprintf(stderr, "\n\nExtraction failed.\n");
-    fprintf(stderr, "Error: %s (%d).\n", strerror(errno), errno);
-    fprintf(stderr, "Possible directory traversal attack for: %s\n", filename);
-    fprintf(stderr, "To be placed at: %s\n\n", real_filename);
+    fprintf(stderr, "Possible directory traversal attack ");
+    if (filename) {
+       fprintf(stderr, " for: %s\n", filename);
+       fprintf(stderr, "Error: %s (%d).\n", strerror(errno), errno);
+       fprintf(stderr, "To be placed at: %s\n\n", real_filename);
+    }
     success = false;
   }
   if (!success)
   {
-    fprintf(stderr, "Failed to extract file '%s'.%s\n",
-        unshield_file_name(unshield, index),
-        (log_level < 3) ? "Run unshield again with -D 3 for more information." : "");
+    fprintf(stderr, "Failed to extract file '%s'.%s\n", basename,
+            (log_level < 3) ? "Run unshield again with -D 3 for more information." : "");
     unlink(filename);
     exit_status = 1;
   }
-  free(real_filename);
-  free(real_output_directory);
-  free(dirname);
-  free(filename);
+  FREE (real_filename);
+  FREE (real_output_directory);
+  FREE (dirname);
+  FREE (filename);
   return success;
 }
 

@wdlkmpx
Copy link
Contributor

wdlkmpx commented Jun 1, 2021

My assumptions were correct, the realpath fix should be removed, that fix itself can be considered a bug.

The string to check is the full path not including the output directory (-d dir), I see the fix for traversal attacks in unace, it just checks the dots and the dir separator, a simplified version:

static int is_directory_traversal(char *str)
{
  if (*str == '.' && (!strncmp(str,"../",3) || !strncmp(str,"..\\",3))) {
      return 1;
  }
  if (strstr(str, "/../")  || strstr(str, "\\..\\")) {
      return 1;
  }
  return 0;
}

Just like ace/WinAce, InstallShield is a closed source Windows app, so the same logic applies, and the app should print and work with \ separator, it should only be changed to / when producing output files (extracted files/dirs).
Because there is a special dir traversal attack that is hard to address, but this is the fix I came up with:

   if (extract)
   {
      // '/': UNIX-specific attack
      //  - ACE32.EXE creates:
      //       man\man8\e2mmpstatus.8
      //  - tests/dirtraversal2.ace:
      //       /tmp/unace-dir-traversal
      //       ('/' must not be present in the string)
     if (is_directory_traversal(s) || strchr(s,'/')) {
        f_err = ERR_WRITE;
        printf("\n    Directory traversal attempt:  %s\n", s);
        return NULL;
     }
   }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants