这个要从一个CTF题目讲起了

0x00 题目

今天复现一个TCTF线下赛的题目Lucky Game
题目源码很少,只有一个文件

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<?php session_start(); ?>
<!DOCTYPE html>
<html>
<head>
<title>Lucky Game</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway:200">
<link href="https://fonts.googleapis.com/css?family=Noto+Sans" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/purecss@0.6.2/build/pure-min.css" integrity="sha384-UQiGfs9ICog+LwheBSRCt1o5cbyKIHbwjWscjemyBMT9YCUMZffs6UqUTd0hObXD" crossorigin="anonymous">
<link rel="stylesheet" href="https://purecss.io/combo/1.18.13?/css/main-grid.css&amp;/css/main.css&amp;/css/menus.css&amp;/css/rainbow/baby-blue.css">
<style>
.header{font-family: 'Noto Sans', sans-serif;}
.header h1{color: rgb(202, 60, 60);}
.button-error {background: rgb(202, 60, 60);}
.button-success {background: rgb(28, 184, 65);}
</style>
</head>
<body>
<div id="layout">
<div id="menu">
<div class="pure-menu">
<a class="pure-menu-heading" href="#">TCTF</a>
</div>
</div>
<div id="main">
<div class="header">
<h1>幸运数字</h1>
<h2>Shall we play a "lucky" game?</h2>
</div>
<div class="content">
<?php

require 'config.php';
if (!$link=mysqli_connect('localhost', MYSQL_USER, MYSQL_PASS)) die('Connection error');
if (!mysqli_select_db($link,'lucky')) die('Database error');

$tbls = "SELECT group_concat(table_name SEPARATOR '|') FROM information_schema.tables WHERE table_schema=database()";
$cols = "SELECT group_concat(column_name SEPARATOR '|') FROM information_schema.columns WHERE table_schema=database()";
$query = mysqli_query($link,$tbls,MYSQLI_USE_RESULT);
$tbls_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);
$query = mysqli_query($link,$cols,MYSQLI_USE_RESULT);
$cols_name = mysqli_fetch_array($query)[0];
mysqli_free_result($query);


# CREATE TABLE users(id int NOT NULL,username varchar(24),password varchar(32),points int,UNIQUE KEY(username));
# INSERT INTO users VALUES(1,"admin",md5(password_of_admin),10);
# CREATE TABLE logs(id int NOT NULL,log varchar(64));


foreach($_POST as $k => $v){
if(!empty($v) && is_string($v))
$_POST[$k] = trim(mysqli_escape_string($link,$v));
else
unset($_POST[$k]);
}

foreach($_GET as $k => $v){
if(!empty($v) && is_string($v))
$_GET[$k] = trim(mysqli_escape_string($link,$v));
else
unset($_GET[$k]);
}


function filter($s){
global $tbls_name,$cols_name;
$blacklist = "sleep|benchmark|order|limit|exp|extract|xml|floor|rand|count|".$tbls_name.'|'.$cols_name; # Ninjas need nothing
if(preg_match("/{$blacklist}/is",$s,$a)) die($blacklist."\n".$a[0]."\n".$s."\n"."<aside>0ops!</aside>");
return $s;
}

function register($username,$password){
global $link;
$q = sprintf("INSERT INTO users VALUES (id+1,'%s',md5('%s'),10)",
filter($username),filter($password));
if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
return TRUE;
}

function login($username,$password){
global $link;
$q = sprintf("SELECT * FROM users WHERE username = '%s' AND password = md5('%s')",
filter($username),filter($password));
if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
$result = mysqli_fetch_array($query);
mysqli_free_result($query);
if(count($result)>0){
$_SESSION['id'] = $result['id'];
$_SESSION['user'] = $result['username'];
return TRUE;
} else {
unset($_SESSION['id'],$_SESSION['user']);
return FALSE;
}
}

function user_log($s){
global $link;
$q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
filter($_SESSION['id'].'|'.$s));
if(!$query = mysqli_query($link,$q)) return FALSE;
return TRUE;
}

function update_point($p){
global $link;
$q = sprintf("UPDATE users SET points=points+%d WHERE id = %d",
$p,$_SESSION['id']);
if(!$query = mysqli_query($link,$q)) return FALSE;
if(!user_log("Update ".$p)) return FALSE;
return TRUE;
}

function my_point(){
global $link;
$q = sprintf("SELECT * FROM users WHERE username = '%s'",
filter($_SESSION['user']));
if(!$query = mysqli_query($link,$q,MYSQLI_USE_RESULT)) return FALSE;
$result = mysqli_fetch_array($query);
mysqli_free_result($query);
return (int)($result['points']);
}

switch(@$_GET['action']){
case 'register':
if(!empty($_POST['user']) && !empty($_POST['pass']))
if(!register($_POST['user'],$_POST['pass']))
die("<aside>Something went wrong!</aside>");
break;
case 'login':
if(!empty($_POST['user']) && !empty($_POST['pass']))
login($_POST['user'],$_POST['pass']);
break;
case 'logout':
unset($_SESSION['user'],$_SESSION['id']);
break;
default:
break;
}

if(empty($_SESSION['user'])){
echo <<<EOF
<form action="?action=register" method=POST class="pure-form pure-form-stacked">
<fieldset>
<input type=text name=user required placeholder="Username" />
<input type=password name=pass required placeholder="Password" />
<button type="submit" class="pure-button pure-button-primary">Register</button>
</fieldset>
</form>

<form action="?action=login" method=POST class="pure-form pure-form-stacked">
<fieldset>
<input type=text name=user required placeholder="Username" />
<input type=password name=pass required placeholder="Password" />
<button type="submit" class="pure-button pure-button-primary button-success">Login</button>
</fieldset>
</form>
EOF;
die();
}

$points = my_point();

if($points == 1337){
user_log('winner');
echo "<h3>Well played, we will give you a reward soon.</h3>";
}

echo <<<EOF
<h1>Hello <a href='?action=logout'>{$_SESSION['user']}</a></h1>
<h2>You got {$points} points</h2>
<form method=GET class="grid-panel pure-form-aligned pure-form">
<div class="bet-control pure-control-group">
<label for="bet-input">
Your bet
</label>
<input name="bet" id="bet-input" data-content="bet-input"
type="number" min="0" max="16" value=1>

</div>

<div class="guess-control pure-control-group">
<label for="guess-input">
Your guess
</label>
<input name="guess" id="guess-input" data-content='guess-input'
type="number" min="0" value=1>
</div>
<button type="submit" class="pure-button pure-button-primary button-error">Place</button>
</form>

EOF;

if(!empty($_REQUEST['bet']) && (int)$_REQUEST['bet'] > 0 && !empty($_REQUEST['guess']) && (int)$_REQUEST['guess'] > 0){
echo "<aside>";
if($_REQUEST['bet'] > $points) die("What?! you're cheater!");
$number = rand()%8;
echo "It is...<h1 style='color:#fff'>".$number."</h2><br />";
if( $number == $_REQUEST['guess'] ){
echo "You won!";
if(!update_point($_REQUEST['bet']))
return;
} else {
echo "You lost :(";
if(!update_point(-$_REQUEST['bet']))
return;
}
echo "</aside>";
}

mysqli_close($link);
?>

</div>
</div>
</div>
</body>
</html>

参照小m的Writeup
其中讲到一个trick

要让(int)$_REQUEST['bet'] > 0 if($_REQUEST['bet'] > $points) die("What?! you're cheater!");检查同时通过

在科学计数法字符串转换为数字时,如果 E 后面的数小于某个值会弄成 double 类型,再强制转换为 int 类型时可能会有奇妙的结果,测试发现 bet 为 1e-1000 时已经可以触发这个 bug 绕过两个检查, 使得 bet 既大于 0 又不大于 0。

0x01 验证

命令

1
2
php -r "var_dump((int)('1e-1000')>0);"
php -r "var_dump('1e-1000'>0);"

结果

1
2
3
4
Command line code:1:
bool(true)
Command line code:1:
bool(false)

命令

1
2
php -r "var_dump((int)('1e-10')>0);"
php -r "var_dump('1e-10'>0);"

结果

1
2
3
4
Command line code:1:
bool(true)
Command line code:1:
bool(true)

从这两组结果能看出这个题目的解题点在哪里了。。。

0x02 源码分析

php源码版本:5.5.30
全局寻找intval函数

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
//type.c line 134
PHP_FUNCTION(intval)
{
zval **num;
long arg_base;
int base;

switch (ZEND_NUM_ARGS()) {
case 1:
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &num) == FAILURE) {
return;
}
base = 10;
break;

case 2:
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Zl", &num, &arg_base) == FAILURE) {
return;
}
base = arg_base;
break;

default:
WRONG_PARAM_COUNT;
}

RETVAL_ZVAL(*num, 1, 0);
convert_to_long_base(return_value, base);
}

当参数只有一个的时候,默认使用10进制转换,最后进入convert_to_long_base函数来转换
下面是这个函数的实现

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
ZEND_API void convert_to_long_base(zval *op, int base) /* {{{ */
{
long tmp;

switch (Z_TYPE_P(op)) {
case IS_NULL:
Z_LVAL_P(op) = 0;
break;
case IS_RESOURCE: {
TSRMLS_FETCH();

zend_list_delete(Z_LVAL_P(op));
}
/* break missing intentionally */
case IS_BOOL:
case IS_LONG:
break;
case IS_DOUBLE:
Z_LVAL_P(op) = zend_dval_to_lval(Z_DVAL_P(op));
break;
case IS_STRING:
{
char *strval = Z_STRVAL_P(op);

Z_LVAL_P(op) = strtol(strval, NULL, base);
STR_FREE(strval);
}
break;
case IS_ARRAY:
tmp = (zend_hash_num_elements(Z_ARRVAL_P(op))?1:0);
zval_dtor(op);
Z_LVAL_P(op) = tmp;
break;
case IS_OBJECT:
{
int retval = 1;
TSRMLS_FETCH();

convert_object_to_type(op, IS_LONG, convert_to_long);

if (Z_TYPE_P(op) == IS_LONG) {
return;
}
zend_error(E_NOTICE, "Object of class %s could not be converted to int", Z_OBJCE_P(op)->name);

zval_dtor(op);
ZVAL_LONG(op, retval);
return;
}
default:
zend_error(E_WARNING, "Cannot convert to ordinal value");
zval_dtor(op);
Z_LVAL_P(op) = 0;
break;
}

Z_TYPE_P(op) = IS_LONG;
}

因为我们传入的是字符串类型,所以注意以下代码

1
2
3
4
5
6
7
8
9
10
···
case IS_STRING:
{
char *strval = Z_STRVAL_P(op);

Z_LVAL_P(op) = strtol(strval, NULL, base);
STR_FREE(strval);
}
break;
···

最后整数数值由函数strtol来转换,strval是传入的字符数组,base是进制
由于这个函数不考虑科学计数法,遇到e直接break,所以返回值被截断成1
至此,这个trick大概清楚了~