PowerShell, AWS, Jenkins and continuously enforced security groups

I recently did a series on how to spin up EC2 instances using Powershell, Jenkins and cloudflare. One of the things I really liked about the series was how you could limit the security groups presented to the user when generating the instance. If you have a passing experience with the AWS console you are probably aware it is easy to create a new security group when spinning up an EC2 instance. Since it takes a little bit of extra work to recreate the instance with the correct security group you often end up with a sea of generic security groups. The groups themselves are not a problem, but if your team is leaving RDP and SSH open to the world it can raise additional security concerns.

The script I wrote integrates with Jenkins and allows you to enforce IP rules for these security groups.
In order for the script to work you will need to have completed the “Getting started with AWS powershell plugin
I also recommend you check out  Matthew Hodgkins’ 2 part blog series on getting started with Jenkins and Powershell
There are no additional Jenkins plugins needed.

You can get the script I will be talking about from my github here. I wrote 2 versions, I’ll mostly be focused on the dynamic IP version and that will be what most of the code snippets and Jenkins build will be about in the post. I’ll make notes on the differences for the other Jenkins build later down since that is the major difference in each version.

 

The work flow:

First we’ll focus on the Jenkins build.

The script takes a couple of environment variables from jenkins to launch. We’ll need the “AWSprofile” parameter with the names that were pre-saved in the powershell plugin. Since these are windows profile dependent it is recommended to set them up as the service account running Jenkins.

The other variable the script needs is the ports we want to limit to our current external IP. I wrote this script with the idea of limiting management ports so it will mindlessly lock down all ports listed. So if you enter port 80 in the list then only you will be able to reach port 80 from all your EC2 instances.

 

In the end the build will appear like this:

Note: The script reads the build parameters and uses a ‘;’ colon delimiter.

The code that loads the parameters:

import-module awspowershell
#AWS stored Credential names
$profile_list = $ENV:AWSProfiles -split ";"
#Path for log file
$path_log = $ENV:Path_log
#Uses Value "From Port in AWS" SSH is 22, RDP is 3389. This script expects a standard port 22 maps to port 22 design
$Search_ports = $ENV:Ports_list -split ";"

Next we load the Get-ExternalIP function. This function reaches out to the ipify.org API and parses the Json response and returns the CDR formatted response for the AWS firewall.

function Get-externalCDR() {
try {
$ip = $(invoke-restmethod 'https://api.ipify.org?format=json' | select -expandproperty IP) + "/32"
return $ip
} catch {
return $false
}
}
#Allowed IP Ranges.
$Allowed_IP_Ranges = Get-ExternalCDR

Load the profile names and then start iterating through all the AWS regions:

foreach($profile in $profile_list) {
#Incase of Security group overlap clear the id list for every profile
[array]$updated_group_id = $NULL
#Set the AWS profile to use
Set-AWSCredentials -ProfileName $profile
#Iterate through all possible regions
$region_list = Get-AWSRegion | select -expandproperty Region
foreach($region in $region_list) {

Next we iterate through each instance and grab the assigned security group ID and Name:

$Instance_list = Get-EC2Instance -region $region |select -expandproperty instances
$VPC_list = Get-EC2Vpc -Region $region
foreach ($VPC in $VPC_list) {
$Instance_list | Where-Object {$_.VpcId -eq $VPC.VpcId} | foreach-object {
$Instance_name = ($_.Tags | Where-Object {$_.Key -eq 'Name'}).Value
$SecurityGroups = $_.SecurityGroups.GroupName
$SecurityGroupID = $_.SecurityGroups.GroupID

Next we confirm that we haven’t already touched this particular group. This is just to save time in larger AWS environments. If we update Security Group A then every EC2 instance with Security Group A is already up to date.

if($updated_group_id -notcontains $SecurityGroupID) {

Now the good part. We take the Array of Ports we defined in the Jenkins build to check and make sure the security group we are checking has them present. IF it doesn’t we just skip the group. Then we check that the allowed IP is not in our Array of allowed options. Since this is the Dynamic script it is checking for our extremely limited /32 CDR it’ll remove the rule if it doesn’t match. I utilize the compare-object command in order to skip rules that are correct.

foreach($port in $Search_Ports) {
if($Found_IP_List = $(Get-EC2SecurityGroup $SecurityGroupID -Region $region ).IpPermissions | where { $_.FromPort -eq "$port" } | select -expandproperty IPRange) {
$Removable_IPs = Compare-Object -ReferenceObject $Allowed_IP_Ranges -DifferenceObject $Found_IP_List | where { $_.SideIndicator -eq "=>" } | select -expandproperty InputObject
foreach($IP_Current_Rule in $Removable_IPs) {
$Time = Get-date -format "s"
echo "$Time : Removing $IP_Current_Rule from $SecurityGroups ( $SecurityGroupID ) with $profile in $region found on $Instance_name"
echo "$Time : Removing $IP_Current_Rule from $SecurityGroups ( $SecurityGroupID ) with $profile in $region found on $Instance_name" >> $path_log
Try {
$Firewall_rule = @{ IpProtocol="tcp"; FromPort="$port"; ToPort="$port"; IpRanges= "$IP_Current_Rule" }
Revoke-EC2SecurityGroupIngress -GroupId $SecurityGroupID -IpPermissions $Firewall_rule -Region $region
} catch {
echo "$Time ERROR: REMOVING $port for $SecurityGroups ($SecurityGroupID)"
echo "$Time ERROR: REMOVING $port for $SecurityGroups ($SecurityGroupID)" >> $Path_log
$_
$_ >> $Path_log
exit 1
}
}

We are still in the Foreach loop of $port in $SearchPorts, we are also in the IF check where the $found_IP_list variable is defined. We next need apply allowed IPs to the Security group. We re-compare the ips and make sure that we don’t try to apply IPs twice. The goal in this to clean everything up:

$Allowed_IPs = Compare-Object -ReferenceObject $Allowed_IP_Ranges -DifferenceObject $Found_IP_List | where { $_.SideIndicator -eq "<=" } | select -expandproperty InputObject
foreach($IP in $Allowed_IPs) {
if($Found_IP_List -notcontains $IP) {
$Time = Get-date -format "s"
echo "$Time : Adding $IP from $SecurityGroups ( $SecurityGroupID ) with $profile in $region found on $Instance_name"
echo "$Time : Adding $IP from $SecurityGroups ( $SecurityGroupID ) with $profile in $region found on $Instance_name" >> $path_log
Try {
$Firewall_rule = @{ IpProtocol="tcp"; FromPort="$port"; ToPort="$port"; IpRanges= "$IP" }
Grant-EC2SecurityGroupIngress -GroupId $SecurityGroupID -IpPermission @( $Firewall_rule ) -Region $region
} catch {
echo "$Time ERROR: ADDING $port for $SecurityGroups ($SecurityGroupID)"
echo "$Time ERROR: ADDING $port for $SecurityGroups ($SecurityGroupID)" >> $Path_log
$_
$_ >> $Path_log
exit 1
}
}
}
}

We are now out of the If($Found_IP_List check. There is the final step off adding the group to the skip Variable $updated_group_id. This again is to save time in large environments.

$updated_group_id += $SecurityGroupID.Trim()
}
}
}
}
}

There are some risks associated with this script, but they are pretty minor. The major issue is if the connection to AWS via powershell is interrupted it is possible a Security Group will not have a management port re-added. If it isn’t re-added then re-running the script won’t fix it. You’ll need to go through Log file the script generates to find the old setting. That is the reason I log both to the Jenkins console AND to a file so you have the ability to find a history of old settings. This is a blunt tool that works well for standardizing an environment. You may need to modify it to fit your environment.

 

Bonus Array defined IPs

Earlier I said I had worked on two versions. The other version is almost identical to this version except it accepts an Array of IPs. You find that version in the same github I’d listed earlier.

The major difference is the Jenkins build needs a new parameter added:

And the variable:

$Allowed_IP_Ranges = $ENV:Allowed_IP_list -split ";"

 

Thanks for reading.

I_Script_Stuff

Using Malwaredomains.com DNS Black hole list with Windows 2012 DNS and Powershell

Malwaredomains.com is a project that is constantly adding known malware domains to a giant list.
They have directions for adding there zones files to a windows server but they even describe it as a bit of a work around. They link to a powershell script that uses wmi. Well, I hadn’t worked with the windows 2012 Powershell DNS commands so I threw together a quick little script to handle linking to the malwaredomains.com list using the native commands for windows 2012.

The script pulls and parses the full .txt file www.malwaredomains.com keeps. The Primary Zone is as a non Active directory integrated entry. This will keep it from flooding your active directory and replication with entries. If you choose to add this script I would recommend you place it only on the domain controllers that your users are likely to query for DNS. An example would be if you have two active directory domain controllers to handle an office’s DNS and two domain controllers for FSMO roles and to serve the Datacenter the script should run on the office Domain controllers.

This script can be found on pastebin
And all three of these scripts can be found on my github.
Customize the top variables for your environment, the rest of the script should be self handling:

$tmp_file_holder = "current_list.bk"

$rollback_path = “C:\scripts\current_roll_back.list”
$rollback_date = get-date -format “M_dd_yyyy”
$rollback_backup_file = $rollback_path + “rollback_” + $rollback_date + “.bat”

move-item $rollback_path $rollback_backup_file -force

$domain_list = invoke-webrequest http://mirror1.malwaredomains.com/files/domains.txt | select -expandproperty content
$domain_list -replace “`t”, “;” -replace “;;” > $tmp_file_holder
$domain_content = get-content $tmp_file_holder

$zone_list = get-dnsserverzone | where {$_.IsDsIntegrated -eq $false} | select -expandproperty Zonename

foreach($line in $domain_content){
if(-not($line | select-string “##”)) {
$line_tmp = $line -split “;”
$line = $line_tmp[0]
if($zone_list -notcontains $line) {
Add-DnsServerPrimaryZone “$line” -DynamicUpdate “none” -ZoneFile “$line.dns”
echo “$line” | Out-File -FilePath $rollback_file -Encoding ascii -append
sleep 1
}
}
}

The Malwaredomains.com team often takes down sites off the list that were temporarily added, or wrongfully added. I created a roll back script.
This script can be found on pastebin
And all three of these scripts can be found on my github.
Make sure to modify the top variable to fit your environment and match the original script:

$rollback_path = "C:\scripts\current_roll_back.list"

$domain_content = get-content $rollback_path
$zone_list = get-dnsserverzone | where {$_.IsDsIntegrated -eq $false} | select -expandproperty Zonename

foreach($line in $domain_content) {
if($zone_list -contains $line) {
Remove-DnsServerZone “$line”
sleep 1
}
}

I would suggest setting up 2 scheduled tasks.
One to run weekly adding new domains to the list and keeping the list up to date.
The second task would run the roll back. Clearing out wrongly marked domains, etc. Though how you choose to manage it is up to you.

Since I was messing around with the files anyway I also made a host file generator. Host files are not really a preferable method as there have been reports of large host files slowing down browsing and the like. That said I do use a version of this script on my personal computers and haven’t seen an issue. Malwaredomains.com doesn’t offer host files, but they offer a list of some great host files. That limits the use of the bellow script, but I do like adding my own hostfile entries to the top of the script and run it as a scheduled task once a week.
This can be found on pastebin
And all of the code can be found on my github

Change the variables at the top to fit your needs. I even made it easy to setup a reroute ip to a branded warning for businesses. I would point out that this doesn’t protect against sub domains and the like.


$host_file_path = "C:\windows\system32\drivers\etc\hosts_tmp"
$final_loc = "C:\windows\system32\drivers\etc\hosts"
$tmp_file_holder = ".\current_list.bk"
$reroute = "127.0.0.1"

$Host_File_header = “# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a ‘#’ symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost”

echo “$Host_File_header” > $host_file_path

$domain_list = invoke-webrequest http://mirror1.malwaredomains.com/files/domains.txt | select -expandproperty content
$domain_list -replace “`t”, “;” -replace “;;” > $tmp_file_holder
$domain_content = get-content $tmp_file_holder
foreach($line in $domain_content){
if(-not($line | select-string “##”)) {
$line_tmp = $line -split “;”
$line = $line_tmp[0]
echo “$reroute $line” >> $host_file_path
}
}

move-item $host_file_path $final_loc -force

All the code can be found on my github.com

Thanks for reading.

PHP, Powershell and Shell_Exec()

Over in /r/powershell I have been seeing an increase in posts about using PHP as a front end. The general consensus has been to use shell_exec to launch the script and pass the variables to powershell. Looking around online I haven’t seen any of the tutorials address the security concerns that come with shell_exec.

In this post I’ll show an example of an attack done on shell_exec as it relates to launching powershell. Before we start a quick note: All of the mitigation techniques I am going to show you should be part of a security profile. Some examples of things to consider when deciding on your security profile:
1) Proper delegation of Service account permissions in active directory and on the local system.
2) Installation of tools such as Mod_security
3) Installation of Anti-virus
4) Limiting access of the PHP interface to only authorized users.

