pwntools icon indicating copy to clipboard operation
pwntools copied to clipboard

ssh_process::getenv ignores environment and returns wrong result

Open gdzx opened this issue 7 years ago • 4 comments

Introduction

ssh_process::getenv returns an incorrect result and ignores overridden environment variables with process or system.

Example

With script.py containing:

#!/usr/bin/env python2

from pwn import *
context.log_level = 'error'

user = 'narnia0'
host='narnia.labs.overthewire.org'
password='narnia0'
port=2226

s = ssh(user=user, host=host, password=password, port=port)
p = s.process('/narnia/narnia0', env={'FOO': 'BAR'})
print(hex(p.getenv('FOO')))

Running it gives:

$ ./script.py
1

Possible fix

Using the following patch:

diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py
index fd3a0307..bfcc4ab4 100644
--- a/pwnlib/tubes/ssh.py
+++ b/pwnlib/tubes/ssh.py
@@ -365,6 +365,7 @@ class ssh_process(ssh_channel):
         script = ';'.join(('from ctypes import *',
                            'import os',
                            'libc = CDLL("libc.so.6")',
+                           'libc.getenv.restype = c_voidp',
                            'print os.path.realpath(%r)' % self.executable,
                            'print(libc.getenv(%r))' % variable,))
 
@@ -998,7 +999,7 @@ os.execve(exe, argv, env)
 
             script = 'for py in python2.7 python2 python; do test -x "$(which $py 2>&1)" && exec $py -c %s check; done; echo 2' % sh_string(script)
             with context.local(log_level='error'):
-                python = ssh_process(self, script, tty=True, raw=True, level=self.level, timeout=self.timeout)
+                python = ssh_process(self, script, tty=True, env=env, raw=True, level=self.level, timeout=self.timeout)
 
             try:
                 result = safeeval.const(python.recvline())

It returns:

$ ./script.py
0xffffefe5

Notes

ssh::getenv needs similar patching for the result type (even though I'm not sure why it is used for, since it doesn't handle argv nor env).

gdzx avatar Nov 21 '18 17:11 gdzx

I think the way forward for this will be to force core dumps on the remote process, and use the Coredump API. Unfortunately it's difficult to do this currently, there's no pre-canned solution. It also requires duplicates of the process in the event of a setuid binary.

The current method of shelling out to python on the remote system does not work for...

  1. Mixed architectures (e.g. 64-bit target binary and a 32-bit python)
  2. Foreign architectures (e.g. ARM on Intel, anything that uses qemu-xxx emulation)
  3. Any target machines without python installed at all

In the case of your example, you're running into the issue you identified (env is not passed along) but also running into (1). 0xffffefe5 does not seem like a likely / valid 32-bit stack address.

/narnia/narnia0: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0840ec7ce39e76ebcecabacb3dffb455cfa401e9, not stripped
/usr/bin/python: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6aac156c31a134f49fb9c5c62d223c163fb1b648, stripped

zachriggle avatar Nov 21 '18 20:11 zachriggle

Following up, I believe that we can solve (1) by using Corefile. The issue is getting at the corefile.

The issue with foreign architectures is not solvable, since any generated Corefile will be for the QEMU process itself, not the "emulated" process. What we can do is try to get qemu to generate a corefile by killing the process with e.g. kill -SIGTRAP {pid}.

For native processes, we are also trading a dependency on python for a dependency on gdb.

zachriggle avatar Nov 21 '18 21:11 zachriggle

Indeed, I used a 32-bit VM for testing so that worked properly...

To keep it simple, we could check that python on the remote host matches context.arch.

For mixed architectures, we could rely on something like hellman's fixenv that you use in your write-ups if the remote machine has gcc and is able to build 32-bit executables (but it depends on how the challenges were compiled).

Otherwise, isn't it possible to patch the ELF entrypoint with an int3 instruction ? I haven't done an in-depth test but qemu-*-static seems to dump the inner process core when it hits the instruction and pwntools.Corefile is able to retrieve the environment variables.

gdzx avatar Nov 22 '18 16:11 gdzx

To keep it simple, we could check that python on the remote host matches context.arch.

No, I'd prefer we not do this. The current implementation is ~= broken and really needs a redesign.

To keep it simple, we could check that python on the remote host matches context.arch.

Hellman's fixenv script is a subset of the functionality Pwntools provides, and also has a great many limitations. It's a cool script, but it's a uber hack and a major step backward.

possible to patch the ELF entrypoint

We can patch the entry point, but generally we don't have write access to the challenge binary and would need to create a copy.

Separately, this is generally unnecessary unless the binary terminates immediately without blocking on any input. You can just kill(SIGTRAP, ...) the process and get it to drop core (generally).

Ultimately, ssh.getenv and ssh.process.getenv need to be reworked to rely on Corefile, regardless of the mechanism for acquiring the corefile.

zachriggle avatar Nov 23 '18 22:11 zachriggle