CVE-2023-36845 & CVE-2023-36846

Catalpa 网络安全爱好者

2023 年 8 月,Juniper Networks 发布了影响 EX 和 SRX 中 J-Web 组件的远程代码执行漏洞相关信息,本次公开有 4 个漏洞,其中 CVE-2023-36845 & CVE-2023-36846 影响 SRX 系列设备,本文对这两个漏洞进行分析。

环境准备

镜像下载,这里提供 22.1R2.10 版本:https://pan.baidu.com/s/1muIiz7kkIrZA3qIV7lh13A (haxn)

部署后开机,等待系统初始化完成,使用默认用户 root:空密码 登录到后台,注意到 root 登录后会直接进入 linux shell。

先输入命令 cli,进入控制界面,然后执行以下命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
configure
set system root-authentication plain-text-password
commit

set interfaces ge-0/0/1 unit 0 family inet address 192.168.88.150/24
set security zones security-zone trust
set security zones security-zone trust interfaces ge-0/0/1.0
set security zones security-zone trust interfaces ge-0/0/1.0 host-inbound-traffic system-services ping
set security zones security-zone trust interfaces ge-0/0/1.0 host-inbound-traffic system-services ssh
set security zones security-zone trust interfaces ge-0/0/1.0 host-inbound-traffic system-services http
set security zones security-zone trust interfaces ge-0/0/1.0 host-inbound-traffic system-services https
set system services web-management http interface ge-0/0/1.0
set system services web-management https system-generated-certificate
set system services web-management https interface ge-0/0/1.0
set system services ssh root-login allow
commit

配置好后访问 IP:443 就可以看到 J-Web 登录页面,登录账户为 root:刚刚设置的密码。

CVE-2023-36846

首先来看第二个漏洞,CVE 链接:https://www.cve.org/CVERecord?id=CVE-2023-36846

根据描述可知这是一个文件上传漏洞,攻击者可以通过某个无需身份验证的接口向文件系统某位置上传文件。

SRX 的 J-Web 组件所在目录为 /packages/mnt/jweb-srxtvp-8ae76b91/jail,这是一个 chroot 目录,J-Web 默认运行在 nobody 权限。

web 相关代码位于 html 目录下,我们将已经修复的版本和旧版本代码进行比较,排除一些无关改动之后可以定位到文件 slipstream/preferences/user.php,列举新旧两个版本的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// new
#!/usr/bin/php
<?php
require('../../main.inc.php');
$user = new user(false);
define('USER_PREFERENCE_FILE', '/var/tmp/preference');

$http_method = $_SERVER['REQUEST_METHOD'];
if (!isset($_SESSION)) {
echo ('{"status": "Error - Internal error. Cannot find a valid session."}');
return;
} else {
$username = $_SESSION["username"];
}

if(!isset($username)) {
echo ('{"status": "Error - Internal error. Cannot identify user."}');
return;
}

$payload = file_get_contents('php://input');
switch ($http_method) {
case "GET" :
get_user_preferences($username);
break;
case "PUT" :
put_user_preferences($username);
break;
case "POST" :
put_user_preferences($username);
break;
case "DELETE" :
delete_user_preferences($username);
break;
default:
//echo ("default...");
break;
}

function create_user_preference_file($username) {
$file = get_preference_file_path($username);
$fp = fopen($file, "w");
global $samplejson;
if($fp) {
fwrite($fp, $samplejson);
fclose($fp);
chmod(USER_PREFERENCE_FILE, 0666);
}
}

function get_preference_file_path($username) {
return (USER_PREFERENCE_FILE . "." . $username);
}

