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 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 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 Slide

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

    View 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 Slide

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

    View Slide

  7. !
    [email protected] 2018-09-09

    View Slide

  8. 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 Slide

  9. 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 Slide

  10. 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 Slide

  11. 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 Slide

  12. 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 Slide

  13. READY?
    [email protected] 2018-09-09

    View Slide

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

    View Slide

  15. 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 Slide

  16. 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 Slide

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

    View 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 Slide

  19. 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 Slide

  20. 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 Slide

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

    View Slide

  22. 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 Slide

  23. 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 Slide

  24. 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 Slide

  25. 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 Slide

  26. 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 Slide

  27. 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 Slide

  28. 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 Slide

  29. 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 Slide

  30. 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 Slide

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

    View Slide

  32. 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 Slide

  33. 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 Slide

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

    View Slide

  35. 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 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 Slide

  37. 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 Slide

  38. 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 Slide

  39. 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 Slide

  40. [email protected] 2018-09-09

    View 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 Slide

  42. 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 Slide

  43. 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 Slide

  44. 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 Slide

  45. 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 Slide

  46. 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 Slide

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

    View Slide

  48. 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 Slide

  49. [email protected] 2018-09-09

    View Slide

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

    View Slide

  51. 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 Slide

  52. 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 Slide

  53. !
    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 Slide

  54. [email protected] 2018-09-09

    View Slide

  55. [email protected] 2018-09-09

    View Slide

  56. 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 Slide

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

    View Slide

  58. [email protected] 2018-09-09

    View Slide

  59. 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 Slide

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

    View Slide

  61. 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 Slide

  62. 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 Slide