一个真实环境的XX系统渗透测试

免责声明:本测试的所有内容均在可控的环境内进行,本文章仅供交流学习,请于查阅后四十八小时内主动忘记。

 


目录

    1. PostgreSQL 注入

    2. Vert.x 审计

    3. 幕间

    4. 横向移动

    5. 未完待续……

 

 


前言:本人在刚入学的几天就对此系统做了充分的测试,而这两年间始终未能攻破;直到最近发现了原作者写过的一个类似的套皮项目,才终于得到了打开 RCE 之门的那把唯一的钥匙。由于这样的原因,本文章将偏向于复现一个完整的测试过程。(上面的目录仅供参考)

注意:本文中出现的所有 IP、API Endpoint、数据 均已作模糊化处理。所有业务程序及数据均未被大量读取或恶意篡改。

关键词:PostgreSQL 盲注;PostgreSQL RCE;Vert.x 审计;JRE8 任意写 RCE;Meterpreter 长连接;OverlayFS 漏洞 CVE-2023-32629 提权;Redis RCE

 

 


PostgreSQL 注入

  • 初出茅庐

虽然现在已经不是拿啊D扫天下的时代了,但遇到个网站还是会习惯性地往参数后面加上单引号,说不定有奇迹发生呢?而对于本次的XX系统,很显然它并没有发生。该系统的前后端均采用 JSON 在 /api/ 子路由处交换数据,且所有提交的字段均具有类型验证,如 /api/message/5{"pid": 3},等,限制传参为 int 类型,无法注入。登录 /api/login 处发现字符串字段 username 会被带入数据库,但无法注入,返回类型只有:登陆成功,密码错误,用户名错误三种。

天无绝人之路,发现了一个有意思的提交点 /api/query,其数据为 {"points": [1, 2, 3]} 。这里虽然有对 points 进行类型验证,但数组里面可就不一定了。试着改成 {"points": ["1, 2", "3"]} ,照样成功提交,且两种格式返回的结果具有一致性。这不是白来的 SQL 注入点?里面加个单引号,返回值变成 db exec error 了,虽然没有报错信息,也不知道后端的框架,DBMS,但不必惊慌 —— 直接上 sqlmap 跑它的。两年前,sqlmap 还能顺利跑完,但遗憾的是它说 not injectable ,开 level 5 risk 3 也是一样的结果。现在,sqlmap 没跑到一半呢都就被中间件 WAF 拦下大半了,还顺带 IP 封禁套餐,触发规则平均 30~90 秒后被封。用自动化工具基本上是不可能了,而且也测不出来。

手工注入嘛,勉为其难地猜一下,估计是类似 ','.join(points) 这样的东西,然后在 WHERE point IN () 里拼接,那就简单了,试一个 ") -- " ,然而依旧 db exec error。也有可能是加了引号?不可能,否则 "1, 2" 不可能正常执行,尝试 "\"3\"" 正常,而 "\"1, 2\"" 报错,证明没有引号。但是如果加 "'3'" 的话就会报错,难道还有什么 SQL 支持双引号,不支持单引号???先不管这个问题,有可能是不支持注释,或者后面有其他语句?那就闭合语句,试一个 "1) OR 1 IN (1" ,照样报错。这时候已经开始有些疑惑了,难道是拆成 OR 了?试一个 "1 AND 1=0 ",还是报错。接着尝试了所有常见的组合,闭合了所有网上能搜到的 SELECT IN 的写法,也进行了很多奇妙的 fuzz test。就算它用 Access 也不应该全是 db exec error。一度怀疑过这个地方到底是不是可注入的。

当然,熟悉 PostgreSQL 的朋友们应该能立刻联想到其他的几种写法,但问题是我不太熟 233,而且此时也没有得到关于任何 DBMS 的信息,万一它要是 Oracle DB 的话就更无从下手了。就这样,这个 API endpoint 保持只可远观不可亵玩的状态,直到最近,一场大雨改变了这一切。

 

  • 渐入佳境

