Repository: Gallopsled/pwntools

Issue:
crafted filename could lead to format string error in pwnlib/log.py
Hi,
I found that a crafted filename may lead to a format string error in pwnlib/log.py.

For example
```shell
CVE-2012-0809 [master●] ls -l %p
lrwxrwxrwx 1 root root 4 1月  28 17:11 %p -> sudo
CVE-2012-0809 [master●] checksec ./sudo
[*] '/home/m4x/Projects/CVEs-analysis/CVE-2012-0809/sudo'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
CVE-2012-0809 [master●] checksec %p
Traceback (most recent call last):
  File "/usr/local/bin/checksec", line 11, in <module>
    load_entry_point('pwntools==3.12.1', 'console_scripts', 'checksec')()
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/commandline/common.py", line 32, in main
    pwnlib.commandline.main.main()
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/commandline/main.py", line 51, in main
    commands[args.command](args)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/commandline/checksec.py", line 37, in main
    e = ELF(f.name)
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/elf/elf.py", line 342, in __init__
    self._describe()
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/elf/elf.py", line 429, in _describe
    self.checksec(*a, **kw))))
  File "/usr/local/lib/python2.7/dist-packages/pwnlib/log.py", line 353, in info_once
    m = message % args
TypeError: not enough arguments for format string
```
And the reason seems to be the format string `%` in the crafted filename fails to parse a value.
```python
CVE-2012-0809 [master●] checksec %p
[6] > /usr/local/lib/python2.7/dist-packages/pwnlib/log.py(354)info_once()
-> m = message % args
(Pdb++) l
349              """info_once(message, *args, **kwargs)
350      
351              Logs an info message.  The same message is never printed again.
352              """
353              import pdb; pdb.set_trace()
354  ->            m = message % args
355              if m not in self._one_time_infos:
356                  if self.isEnabledFor(logging.INFO):
357                      self._one_time_infos.add(m)
358                  self._log(logging.INFO, message, args, kwargs, 'info_once')
359      
(Pdb++) p message
"'/home/m4x/Projects/CVEs-analysis/CVE-2012-0809/%p'\nArch:     amd64-64-little\nRELRO:    \x1b[32mFull RELRO\x1b[m\nStack:    \x1b[32mCanary found\x1b[m\nNX:       \x1b[32mNX enabled\x1b[m\nPIE:      \x1b[32mPIE enabled\x1b[m"
(Pdb++) p args
()
(Pdb++) n
TypeError: 'not enough arguments for format string'
[6] > /usr/local/lib/python2.7/dist-packages/pwnlib/log.py(354)info_once()
-> m = message % args
(Pdb++) 
```

And this bug occurs at least in `info_once(), warning_once(), error()` in `pwnlib/log.py`


Discussion:
So this is a bug that's been around a while and I don't have a good way to fix it.

The issue is that we use e.g. `%s` in the format code inside the logging subsystem, and it blows up when there is an extra unescaped format code in the log statement itself.

This can probably be resolved in some way, but even switching to e.g. placeholders or `string.format` will ultimately permit the same issue -- log statements with certain contents will blow up.  Clearly this is undesirable.

If anybody has a suggestion for a good way to handle this (maybe sticking additional fields on the log messages, rather than inline with the text?) I'm all ears.
Wouldn't a simple change from:

```
log.info_once('\n'.join((repr(self.path),
                                '%-10s%s-%s-%s' % ('Arch:', self.arch, self.bits, self.endian),
                                self.checksec(*a, **kw))))
```

to

```
log.info_once('\n'.join([repr(self.path).replace('%', '%%'),
                                '%-10s%s-%s-%s' % ('Arch:', self.arch, self.bits, self.endian),
                                self.checksec(*a, **kw)]))
```

in pwnlib/elf/elf.py:441 solve this?
Alternatively, and I think this is nicer, we can do:

```
log.info_once('%s\n%s\n%s',
    repr(self.path),
    '%-10s%s-%s-%s' % ('Arch:', self.arch, self.bits, self.endian),
    self.checksec(*a, **kw)
)
```
> Alternatively, and I think this is nicer, we can do:
> 
> ```
> log.info_once('%s\n%s\n%s',
>     repr(self.path),
>     '%-10s%s-%s-%s' % ('Arch:', self.arch, self.bits, self.endian),
>     self.checksec(*a, **kw)
> )
> ```

This is good. But I prefer to add some restriction inside these functions  for better encapsulation. Or since python2 won't be matained is a short time, we can introduce some python3 features(`format` function, for example).
> But I prefer to add some restriction inside these functions for better encapsulation.

Sorry, I don't quite understand what you mean by this. Can you clarify? Isn't the general rule that whenever you have a printf-style function to always use a static string for the format string enough of a rule/pattern?

Resolving patch:
diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py
--- a/pwnlib/elf/elf.py
+++ b/pwnlib/elf/elf.py
@@ -438,9 +438,15 @@ def debug(self, argv=[], *a, **kw):
         return pwnlib.gdb.debug([self.path] + argv, *a, **kw)
 
     def _describe(self, *a, **kw):
-        log.info_once('\n'.join((repr(self.path),
-                                '%-10s%s-%s-%s' % ('Arch:', self.arch, self.bits, self.endian),
-                                self.checksec(*a, **kw))))
+        log.info_once(
+            '%s\n%-10s%s-%s-%s\n%s',
+            repr(self.path),
+            'Arch:',
+            self.arch,
+            self.bits,
+            self.endian,
+            self.checksec(*a, **kw)
+        )
 
     def __repr__(self):
         return "ELF(%r)" % self.path


Test patch: