Social

lørdag den 10. august 2019

Azure Function App Managed Service Identity in Visual Studio Code (PowerShell)

I started using MSI in Function Apps a few years ago. Back then it was still a nice quality of life addition, but you had to jump through a few hoops to get it working, ex. this was necessary to use the MSI to login to Azure.

$apiVersion = "2017-09-01"
$resourceURI = "https://management.azure.com/"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
Login-AzAccount -Tenant $TenantId -AccountId $env:WEBSITE_SITE_NAME -AccessToken $accessToken -Scope Process

After not having done too much with Function Apps for a while I jumped back in and just could not wrap my head around how to get MSI working in a local development environment (I use VS Code). No help to find on the Internet, at least not explicitly stating how to do it. And for a good reason; the trick is to do nothing at all!

When running locally everything is done in the context of the logged in user, often the developer him/herself.
Above code would also fail (there is no MSI_ENDPOINT and much less an MSI_SECRET). But all the magic happens in profile.ps1 that is auto-created for each function app project. It is already outfitted with below code.


# Authenticate with Azure PowerShell using MSI.
# Remove this if you are not planning on using MSI or Azure PowerShell.
if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) {
    Connect-AzAccount -Identity
}


If there is an MSI_SECRET in the environment variables (which there should not be in a local development environment), then connect using an MSI, which I imagine doing something similar to the first piece of code I posted.
Another advantage is that you only do the login once (when the function app does a "cold start"). Previously it would be rather dodgy, and to be on the safe side, I would login in each function run, adding significant overhead.

And when running locally the entire thing is piggybacking off the logged in user. If you want to simulate the access the MSI has when running in Azure, just create a service principal, assign it the same roles as the MSI and login before running your code locally.

Running PowerShell in Azure Function App has come a long way. With the new Push-OutputBinding it is even easier than before to put the result of a function execution into a queue, table storage, etc.

tirsdag den 28. maj 2019

Monitoring Blocked Requests in Azure Web Application Firewall

The Azure Application Gateway can also function as a Web Application Firewall (WAF), and is a must have in any enterprise environment. In order to audit the firewall events the ApplicationGatewayFirewallLog must be ex. achieved to a storage account or even better, send to log analytics. This can be setup in the Diagnostic settings tab in the WAF.

The WAF has more than 300 rules it matches each request against, and if the Advanced rule configuration is disabled then all rules enabled and a single match will result in a request being blocked. Each rule is also identified by an Id, and with the amount of rules we will need to map the Id to a descriptive text. Microsoft has this readily available in markdown at https://raw.githubusercontent.com/MicrosoftDocs/azure-docs/master/articles/application-gateway/application-gateway-crs-rulegroups-rules.md, which is what they use to generate documentation for the WAF. Currently it is missing a few rule Ids. But we will get back to this part.

The end goal is to get something like this, presented in a Azure dashboard.


Let's look at the query we need to write first.


// variables
let resourceId = "/SUBSCRIPTIONS/<SUBSCRIPTION_ID>/RESOURCEGROUPS/<RESOURCEGROUPNAME>/PROVIDERS/MICROSOFT.NETWORK/APPLICATIONGATEWAYS/<WAF_NAME>";
let period = 3d;
// show transactions with a block action
AzureDiagnostics
| where TimeGenerated > ago(period)
| where Category == "ApplicationGatewayFirewallLog" 
| where ResourceId == resourceId
| where action_s == "Matched" 
| project transactionId_s, ruleId_s, hostname_s, requestUri_s, TimeGenerated
| join (
   AzureDiagnostics
    | where TimeGenerated > ago(period)
    | where Category == "ApplicationGatewayFirewallLog" 
    | where ResourceId == resourceId
    | where action_s == "Blocked" 
    // using distinct as there will always be 0 or 2 "Blocked" entries for the transaction - resulting in duplicates when joining
    | distinct transactionId_s
) on transactionId_s
| sort by TimeGenerated desc 
| take 50
| project Rule = strcat(ruleId_s, ' - ', rule_mapping[ruleId_s]), Uri = strcat(hostname_s,requestUri_s),Timestamp = TimeGenerated

First find your WAF resource Id and paste it in. This query will look at past 3 days of data (and we will eventually take the latest 50 entries). The last part is only relevant in the Log Analytics query editor, but it is best practice to limit data by date early on.

The left part of the join will get the transactions where the action is Matched. Each request will generate 0 (no rule matches) or more transactions, one for each matching rule, and two more (block actions) if one of the rules are enabled.
Finally only the properties that are needed is projected.

The right part of the join is very similar, only we look transactions where the action is Blocked. And then we project, using distinct, the property transactionId_s. This is the only property we need for the join, and we use distinct as there will for each transaction be either 0 or 2 entries. If we used project the join would double up each Matched entry.

The results is then sorted by date and we take the latest 50 entries. In the projection we use a dynamic array to map each rule Id to a descriptive text, which is explained next.

The following script will generate the code we need to put before the Log Analytics query.


$Markdown = (wget 'https://raw.githubusercontent.com/MicrosoftDocs/azure-docs/master/articles/application-gateway/application-gateway-crs-rulegroups-rules.md').Content

function Is-RuleId ($Value) {
    return $Value -match "^[\d\.]+$"
}

# remove each line starting with # and blank lines
cls
# $VerbosePreference = 'Continue'
$Lines = $Markdown.Split('|')
$RuleId = $null
$Rules = @{}
foreach ($line in $Lines) {
    
    if($RuleId)
    {
        # we just had a rule id, the next must be description
        $Rules[$RuleId] = $line
        $RuleId = $null
    }
    elseif(Is-RuleId $line)
    {
        $RuleId = [int]$line
    }
}

