File: template.py

package info (click to toggle)
pwntools 4.15.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 18,508 kB
  • sloc: python: 59,870; ansic: 48,351; asm: 45,047; sh: 396; makefile: 256
file content (238 lines) | stat: -rw-r--r-- 10,136 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from pwn import *
from pwnlib.commandline import common
from pwnlib.util.misc import which, parse_ldd_output, write

from sys import stderr
from mako.lookup import TemplateLookup, Template

parser = common.parser_commands.add_parser(
    'template',
    help = 'Generate an exploit template',
    description = 'Generate an exploit template. If no arguments are given, '
                    'the current directory is searched for an executable binary and ' 
                    'libc. If only one binary is found, it is assumed to be the '
                    'challenge binary.'
)

# change path to hardcoded one when building the documentation
printable_data_path = "pwnlib/data" if 'sphinx' in sys.modules else pwnlib.data.path

parser.add_argument('exe', nargs='?', help='Target binary. If not given, the current directory is searched for an executable binary.')
parser.add_argument('--host', help='Remote host / SSH server')
parser.add_argument('--port', help='Remote port / SSH port', type=int)
parser.add_argument('--user', help='SSH Username')
parser.add_argument('--pass', '--password', help='SSH Password', dest='password')
parser.add_argument('--libc', help='Path to libc binary to use. If not given, the current directory is searched for a libc binary.')
parser.add_argument('--path', help='Remote path of file on SSH server')
parser.add_argument('--quiet', help='Less verbose template comments', action='store_true')
parser.add_argument('--color', help='Print the output in color', choices=['never', 'always', 'auto'], default='auto')
parser.add_argument('--template', help='Path to a custom template. Tries to use \'~/.config/pwntools/templates/pwnup.mako\', if it exists. '
                                   'Check \'%s\' for the default template shipped with pwntools.' % 
                                        os.path.join(printable_data_path, "templates", "pwnup.mako"))
parser.add_argument('--no-auto', help='Do not automatically detect missing binaries', action='store_false', dest='auto')

def get_docker_image_libraries():
    """Tries to retrieve challenge libraries from a Docker image built from the Dockerfile in the current working directory.
    
    The libraries are retrieved by parsing the output of running ldd on /bin/sh.
    Supports regular Docker images as well as jail images.
    """
    with log.progress("Extracting challenge libraries from Docker image") as progress:
        if not which("docker"):
            progress.failure("docker command not found")
            return None, None
        # maps jail image name to the root directory of the child image
        jail_image_to_chroot_dir = {
            "pwn.red/jail": "/srv",
        }
        dockerfile = open("Dockerfile", "r").read()
        jail = None
        chroot_dir = "/"
        for jail_image in jail_image_to_chroot_dir:
            if re.search(r"^FROM %s" % jail_image, dockerfile, re.MULTILINE):
                jail = jail_image
                chroot_dir = jail_image_to_chroot_dir[jail_image]
                break
        try:
            progress.status("Building image")
            image_sha = subprocess.check_output(["docker", "build", "-q", "."], stderr=subprocess.PIPE, shell=False).decode().strip()

            progress.status("Retrieving library paths")
            ldd_command = ["-c", "chroot %s /bin/sh -c 'ldd /bin/sh'" % chroot_dir]
            ldd_output = subprocess.check_output([
                    "docker",
                    "run",
                    "--rm",
                    "--entrypoint",
                    "/bin/sh",
                    ] + (["--privileged"] if jail else []) + [
                        image_sha,
                    ] + ldd_command,
                stderr=subprocess.PIPE, 
                shell=False
            ).decode()
            
            libc, ld = None, None
            libc_basename, ld_basename = None, None
            for lib_path in parse_ldd_output(ldd_output):
                if "libc." in lib_path:
                    libc = lib_path
                    libc_basename = os.path.basename(lib_path)
                if "ld-" in lib_path:
                    ld = lib_path
                    ld_basename = os.path.basename(lib_path)

            if not (libc and ld):
                progress.failure("Could not find libraries")
                return None, None
            
            progress.status("Copying libraries to current directory")
            for filename, basename in zip((libc, ld), (libc_basename, ld_basename)):
                cat_command = ["-c", "chroot %s /bin/sh -c '/bin/cat %s'" % (chroot_dir, filename)]
                contents = subprocess.check_output([
                        "docker",
                        "run",
                        "--rm",
                        "--entrypoint",
                        "/bin/sh",
                        ] + (["--privileged"] if jail else []) + [
                            image_sha
                        ] + cat_command,
                    stderr=subprocess.PIPE, 
                    shell=False
                )
                write(basename, contents)

        except subprocess.CalledProcessError as e:
            print(e.stderr.decode())
            log.error("docker failed with status: %d" % e.returncode)

        progress.success("Retrieved libraries from Docker image")
    return libc_basename, ld_basename

