Slide 1

Slide 1 text

JAVA INSTRUMENTATION AT CONTRAST SECURITY How Contrast Security uses the powerful and obscure Java Instrumentation API to detect vulnerable code paths, in two acts johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 2

Slide 2 text

ABOUT ME Johnathan Gilday Java agent developer at Contrast Security. Traditionally, full-stack web application and data services developer github.com/gilday @jdgilday johnathangilday.com johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 3

Slide 3 text

JAVA INSTRUMENTATION AT CONTRAST SECURITY How Contrast Security uses the powerful and obscure Java Instrumentation API to detect vulnerable code paths, in two acts johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 4

Slide 4 text

ACT ONE: JAVA AGENTS johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 5

Slide 5 text

DID YOU KNOW THE JDK SHIPS WITH AN INSTRUMENTATION PACKAGE THAT CONTAINS JUST 5 CLASSES WHICH ALLOWS DEVELOPERS TO TRANSFORM JVM BYTE CODE AT RUNTIME? johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 6

Slide 6 text

WE ARE TALKING ABOUT JAVA.LANG.INSTRUMENT johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 7

Slide 7 text

! johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 8

Slide 8 text

JAVA.LANG.INSTRUMENT Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods. — package docs johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 9

Slide 9 text

HAVE YOU INSTRUMENTED YOUR CODE WITH A JAVA AGENT? > ⚡ APM: App Dynamics, New Relic > ✅ Code Coverage: JaCoCo > # Security: Contrast johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 10

Slide 10 text

HOW TO USE AN AGENT? $ java -javaagent:contrast.jar -jar webgoat-container-7.0.1-war-exec.jar [Contrast] Sun Sep 09 20:24:43 EDT 2018 Starting Contrast (build 3.5.6.582) Pat. 8,458,789 B2 [Contrast] Sun Sep 09 20:24:43 EDT 2018 Logging messages to .contrast/contrast.log [Contrast] Sun Sep 09 20:24:43 EDT 2018 Logging security messages to .contrast/security.log [Contrast] Sun Sep 09 20:24:44 EDT 2018 Loading pre-packaged configuration [Contrast] Sun Sep 09 20:24:44 EDT 2018 Using instructions from TeamServer (Assess=on, Protect=off) [Contrast] Sun Sep 09 20:24:54 EDT 2018 Starting JVM [10711ms] Sep 09, 2018 8:24:55 PM org.apache.coyote.http11.Http11Protocol init INFO: Initializing ProtocolHandler ["http-bio-8080"] Sep 09, 2018 8:24:55 PM org.apache.catalina.core.StandardService startInternal INFO: Starting service Tomcat Typically use the -javaagent JVM flag to static load the agent johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 11

Slide 11 text

SHOW ME THE CODE! Emoji Agent Replaces boring string literals with something more exciting $ java HelloWorld hello, world! $ java \ -javaagent:emoji-agent.jar \ -cp asm-all-5.2.jar:. \ HelloWorld ! hello, world! ignore this asm-all-5.2.jar for now , we'll get there johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 12

Slide 12 text

WHERE CAN I FIND A SAMPLE JAVA PROGRAM? public final class HelloWorld { public static void main(final String[] args) { System.out.println("hello, world!"); } } javac HelloWorld.java johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 13

Slide 13 text

READY? johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 14

Slide 14 text

HOW TO BUILD AN AGENT ☐ register bytecode transform function ☐ Transform JVM Bytecode johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 15

Slide 15 text

ENTRYPOINT The manifest of the agent JAR file must contain the attribute Premain-Class. The value of this attribute is the name of the agent class. The agent class must implement a public static premain method similar in principle to the main application entry point. — java.lang.instrument JavaDoc public static void premain(String agentArgs, Instrumentation inst); johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 16

Slide 16 text

EMOJIAGENT.JAVA package com.johnathangilday; public final class EmojiAgent { public static void premain(String args, Instrumentation instrumentation) { } } MANIFEST.MF Premain-Class: com.johnathangilday.EmojiAgent Can-Redefine-Classes: true johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 17

Slide 17 text

USE Instrumentation TO REGISTER A ClassFileTransformer johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 18

Slide 18 text

public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new EmojiClassFileTransformer()); } static final class EmojiClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform( final ClassLoader loader, final String className, final Class> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { throw new RuntimeException("not yet implemented"); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 19

Slide 19 text

public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new EmojiClassFileTransformer()); } static final class EmojiClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform( final ClassLoader loader, final String className, final Class> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { throw new RuntimeException("not yet implemented"); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 20

Slide 20 text

public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new EmojiClassFileTransformer()); } static final class EmojiClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform( final ClassLoader loader, final String className, final Class> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { throw new RuntimeException("not yet implemented"); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 21

Slide 21 text

HALF-WAY DONE? ☑ register bytecode transform function ☐ Transform JVM Bytecode johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 22

Slide 22 text

WHAT DOES HELLOWORLD BYTECODE LOOK LIKE? HelloWorld.java public final class HelloWorld { public static void main(final String[] args) { System.out.println("hello, world!"); } } HelloWorld.class ? johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 23

Slide 23 text

HELLO, WORLD! IN BYTECODE $ javap -verbose -c HelloWorld.class Classfile /Users/johnathangilday/OneDrive - Contrast Security/Documents/instrumentation-talk/emoji-agent/HelloWorld.class Last modified Sep 9, 2018; size 427 bytes MD5 checksum 88c3d00dc24442b1e38d3ee7ec52b31b Compiled from "HelloWorld.java" public final class HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // hello, world! #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // HelloWorld #6 = Class #22 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 HelloWorld.java #15 = NameAndType #7:#8 // "":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello, world! #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 HelloWorld #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { public HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello, world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 } SourceFile: "HelloWorld.java" johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 24

Slide 24 text

HELLO, WORLD! IN LESS BYTECODE $ javap -c HelloWorld.class Compiled from "HelloWorld.java" public final class HelloWorld { public HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello, world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 25

Slide 25 text

HELLO, WORLD! IN LESS BYTECODE $ javap -c HelloWorld.class Compiled from "HelloWorld.java" public final class HelloWorld { public HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello, world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 26

Slide 26 text

BYTECODE MANIPULATION 101 > ASM is the defacto Java library for reading and writing bytecode > ASM uses a low-level, visitor pattern based reading and writing API > Higher-level libraries built on ASM are typically desirable johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 27

Slide 27 text

EMOJIAGENT ASM 1. ClassReader for reading bytecode from byte[] 2. ClassWriter a visitor that writes instructions to byte[] buffer 3. EmojiClassVisitor a class visitor that composes the EmojiMethodVisitor 4. EmojiMethodVisitor a method visitor that looks for String constants to replace johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 28

Slide 28 text

ASM MethodVisitor private static final class EmojiMethodVisitor extends MethodVisitor { EmojiMethodVisitor(final MethodVisitor methodVisitor) { super(ASM5, methodVisitor); } @Override public void visitLdcInsn(final Object value) { final Object newValue = "hello, world!".equals(value) ? " ! hello, world!" : value; super.visitLdcInsn(newValue); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 29

Slide 29 text

ASM ClassVisitor private static final class EmojiClassVisitor extends ClassVisitor { EmojiClassVisitor(final ClassVisitor classVisitor) { super(ASM5, classVisitor); } @Override public MethodVisitor visitMethod( final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { final MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); return new EmojiMethodVisitor(mv); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 30

Slide 30 text

ADD ASM-FU TO THE EmojiClassFileTransformer static final class EmojiClassFileTransformer implements ClassFileTransformer { public byte[] transform( final ClassLoader loader, final String className, final Class> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { final ClassReader reader; try { reader = new ClassReader(new ByteArrayInputStream(classfileBuffer)); } catch (IOException e) { throw new IllegalArgumentException("failed to read class " + className, e); } final int flags = 0; final ClassWriter writer = new ClassWriter(flags); final ClassVisitor visitor = new EmojiClassVisitor(writer); reader.accept(visitor, flags); return writer.toByteArray(); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 31

Slide 31 text

HOW TO BUILD AN AGENT ☑ register bytecode transform function ☑ Transform JVM Bytecode johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 32

Slide 32 text

SHIP IT $ javac \ > -cp ~/.m2/repository/org/ow2/asm/asm-all/5.2/asm-all-5.2.jar \ > com/johnathangilday/EmojiAgent.java $ jar cfm emoji-agent.jar MANIFEST.MF com/johnathangilday/EmojiAgent*.class $ java \ > -javaagent:emoji-agent.jar \ > -cp ~/.m2/repository/org/ow2/asm/asm-all/5.2/asm-all-5.2.jar:. \ HelloWorld ! hello, world! johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 33

Slide 33 text

DOES THIS WORK? public final class HelloWorld { public static void main(String[] args) { final String audience = args.length == 1 ? args[0] : "world"; System.out.println("hello, " + audience + "!"); } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 34

Slide 34 text

ACT TWO: DATA FLOW ANALYSIS VIA INSTRUMENTATION johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 35

Slide 35 text

DATA FLOW ANALYSIS Many vulnerabilities, including XSS, SQL injection, command injection, LDAP injection, XML injection, and more happen when programmers send untrusted data to dangerous calls.1 — Jeff Williams, Contrast Co-Founder 1 https://www.contrastsecurity.com/security-influencers/why-appsec-tools-need-great-data-flow-analysis johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 36

Slide 36 text

WHERE IS THE DANGEROUS FLOW? @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 37

Slide 37 text

WHERE IS THE DANGEROUS FLOW? @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 38

Slide 38 text

WHERE IS THE DANGEROUS FLOW? @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 39

Slide 39 text

XSS VULNERABILITY Harmless $ curl http://localhost:8080/hello-world?audience=mars

Hello,mars

Pwned $ curl 'http://localhost:8080/hello-world?audience=alert(1)'

Hello,alert(1)

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 40

Slide 40 text

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 41

Slide 41 text

HOW DOES CONTRAST DISCOVER THE DATA FLOW USING INSTRUMENTATION? Instruments four categories of methods 1. Sources 2. Propagators 3. Sinks 4. Sanitizers johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 42

Slide 42 text

HOW DOES CONTRAST DISCOVER THE DATA FLOW USING INSTRUMENTATION? Instruments four categories of methods 1. Sources 2. Propagators 3. Sinks 4. Sanitizers johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 43

Slide 43 text

SOURCE ! HttpServletRequest.getParameter() is a source - the String it returns is untrusted data. Remember the object reference of that String @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 44

Slide 44 text

HOW DOES CONTRAST DISCOVER THE DATA FLOW USING INSTRUMENTATION? Instruments four categories of methods 1. Sources 2. Propagators 3. Sinks 4. Sanitizers johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 45

Slide 45 text

PROPAGATOR @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } Where is the propagator method? johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 46

Slide 46 text

RECALL, THE JAVA COMPILER IMPLEMENTS THE String CONCATENATION OPERATOR AS METHOD CALLS final String greeting = "

Hello," + audience + "

"; Becomes final String greeting = new StringBuilder() .append("

Hello,") .append(audience) .append("

") .toString(); johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 47

Slide 47 text

StringBuilder.append(String) AND StringBuilder.toString() ARE THE PROPAGATORS johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 48

Slide 48 text

PROPAGATOR ! The StringBuilder.append(String) method is a propagator - it propagates the untrusted data passed in as a method parameter to the StringBuilder. Remember that the String built by this StringBuilder contains untrusted data @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 49

Slide 49 text

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 50

Slide 50 text

PROPAGATORS STORE THE UNTRUSTED SUBSET final String greeting = "

Hello," + audience + "

";

Hello,mars!

^--^ untrusted data ^--------^ ^----^ safe constants johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 51

Slide 51 text

HOW DOES CONTRAST DISCOVER THE DATA FLOW USING INSTRUMENTATION? Instruments four categories of methods 1. Sources 2. Propagators 3. Sinks 4. Sanitizers johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 52

Slide 52 text

SINK ! The PrintWriter.println(String) method of an instance returned by HttpServletResponse.getWriter() is a sink - when untrusted data that has not been HTML encoded is passed to this PrintWriter, there is an XSS vulnerability @WebServlet("/hello-world") public final class VulnerableHelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String greeting = "

Hello," + audience + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 53

Slide 53 text

! XSS VULNERABILITY DETECTED Data from the HttpServletRequest source was propagated to a new String which was passed to the PrintWriter.println(String) sink without being sanitized johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 54

Slide 54 text

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 55

Slide 55 text

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 56

Slide 56 text

HOW DOES CONTRAST DISCOVER THE DATA FLOW USING INSTRUMENTATION? Instruments four categories of methods 1. Sources 2. Propagators 3. Sinks 4. Sanitizers johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 57

Slide 57 text

THERE ARE NO SANITIZERS IN THE VulnerableHelloWorldServlet (THAT'S WHY IT'S VULNERABLE) johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 58

Slide 58 text

johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 59

Slide 59 text

LET'S PATCH THE VulnerableHelloWorldServlet ! The Apache Commons StringEscapeUtils.escapeHTML method is a sanitizer - the String instance returned by this method is HTML encoded @WebServlet("/hello-world") public final class HelloWorldServlet extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { final String param = "audience"; final String audience = request.getParameter(param) == null ? "world" : request.getParameter(param); response.setContentType("text/html"); try (final PrintWriter out = response.getWriter()) { final String sanitized = StringEscapeUtils.escapeHTML(audience); final String greeting = "

Hello," + sanitized + "

"; out.println(greeting); } } } johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 60

Slide 60 text

VULNERABILITY PATCHED johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 61

Slide 61 text

WHERE ARE YOUR VULNERABILITIES? Contrast Community Edition is free for Java developers Try it out then tell us how you feel! johnathan.gilday@contrastsecurity.com 2018-09-09

Slide 62

Slide 62 text

IMAGE CREDITS > Marek Cyzio https://flic.kr/p/22MbuxK > Pål-Kristian Hamre https://flic.kr/p/mn3ScM > ricky montalvo https://flic.kr/p/6882RG > Porapak Apichodilok https://www.pexels.com/photo/adult-barista-beverage-cafe-373639/ > Raw Pixel https://www.pexels.com/photo/yeah-with-brown-wooden-frame-745407/ > Photo by Patrick Fore on Unsplash > Photo by Christian Wiediger on Unsplash > Photo by Magda Ehlers from Pexels > Photo by Jairo Alzate on Unsplash > Photo by Waranya Mooldee on Unsplash > Photo by Leio McLaren (@leiomclaren) on Unsplash johnathan.gilday@contrastsecurity.com 2018-09-09