# we can add missing mappings like this
$Rules['0'] = 'Mandatory rule. Inbound Anomaly Score Exceeded'
$Rules['941160'] = 'NoScript XSS InjectionChecker: HTML Injection'
$Rules['942240'] = 'Detects MySQL charset switch and MSSQL DoS attempts'
$Rules['942180'] = 'Detects basic SQL authentication bypass attempts 1/3'
$Rules['942390'] = 'SQL Injection Attack'
#$Rules['XXX'] = ''

#next we format to a Kusto mapping

#$Rules.Keys | ForEach-Object {"`t`t`"$_`" : `"$($Rules[$_])`","}

$KustoMapping = 
@"
let rule_mapping = dynamic(
  {
    $(
        $Rules.Keys | ForEach-Object {"    `"$_`" : `"$($Rules[$_])`",`n"}
    )
  });
"@

# $Rules.Keys.Count

# removes the very last , and put the result into clipboard
$KustoMapping -replace "(?s)(.*),(.*)", '' | clip

After pasting be sure to remove any blank lines in the query. The result can then be pinned to a dashboard of your choice.

Note that if you have all rules enabled, you can skip the join entirely. Each rule matched will result in the request being blocked. But you may at some point disable some rules (some are very stringent), or perhaps in a future version of WAF it can be customized at what score treshold the request is being blocked; each matched rule will provide a score, the sum of the score is then matched against a treshold I think is "greater than 0" currently. The point of this is that some rules matched is more dangerous than others, and the sum of them all provides a way to determine the risk of not blocking the request.

The result of the script can be seen below.


let rule_mapping = dynamic(
  {
        "941320" : "Possible XSS Attack Detected - HTML Tag Handler",
     "960343" : "Total uploaded files size too large",
     "960342" : "Uploaded file size too large",
     "960341" : "Total arguments size exceeded",
     "960335" : "Too many arguments in request",
     "958033" : "Cross-site Scripting (XSS) Attack",
     "973315" : "IE XSS Filters - Attack Detected.",
     "981253" : "Detects MySQL and PostgreSQL stored procedure/function injections",
     "920340" : "Request Containing Content but Missing Content-Type header",
     "958423" : "Cross-site Scripting (XSS) Attack",
     "958422" : "Cross-site Scripting (XSS) Attack",
     "958421" : "Cross-site Scripting (XSS) Attack",
     "958420" : "Cross-site Scripting (XSS) Attack",
     "958419" : "Cross-site Scripting (XSS) Attack",
     "958418" : "Cross-site Scripting (XSS) Attack",
     "958417" : "Cross-site Scripting (XSS) Attack",
     "958416" : "Cross-site Scripting (XSS) Attack",
     "958415" : "Cross-site Scripting (XSS) Attack",
     "958414" : "Cross-site Scripting (XSS) Attack",
     "958413" : "Cross-site Scripting (XSS) Attack",
     "958412" : "Cross-site Scripting (XSS) Attack",
     "958411" : "Cross-site Scripting (XSS) Attack",
     "958410" : "Cross-site Scripting (XSS) Attack",
     "958409" : "Cross-site Scripting (XSS) Attack",
     "958408" : "Cross-site Scripting (XSS) Attack",
     "958407" : "Cross-site Scripting (XSS) Attack",
     "958406" : "Cross-site Scripting (XSS) Attack",
     "958405" : "Cross-site Scripting (XSS) Attack",
     "958404" : "Cross-site Scripting (XSS) Attack",
     "950922" : "Backdoor access",
     "958059" : "Cross-site Scripting (XSS) Attack",
     "950120" : "Possible Remote File Inclusion (RFI) Attack = Off-Domain Reference/Link",
     "950119" : "Remote File Inclusion Attack",
     "950118" : "Remote File Inclusion Attack",
     "950117" : "Remote File Inclusion Attack",
     "950116" : "Unicode Full/Half Width Abuse Attack Attempt",
     "0" : "Mandatory rule. Inbound Anomaly Score Exceeded",
     "950110" : "Backdoor access",
     "950109" : "Multiple URL Encoding Detected",
     "950108" : "URL Encoding Abuse Attack Attempt",
     "950107" : "URL Encoding Abuse Attack Attempt",
     "973305" : "XSS Attack Detected",
     "950103" : "Path Traversal Attack",
     "960209" : "Argument name too long",
     "960208" : "Argument value too long",
     "981270" : "Finds basic MongoDB SQL injection attempts",
     "981320" : "SQL Injection Attack = Common DB Names Detected",
     "981317" : "SQL SELECT Statement Anomaly Detection Alert",
     "981316" : "Rule 981316",
     "981315" : "Rule 981315",
     "981314" : "Rule 981314",
     "981313" : "Rule 981313",
     "981312" : "Rule 981312",
     "981311" : "Rule 981311",
     "981310" : "Rule 981310",
     "981309" : "Rule 981309",
     "981308" : "Rule 981308",
     "981307" : "Rule 981307",
     "981306" : "Rule 981306",
     "981305" : "Rule 981305",
     "981304" : "Rule 981304",
     "981303" : "Rule 981303",
     "981302" : "Rule 981302",
     "981301" : "Rule 981301",
     "981300" : "Rule 981300",
     "932120" : "Remote Command Execution = Windows PowerShell Command Found",
     "933110" : "PHP Injection Attack = PHP Script File Upload Found",
     "981276" : "Looking for basic sql injection. Common attack string for mysql oracle and others.",
     "950921" : "Backdoor access",
     "981272" : "Detects blind sqli tests using sleep() or benchmark().",
     "958295" : "Multiple/Conflicting Connection Header Data Found.",
     "958291" : "Range = field exists and begins with 0.",
     "950019" : "Email Injection Attack",
     "950018" : "Universal PDF XSS URL Detected.",
     "960911" : "Invalid HTTP Request Line",
     "981260" : "SQL Hex Encoding Identified",
     "950012" : "HTTP Request Smuggling Attack.",
     "950011" : "SSI injection Attack",
     "950010" : "LDAP Injection Attack",
     "950009" : "Session Fixation Attack",
     "950008" : "Injection of Undocumented ColdFusion Tags",
     "950007" : "Blind SQL Injection Attack",
     "950006" : "System Command Injection",
     "950005" : "Remote File Access Attempt",
     "981250" : "Detects SQL benchmark and sleep injection attempts including conditional queries",
     "950003" : "Session Fixation",
     "950002" : "System Command Access",
     "950001" : "SQL Injection Attack",
     "950000" : "Session Fixation",
     "981241" : "Detects conditional SQL injection attempts",
     "981251" : "Detects MySQL UDF injection and other data/structure manipulation attempts",
     "950911" : "HTTP Response Splitting Attack",
     "950910" : "HTTP Response Splitting Attack",
     "950908" : "SQL Injection Attack.",
     "981231" : "SQL Comment Sequence Detected.",
     "981227" : "Apache Error = Invalid URI in Request.",
     "959151" : "PHP Injection Attack",
     "958230" : "Range = Invalid Last Byte Value.",
     "933100" : "PHP Injection Attack = Opening/Closing Tag Found",
     "973318" : "IE XSS Filters - Attack Detected.",
     "942370" : "Detects classic SQL injection probings 2/2",
     "960038" : "HTTP header is restricted by policy",
     "960035" : "URL file extension is restricted by policy",
     "960034" : "HTTP protocol version is not allowed by policy",
     "960032" : "Method is not allowed by policy",
     "958057" : "Cross-site Scripting (XSS) Attack",
     "973313" : "XSS Attack Detected",
     "960010" : "Request content type is not allowed by policy",
     "960024" : "Meta-Character Anomaly Detection Alert - Repetitive Non-Word Characters",
     "960022" : "Expect Header Not Allowed for HTTP 1.0.",
     "960021" : "Request Has an Empty Accept Header",
     "960020" : "Pragma Header requires Cache-Control Header for HTTP/1.1 requests.",
     "960018" : "Invalid character in request",
     "200004" : "Possible Multipart Unmatched Boundary.",
     "960016" : "Content-Length HTTP header is not numeric.",
     "960015" : "Request Missing an Accept Header",
     "960012" : "POST request missing Content-Length Header.",
     "960011" : "GET or HEAD Request with Body Content.",
     "973303" : "XSS Attack Detected",
     "960009" : "Request Missing a User Agent Header",
     "960008" : "Request Missing a Host Header",
     "960007" : "Empty Host Header",
     "960006" : "Empty User Agent Header",
     "981136" : "Rule 981136",
     "981134" : "Rule 981134",
     "981133" : "Rule 981133",
     "960914" : "Multipart request body failed strict validation",
     "960912" : "Failed to parse request body.",
     "959073" : "SQL Injection Attack",
     "950801" : "UTF8 Encoding Abuse Attack Attempt",
     "913120" : "Found request filename/argument associated with security scanner",
     "960904" : "Request Containing Content but Missing Content-Type header",
     "960902" : "Invalid Use of Identity Encoding.",
     "960901" : "Invalid character in request",
     "913110" : "Found request header associated with security scanner",
     "920460" : "Abnormal escape characters",
     "913102" : "Found User-Agent associated with web crawler/bot",
     "913101" : "Found User-Agent associated with scripting/generic HTTP client",
     "913100" : "Found User-Agent associated with security scanner",
     "920450" : "HTTP header is restricted by policy (%@{MATCHED_VAR})",
     "921120" : "HTTP Response Splitting Attack",
     "920440" : "URL file extension is restricted by policy",
     "920430" : "HTTP protocol version is not allowed by policy",
     "920420" : "Request content type is not allowed by policy",
     "920410" : "Total uploaded files size too large",
     "958002" : "Cross-site Scripting (XSS) Attack",
     "942460" : "Meta-Character Anomaly Detection Alert - Repetitive Non-Word Characters",
     "920400" : "Uploaded file size too large",
     "942450" : "SQL Hex Encoding Identified",
     "920390" : "Total arguments size exceeded",
     "942440" : "SQL Comment Sequence Detected.",
     "920380" : "Too many arguments in request",
     "958977" : "PHP Injection Attack",
     "958976" : "PHP Injection Attack",
     "958056" : "Cross-site Scripting (XSS) Attack",
     "958054" : "Cross-site Scripting (XSS) Attack",
     "942430" : "Restricted SQL Character Anomaly Detection (args): # of special characters exceeded (12)",
     "958052" : "Cross-site Scripting (XSS) Attack",
     "958051" : "Cross-site Scripting (XSS) Attack",
     "958049" : "Cross-site Scripting (XSS) Attack",
     "958047" : "Cross-site Scripting (XSS) Attack",
     "958046" : "Cross-site Scripting (XSS) Attack",
     "958045" : "Cross-site Scripting (XSS) Attack",
     "981018" : "Rule 981018",
     "958041" : "Cross-site Scripting (XSS) Attack",
     "958040" : "Cross-site Scripting (XSS) Attack",
     "958039" : "Cross-site Scripting (XSS) Attack",
     "958038" : "Cross-site Scripting (XSS) Attack",
     "958037" : "Cross-site Scripting (XSS) Attack",
     "958036" : "Cross-site Scripting (XSS) Attack",
     "958034" : "Cross-site Scripting (XSS) Attack",
     "942410" : "SQL Injection Attack",
     "958032" : "Cross-site Scripting (XSS) Attack",
     "958031" : "Cross-site Scripting (XSS) Attack",
     "958030" : "Cross-site Scripting (XSS) Attack",
     "920350" : "Host header is a numeric IP address",
     "958028" : "Cross-site Scripting (XSS) Attack",
     "958027" : "Cross-site Scripting (XSS) Attack",
     "958026" : "Cross-site Scripting (XSS) Attack",
     "958025" : "Cross-site Scripting (XSS) Attack",
     "958024" : "Cross-site Scripting (XSS) Attack",
     "958023" : "Cross-site Scripting (XSS) Attack",
     "958022" : "Cross-site Scripting (XSS) Attack",
     "958020" : "Cross-site Scripting (XSS) Attack",
     "958019" : "Cross-site Scripting (XSS) Attack",
     "958018" : "Cross-site Scripting (XSS) Attack",
     "958017" : "Cross-site Scripting (XSS) Attack",
     "958016" : "Cross-site Scripting (XSS) Attack",
     "958013" : "Cross-site Scripting (XSS) Attack",
     "958012" : "Cross-site Scripting (XSS) Attack",
     "958011" : "Cross-site Scripting (XSS) Attack",
     "958010" : "Cross-site Scripting (XSS) Attack",
     "920330" : "Empty User Agent Header",
     "958008" : "Cross-site Scripting (XSS) Attack",
     "958007" : "Cross-site Scripting (XSS) Attack",
     "958006" : "Cross-site Scripting (XSS) Attack",
     "958005" : "Cross-site Scripting (XSS) Attack",
     "958004" : "Cross-site Scripting (XSS) Attack",
     "958003" : "Cross-site Scripting (XSS) Attack",
     "942350" : "Detects MySQL UDF injection and other data/structure manipulation attempts",
     "958001" : "Cross-site Scripting (XSS) Attack",
     "958000" : "Cross-site Scripting (XSS) Attack",
     "920320" : "Missing User Agent Header",
     "933180" : "PHP Injection Attack = Variable Function Call Found",
     "920311" : "Request Has an Empty Accept Header",
     "920310" : "Request Has an Empty Accept Header",
     "942360" : "Detects concatenated basic SQL injection and SQLLFI attempts",
     "960017" : "Host header is a numeric IP address",
     "920300" : "Request Missing an Accept Header",
     "933161" : "PHP Injection Attack = Low-Value PHP Function Call Found",
     "933160" : "PHP Injection Attack = High-Risk PHP Function Call Found",
     "920290" : "Empty Host Header",
     "973346" : "IE XSS Filters - Attack Detected.",
     "933151" : "PHP Injection Attack = Medium-Risk PHP Function Name Found",
     "942340" : "Detects basic SQL authentication bypass attempts 3/3",
     "920280" : "Request Missing a Host Header",
     "920274" : "Invalid character in request headers (outside of very strict set)",
     "920273" : "Invalid character in request (outside of very strict set)",
     "920272" : "Invalid character in request (outside of printable chars below ascii 127)",
     "920271" : "Invalid character in request (non printable characters)",
     "920270" : "Invalid character in request (null character)",
     "933131" : "PHP Injection Attack = Variables Found",
     "933130" : "PHP Injection Attack = Variables Found",
     "942390" : "SQL Injection Attack",
     "921180" : "HTTP Parameter Pollution (%@{TX.1})",
     "920260" : "Unicode Full/Half Width Abuse Attack Attempt",
     "933120" : "PHP Injection Attack = Configuration Directive Found",
     "921170" : "HTTP Parameter Pollution",
     "920250" : "UTF8 Encoding Abuse Attack Attempt",
     "933111" : "PHP Injection Attack = PHP Script File Upload Found",
     "942300" : "Detects MySQL comments, conditions and ch(a)r injections",
     "921160" : "HTTP Header Injection Attack via payload (CR/LF and header-name detected)",
     "920240" : "URL Encoding Abuse Attack Attempt",
     "942290" : "Finds basic MongoDB SQL injection attempts",
     "921151" : "HTTP Header Injection Attack via payload (CR/LF detected)",
     "921150" : "HTTP Header Injection Attack via payload (CR/LF detected)",
     "920230" : "Multiple URL Encoding Detected",
     "932171" : "Remote Command Execution = Shellshock (CVE-2014-6271)",
     "932170" : "Remote Command Execution = Shellshock (CVE-2014-6271)",
     "921140" : "HTTP Header Injection Attack via headers",
     "920220" : "URL Encoding Abuse Attack Attempt",
     "942270" : "Looking for basic sql injection. Common attack string for mysql oracle and others.",
     "941350" : "UTF-7 Encoding IE XSS - Attack Detected.",
     "921130" : "HTTP Response Splitting Attack",
     "920210" : "Multiple/Conflicting Connection Header Data Found.",
     "942260" : "Detects basic SQL authentication bypass attempts 2/3",
     "941340" : "IE XSS Filters - Attack Detected.",
     "920202" : "Range = Too many fields for pdf request (6 or more)",
     "920201" : "Range = Too many fields for pdf request (35 or more)",
     "920200" : "Range = Too many fields (6 or more)",
     "942251" : "Detects HAVING injections",
     "941330" : "IE XSS Filters - Attack Detected.",
     "921110" : "HTTP Request Smuggling Attack",
     "920190" : "Range = Invalid Last Byte Value.",
     "973345" : "IE XSS Filters - Attack Detected.",
     "932130" : "Remote Command Execution = Unix Shell Expression Found",
     "921100" : "HTTP Request Smuggling Attack.",
     "920180" : "POST request missing Content-Length Header.",
     "942230" : "Detects conditional SQL injection attempts",
     "941310" : "US-ASCII Malformed Encoding XSS Filter - Attack Detected.",
     "920170" : "GET or HEAD Request with Body Content.",
     "990012" : "Rogue web site crawler",
     "941300" : "XSS using 'object' tag",
     "920160" : "Content-Length HTTP header is not numeric.",
     "990002" : "Request Indicates a Security Scanner Scanned the Site",
     "941290" : "XSS using 'applet' tag",
     "911100" : "Method is not allowed by policy",
     "943120" : "Possible Session Fixation Attack = SessionID Parameter Name with No Referrer",
     "942200" : "Detects MySQL comment-/space-obfuscated injections and backtick termination",
     "941280" : "XSS using 'base' tag",
     "920370" : "Argument value too long",
     "920140" : "Multipart request body failed strict validation",
     "990902" : "Request Indicates a Security Scanner Scanned the Site",
     "990901" : "Request Indicates a Security Scanner Scanned the Site",
     "943110" : "Possible Session Fixation Attack = SessionID Parameter Name with Off-Domain Referrer",
     "942190" : "Detects MSSQL code execution and information gathering attempts",
     "941270" : "XSS using 'link' href",
     "920130" : "Failed to parse request body.",
     "932160" : "Remote Command Execution = Unix Shell Code Found",
     "933150" : "PHP Injection Attack = High-Risk PHP Function Name Found",
     "943100" : "Possible Session Fixation Attack = Setting Cookie Values in HTML",
     "941260" : "XSS using 'meta' tag",
     "942330" : "Detects classic SQL injection probings 1/2",
     "942170" : "Detects SQL benchmark and sleep injection attempts including conditional queries",
     "942160" : "Detects blind sqli tests using sleep() or benchmark().",
     "941240" : "XSS using 'import' or 'implementation' attribute",
     "931130" : "Possible Remote File Inclusion (RFI) Attack = Off-Domain Reference/Link",
     "920100" : "Invalid HTTP Request Line",
     "960915" : "Multipart parser detected a possible unmatched boundary.",
     "942150" : "SQL Injection Attack",
     "941230" : "XSS using 'embed' tag",
     "931120" : "Possible Remote File Inclusion (RFI) Attack = URL Payload Used w/Trailing Question Mark Character (?)",
     "942180" : "Detects basic SQL authentication bypass attempts 1/3",
     "942140" : "SQL Injection Attack = Common DB Names Detected",
     "941220" : "XSS using obfuscated VB Script",
     "931110" : "Possible Remote File Inclusion (RFI) Attack = Common RFI Vulnerable Parameter Name used w/URL Payload",
     "958009" : "Cross-site Scripting (XSS) Attack",
     "942130" : "SQL Injection Attack: SQL Tautology Detected.",
     "941210" : "XSS using obfuscated Javascript",
     "931100" : "Possible Remote File Inclusion (RFI) Attack = URL Parameter using IP Address",
     "941200" : "XSS using VML frames",
     "942110" : "SQL Injection Attack: Common Injection Testing Detected",
     "941190" : "XSS using style sheets",
     "973348" : "IE XSS Filters - Attack Detected.",
     "942100" : "SQL Injection Attack Detected via libinjection",
     "941180" : "Node-Validator Blacklist Keywords",
     "920360" : "Argument name too long",
     "973338" : "XSS Filter - Category 3 = Javascript URI Vector",
     "973336" : "XSS Filter - Category 1 = Script Tag Vector",
     "973331" : "IE XSS Filters - Attack Detected.",
     "973330" : "IE XSS Filters - Attack Detected.",
     "973329" : "IE XSS Filters - Attack Detected.",
     "973328" : "IE XSS Filters - Attack Detected.",
     "973327" : "IE XSS Filters - Attack Detected.",
     "973326" : "IE XSS Filters - Attack Detected.",
     "973324" : "IE XSS Filters - Attack Detected.",
     "930130" : "Restricted File Access Attempt",
     "973321" : "IE XSS Filters - Attack Detected.",
     "973320" : "IE XSS Filters - Attack Detected.",
     "942320" : "Detects MySQL and PostgreSQL stored procedure/function injections",
     "973317" : "IE XSS Filters - Attack Detected.",
     "941150" : "XSS Filter - Category 5 = Disallowed HTML Attributes",
     "973314" : "XSS Attack Detected",
     "930120" : "OS File Access Attempt",
     "941160" : "NoScript XSS InjectionChecker: HTML Injection",
     "973311" : "XSS Attack Detected",
     "973309" : "XSS Attack Detected",
     "973308" : "XSS Attack Detected",
     "973307" : "XSS Attack Detected",
     "973306" : "XSS Attack Detected",
     "941140" : "XSS Filter - Category 4 = Javascript URI Vector",
     "973304" : "XSS Attack Detected",
     "930110" : "Path Traversal Attack (/../)",
     "973302" : "XSS Attack Detected",
     "973301" : "XSS Attack Detected",
     "973300" : "Possible XSS Attack Detected - HTML Tag Handler",
     "942240" : "Detects MySQL charset switch and MSSQL DoS attempts",
     "941130" : "XSS Filter - Category 3 = Attribute Vector",
     "930100" : "Path Traversal Attack (/../)",
     "941110" : "XSS Filter - Category 1 = Script Tag Vector",
     "973323" : "IE XSS Filters - Attack Detected.",
     "941100" : "XSS Attack Detected via libinjection",
     "932140" : "Remote Command Execution = Windows FOR/IF Command Found"

  });

søndag den 6. januar 2019

Installing PowerShell Modules to Azure Function App

I just realized a really easy way to install PS modules to an Azure Function App. Just run below code in a function.
Make sure that $ModulePath points to a folder path that exists. Look into KUDU to set this up.



<#
$ModulePath must not already contain the modules or this may fail
#>

$ModulePath = 'D:\home\lib\PSModules'

$NuGet = Get-PackageProvider -Name NuGet
if($null -eq $NuGet)
{
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser
}

"Saving modules to $ModulePath"
Save-Module AzureRm -Path $ModulePath -Force

"Listing import commands for every module"
Get-ChildItem -Path $ModulePath -Include "*.psd1" -Recurse | ForEach-Object {
    "Import-Module '$($_.FullName)'"
}

I have created a Github repository containing this and other small function app snippets. Above code will be updated here: https://github.com/spaelling/AzureFunctionAppSnippets/blob/master/PowerShell/HT_InstallModule.ps1

onsdag den 29. august 2018

PowerShell in Azure Functions - Lessons Learned

I love writing PowerShell in Azure Functions - it is a mixed blessing not having to worry about a VM (or VMs), but I hope to share a few tips that will result in fewer hairs being torn.

Forget Write-* except Write-Output. Write-Error will work just like throw (which I would then much prefer to use).
It can be inconvenient that Write-Verbose in (ordinary PowerShell) functions is lost, and you cannot use Write-Output in a function as that would go towards the output of the function, and not the log stream. But there is a trick if you really need that verbose output, you can redirect it (read more on that here). Try running below.


# this will do nothing
Write-Verbose "Verbose"
# redirect verbose stream
Write-Verbose "Verbose redirected to success stream" -Verbose 4>&1
# verbose output in function
function function_with_verbose {
    [CmdletBinding()]
    param (
        
    )
    Write-Verbose "this is verbose"
    Write-Verbose "more verbose"
    # output result
    4
}
# redirect verbose stream
$result = function_with_verbose -Verbose 4>&1
# assuming just a single out
Write-Output "output from function"
($result | Select-Object -Last 1)
Write-Output "The verbose stream"
# everything but the last
$result[0..($result.length-2)]

All depending this may or may not be worth the trouble. I think that at some point the other streams will be displayed in the logging output.

There is some documentation on importing modules in a Function App, but what I found the best was to first use Save-Module to download to disk, then in Platform features in the app there is something called Advanced tools (Kudu). Click that and a new tab opens. In the top click Debug Console and select either.
I usually create a new folder (the big + sign) in root, lib, and in that another folder modules. Here you can drag and drop the module folder you just downloaded.
You can zip the module folders before uploading if you like, they are unzipped automatically. Note down the full path to the psd1 file that you will import. When importing in the function app simply

Import-Module "D:\home\lib\modules\AzureRM.profile\5.3.4\AzureRM.Profile.psd1"

I I have often seen a -global appended to this command. Not sure why, I have had no luck getting global variables to work. This leads to my next point, when using any Azure PowerShell modules you need to authenticate using ex. Login-AzureRmAccount. Problem with this is that if you have multiple functions running at the same time they will leak into each other, especially something like Select-AzureRmSubscription will mess you up!

Luckily there is a solution for this (same for Login-AzureRmAccount).

$DefaultProfile = Select-AzureRmSubscription -SubscriptionId $SubscriptionId -Tenant $TenantId -Scope Process

The $DefaultProfile is then used in all subsequent calls ex.

Get-AzureRmResource -ResourceType 'Microsoft.DevTestLab/labs/virtualMachines' -ResourceGroupName $ResourceGroupName -ExpandProperties -DefaultProfile $DefaultProfile

Now in case a different instance of the same function runs at the same time, it will not interfere. As it is tedious to add this everywhere you can use $PSDefaultParameterValues and also removes the risk you forgot this somewhere.

$PSDefaultParameterValues = @{'*:DefaultProfile' = $DefaultProfile}

I use a Managed Service Identity to login to Azure. Under Platform features there is an item "Managed service identity" - click it and select On.

To run below you need the MSI application Id. You find it in Azure Active Directory under App Registrations (select All apps) and search for your function app name. Copy the application Id. I have added it to Application Settings, and then accessible from $env:


$apiVersion = "2017-09-01"
$resourceURI = "https://management.azure.com/"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
$DefaultProfile = Login-AzureRmAccount -Tenant $TenantId -AccountId $env:ApplicationId -AccessToken $accessToken -Scope Process

Note that this may fail if the MSI has access to no resources in any subscription. Anyways, it would be rather pointless if it does not.

Migrate Azure function app from consumption plan

If you have your function app running on a consumption plan and ever considered to move it to a different plan then this may have stopped you

Being greyed out does not mean it is not possible, only that there is not yet any portal support for it. But luckily this is possible using PowerShell. The easiest way to achieve this is likely to start the Azure Cloud Shell (top bar in the portal)

The shell will pop up in the bottom of the browser. Where it reads Bash, click and select PowerShell.
First we need to select the relevant subscription (you are already logged in). If you only have a single subscription in your tenant, skip this step.
The subscription id is what follows /subscriptions/ in the browsers url, ex.

https://portal.azure.com/#@mytenant.onmicrosoft.com/resource/subscriptions/9734191f-63d9-4b3d-880e-8de9a40942f2
/resourceGroups/rgfuncapp/providers/Microsoft.Web/sites/funcapp/appServices

Copy this value and enter (paste using mouse right click)

Select-AzureRmSubscription -SubscriptionId your_subscription_id

Before continuing make sure you have a new plan (the one you wish to move to) in the same resource group and in the same region.

To move you need just a single command

Set-AzureRmWebApp -Name "[function name]" -ResourceGroupName "[resource group]" -AppServicePlan "[new app service plan name]"

You need to reload the browser before you see the change in the portal.

Please refer to https://github.com/Azure/Azure-Functions/issues/155 for further details. As you may notice this blogpost is simply elaborating on a comment made on the issue, but it took me a while to find that, so hopefully this helps someone while we wait for portal support.



fredag den 24. august 2018

PowerShell: MS Graph API authentication with Service Principal

I had to access the MS Graph API from Azure Function App and after wasting some time trying to get it to work with a Managed Service Identity (you can get a token, but cannot assign the MSI any roles, yet), I opted for the good ol' Service Principal (SP).

There are several blogpost on how to get a token for various Microsoft APIs, and most of the code is very similar, but they are all lacking one essential detail, without it you may get varying results.

I experienced getting a token that the API claimed was invalid, one that was expired, not getting a token because the SP secret was incorrect (it was not), and maybe just that no overload of the function could be found. Clearly I was doing something wrong.

For good measure, here is a short guide on getting a working SP. Go to Azure AD,-> App Registrations, and create a new app registration.
The application type is web app/API, you can put anything in the sign-on URL.


This will also create an enterprise application.

In the settings of the newly registered app click Settings, and under API Access click Required permissions. Add a new permission and select the Microsoft Graph API, and check off the permissions needed.



When done you need to click Grant Permissions.

Next click Keys. Fill in a description, select when the key should expire and click Save. The key will be generated and shown. Save this for later.

Back in your app registration copy the Application ID.

Next we need a dll file, Microsoft.IdentityModel.Clients.ActiveDirectory.dll - this is the main contribution of this blog. It just happens that there is many different versions of this, and you need the right one to get a working token.
I found that the one in AzureRm.Profile 5.3.4 works just fine. I would guess versions close to this one is the same. You can get it using Save-Module:

Save-Module AzureRm.Profile -RequiredVersion 5.3.4 -Path C:\Temp

Now find Microsoft.IdentityModel.Clients.ActiveDirectory.dll and use Add-Type to load it

Add-Type -Path "C:\Temp\AzureRM.profile\5.3.4\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"

Next we need an important piece of information. Run this in a fresh PS-session:

[appdomain]::currentdomain.getassemblies() | Where-Object {$_.fullname -like "Microsoft.IdentityModel.Clients.ActiveDirectory*"} | Select-Object -Property Fullname

The result is what we need in the following function. We can use it to specify that it is that exact dll-file we are referring to. There could be many of these loaded, and if the wrong one is used we get a non-desirable result.

Function Get-AADToken {
    [CmdletBinding()]
    [OutputType([string])]
    Param (
        [String]$TenantID,
        [string]$ServicePrincipalId,
        [securestring]$ServicePrincipalPwd,
        $resourceAppIdURI = 'https://graph.microsoft.com/'
    )
    Try {
        # Set Authority to Azure AD Tenant
        $authority = 'https://login.windows.net/' + $TenantId

        $ClientCred = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential, Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.28.3.860, Culture=neutral, PublicKeyToken=31bf3856ad364e35]::new($ServicePrincipalId, $ServicePrincipalPwd)
        $authContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext, Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.28.3.860, Culture=neutral, PublicKeyToken=31bf3856ad364e35]::new($authority)
        $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI, $ClientCred)

        if($authResult.Exception)
        {
            throw $authResult.Exception.InnerException.Message
        }
        
        $Token = $authResult.Result.AccessToken
    }
    Catch {
        Throw $_
    }
    $Token
}

The function is called as follows

# load this specific Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Add-Type -Path "C:\temp\AzureRM.profile\5.3.4\Microsoft.IdentityModel.Clients.ActiveDirectory.dll"

# your Azure AD tenant
$TenantId = '55da3b96-2993-4ef3-ad6f-f0a066401f60'

# the application id from the app registration
$AppId = '135fee95-c7c3-48f5-9821-fcaf29fd8a3c'

# the key we created - obviously do not store this in cleartext
$ServicePrincipalPwd = '5a1mXQYcZNZADD8h2lSYxzSGHSF0U+chrpk0L5E0Cgw=' | ConvertTo-SecureString -AsPlainText -Force
# get the token
$Token = Get-AADToken -TenantID $TenantId -ServicePrincipalId $AppId -ServicePrincipalPwd $ServicePrincipalPwd

Now that we have a token, it is time to put it to work

$Headers = @{
    "Authorization" = "Bearer $token"
}

try {
    $Response = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/users/' -Method Get -UseBasicParsing -Headers $Headers
}
catch {
    $_
    $_.Exception.ErrorDetails.Message
}

lørdag den 28. juli 2018

Azure Function App - Frontend and Backend

There are (many) different ways Function Apps can call other function apps. The perhaps most obvious (classic) way is making a web-request, from one function-endpoint to another. I have my "frontends" in Function App functions protected with "App Service Authentication" - one must login with Azure AD to authenticate one self (use the "express" settings to configure this to get it quickly setup).

Once configured add your users to the Managed application in the "Users and groups" tab.


These users will be allowed access to all the functions in your Function App. That seems pretty secure! You can even add Conditional Access to the application for added security.

Only problem is that if you want to make requests to other functions in the same Function App, then you would also have to authenticate, from the function, and I have so far given up to get this to work.

So I had to cook up some alternative. What I found was having 2 Function App instances, one is the frontend, and authentication is done using AAD as mentioned before, the backend is not protected by AAD authentication, but you do need a function key to access a given function (ie. no anonymous calls to this endpoint), and we can encrypt the response (also with a key), and both keys will be stored in Azure Key Vault.

Create 2 function apps and a key vault. In the key vault create a secret called encryptionKey, the value should be 32 characters long (256 bits), and the other is named to match the function and the value being the functions key (found in the Manage tab of a function, named default).


Next step is to enable Managed service identity on both Function Apps. You can do this under platform features, same place as you find Application settings. Now you need to note down the application id of both function apps, you can find that in the Azure portal under Azure Active Directory->Enterprise Applications. They will be named the same as your function apps.
Add these values to their respective application settings under the name ApplicationId.

In both Function Apps create a PowerShell Http trigger function.

Code for the frontend

# get a token for the key vault
$apiVersion = "2017-09-01"
$resourceURI = "https://vault.azure.net"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token

# remember to set these
if(-not $accessToken) {throw "unable to fetch access token"}
if(-not $env:ApplicationId) {throw "application id not set in environmental settings"}

# get the function key first
# get the base url from the "overview" tab in the key vault
$secretName = 'somebackend'
$uri = "https://cbfuncappkv.vault.azure.net/secrets/{0}?api-version=2016-10-01" -f $secretName
$Headers = @{Authorization ="Bearer $accessToken"}
$KeyvaultResponse = (Invoke-WebRequest -UseBasicParsing -Uri $uri -Method Get -Headers $Headers).Content | ConvertFrom-Json
# get the value of the secret
$FunctionKey = $KeyvaultResponse | Select-Object -ExpandProperty value

# now ready to make a request to the backend
$uri = "https://cbfuncappbackend.azurewebsites.net/api/somebackend?code={0}" -f $FunctionKey
$Headers = @{'content-type' = "application/x-www-form-urlencoded"}
# oh oh, the response we got back is encrypted!
$EncryptedOutput = (Invoke-WebRequest -UseBasicParsing -Uri $uri -Method Get -Headers $Headers).Content | ConvertFrom-Json

# retrieve the encryption key from key vault
$secretName = 'encryptionKey'
# get the base url from the "overview" tab in the key vault
$uri = "https://cbfuncappkv.vault.azure.net/secrets/{0}?api-version=2016-10-01" -f $secretName
$Headers = @{Authorization ="Bearer $accessToken"}
$KeyvaultResponse = (Invoke-WebRequest -UseBasicParsing -Uri $uri -Method Get -Headers $Headers).Content | ConvertFrom-Json
# get the value of the secret
$encryptionKey = $KeyvaultResponse | Select-Object -ExpandProperty value

$Key = ([system.Text.Encoding]::UTF8).GetBytes($encryptionKey)
# decrypt the secure string
$DecryptedSecureString = $EncryptedOutput | ConvertTo-SecureString -Key $Key
# copies the content of the secure string into unmanaged memory
$ptr = [System.Runtime.InteropServices.marshal]::SecureStringToBSTR($DecryptedSecureString)
# convert to a string
$DecryptedOutput = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($ptr)

# html part - 
$html = @"
<head><style>$style</style></head>
<title>Hello PS Backend</title>
<h1>Hello PS Backend</h1>
<h5>Time is $(Get-Date)</h2>
$DecryptedOutput
"@

# output as a webpage
@{
    headers = @{ "content-type" = "text/html"}
    body    = $html
} | ConvertTo-Json | Out-File -Encoding Ascii -FilePath $res

And for the backend


# get a token for the key vault
$apiVersion = "2017-09-01"
$resourceURI = "https://vault.azure.net"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token

# remember to set these
if(-not $accessToken) {throw "unable to fetch access token"}
if(-not $env:ApplicationId) {throw "application id not set in environmental settings"}

# retrieve the encryption key from key vault
$secretName = 'encryptionKey'
# get the base url from the "overview" tab in the key vault
$uri = "https://cbfuncappkv.vault.azure.net/secrets/{0}?api-version=2016-10-01" -f $secretName
$Headers = @{Authorization ="Bearer $accessToken"}
$KeyvaultResponse = (Invoke-WebRequest -UseBasicParsing -Uri $uri -Method Get -Headers $Headers).Content | ConvertFrom-Json
# get the value of the secret
$encryptionKey = $KeyvaultResponse | Select-Object -ExpandProperty value

# secure and encrypt the below output
$Output = "Hello from the backend" 
# convert our encryption key to byte array, if string is 32 characters, we get 8*32=256 bit encryption
$Key = ([system.Text.Encoding]::UTF8).GetBytes($encryptionKey)
# convert to secure string, then to en encrypted string (the string must be secure before it can be encrypted)
$EncryptedOutput = $Output | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString -key $key

# write encrypted output
Out-File -Encoding Ascii -FilePath $res -inputObject $EncryptedOutput

Lastly we need to grant access to the secrets in the key vault, Get operation on secrets is sufficient.



Optionally enable AAD authentication on the frontend Function App before running the example, and in that case remember to add your own user!

For added security you could add a timed trigger function that resets the keys in the key vault at regular intervals. To make sure matching encryption keys are used (in both ends), you could provide the version of the encryption key as part of the response.
I also think that you can use service endpoints on the key vault so that only these functions are able to retrieve the key in the first place.

The result should look like this


Søg i denne blog