LCTF中的一道趣题

源码

phpstorm的/.idea/workspace.xml拿到源码.

register.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
31
32
33
34
<?php
include('config.php');
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
$code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';
if (strlen($username) > 16 || strlen($username) > 16) {
die('Invalid input');
}
$sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch() !== false) {
die('username has been registered');
}
$sth = $pdo->prepare('INSERT INTO users (username, password) VALUES (:username, :password)');
$sth->execute([':username' => $username, ':password' => $password]);
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
if (count($matches) === 3 && $admin === $matches[0]) {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, :identity)');
$sth->execute([':username' => $username, ':identity' => $matches[1]]);
} else {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, "GUEST")');
$sth->execute([':username' => $username]);
}
echo '<script>alert("register success");location.href="./index.html"</script>';

login.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
<?php
session_start();
include('config.php');
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
if (strlen($username) > 32 || strlen($password) > 32) {
die('Invalid input');
}
$sth = $pdo->prepare('SELECT password FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch()[0] !== $password) {
die('wrong password');
}
$_SESSION['username'] = $username;
unset($_SESSION['is_logined']);
unset($_SESSION['is_guest']);
#echo $username;
header("Location: member.php");
?>

member.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
31
32
33
34
35
36
<?php
error_reporting(0);
session_start();
include('config.php');
if (isset($_SESSION['username']) === false) {
die('please login first');
}
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$sth = $pdo->prepare('SELECT identity FROM identities WHERE username = :username');
$sth->execute([':username' => $_SESSION['username']]);
if ($sth->fetch()[0] === 'GUEST') {
$_SESSION['is_guest'] = true;
}
$_SESSION['is_logined'] = true;
if (isset($_SESSION['is_logined']) === false || isset($_SESSION['is_guest']) === true) {
}else{
if(isset($_GET['file'])===false)
echo "None";
elseif(is_file($_GET['file']))
echo "you cannot give me a file";
else
readfile($_GET['file']);
}
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<p style="color:orange">你好啊,但是你好像不是XDSEC的人,所以我就不给你flag啦~~</p>
</body>
</html>

解法1

这是预期解法.

问题出在正则上,正则匹配里有一个”回溯”概念.例如:

1
2
正则: (a*)ab
字符串: aaab

a*是贪婪匹配,会尽可能多的吃入字符,所以a*匹配上aaa,然后a去匹配b,发现匹配不上了;

a*吐出1个a,匹配aa,然后a匹配a,b匹配b,匹配成功;

这个”吐出”的过程,就是回溯.

当回溯次数很多的时候,就会引起PHP超时.

题目中,

1
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);

(?:###|\w)+)部分使用了贪婪匹配,

回溯次数会随着a的个数的增加而增加,直至超时.

这样就导致preg_match()函数后的代码未执行,而用户又成功插入到了数据库中.

从而导致member.php中$sth->fetch()[0的查询结果为空,绕过限制.

解法2

利用条件竞争.

看wp的时候得知这个解法,非预期就是叼.

运维师傅会隔一段时间清空一次数据库,所以,只要注册之后登陆,保持登陆状态,数据库清空之后就绕过了`$sth->fetch()[0] === ‘GUEST’

👍

一个错误思路❌

我在读php手册的时候发现,str_shuffle()的原理还是随机数来实现的.所以,如果这串字符打乱的所有可能是一个可接受的数字的话,我们就能利用PHP在一个HTTP连接里不会更换随机数种子的特性,通过大量注册账号来碰撞某个乱序的字符串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$result = [];
for($i=0;$i<8200;$i++) {
$tmp = str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
if(in_array($tmp, $result)) {
echo array_search($tmp, $result).'=';
echo $i.'='.$tmp;
echo '<br />';
}
array_push($result, $tmp);
}
echo 'All:'.count($result);
echo '<br />';
echo 'unique:'.count(array_unique($result));

得到结果:

1
2
3
4
5
6
7
8
9
10
0=8192=arfi_eoh_r_ehasyfsgtrmc_xu_lrmoed_yueo_ee__be
1=8193=ee_u__ereoe_rcxmosmgfhabferl_i_sduate__o_yhyr
2=8194=hf_t_yemamfgrecadlo_rsyer_reu_e_oeeubi_x__hos
3=8195=af_rrc__meuhi_yoea_lebru_erf_dx_tesheeogosm_y
4=8196=yt_rud_c_reyamorfiosfe_umeergeebh_eahs_lx_o__
5=8197=sxiurg_e_udmhcarf_e_aoyel_freorh_oeebymes__t_
6=8198=_meutrea_oe_yhui__oe_hrsrmexe__ofdlagbyc_erfs
7=8199=ecu_gehysouf__oeyxmrtmobl__irarshfe__d_eaee_r
All:8200
unique:8192

可以看到,实际一次最多产生8192种结果,一轮之后就开始重复了.当时我天真的以为指定一个任意顺序去注册8192个账号就能撞上了🙃

但是这8192只是某一个随机数种子产生的,换一个种子就又是完全不同的另外8192种顺序了,所以实际是不能这样碰撞的(除非人品爆表).

参考:

http://www.4hou.com/technology/7374.html

http://seaii-blog.com/index.php/2017/11/21/75.html