通常对于长这样的系统,盲猜 vue/react + nodejs + mysql 是很合理的,基本上八九不离十。后来证明了这是一个巨大的失策。鉴于作者在页面最底下留了版权信息,自然是得进他们的 GitHub 主页参观参观。大致扫了一眼,fork 的不看,跟 js 相关的重点看,然而并没有发现什么有价值的东西。直到前几天。突然想看看学长们写的课设长啥样,就每个项目点进去都扫一遍。其中有一个叫“毕业设计(代写版)”,Java 写的,想着看看主路由完事,结果直接震惊一百年:咋长得这么像呢?那熟悉的 /api/ endpoint,熟悉的全 json 传参,心想,不会吧?确信度 20%。

来到鉴权路由,一看是获取在 json body 里的 token 字段,且经过 AES + BASE64 加密,简直完全一样!确信度 50%。但这也只能表明作者的构架偏好,不能直接证明使用框架的相关性。Vert.x,不怎么听说过。没发现什么现成的洞。发动技能:奇技淫巧;搜索 404 界面内容,结果:

还真搜到了。。。这下可以 100% 确认后端使用的是 Vert.x,竟然是 Java,想都没想过,隐藏得太深。最后再确认一下,直接访问 /api/ 会出一段提示信息,”Hello from api endpoint. You should normally not see this.”,跟“毕业设计(代写版)”有 99% 的重合度,这下可以放心了。虽然 endpoint 长得不太一样,但核心代码的重合度肯定是很高的。这下好了,黑盒变白盒,直接开审。

看源代码发现,传参使用的是 io.vertx.core.json.JsonObject ,调用最多的就是 param.getInteger(),且登陆路由使用 io.reactiverse.reactivex.pgclient.PgConnection 的 rxPreparedQuery() 执行 SQL,这个 PreparedStatement 就别想注入了,跟预期一样。重要的是传入 JSON 数组的地方,什么个流程?定位到相关代码:

继续跟进 Database 相关函数:

这下BBQ了,直接拼进去,也就造成了 SQL 注入的可能。不过参数看着有些陌生,从这里开始入坑 PostgreSQL:把 JSONArrayencode() 完变成 ["1, 2", "3"] ,然后进行一波奇妙的替换变成 '{"1, 2", "3"}' 直接拼进 SQL 语句里。咋回事捏?先看看 SEARCH_SQL 是啥:

竟然是个函数。。。从一开始大方向就走错了。看看内容:

plpgsql 函数里面的这个 select 是不可注入的。所以目标很明确,控制原 select 的流程。由于 PostgreSQL 默认支持 stacked queries,直接闭合这个函数,传 "}'); -- ",然后还是熟悉的 db exec error 。。。可能是参数的位置不对?原来不是只有一个 points 吗。。。然后想起之前 fuzz test 时的单双引号问题,单引号的问题得到了合理的解释,而XX系统中由 JSONArray 而来的字符串是不自带双引号的。与这里的代码存在一定差异。薛定谔的盒测试。也尝试在后面补不同个数类型的其他参数,仍然无济于事。当务之急是要确认XX系统里到底是不是这么写的,于是只能尝试闭合 "'{}'",使函数仍然正常调用。

即使不熟悉 pgSQL 也应该猜到了,"'{}'" 这是数组的写法。pgSQL 里函数的调用存在类型检查,必须保持插入 payload 后该处参数的类型仍为 integer[]。开始尝试:

可以发现,若是单个 string,pgSQL 可以将其隐式转至数组类型;而使用 || 运算符则默认合并的是字符串,由于从左往右运算,仅需保证左边出现一个数组即可。立马构造一个 "1}'::int[] || '{1",终于,不是 db exec error,正常地查询出了结果,说明走在一条康庄大道上!!

立马试一个 pg_sleep() ,然后发现被中间件 WAF 拦下来了。。。识别的是老老实实的 pg_sleep 这几个字符,包括大小写,JSON 的 \u 解码也给它做上了,难以绕过。。。不过幸好 pgSQL 提供了方便的 query_to_xml() 函数可以执行任意字符串存储的 SELECT 语句,但如何把它嵌入至 payload 中,还需要一番尝试:

可以发现,pgSQL 是一定的强类型语言。对着在官网文档上找到的运算符表及优先级构造,最终得到了一个可行的 payload。(后来发现,还可以采用 LENGTH(query_to_xml('select 1',true,true,'')::text) 这样的构造法,其实有很多)

字符串的话,由于单引号没过滤根本,直接拆分就行,保险一点使用 CHR() 函数并起来也行,又或者直接从 HEX 转换也行 convert_from(decode('00000000','hex'),'UTF8')

提交如上构造好的 payload,观察到 pg_sleep() 成功执行了!!简单地搓一个 tamper ,把 SQL 全部塞到 query_to_xml 里面,然后直接扔进 sqlmap 跑:

多少心酸多少泪。

 

  • 柳暗花明

获取一下基本信息,PostgreSQL 的提权由于其强大的功能,显得比较容易:

这下又BBQ克,有 superuser 权限,可以直接通过 COPY FROM 命令来 RCE。然后问题又来了:

是的,query_to_xml 系列函数是 READ-ONLY 的,也就是说在这里只能执行 SELECT 语句,CREATE / INSERT / UPDATE 都是通通不行的。而像 EXECUTE 这种只能存在于 plpgsql 声明的函数里。也尝试过其他的 getshell 方案,只用 select 的话虽然能做到任意列目录读写文件,但由于这里是时间盲注,可能会对数据库及配置文件造成潜在的损害,这在生产环境中是无论如何都要避免的。

另辟蹊径,pg_stat_activity 中存放着当前执行的 SQL 语句。如果能爆出原来 SELECT 语句的具体结构,那不就可以闭合,然后直接堆叠注入了吗?当然,这也得建立在 WAF 没有过滤相应关键字的情况下。不过值得一试:

这里有一个坑点:pg_stat_activity 中默认只会存放 sql query 的前 1024 个字节,也就是说如果上边用了 CHR() 加密 payload,那就会读不完。。。另外,query_to_xml 里面再次执行的 SQL 语句不会出现在这里,所以可以简单地用 query like '' 来匹配需要的那条语句。

sqlmap 跑出来的结果是: F8'),true,true,'')) ISNULL ::int] || '{2,2}','20') 。啊?就这么简单?后面多了个参数就?刚才咋没测出来??然后发现刚才都是拿的数字 0 或者空字符串进去测的,这里显然要求是一个正整数,属性相克了。。。

到这里,一切水落石出,柳暗花明,需要的只是把 rev shell 开好,然后提交如下的 payload 即可:

有意思的是,这里的表名本来默认是 cmd_exec,然后被 WAF 拦下了,当时就很紧张。测试到后面发现检测的竟然是 exec( 这个组合。。。

后话:明文流量被信网办(SANGFOR STA 审计设备)监测到了,还是个高危风险。。。

 

 


Vert.x 审计

  • 有限制的任意下载

话接上集,PostgreSQL 竟然是一个单独的服务器,256G 内存 2T 硬盘就跑一个数据库。。。能看到从另外两个 IP 过来的连接,看来距离完全拿下业务系统,还有一段路要走。

有了源代码作为参考,其他的 API 自然是需要重审一遍。来到了 /api/download 处,在 fuzz test 时试过 ../ 进行 Directory Traversal,但没有成功,返回类型只有文件内容或“download failed”。定位到相关代码处:

可以发现,确实是直接拼进去的。那为啥什么都读不出来呢??这里需要提一嘴 WAF。这个东西,会 block 以下的请求:etc/ logs/ ,虽然没有直接拦截 ../ ,但会秋后算账拉清单,只要访问了,一分钟后必封 IP。然后 Windows 的重 DHCP lease 速度是可以想象的。。。也就是说,这个地方测起来时间成本特别高。那么回到这个任意下载点,为什么说它是有限制的呢,一是存在这么个 WAF,主要是 etc/ 都过滤了还能读个啥东西;二是当 ../ 的数目超过当前 URI path 长度时,会返回熟悉的 nginx 400 bad request。。。是的,前端存在一个 nginx 反代限制了能跳回的父目录层级;三是我的个人习惯,/etc/issue 读不了就读 /proc/version,后来证明了这是一个巨大的失策。

一层一层解决。首先在本地起一个 Vert.x 相同 Router 代码,然后上 nginx 同版本 1.18.0 绕过,最后测试目标环境。对于 Vert.x 的路由参数处理逻辑,就不审代码了,直接测出一些基本结果:

可以发现,/:filename 并不是通配符匹配,但会自动解码 %2F 然后拼入 filepath,存在目录穿越漏洞。这里存在两个坑:一是当尝试读 /proc/version 时,会发现返回的文件内容为空,是读取失败了吗?跟进 sendFile() 函数相关逻辑:

可以发现这里使用 file.length() 以及 offset (从 sendFile() 调用时为 0)控制发送的字节位置。然后一个广为人知的事实就是,/proc/ 里面的文件大小都是 0 字节,所以在这样的逻辑下是读不到的。。。这样也就排除了读 /proc/self/cmdline 等的可能,简直是不任意的文件下载。

然后第二个坑是,另一个广为人知的事实,对于跳回父目录的 ../ ,只要路径中存在一个不存在的子目录,那么 Linux 会立即返回 file not found。继续跟进上图中 487 行的 resolveFile(),会进入 java.io.File.exists() ,最终通过 JVM C Native 接口调用 stat() 函数判断文件是否存在。比如说,stat "/etc/" 是存在的,而 stat "/etc/non-existence-dir/../" 是不存在的。这要求我们的控制的路径参数必须以 ../ 开始 ,不能包含多余的不存在目录,为之后的绕过埋下了伏笔。

接下来搭一个 nginx 1.18.0 并配置 /api/ 反代。但是这里存在几种写法上的区别:location 加不加 /proxy_pass 加不加 / ?写没写到 /api ?没有一点办法,只能手动枚举各种情况,控制变量法确定配置文件的写法。基于以下的事实:

    1. 直接访问 /api 不会被转跳至 /api/
    2. 访问 /api/download/.. 相当于访问 /api
    3. 访问 /api/download/../../ 相当于访问主页,注意是前端的静态主页。

可以合理地推测出配置文件是这样的:

不存在一些离谱的配置错误。现在考虑绕过 nginx 的目录层级限制。相信有经验的朋友应该能立刻联想到 CVE-2021-43798 也就是 Grafana LFI 的那个洞,PoC 为 /public/plugins/welcome/#/../../../../../../../../../etc/passwd ,其中 nginx 通过 /#/ 进行绕过。但是在这个XX系统中,直接传入 /api/download/#%2F..%2F..%2F...... 虽然得以绕过 nginx 的层级限制,但还记得上面的坑二,Java 未经 normalize 直接把 filepath 传进 stat() 调用吗?也就是说,# 这个目录不存在,即使跳再多的父目录,最终的文件都是不存在的,根本读不到。陷入瓶颈。

在这个时候尝试过各种 HTTP Smuggle 的方法,包括 nginx ≤ 1.18.0 的那个经典 CVE (虽然从未成功复现过),也包括 Vert.x 底层使用的 Netty 低版本那几个 CVE,但始终无法成功。还是得回到问题本身。仔细回想至此的所有特性,不知是否有灵光一现:使用 /api/download/#/../..%2F..%2F...... 不就可以啦?nginx 跳过 /#/ 之后的路径检查,而 Vert.x 检测到未编码的 /#/../ 选择向上跳一级回到 /api/download/ 路由,之后的 ..%2F 不就随便写啦?这是 nginx 与 Vert.x 的解析差异。

到这里虽然很兴奋,但鉴于 etc logs proc 全都读不了,一连试了好几个其他常见的文件,全都不存在。比较难搞。发动技能:奇技淫巧;读一个 /proc/self/exe 看看,不看不要紧,一看吓一跳,在里面找到了 GCC: (Alpine 8.2.0) 8.2.0 字串。再读一个 /bin/busybox,果然有。Alpine 我还只在 docker 里用过。。。读一个 /.dockerenv ,然后还真tmd有。。。本来只想备份一下就跑路,然后 jar 包名死活猜不出来。。。既然是在 docker 环境里的话,其他也没什么有用的东西了。

 

  • 有限制的任意上传

其实在前端 webpack 过后的 js 里搜路由的时候还发现了一个 /api/upload ,只是尝试下来需要特定用户权限,在那时还无法进行测试。但是别忘了,PostgreSQL 服务器已经纳入囊中,于配置备份文件处发现泄露的 postgre 明文密码。在数据库中查询到相应权限的用户,MD5 破不出来没关系,直接改,主要突出一个完全掌控。

登陆之后测试上传任意文件成功,同样找到对应源代码处开始审计:

BBQ,filename 直接拼进去,理论上可以覆盖任意文件,无法创建目录。然后上传完是没有回显的。。。没错,根本不知道上传到哪了,之后才在数据库里发现相应的记录。测试上传到 ../../../../../../../../../root/test.txt ,然后使用上面的任意下载成功。初步的胜利。现在分别有了一个有限制的任意下载与上传,它们两个共同演奏出的,是动听的交响曲,还是恶魔的旋律?

 

  • 二重奏的 RCE

鉴于是在 docker 环境里,可下载的东西少得可怜,可覆盖的东西也少得可怜。cron 肯定是没有的。又由于是 java,感觉不怎么会 reload 或者往外加载啥东西。由于是生产环境,也不敢乱覆盖 libc 这样的东西,万一打挂了咋办。

然后在佛前苦苦求了几千年,找到了大佬的文章《Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索》,大体上来说就是 JVM 会延迟加载某些内置的 jar 文件,比如 jre/lib/charsets.jar 会在第一次调用 Charset.forName() 后才被打开读入内存,使得以下的操作成为可能:覆盖 charsets.jar → 第一次触发 Charset.forName() → 读入 charsets.jar 并执行恶意 class 代码。

原文是在 Springboot 里的,而 Vert.x 使用的是 Netty。不过大差不差,直接开搜:

定位到可疑代码处:

回溯一层:

可以发现,this.currentFieldAttributes 是完全可控的,也就是说,提交如下的 multipart 字段:

即可触发 JVM 读取 charsets.jar 里面的 IBM037 编码。

由于只有一次机会,所以必须特别谨慎。在本机起一个 java -XX:+TraceClassLoading -jar demo.jar 观察:

在包含如上的 multipart 字段后,记录里亮眼的 Opened /opt/java/openjdk/jre/lib/charsets.jarLoaded sun.nio.cs.ext.IBM037 证明了该方法的可行性。

于是选择冤大头 IBM037 下手,其他代码全都不需要,只留个:

上传覆盖 charsets.jar ,然后触发 Charset.forName ,可以看到 Hello World 程序执行了!!当然,由于懒得找反编译进去的时候需要的其他包,这里会有一个报错,但是并不重要。它!执!行!了!

路已经铺好了,接下来只需要把它走完。

由于是 Alpine,读一个 /lib/apk/db/installed 泄露出 JRE 的路径 /usr/lib/jvm/java-1.8-openjdk/jre/ ,为了防止不兼容直接下一个上面的 charsets.jar 改。这里需要注意的是,nginx 默认上传大小是 1MB,而这里的 charsets.jar 有 1.8MB。。。需要删一点看起来不怎么有用的编码(比如说,除了 IBM037 之外所有 IBM 打头的 233)。然后还是相同的流程。成功上线:

接着发现几件无语的事情:文件下载处,实际上离根目录只有两层,根本没必要绕过 nginx。。。然后在同目录下 config.json 就可以直接读到 pgSQL 的密码,是开端口的,可以直接连。。。君子不计小人过,都 RCE 了,就算了。

 

 


幕间

  • IP 风波

由于第一次 exp 时往 charsets.jar 里写的是硬编码的 IP,然后中间出去上了一天课,回来发现 IP 变了。。。这就比较尴尬了。于是只能想办法把原来的 IP 刷回来。手动换 MAC 地址感觉不是个头,就叫 ChatGPT 写了个脚本刷 IP。

本来以为刷一个 C 段就完事了,没想到它出现了第二个。那行吧,继续等等看,结果出现了第三个。。。看一下 IP ASN 信息,发现整个 /16 都是学校的,大无语,只能挂机刷着。睡一觉醒来发现刷到第七个的时候卡住了,估计是被 DHCP 服务器 rate limit 了。等了半天继续开刷,这下终于第一个段的 IP 过期了,又重新回来了。半小时刷到之前使用的 IP,然后当做无事发生。

 

  • Msf 风波

深信服的检测设备真tmdnb,吓得我连夜上了 msf。然后最安全的应该属 linux/x64/meterpreter_reverse_https 。按照官网文档配好 persistent 长连接,睡一觉起来发现 session 竟然掉了。试了第二次,第三次,也都掉了。而且情况十分诡异。然后发现这是 meterpreter 的 bug,比较无语。。。只要 LHOST 无法连接,过一段时间就会 memory leak 然后崩掉,OOM 或者哪溢出了,总之再也连不上。DDNS 表示:我做错什么了。

已经提了 issue,持续跟进中 https://github.com/rapid7/metasploit-framework/issues/18342。这阵子比较忙,可能之后有空读读 meterpreter 的源代码[坑1]。没办法,只能 fallback 到 reverse_tcp,但看起来也能使用 SSL 加密,凑合吧。

然后过了几天发现 reverse_tcp 也有点 bug,准确地说应该是 TCP 的 bug,具体来说就是 msfconsole 的 IP 被立即下线(封禁)以后,meterpreter 那边的连接一直会是 ESTABLISHED 的,也就是说再也连不回来。。。没找到地方开 TCP Keepalive,比较蛋疼。

最后是的,meterpreter 的 portfwd 也有问题。经常连接一断,session 也就断了,特别难用。还是得 ssh 把端口转出来:

 

 


横向移动

  • 漫漫提权路

以 postgres 用户的权限虽然能读到小部分配置文件(主要通过备份泄露),但总感觉不够爽快,要打就得往死里打。那么首先,当然是使用亲爱的 local_exploit_suggester 扫一扫:

典型(typical),全部试了一下,果然都不行。鉴于是 ubuntu 20.04 和一个算不上旧的内核版本,暂且寻找其他可提权的利用点:

    1. 扫 SUID,自然是啥也没有,又不是打 CTF 呢;
    2. 找可写文件,crontab,自然也是啥也没有;
    3. 有什么奇技淫巧就不一一去试了,因为是比较标准默认安装的 ubuntu 新版本;
    4. 查看运行服务,nginx / rsync / vsftpd,都是新版本,无提权漏洞;
    5. rsync 以 root 运行,明文密码于备份文件泄露,但是 read only;
    6. vsftpd 密码不知道,虽然与 rsync 开放的是同目录,但权限是 www-data ,且 nginx 上没有任何动态服务(绕!);
    7. 查看 home 目录,发现一个以用户权限运行的 .service 程序是 777,这下开心坏了!拉回来 IDA 一看,go 写的。。。同步数据库用的,程序本身没啥问题,也没找到能让它崩溃重启的点,所以这里就算覆写了也没用;
    8. 这下穷途末路了。

然后偶然间看到了 Mr. CVE-2023-32629,PoC 都在那了没有不试的道理啊:

结果尴尬了,这个 root 啥文件也读不了。它就是个假 root!!高兴得太早。为什么这么说呢,之后看代码才发现它的功能等价于 unshare -rm sh -c "id" ,简直无语。不过相对地,CVE-2023-2640 的 PoC 确实是真实的:

简单分析一下,其实很显然,在 namespace 里进行 setcap,然后 mount overlay 并触发 copy_up,发现 capabilities 成功逃逸至原环境。但是这里的问题是,没错,目标机器 5.4.0-148,是不受影响的版本。然后再看一眼 CVE-2023-32629,很幸运地,直到 5.4.0-155 才被修复,也就是说这个漏洞是利用可能的!!苦于网上没有现成的 PoC,过几天它内核版本说不定就滚上去了,必须说干就干,根据现有的信息写一个出来。

首先整理一下 OverlayFS 的 timeline,可以发现主线都是围绕 copy_up 的权限检测错误:

    1. CVE 2016-1576 :copy_up 允许从自定义的 fuse 中拷贝任意 UID/GID 及 SUID 的程序。
    2. CVE-2021-3493 :copy_up 里的 vfs_setxattr() 缺乏对 namespace 的隔离,允许 capabilities 的直接拷贝。
    3. CVE-2021-3847 :不详。
    4. CVE-2023-0386 :同 CVE 2016-1576 ,而且是完全相同。(旧洞新修?不是很能理解)
    5. CVE-2023-2640 :CVE-2021-3493 的不完全修复(在 ubuntu 中被 ovl_copy_xattr() 重新引入),通过 ovl_do_xattr() 触发。
    6. CVE-2023-32629 :同上,但被 ovl_copy_up_meta_inode_data() 新引入,同样通过修复不完全的 ovl_do_xattr() 触发。

而已有的信息是,metacopy=on 是触发的入口点。根据 metacopy 的特性,进行如下的测试:

可以发现,开启 metacopy 后,对 mount 里的文件进行 chmod/chown 等操作时,不会进行文件内容的 copy_up,而只是创建一个新的 inode 保存其属性。上面的 total 5.3M 跟底下的 total 12K 证明了这一点。也就是说,进行 setcap 时也会有类似的行为,由于漏洞函数的存在,最终造成了 capabilities 的任意拷贝!

这里需要注意的一点是,在 setcap 完后需要 touch 一下触发实际文件内容的 copy_up,否则是执行不了的:-bash: ./u/python3: cannot execute binary file: Exec format error。这里的 copy_up 并不会覆盖 upper 里已经设好的 capabilities。于是:

即为 CVE-2023-32629 的 PoC。这下可是实打实的 root 权限,够硬!

 

  • 历经艰难终成大业

有了 root 权限,就有了 .ssh 里的 id_rsa,也就有了其他机子的控制权。虽然从 .bash_history 里能看到很多 IP,但实测只有俩能用当前的密钥连上。前面的 Vert.x 就跑在其中一台的 docker 里。回想起 MS17-010 刚出来那会,要进机房里的哪台 XP 完全看的是心情。现在也有点儿类似的感受。虽然只有两台。

这两台已经是业务核心机了,里面有主程序的配置文件。其实在之前的探测过程中,曾经发现过另一台开 MySQL 的机子,但 secure_file_priv 为默认值,无法利用。然后在配置文件里发现了对其 Redis 的连接。内网,所以不用密码。从 INFO 得到版本信息:redis_version:3.2.9 及 os:Linux 3.10.0-1127.10.1.el7.x86_64 。这可够旧的。CentOS 的老古董都是这样的。

想着尽量不覆盖文件,然后发现通过主从复制 + module load 来 getshell 的方法只适用 4.x ~ 5.x,太旧啦!!覆盖 root 的 ssh key 肯定是万不得已的时候考虑的。那么可能性所指引出的道路就只有一条:覆盖 /var/spool/cron/root ,然后祈祷它原来是没有内容的。

这里的方法是通用的。首先获取原配置文件,根据需求 select 一个 db,这里 dbsize0 所以就不用了,写入 k-v 对后更改 dirdbfilename,最后一个 save。有一点需要注意的是,在有些场景下可能需要先关闭 rdbcompression。可以根据需要在利用后还原配置。

很幸运,这里 redis 是以 root 权限跑的。随着一声清脆的 [*] Meterpreter session opened 落下,至此,两台核心业务机 + 两台数据库均被收入麾下。

从一个奇怪的 PostgreSQL 注入开始的旅程,也即将迎来尾声。

 

 

 

 

 

《一个真实环境的XX系统渗透测试》上有1条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注