• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Gallopsled / pwntools / 1461b486abbb14c5fe4ad10e86ac2a921d486203

pending completion
1461b486abbb14c5fe4ad10e86ac2a921d486203

push

github-actions

GitHub
Fix documentation of tube.recvall (#2163)

3873 of 6370 branches covered (60.8%)

12198 of 16609 relevant lines covered (73.44%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.75
/pwnlib/util/sh_string.py
1
# -*- coding: utf-8 -*-
2
r"""
1✔
3
Routines here are for getting any NULL-terminated sequence of bytes evaluated
4
intact by any shell.  This includes all variants of quotes, whitespace, and
5
non-printable characters.
6

7
Supported Shells
8
----------------
9

10
The following shells have been evaluated:
11

12
- Ubuntu (dash/sh)
13
- MacOS (GNU Bash)
14
- Zsh
15
- FreeBSD (sh)
16
- OpenBSD (sh)
17
- NetBSD (sh)
18

19
Debian Almquist shell (Dash)
20
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21

22
Ubuntu 14.04 and 16.04 use the Dash shell, and /bin/sh is actually just a
23
symlink to /bin/dash.  The feature set supported when invoked as "sh" instead
24
of "dash" is different, and we focus exclusively on the "/bin/sh" implementation.
25

26
From the `Ubuntu Man Pages`_, every character except for single-quote
27
can be wrapped in single-quotes, and a backslash can be used to escape unquoted
28
single-quotes.
29

30
::
31

32
    Quoting
33
      Quoting is used to remove the special meaning of certain characters or
34
      words to the shell, such as operators, whitespace, or keywords.  There
35
      are three types of quoting: matched single quotes, matched double quotes,
36
      and backslash.
37

38
    Backslash
39
      A backslash preserves the literal meaning of the following character,
40
      with the exception of ⟨newline⟩.  A backslash preceding a ⟨newline⟩ is
41
      treated as a line continuation.
42

43
    Single Quotes
44
      Enclosing characters in single quotes preserves the literal meaning of
45
      all the characters (except single quotes, making it impossible to put
46
      single-quotes in a single-quoted string).
47

48
    Double Quotes
49
      Enclosing characters within double quotes preserves the literal meaning
50
      of all characters except dollarsign ($), backquote (`), and backslash
51
      (\).  The backslash inside double quotes is historically weird, and
52
      serves to quote only the following characters:
53
            $ ` " \ <newline>.
54
      Otherwise it remains literal.
55

56
GNU Bash
57
~~~~~~~~
58

59
The Bash shell is default on many systems, though it is not generally the default
60
system-wide shell (i.e., the `system` syscall does not generally invoke it).
61

62
That said, its prevalence suggests that it also be addressed.
63

64
From the `GNU Bash Manual`_, every character except for single-quote
65
can be wrapped in single-quotes, and a backslash can be used to escape unquoted
66
single-quotes.
67

68
::
69

70
    3.1.2.1 Escape Character
71

72
    A non-quoted backslash ‘\’ is the Bash escape character. It preserves the
73
    literal value of the next character that follows, with the exception of
74
    newline. If a ``\\newline`` pair appears, and the backslash itself is not
75
    quoted, the ``\\newline`` is treated as a line continuation (that is, it
76
    is removed from the input stream and effectively ignored).
77

78
    3.1.2.2 Single Quotes
79

80
    Enclosing characters in single quotes (‘'’) preserves the literal value of
81
    each character within the quotes. A single quote may not occur between single
82
    uotes, even when preceded by a backslash.
83

84
    3.1.2.3 Double Quotes
85

86
    Enclosing characters in double quotes (‘"’) preserves the literal value of a
87
    ll characters within the quotes, with the exception of ‘$’, ‘`’, ‘\’, and,
88
    when history expansion is enabled, ‘!’. The characters ‘$’ and ‘`’ retain their
89
    pecial meaning within double quotes (see Shell Expansions). The backslash retains
90
    its special meaning only when followed by one of the following characters:
91
    ‘$’, ‘`’, ‘"’, ‘\’, or newline. Within double quotes, backslashes that are
92
    followed by one of these characters are removed. Backslashes preceding
93
    characters without a special meaning are left unmodified. A double quote may
94
    be quoted within double quotes by preceding it with a backslash. If enabled,
95
    history expansion will be performed unless an ‘!’ appearing in double quotes
96
    is escaped using a backslash. The backslash preceding the ‘!’ is not removed.
97

98
    The special parameters ‘*’ and ‘@’ have special meaning when in double quotes
99
    see Shell Parameter Expansion).
100

101
Z Shell
102
~~~~~~~
103

104
The Z shell is also a relatively common user shell, even though it's not generally
105
the default system-wide shell.
106

107
From the `Z Shell Manual`_, every character except for single-quote
108
can be wrapped in single-quotes, and a backslash can be used to escape unquoted
109
single-quotes.
110

111
::
112

113
    A character may be quoted (that is, made to stand for itself) by preceding
114
    it with a ‘\’. ‘\’ followed by a newline is ignored.
115

116
    A string enclosed between ‘$'’ and ‘'’ is processed the same way as the
117
    string arguments of the print builtin, and the resulting string is considered
118
    o be entirely quoted. A literal ‘'’ character can be included in the string
119
    by using the ‘\\'’ escape.
120

121
    All characters enclosed between a pair of single quotes ('') that is not
122
    preceded by a ‘$’ are quoted. A single quote cannot appear within single
123
    quotes unless the option RC_QUOTES is set, in which case a pair of single
124
    quotes are turned into a single quote. For example,
125

126
    print ''''
127
    outputs nothing apart from a newline if RC_QUOTES is not set, but one single
128
    quote if it is set.
129

130
    Inside double quotes (""), parameter and command substitution occur, and
131
    ‘\’ quotes the characters ‘\’, ‘`’, ‘"’, and ‘$’.
132

133
FreeBSD Shell
134
~~~~~~~~~~~~~
135

136
Compatibility with the FreeBSD shell is included for completeness.
137

138
From the `FreeBSD man pages`_, every character except for single-quote
139
can be wrapped in single-quotes, and a backslash can be used to escape unquoted
140
single-quotes.
141

142
::
143

144
     Quoting is used to remove the special meaning of certain characters or
145
     words to the shell, such as operators, whitespace, keywords, or alias
146
     names.
147

148
     There are four types of quoting: matched single quotes, dollar-single
149
     quotes, matched double quotes, and backslash.
150

151
     Single Quotes
152
         Enclosing characters in single quotes preserves the literal mean-
153
         ing of all the characters (except single quotes, making it impos-
154
         sible to put single-quotes in a single-quoted string).
155

156
     Dollar-Single Quotes
157
         Enclosing characters between $' and ' preserves the literal mean-
158
         ing of all characters except backslashes and single quotes.  A
159
         backslash introduces a C-style escape sequence:
160

161
         ...
162

163
     Double Quotes
164
         Enclosing characters within double quotes preserves the literal
165
         meaning of all characters except dollar sign (`$'), backquote
166
         (``'), and backslash (`\\').  The backslash inside double quotes
167
         is historically weird.  It remains literal unless it precedes the
168
         following characters, which it serves to quote:
169

170
           $     `     "     \     \\n
171

172
     Backslash
173
         A backslash preserves the literal meaning of the following char-
174
         acter, with the exception of the newline character (`\\n').  A
175
         backslash preceding a newline is treated as a line continuation.
176

177
OpenBSD Shell
178
~~~~~~~~~~~~~
179

180
From the `OpenBSD Man Pages`_, every character except for single-quote
181
can be wrapped in single-quotes, and a backslash can be used to escape unquoted
182
single-quotes.
183

184
::
185

186
    A backslash (\) can be used to quote any character except a newline.
187
    If a newline follows a backslash the shell removes them both, effectively
188
    making the following line part of the current one.
189

190
    A group of characters can be enclosed within single quotes (') to quote
191
    every character within the quotes.
192

193
    A group of characters can be enclosed within double quotes (") to quote
194
    every character within the quotes except a backquote (`) or a dollar
195
    sign ($), both of which retain their special meaning. A backslash (\)
196
    within double quotes retains its special meaning, but only when followed
197
    by a backquote, dollar sign, double quote, or another backslash.
198
    An at sign (@) within double quotes has a special meaning
199
    (see SPECIAL PARAMETERS, below).
200

201
NetBSD Shell
202
~~~~~~~~~~~~
203

204
The NetBSD shell's documentation is identical to the Dash documentation.
205

206
Android Shells
207
~~~~~~~~~~~~~~
208

209
Android has gone through some number of shells.
210

211
- Mksh, a Korn shell, was used with Toolbox releases (5.0 and prior)
212
- Toybox, also derived from the Almquist Shell (6.0 and newer)
213

214
Notably, the Toolbox implementation is not POSIX compliant
215
as it lacks a "printf" builtin (e.g. Android 5.0 emulator images).
216

217
Toybox Shell
218
~~~~~~~~~~~~
219

220
Android 6.0 (and possibly other versions) use a shell based on ``toybox``.
221

222
While it does not include a ``printf`` builtin, ``toybox`` itself includes
223
a POSIX-compliant ``printf`` binary.
224

225
The Ash shells should be feature-compatible with ``dash``.
226

227
BusyBox Shell
228
~~~~~~~~~~~~~
229

230
`BusyBox's Wikipedia page`_ claims to use an ``ash``-compliant shell,
231
and should therefore be compatible with ``dash``.
232

233

234
.. _Ubuntu Man Pages: https://manpages.ubuntu.com/manpages/trusty/man1/dash.1.html
235
.. _GNU Bash Manual: https://www.gnu.org/software/bash/manual/bash.html#Quoting
236
.. _Z Shell Manual: https://zsh.sourceforge.io/Doc/Release/Shell-Grammar.html#Quoting
237
.. _FreeBSD man pages: https://www.freebsd.org/cgi/man.cgi?query=sh
238
.. _OpenBSD Man Pages: https://man.openbsd.org/sh#SHELL_GRAMMAR
239
.. _BusyBox's Wikipedia page: https://en.wikipedia.org/wiki/BusyBox#Features
240
"""
241
from __future__ import absolute_import
1✔
242
from __future__ import division
1✔
243

244
import six
1✔
245
import string
1✔
246
import subprocess
1✔
247

248
from pwnlib.context import context
1✔
249
from pwnlib.log import getLogger
1✔
250
from pwnlib.tubes.process import process
1✔
251
from pwnlib.util import fiddling
1✔
252
from pwnlib.util.misc import which, normalize_argv_env
1✔
253

254
log = getLogger(__name__)
1✔
255

256
def test_all():
1✔
257
    test('a') ##
×
258
    test('ab') ##
×
259
    test('a b') ##
×
260
    test(r"a\'b") ##
×
261
    everything_1 = b''.join(six.int2byte(c) for c in range(1,256))
×
262
    for s in everything_1:
×
263
        test(s)
×
264
        test(s*4)
×
265
        test(s * 2 + b'X')
×
266
        test(b'X' + s * 2)
×
267
        test((s*2 + b'X') * 2)
×
268
        test(s + b'X' + s)
×
269
        test(s*2 + b'X' + s*2)
×
270
        test(b'X' + s*2 + b'X')
×
271
    test(everything_1)
×
272
    test(everything_1 * 2)
×
273
    test(everything_1 * 4)
×
274
    everything_2 = b''.join(six.int2byte(c) * 2 for c in range(1,256)) ##
×
275
    test(everything_2)
×
276

277
    test(randoms(1000, everything_1))
×
278

279

280
def test(original):
1✔
281
    r"""Tests the output provided by a shell interpreting a string
282

283
    >>> test(b'foobar')
284
    >>> test(b'foo bar')
285
    >>> test(b'foo bar\n')
286
    >>> test(b"foo'bar")
287
    >>> test(b"foo\\\\bar")
288
    >>> test(b"foo\\\\'bar")
289
    >>> test(b"foo\\x01'bar")
290
    >>> test(b'\n')
291
    >>> test(b'\xff')
292
    >>> test(os.urandom(16 * 1024).replace(b'\x00', b''))
293
    """
294
    input = sh_string(original)
1✔
295

296
    if not isinstance(input, str):
1!
297
        input = input.decode('latin1')
×
298

299
    cmdstr = six.b('/bin/echo %s' % input)
1✔
300

301
    SUPPORTED_SHELLS = [
1✔
302
        ['ash', '-c', cmdstr],
303
        ['bash', '-c', cmdstr],
304
        ['bash', '-o', 'posix', '-c', cmdstr],
305
        ['ksh', '-c', cmdstr],
306
        ['busybox', 'ash', '-c', cmdstr],
307
        ['busybox', 'sh', '-c', cmdstr],
308
        ['zsh', '-c', cmdstr],
309
        ['posh', '-c', cmdstr],
310
        ['dash', '-c', cmdstr],
311
        ['mksh', '-c', cmdstr],
312
        ['sh', '-c', cmdstr],
313
        # ['adb', 'exec-out', cmdstr]
314
    ]
315

316
    for shell in SUPPORTED_SHELLS:
1✔
317
        binary = shell[0]
1✔
318

319
        if not which(binary):
1✔
320
            log.warn_once('Shell %r is not available' % binary)
1✔
321
            continue
1✔
322

323
        progress = log.progress('%s: %r' % (binary, original))
1✔
324

325
        with context.quiet:
1✔
326
            with process(shell) as p:
1✔
327
                data = p.recvall(timeout=2)
1✔
328
                p.kill()
1✔
329

330
        # Remove exactly one trailing newline added by echo
331
        # We cannot assume "echo -n" exists.
332
        data = data[:-1]
1✔
333

334
        if data != original:
1!
335
            for i,(a,b) in enumerate(zip(data, original)):
×
336
                if a == b:
×
337
                    continue
×
338
                log.error(('Shell %r failed\n' +
×
339
                          'Expect %r\n' +
340
                          'Sent   %r\n' +
341
                          'Output %r\n' +
342
                          'Mismatch @ %i: %r vs %r') \
343
                        % (binary, original, input, data, i, a, b))
344

345
        progress.success()
1✔
346

347

348

349
SINGLE_QUOTE = "'" ##
1✔
350
ESCAPED_SINGLE_QUOTE = r"\'" ##
1✔
351

352
ESCAPED = {
1✔
353
    # The single quote itself must be escaped, outside of single quotes.
354
    "'": "\\'", ##
355

356
    # Slashes must themselves be escaped
357
    #
358
    # Additionally, some shells coalesce any number N>1 of '\' into
359
    # a single backslash literal.
360
    # '\\': '"\\\\\\\\"'
361
}
362

363
def sh_string(s):
1✔
364
    r"""Outputs a string in a format that will be understood by /bin/sh.
365

366
    If the string does not contain any bad characters, it will simply be
367
    returned, possibly with quotes. If it contains bad characters, it will
368
    be escaped in a way which is compatible with most known systems.
369

370
    Warning:
371
        This does not play along well with the shell's built-in "echo".
372
        It works exactly as expected to set environment variables and
373
        arguments, **unless** it's the shell-builtin echo.
374

375
    Argument:
376
        s(str): String to escape.
377

378
    Examples:
379

380
        >>> sh_string('foobar')
381
        'foobar'
382
        >>> sh_string('foo bar')
383
        "'foo bar'"
384
        >>> sh_string("foo'bar")
385
        "'foo'\\''bar'"
386
        >>> sh_string("foo\\\\bar")
387
        "'foo\\\\bar'"
388
        >>> sh_string("foo\\\\'bar")
389
        "'foo\\\\'\\''bar'"
390
        >>> sh_string("foo\\x01'bar")
391
        "'foo\\x01'\\''bar'"
392
    """
393
    orig_s = s
1✔
394
    if isinstance(s, (bytes, bytearray)):
1✔
395
        s = s.decode('latin1')
1✔
396
    if '\x00' in s: ##
1!
397
        log.error("sh_string(): Cannot create a null-byte")
×
398

399
    if not s:
1!
400
        quoted_string = "''" ##
×
401
        if isinstance(orig_s, (bytes, bytearray)):
×
402
            quoted_string = quoted_string.encode('latin1')
×
403
        return quoted_string
×
404

405
    chars = set(s)
1✔
406
    very_good = set(string.ascii_letters + string.digits + "_+.,/-") ##
1✔
407

408
    # Alphanumeric can always just be used verbatim.
409
    if chars <= very_good:
1✔
410
        return orig_s
1✔
411

412
    # If there are no single-quotes, the entire thing can be single-quoted
413
    if not (chars & set(ESCAPED)):
1✔
414
        quoted_string = "'%s'" % s ##
1✔
415
        if isinstance(orig_s, (bytes, bytearray)):
1!
416
            quoted_string = quoted_string.encode('latin1')
1✔
417
        return quoted_string
1✔
418

419
    # If there are single-quotes, we can single-quote around them, and simply
420
    # escape the single-quotes.
421
    quoted_string = '' ##
1✔
422
    quoted = False
1✔
423
    for char in s: ##
1✔
424
        if char not in ESCAPED:
1✔
425
            if not quoted:
1✔
426
                quoted_string += SINGLE_QUOTE
1✔
427
                quoted = True
1✔
428
            quoted_string += char ##
1✔
429
        else:
430
            if quoted:
1✔
431
                quoted = False
1✔
432
                quoted_string += SINGLE_QUOTE
1✔
433
            quoted_string += ESCAPED[char]
1✔
434

435
    if quoted:
1!
436
        quoted_string += SINGLE_QUOTE
1✔
437

438
    if isinstance(orig_s, (bytes, bytearray)):
1!
439
        quoted_string = quoted_string.encode('latin1')
1✔
440
    return quoted_string
1✔
441

442
def sh_prepare(variables, export = False):
1✔
443
    r"""Outputs a posix compliant shell command that will put the data specified
444
    by the dictionary into the environment.
445

446
    It is assumed that the keys in the dictionary are valid variable names that
447
    does not need any escaping.
448

449
    Arguments:
450
      variables(dict): The variables to set.
451
      export(bool): Should the variables be exported or only stored in the shell environment?
452
      output(str): A valid posix shell command that will set the given variables.
453

454
    It is assumed that `var` is a valid name for a variable in the shell.
455

456
    Examples:
457

458
        >>> sh_prepare({'X': 'foobar'})
459
        b'X=foobar'
460
        >>> r = sh_prepare({'X': 'foobar', 'Y': 'cookies'})
461
        >>> r == b'X=foobar;Y=cookies' or r == b'Y=cookies;X=foobar' or r
462
        True
463
        >>> sh_prepare({'X': 'foo bar'})
464
        b"X='foo bar'"
465
        >>> sh_prepare({'X': "foo'bar"})
466
        b"X='foo'\\''bar'"
467
        >>> sh_prepare({'X': "foo\\\\bar"})
468
        b"X='foo\\\\bar'"
469
        >>> sh_prepare({'X': "foo\\\\'bar"})
470
        b"X='foo\\\\'\\''bar'"
471
        >>> sh_prepare({'X': "foo\\x01'bar"})
472
        b"X='foo\\x01'\\''bar'"
473
        >>> sh_prepare({'X': "foo\\x01'bar"}, export = True)
474
        b"export X='foo\\x01'\\''bar'"
475
        >>> sh_prepare({'X': "foo\\x01'bar\\n"})
476
        b"X='foo\\x01'\\''bar\\n'"
477
        >>> sh_prepare({'X': "foo\\x01'bar\\n"})
478
        b"X='foo\\x01'\\''bar\\n'"
479
        >>> sh_prepare({'X': "foo\\x01'bar\\n"}, export = True)
480
        b"export X='foo\\x01'\\''bar\\n'"
481
    """
482

483
    out = []
1✔
484
    export = b'export ' if export else b''
1✔
485

486
    _, variables = normalize_argv_env([], variables, log)
1✔
487

488
    for k, v in variables:
1✔
489
        out.append(b'%s%s=%s' % (export, k, sh_string(v)))
1✔
490

491
    return b';'.join(out)
1✔
492

493
def sh_command_with(f, *args):
1✔
494
    r"""sh_command_with(f, arg0, ..., argN) -> command
495

496
    Returns a command create by evaluating `f(new_arg0, ..., new_argN)`
497
    whenever `f` is a function and `f % (new_arg0, ..., new_argN)` otherwise.
498

499
    If the arguments are purely alphanumeric, then they are simply passed to
500
    function. If they are simple to escape, they will be escaped and passed to
501
    the function.
502

503
    If the arguments contain trailing newlines, then it is hard to use them
504
    directly because of a limitation in the posix shell. In this case the
505
    output from `f` is prepended with a bit of code to create the variables.
506

507
    Examples:
508

509
        >>> sh_command_with(lambda: "echo hello")
510
        'echo hello'
511
        >>> sh_command_with(lambda x: "echo " + x, "hello")
512
        'echo hello'
513
        >>> sh_command_with(lambda x: "/bin/echo " + x, "\\x01")
514
        "/bin/echo '\\x01'"
515
        >>> sh_command_with(lambda x: "/bin/echo " + x, "\\x01\\n")
516
        "/bin/echo '\\x01\\n'"
517
        >>> sh_command_with("/bin/echo %s", "\\x01\\n")
518
        "/bin/echo '\\x01\\n'"
519
    """
520

521
    args = list(args)
1✔
522
    out = []
1✔
523

524
    for n in range(len(args)):
1✔
525
        args[n] = sh_string(args[n])
1✔
526
    if hasattr(f, '__call__'):
1✔
527
        out.append(f(*args))
1✔
528
    else:
529
        out.append(f % tuple(args))
1✔
530
    return ';'.join(out)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc