Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Java Instrumentation at Contrast Security

Johnathan Gilday
September 12, 2018

Java Instrumentation at Contrast Security

The powerful Java Instrumentation API, and how Contrast Security uses it to detect vulnerable code paths, in two acts

Try Contrast Community Edition for free! https://www.contrastsecurity.com/contrast-community-edition

Johnathan Gilday

September 12, 2018
Tweet

More Decks by Johnathan Gilday

Other Decks in Technology

Transcript

  1. JAVA INSTRUMENTATION AT CONTRAST SECURITY
    How Contrast Security uses the powerful and obscure
    Java Instrumentation API to detect vulnerable code paths,
    in two acts
    [email protected] 2018-09-09

    View full-size slide

  2. 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
    [email protected] 2018-09-09

    View full-size slide

  3. JAVA INSTRUMENTATION AT CONTRAST SECURITY
    How Contrast Security uses the powerful and obscure
    Java Instrumentation API to detect vulnerable code paths,
    in two acts
    [email protected] 2018-09-09

    View full-size slide

  4. ACT ONE: JAVA AGENTS
    [email protected] 2018-09-09

    View full-size slide

  5. 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?
    [email protected] 2018-09-09

    View full-size slide

  6. WE ARE TALKING ABOUT
    JAVA.LANG.INSTRUMENT
    [email protected] 2018-09-09

    View full-size slide

  7. 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
    [email protected] 2018-09-09

    View full-size slide

  8. HAVE YOU INSTRUMENTED YOUR CODE WITH
    A JAVA AGENT?
    >

    APM: App Dynamics, New Relic
    >

    Code Coverage: JaCoCo
    >
    #
    Security: Contrast
    [email protected] 2018-09-09

    View full-size slide

  9. 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
    [email protected] 2018-09-09

    View full-size slide

  10. 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
    [email protected] 2018-09-09

    View full-size slide

  11. 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
    [email protected] 2018-09-09

    View full-size slide

  12. HOW TO BUILD AN AGENT
    ☐ register bytecode transform function
    ☐ Transform JVM Bytecode
    [email protected] 2018-09-09

    View full-size slide

  13. 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);
    [email protected] 2018-09-09

    View full-size slide

  14. 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
    [email protected] 2018-09-09

    View full-size slide

  15. USE Instrumentation TO REGISTER A
    ClassFileTransformer
    [email protected] 2018-09-09

    View full-size slide

  16. 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");
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  17. 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");
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  18. 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");
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  19. HALF-WAY DONE?
    ☑ register bytecode transform
    function
    ☐ Transform JVM Bytecode
    [email protected] 2018-09-09

    View full-size slide

  20. 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 ?
    [email protected] 2018-09-09

    View full-size slide

  21. 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"
    [email protected] 2018-09-09

    View full-size slide

  22. 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
    }
    [email protected] 2018-09-09

    View full-size slide

  23. 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
    }
    [email protected] 2018-09-09

    View full-size slide

  24. 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
    [email protected] 2018-09-09

    View full-size slide

  25. 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
    [email protected] 2018-09-09

    View full-size slide

  26. 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);
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  27. 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);
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  28. 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();
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  29. HOW TO BUILD AN AGENT
    ☑ register bytecode transform function
    ☑ Transform JVM Bytecode
    [email protected] 2018-09-09

    View full-size slide

  30. 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!
    [email protected] 2018-09-09

    View full-size slide

  31. 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 + "!");
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  32. ACT TWO: DATA FLOW ANALYSIS VIA INSTRUMENTATION
    [email protected] 2018-09-09

    View full-size slide

  33. 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
    [email protected] 2018-09-09

    View full-size slide

  34. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  35. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  36. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  37. 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)
    [email protected] 2018-09-09

    View full-size slide

  38. HOW DOES CONTRAST DISCOVER THE DATA FLOW
    USING INSTRUMENTATION?
    Instruments four categories of methods
    1. Sources
    2. Propagators
    3. Sinks
    4. Sanitizers
    [email protected] 2018-09-09

    View full-size slide

  39. HOW DOES CONTRAST DISCOVER THE DATA FLOW
    USING INSTRUMENTATION?
    Instruments four categories of methods
    1. Sources
    2. Propagators
    3. Sinks
    4. Sanitizers
    [email protected] 2018-09-09

    View full-size slide

  40. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  41. HOW DOES CONTRAST DISCOVER THE DATA FLOW
    USING INSTRUMENTATION?
    Instruments four categories of methods
    1. Sources
    2. Propagators
    3. Sinks
    4. Sanitizers
    [email protected] 2018-09-09

    View full-size slide

  42. 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?
    [email protected] 2018-09-09

    View full-size slide

  43. 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();
    [email protected] 2018-09-09

    View full-size slide

  44. StringBuilder.append(String) AND StringBuilder.toString()
    ARE THE PROPAGATORS
    [email protected] 2018-09-09

    View full-size slide

  45. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  46. PROPAGATORS STORE THE UNTRUSTED SUBSET
    final String greeting = "Hello," + audience + "";
    Hello,mars!
    ^--^ untrusted data
    ^--------^ ^----^ safe constants
    [email protected] 2018-09-09

    View full-size slide

  47. HOW DOES CONTRAST DISCOVER THE DATA FLOW
    USING INSTRUMENTATION?
    Instruments four categories of methods
    1. Sources
    2. Propagators
    3. Sinks
    4. Sanitizers
    [email protected] 2018-09-09

    View full-size slide

  48. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  49. !
    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
    [email protected] 2018-09-09

    View full-size slide

  50. HOW DOES CONTRAST DISCOVER THE DATA FLOW
    USING INSTRUMENTATION?
    Instruments four categories of methods
    1. Sources
    2. Propagators
    3. Sinks
    4. Sanitizers
    [email protected] 2018-09-09

    View full-size slide

  51. THERE ARE NO SANITIZERS IN THE
    VulnerableHelloWorldServlet
    (THAT'S WHY IT'S VULNERABLE)
    [email protected] 2018-09-09

    View full-size slide

  52. 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);
    }
    }
    }
    [email protected] 2018-09-09

    View full-size slide

  53. VULNERABILITY PATCHED
    [email protected] 2018-09-09

    View full-size slide

  54. WHERE ARE YOUR VULNERABILITIES?
    Contrast Community Edition is free for Java developers
    Try it out then tell us how you feel!
    [email protected] 2018-09-09

    View full-size slide

  55. 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
    [email protected] 2018-09-09

    View full-size slide