function get_user_preferences($username) {
$file = get_preference_file_path($username);
//print("<br/> file = " . $file);
try
{
if (!file_exists($file) ) {
echo ('{"status": "Error - Internal error. File not found."}');
write_dummy_content();
}

$fp = fopen($file, "rb");
print_log(__FUNCTION__, "opening file for reading ..." . print_r($file, true) . " status " . print_r($fp, true));
if (is_bool($fp) && $fp == FALSE) {
echo ('{"status": "Error - Internal error. File open failed."}');
}
if(flock($fp, LOCK_SH)) {
print_log(__FUNCTION__,"Unlocked the file for reading ..." . print_r($file, true) . " status " . print_r($fp, true));
$str = stream_get_contents($fp);
}

$fw = fclose($fp);
print_log(__FUNCTION__,"closing file for reading ..." . print_r($file, true) . " status " . print_r($fw, true));

if(!is_bool($str) && strlen($str)>0)
echo ($str);
else {
echo ("{}");
print_log(__FUNCTION__,"error while reading ..." . print_r($file, true) . " status " . print_r($str, true));
}
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
}

function write_dummy_content() {
global $payload;
$payload = "{}";
$username = $_SESSION["username"];
if(!isset($username))
$username = "root";
put_user_preferences($username);
}

function put_user_preferences ($username) {
global $payload;
$file = get_preference_file_path($username);

try
{
$fp = fopen($file, "w");
print_log(__FUNCTION__,"opening file for writing ..." . print_r($file, true) . " status " . print_r($fp, true));
if(is_bool($fp) && $fp == FALSE) {
echo ('{"status": "Error - Internal error. File open failed."}');
} else {
if (flock($fp, LOCK_EX)) {
print_log(__FUNCTION__,"File locked! ..." . print_r($fp, true));
$fw = fwrite($fp, $payload);
if(is_bool($fw) && $fw == FALSE) {
echo ('{"status": "Error - Internal error. File writing failed."}');
} else {
print_log(__FUNCTION__,"Payload written to file ... of " . print_r($fw, true) . " bytes");
}
if(flock($fp, LOCK_UN)) {
print_log(__FUNCTION__,"File unlocked! ..." . print_r($fp, true));
}
print_log(__FUNCTION__,"$file was last modified: " . date ("F d Y H:i:s.", filemtime($file)));
}
echo ('{"status": "Success - Updated preferences for user"}');
}
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
$fc = fclose($fp);
print_log(__FUNCTION__,"closing file..." . print_r($file, true) . " status " . print_r($fc, true));
}

function delete_user_preferences ($username) {
global $payload;
$file = get_preference_file_path($username);

try
{
if (file_exists($file)) {
@unlink($file);
}
echo ('{"status": "Success - Updated preferences for user"}');
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
}

function print_log($fname, $msg) {
error_log($fname . " : " . $msg);
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// old
#!/usr/bin/php
<?php
require('../../main.inc.php');
$user = new user(false);
define('USER_PREFERENCE_FILE', '/var/tmp/preference');

$http_method = $_SERVER['REQUEST_METHOD'];
if (!isset($_SESSION)) {
echo ('{"status": "Error - Internal error. Cannot find a valid session."}');
} else {
$username = $_SESSION["username"];
}

if(!isset($username)) {
echo ('{"status": "Error - Internal error. Cannot identify user."}');
}

$payload = file_get_contents('php://input');
switch ($http_method) {
case "GET" :
get_user_preferences($username);
break;
case "PUT" :
put_user_preferences($username);
break;
case "POST" :
put_user_preferences($username);
break;
case "DELETE" :
delete_user_preferences($username);
break;
default:
//echo ("default...");
break;
}

function create_user_preference_file($username) {
$file = get_preference_file_path($username);
$fp = fopen($file, "w");
global $samplejson;
if($fp) {
fwrite($fp, $samplejson);
fclose($fp);
chmod(USER_PREFERENCE_FILE, 0666);
}
}

function get_preference_file_path($username) {
return (USER_PREFERENCE_FILE . "." . $username);
}

function get_user_preferences($username) {
$file = get_preference_file_path($username);
//print("<br/> file = " . $file);
try
{
if (!file_exists($file) ) {
echo ('{"status": "Error - Internal error. File not found."}');
write_dummy_content();
}

$fp = fopen($file, "rb");
print_log(__FUNCTION__, "opening file for reading ..." . print_r($file, true) . " status " . print_r($fp, true));
if (is_bool($fp) && $fp == FALSE) {
echo ('{"status": "Error - Internal error. File open failed."}');
}
if(flock($fp, LOCK_SH)) {
print_log(__FUNCTION__,"Unlocked the file for reading ..." . print_r($file, true) . " status " . print_r($fp, true));
$str = stream_get_contents($fp);
}

$fw = fclose($fp);
print_log(__FUNCTION__,"closing file for reading ..." . print_r($file, true) . " status " . print_r($fw, true));

if(!is_bool($str) && strlen($str)>0)
echo ($str);
else {
echo ("{}");
print_log(__FUNCTION__,"error while reading ..." . print_r($file, true) . " status " . print_r($str, true));
}
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
}

function write_dummy_content() {
global $payload;
$payload = "{}";
$username = $_SESSION["username"];
if(!isset($username))
$username = "root";
put_user_preferences($username);
}

function put_user_preferences ($username) {
global $payload;

$file = get_preference_file_path($username);
try
{
$fp = fopen($file, "w");
print_log(__FUNCTION__,"opening file for writing ..." . print_r($file, true) . " status " . print_r($fp, true));
if(is_bool($fp) && $fp == FALSE) {
echo ('{"status": "Error - Internal error. File open failed."}');
} else {
if (flock($fp, LOCK_EX)) {
print_log(__FUNCTION__,"File locked! ..." . print_r($fp, true));
$fw = fwrite($fp, $payload);
if(is_bool($fw) && $fw == FALSE) {
echo ('{"status": "Error - Internal error. File writing failed."}');
} else {
print_log(__FUNCTION__,"Payload written to file ... of " . print_r($fw, true) . " bytes");
}
if(flock($fp, LOCK_UN)) {
print_log(__FUNCTION__,"File unlocked! ..." . print_r($fp, true));
}
print_log(__FUNCTION__,"$file was last modified: " . date ("F d Y H:i:s.", filemtime($file)));
}
echo ('{"status": "Success - Updated preferences for user"}');
}
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
$fc = fclose($fp);
print_log(__FUNCTION__,"closing file..." . print_r($file, true) . " status " . print_r($fc, true));
}

function delete_user_preferences ($username) {
global $payload;
$file = get_preference_file_path($username);

try
{
if (file_exists($file)) {
@unlink($file);
}
echo ('{"status": "Success - Updated preferences for user"}');
} catch ( Exception $e ) {
echo ('{"status": "Error - Internal error.' . $e->getMessage() . '"}');
}
}

function print_log($fname, $msg) {
error_log($fname . " : " . $msg);
}
?>

代码中看到新版添加了两处 return,旧版本中此接口鉴权失败时只会输出一些错误信息,之后会继续向下执行,这样一个未授权的用户也可以使用接口中的主要功能。

此接口会读取用户提交的请求,当请求为 POST 时,代码会打开 /var/tmp/preference.xxx 文件,其中 xxx 为当前用户名,然后将请求体数据写入这个文件。当用户未经授权访问时,username 参数为空,代码会打开 /var/tmp/preference. 文件并写入内容。

/var/tmp 是一个临时目录,写入用户权限为 nobody 且位于 chroot 隔离环境,单纯来看这个漏洞并不能产生很大的影响。

CVE-2023-36845

CVE 链接:https://www.cve.org/CVERecord?id=CVE-2023-36845

根据描述信息来看,未授权用户通过利用此漏洞可以控制关键的环境变量,其中包括 PHP 使用的某些变量,从而导致一些问题。

前面已经对比了 PHP 代码,其中并没有和环境变量相关联的改动,考虑到 J-Web 是一种 CGI 服务结构,和环境变量有关系的逻辑大概率位于 web 服务器中。

J-Web 组件使用的 web 服务器由 appweb 修改而来,程序位于 /packages/mnt/jweb-srxtvp-8ae76b91/usr/sbin 目录。

appweb 是 Embedthis 开发的一款嵌入式 web 服务器,Embedthis 旗下还有 GoAhead 项目,也是一个比较出名的嵌入式 web 服务器。看到这些项目,首先想到 CVE-2017-17562 以及 CVE-2021-42342,这两个是 goahead 的环境变量注入漏洞,成因是代码在处理用户提交的参数时没有合理限制敏感字符,导致可以注入如 LD_PRELOAD 等关键环境变量,appweb 中可能也存在类似的问题。

appweb 在 cgiHandler.c 中包含设置 CGI 环境变量的代码片段

1
2
3
4
5
6
7
varCount = mprGetHashLength(rx->headers) + mprGetHashLength(rx->svars) + mprGetJsonLength(rx->params);
if ((envv = mprAlloc((varCount + 1) * sizeof(char*))) != 0) {
count = copyParams(conn, envv, 0, rx->params, route->envPrefix);
count = copyVars(conn, envv, count, rx->svars, "");
count = copyVars(conn, envv, count, rx->headers, "HTTP_");
assert(count <= varCount);
}

我们参考 appweb 源代码和 API 文档,发现有一个叫做 httpSetRouteEnvPrefix 的函数,手册中定义如下

1
2
3
4
Define a prefix string for environment variables.

Description:
When mapping URI query parameters and form variables to environment variables, it is important to prevent important system variables like SHELL, PATH and IFS being overwritten or corrupted. Defining a unique prefix for such parameters ensures they have their own namespace.

使用这个 API 即可给环境变量设置前缀,但是在 httpd 程序中没有找到对此函数的引用。

从代码层面初步来看这个 httpd 可能也存在类似 goahead 的环境变量注入问题,我们可以构造 poc 进行测试,进一步确认问题是否存在。

构造测试请求如下

1
2
GET /?LD_PRELOAD=/abcd HTTP/1.1
Host: 127.0.0.1

发送这个请求服务器会返回 502,当去除 LD_PRELOAD 参数时响应又正常。查看 httpd 的日志 (/packages/mnt/jweb-srxtvp-8ae76b91/jail/var/log/httpd-trace.log) 存在以下内容

1
2
08/23/23 16:53:41 RECV event=http.rx.request type=request method:'GET', uri:'/?LD_PRELOAD=/abcd', protocol:'1'
08/23/23 16:53:41 RECV event=cgi.error type=error msg="CGI failed uri='/index.php',details: ld-elf.so.1: Cannot open "/abcd"

说明 httpd 确实存在环境变量注入问题,可以尝试进一步利用这个漏洞。

利用思路

目前我们已经复现了这两个漏洞,现在要将它们组合起来尝试实现代码执行。

在网上查找相关思路时发现了一篇关于环境变量安全的文章:https://www.elttam.com/blog/env/#content

参考这篇文章得知 PHP 在执行时会读取一些环境变量,其中一个变量为 PHPRC,用于指定 php.ini 配置文件的路径。而 PHP 配置中又存在一个叫做 auto_prepend_file 的变量,通过此变量指定一个文件,当 PHP 执行真正的脚本之前会首先包含此文件,类似于在文件开头调用了 require(auto_prepend_file)。

所以可以先利用文件上传漏洞在 /var/tmp 构造合适的 php.ini 文件,将 auto_prepend_file 值也设置为此文件路径,然后在文件中插入一句话木马。接着利用环境变量注入漏洞,修改 PHPRC 为刚刚构造的配置文件,最后实现 require(/var/tmp/preference.),执行任意 PHP 代码。

实现代码执行只是第一步,J-Web 运行在 nobody 权限并且位于 chroot 隔离环境,还需要进一步提升权限。分析 J-Web 各个组件时发现用户登录后 session 令牌会被存放在 (chroot)/var/sess 目录下,具有一定格式。例如以下内容为 root 用户登录后产生的令牌文件:

1
language|s:7:"english";device-hostname|s:6:"NoName";device-model|s:4:"vsrx";super-user|s:10:"super-user";lsysuser|s:0:"";tenantuser|s:0:"";super|s:5:"super";template-username|s:4:"root";username|s:4:"root";lsysname|s:0:"";tenantname|s:0:"";csrf_key|s:32:"00000000000000000000000000000000";csrf_token|s:56:"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==";debug-asp|s:8:"sp-0/0/0";debug-wizard-commit|b:1;jweb-authenticated|b:1;jweb-user-timeout|s:4:"1800";jweb-last-access|i:1692806139;GLOBAL_MODE|s:7:"Not set";junos-version|s:9:"22.1R2.10";isModelL2NG|b:1;jweb-commit-mode|s:12:"commit-check";TAP_MODE|b:0;SKYATP_ENABLED|b:0;DEVICE_ON_SDCLOUD|b:0;report_enabled|b:0;

参照这种格式利用代码执行漏洞在 sess 目录下新建一个令牌,使用此令牌即可以管理员身份登录到 J-Web 后台,控制设备的大部分配置。

实际上,环境变量注入漏洞可以不依赖文件上传而直接实现远程代码执行,由于 web 服务使用 stdin 读取用户数据,所以可以将 PHPRC 直接指向 /dev/fd/0 文件,然后在请求体中注入相关配置,即可执行任意 PHP 代码,具体可参考相关开源工具

总结

除文中提到的思路外,还可以利用设备某些功能从 chroot 环境中逃逸出来,造成更加严重的安全风险。深入利用就留给感兴趣的朋友自行探索吧。

本次 Juniper EX 和 SRX 漏洞利用了 web 服务器环境变量问题和文件上传,实现了较为稳定的远程代码执行。漏洞危害较大,目前官方已经发布了安全补丁,建议用户及时更新。

参考文章

https://supportportal.juniper.net/JSA72300

https://www.cnblogs.com/lisenlin/p/10302318.html

https://forum.butian.net/index.php/share/1942

https://www.elttam.com/blog/env/#content

https://github.com/kljunowsky/CVE-2023-36845

  • Title: CVE-2023-36845 & CVE-2023-36846
  • Author: Catalpa
  • Created at : 2023-08-25 00:00:00
  • Updated at : 2024-10-17 08:50:26
  • Link: https://wzt.ac.cn/2023/08/25/CVE-2023-36845-36846/
  • License: This work is licensed under CC BY-NC-SA 4.0.