The Attack:

First we’ll need a php page that launches a powershell script. A txt copy of the code for the examples can be found here in pastebin or on my github Pictures were used because wordpress formatting was driving me nuts.
PHP
index_php_basic

Powershell
powershell_basic

Ideally a user enters data into the php form. The data is logged and returned to the user.

For this example I submitted “test123”. The text was logged in log.txt and the expected response was sent back:

output_expected_basic

Now lets send the attack string to the form:

test11;” dir c:\ >> C:\inetpub\wwwroot\dir_list.txt

This time the response isn’t exactly what we expected. The webpage output the same response but 1/2 of the attack string is missing:

output_unexpected_basic

The log file is also only showing test11. If we go to C:\inetpub\wwwroot\dir_list.txt or simply download it from the root of our webserver we get a directory listing like so:

Directory: C:\

Mode LastWriteTime Length Name
—- ————- —— —-
d—– 6/7/2016 9:54 PM inetpub
d—– 8/22/2013 8:52 AM PerfLogs
d-r— 5/27/2016 7:31 AM Program Files
d—– 5/27/2016 7:31 AM Program Files (x86)
d—– 6/4/2016 8:44 PM Scripts
d—– 5/21/2016 6:51 PM tmp
d-r— 5/24/2016 11:55 PM Users
d—– 5/21/2016 6:34 PM Windows