def detect_missing_binaries(args):
    """Automatically detects challenge binaries and libraries in the current directory.
    
    This function scans the current directory for executable files, libc, and ld libraries.
    If a Dockerfile is present and no libraries are found, it attempts to extract them from
    the Docker image, but only if the binary is not statically linked.
    
    Args:
        args: Argument namespace containing exe and libc attributes.
        
    Returns:
        tuple: A pair of (executable_path, libc_path) where either may be None if not found.
    """
    log.info("Automatically detecting challenge binaries...")
    # look for challenge binary, libc, and ld in current directory
    exe, libc, ld = args.exe, args.libc, None
    has_dockerfile = False
    other_files = []
    for filename in os.listdir("."):
        if not os.path.isfile(filename):
            continue
        if not libc and ('libc-' in filename or 'libc.' in filename):
            libc = filename
        elif not ld and 'ld-' in filename:
            ld = filename
        elif filename == "Dockerfile":
            has_dockerfile = True
        else:
            if os.access(filename, os.X_OK):
                other_files.append(filename)
    if not exe:
        if len(other_files) == 1:
            exe = other_files[0]
        elif len(other_files) > 1:
            log.warning("Failed to find challenge binary. There are multiple binaries in the current directory: %s", other_files)
    
    # Check if the binary is statically linked before trying to extract libraries from Docker
    is_statically_linked = False
    if exe:
        try:
            binary = ELF(exe, checksec=False)
            is_statically_linked = binary.statically_linked
            if is_statically_linked:
                log.info("Binary is statically linked, no need for external libraries")
        except Exception as e:
            log.warning("Could not check if binary is statically linked: %s", e)
    
    # Only extract libraries from Docker if the binary is not statically linked
    if has_dockerfile and exe and not (libc or ld) and not is_statically_linked: 
        libc, ld = get_docker_image_libraries()

    if exe != args.exe:
        log.success("Found challenge binary %r", exe)
    if libc != args.libc:
        log.success("Found libc binary %r", libc)
    return exe, libc

def main(args):

    lookup = TemplateLookup(
        directories      = [
            os.path.expanduser('~/.config/pwntools/templates/'),
            os.path.join(pwnlib.data.path, 'templates')
        ],
        module_directory = None
    )

    # For the SSH scenario, check that the binary is at the
    # same path on the remote host.
    if args.user:
        if not (args.path or args.exe):
            log.error("Must specify --path or a exe")

        with ssh(args.user, args.host, args.port or 22, args.password or None) as s:
            try:
                remote_file = args.path or args.exe
                s.download(remote_file)
            except Exception:
                log.warning("Could not download file %r, opening a shell", remote_file)
                s.interactive()
                return

        if not args.exe:
            args.exe = os.path.basename(args.path)

    if args.auto and (args.exe is None or args.libc is None):
        args.exe, args.libc = detect_missing_binaries(args)
    
    if args.template:
        template = Template(filename=args.template) # Failing on invalid file is ok
    else:
        template = lookup.get_template('pwnup.mako')
    
    output = template.render(args.exe,
                             args.host,
                             args.port,
                             args.user,
                             args.password,
                             args.libc,
                             args.path,
                             args.quiet)

    # Fix Mako formatting bs
    output = re.sub('\n\n\n', '\n\n', output)

    # Colorize the output if it's a TTY
    if args.color == 'always' or (args.color == 'auto' and sys.stdout.isatty()):
        from pygments import highlight
        from pygments.formatters import TerminalFormatter
        from pygments.lexers.python import PythonLexer
        output = highlight(output, PythonLexer(), TerminalFormatter())

    print(output)

    # If redirected to a file, make the resulting script executable
    if not sys.stdout.isatty():
        try: os.fchmod(sys.stdout.fileno(), 0o700)
        except OSError: pass

if __name__ == '__main__':
    pwnlib.commandline.common.main(__file__, main)