[DSACTF2022七月]ezgetshell【上】
目录
0x01题目简单介绍:
打开题目:
有一个图面上传和图面查看的功能
-
很容易想到可能是考察文件上传绕过的能力,但是我们通过上传文件发现,人家并不会提供上传后文件的位置,也就是说即便是我们上传上去了木马,也被执行,但是我们找不到木马的位置,到此处就说明第一条思路断了。
-
既然上传不能走得通,那就看一下能不能从查看图片中发现什么信息。首先通过F12发现,人家是通过调用upload.php来实现上传,通过file.php来实现查看图片的。
-
具体过程是一段js代码来发起请求的,我们着重看一下file.php的调用
$('.search').click(function(){ $('.result').children().remove(); $('.result').text(''); var content = $('.box').val(); console.log(content); $.ajax({ type: "GET", // dataType: "text", contentType: "application/x-www-form-urlencoded", url: "./file.php", cache: false, data: "f="+content, success: function(result) { $('.result').append(result); }, error: function() { $('.result').text('error!!'); } }); })
可以看出参数是
f=参数
请求方法是get,当然通过抓包也可以看到请求的抓包可以看出来人家是有一个另外的参数的,这个影响不大的。
-
了解了人家的get图片的方法后,我们试一下能不能用这个file.php读取到其他文件(这个web就两个功能,上传→ 上传木马,查看/读取→ 读取源码或者敏感信息,再没有其他功能可供利用了。)
-
读取payload
file.php?f=index.php
得到图片的base64,解码后得到index.php的源码:
用同样的方法得到另外的两个文件的源码
/file.php?f=file.php
/file.php?f=upload.php
得到源码:file.php
upload.php:
ps : 网页的base64编码不需要自己去单独解码,直接从前端源码中双击就能看到转义后的结果
可以看出这两个文件都调用了一个class.php的文件,这里猜测大概率是和反序列pop链有关的。
重复上面的方式得到这个class.php的文件的源码:
/file.php?f=class.php
得到源码:
0x02审计class.php文件
这个class.php文件有三个类
Upload类
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES; # 当new一个新对象的时候就把保存的文件复制给$f
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png"; # 将文件的名字md5之后然后在加上.png。 这里可以触发__toString()方法,咱不知道有无用处
if(file_exists('./upload/'.$fname)) { # 如果文件存在,就删除文件
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname); # 将新文件移动到新的位置 ”upload/“
echo "upload success! :D"; # 打印成功
}
function __toString(){ //这个类在程序中没有被用上,属于是专门给我们准备的
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() { # 这个是被调用的函数,也是第一个执行的方法
if($this->file_check()) {
$this->savefile(); # 如果文件检测成功了,就调用文件保存方法
}
}
function file_check() {
$allowed_types = array("png"); #规定的允许类型是png
$temp = explode(".",$this->f["file"]["name"]); # 将字符串以"."为标志打散为数组,分别存储到file和name中
$extension = end($temp);
if(empty($extension)) { # 如果没有后缀,就执行下面的第一条
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) { # 如果后缀符合,就执行下面的语句
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]); # 取出文件内容到f中
if(preg_match_all($filter,$f)){ # 如果文件匹配成功,就报错并退出,全局匹配
echo 'what are you doing!! :C';
return false;
}
return true;
}
else { # 如果后缀不符合就退出
echo 'png onlyyy! XP';
return false;
}
}
}
}
\$_FILES["file"]["tmp_name"] 表示上传的文件,其中tmp_name是上传后临时保存的变量。
该类被创建的时候就会读取当前的文件,并保存到变量 \$f中,类的入口是uploadfile()此方法会调用file_check()方法,通过检测文件的后缀和文件内容是否合格,然后调用savefile()方法给文件名字,并保存它。
Show类
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname; # 文件名字传入source变量中
}
public function show() # 类的入口
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}
}
function __get($name) # 获取不存在变量或者私有变量就触发该魔术方法,name为这个变量的名字
{
$this->ok($name);
}
public function __call($name, $arguments) # 当调用不存在的方式时就调用该魔术方法,比如上面的ok函数,其中argument变量中存在这不可访问的name方法的参数
{
if(end($arguments)=='phpinfo'){ //提供更多的数据
phpinfo();
}else{
$this->backdoor(end($arguments)); //后门函数
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup() //执行反序列化时候就执行这个方法
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
调用ok函数的时候触发__call()
函数其中$name
形参就是ok
,$arrgument
就是传入给ok
的name
变量
Test类:
class Test{
public $str;
public function __construct(){ //类被创建的时候赋值
$this->str="It's works";
}
public function __destruct()
{
echo $this->str; //被销毁时echo $str
}
}
0x03phar反序列化准备工作:
phar反序列化:
phar反序列化即在文件系统函数(file_exists()
、is_dir()
等)参数可控的情况下,配合phar://伪协议
,可以不依赖unserialize()
直接进行反序列化操作。
phar反序列化优点:
可以不管文件的后缀,只要调用过文件系统函数就可以,适合这种后缀不可控的情况下
phar反序列化可以加密,从而绕过过滤
所受影响的函数:
file_get_contents() 本题所用到的函数,作用是将文件读入一个字符串中。
fileatime,filecttime,file_exists,file_putcontents,file,fopen,is_dir等等
以及对于调用了php_stream_open_wrapper
的函数,都存在这样的问题。
利用条件:
-
反序列化的phar文件要能上传到服务端
-
要有可用的魔术方法作为“跳板”
-
文件操作函数可控,且
:
,/
,phar
等字符可用
利用原理:
phar序列化文件简单来说就是一个文件头和一个反序列化文件
通过phar://
反序列化这个文件的时候就有可能调用魔术方法比如:__wakeup
然后就是代码执行。
最终影响就是任意代码执行
phar文件制作:
制作phar文件我们使用的是phar类
<?php
@unlink("shell.phar"); //删除已经存在的shell,为了反复使用,@是用来关闭提示的
$phar = new Phar("shell.phar"); //注明自己要生成的phar文件的地址和文件名
$phar->startBuffering(); //开始缓冲的phar文件写入
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub头,是phar文件的标识符,有时候为了过墙,可以添加GIF89a之类的头
$phar->setMetadata($test); //这个$test就是自己需要反序列化的函数
$phar->addFromString("test.txt", "test"); //写入压缩内容,不然无法生成文件
$phar->stopBuffering(); // 将缓冲中的内容写入到文件中
?>
0x04 php session上传准备
创建session文件
在php服务端开启session之后,每次会话服务端都会保存一个会话,这个会话信息保存在ini文件中的session.save_path
选项下的,默认为/tem或者/var/lib/php/session中
保存的名字为sess_PHPSESSIONID
其中PHPSESSIONID是会话的cookie上传的。
这个sessionid当在ini文件中session.use_strict_mode为off的时候是可以伪造的,这个值默认是off,可以使用任意sessionid来创建一个session文件
写入session文件
当我们在上传文件的时候,有如下四个ini配置文件值得注意
1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
这个配置官方的解释是 实时监测上传进度
0x05 python脚本编写
会话维持
由于涉及到session的存储,所以我们需要创建一个session会话来保存数据
s=requests.Session()
# 之后发送信息就可以使用s.post() 或者s.get()
# 本例中没看出来有什么用,但是确实学到了
文件发送
当我们需要发送一个没有什么用的文件,但是又需要这个发送文件的操作的时候,可以这样写:
file={"file1":("file.png",b'a'*50*1024)}
s.post(url=url,files=file)
本例中,在文件结构体file中,file1对应的是上传文件表单的name属性,需要和后端对应起来才可以接收到这个表单
后面的元组表示文件部分:file.png是所上传文件的名称,服务端会自动读取,紧接着的就是文件内容。
当然也可以使用io操作来生成一段字符:
import io
filebytes = io.BytesIO(b'a' * 1024 * 50)
files={
'file': ('Lxxx.jpg', filebytes)
}
多线程
由于只是简单的条件竞争,所以这里只是学习一下如何开多个线程就可以了
# 首先引入多线程库
import threading
# 创建一个要多线程的函数
def fun1(a):
print(a)
# 创建一个线程并且启动它,其中target是目标函数,args是参数,用元组传入即可
threading.Thread(target=fun1,args=(1)).start()