This is a very basic proof of concept. The attacks can become much more complex leading to a fully compromised Web server or a compromised Active Directory instance if the Service Account is a domain admin, etc.

Mitigating the attacks:
One of the most common defenses, but hardest to do correctly, is to use a combination of regular expressions, escapeshellcmd, and escapeshellarg.
An example like this might work updating a title, firstname or last name:

preg_match_index_php

Code found here on pastebin or the github still contains everything

A better solution would be to submit your information to Mysql, or a file format of your choosing ( XML, Json, CSV) on the local drive. Then use shell_Exec to launch the powershell script without any direct user input. Let the powershell script parse the data and submit it. You can also use a scheduled task to launch this method which has the added advantage of not letting the Service Account user anywhere near IIS.

In some cases you need to allow the full range of characters and absolutely must take user input and the data can not be allowed to rest in a file or mysql table. For these rare cases I would suggest encoding the data. In this example Base64 is used:
Php:
base_64_php

Powershell:
base_64_powershell

Text version found on pastebin or the github still contains everything
When we submit the attack code again: test11;” dir c:\ >> C:\inetpub\wwwroot\dir_list.txt
We see the full attack returned as well as logged:
attack_Return_base64

Base64 encoding can be a bit system heavy and probably wouldn’t work for a bigger site.
I hope some part of this has proven to be informative. All code examples can be found here: https://github.com/ryanlangley4/ps_and_php_sec

*None of the examples done here limit the length of user input, or any number of other things like accept spaces when sending to powershell. The goal is to focus on shell_exec and not get too bogged down with details.